From 2ca14b025fc1bea3b5db6d9e23a259db02c4bf0b Mon Sep 17 00:00:00 2001 From: ojw28 Date: Tue, 7 Mar 2017 15:02:52 +0000 Subject: [PATCH 0001/2472] Update ISSUE_TEMPLATE --- ISSUE_TEMPLATE | 66 +++++++++++++++++++++++++++++++++++++------------- 1 file changed, 49 insertions(+), 17 deletions(-) diff --git a/ISSUE_TEMPLATE b/ISSUE_TEMPLATE index 6e55f3dcd6..8c9263b8d1 100644 --- a/ISSUE_TEMPLATE +++ b/ISSUE_TEMPLATE @@ -1,19 +1,51 @@ -*** PLEASE DO NOT IGNORE THIS ISSUE TEMPLATE *** +*** ISSUES THAT IGNORE THIS TEMPLATE WILL BE CLOSED WITHOUT INVESTIGATION *** -Please search the existing issues before filing a new one, including issues that -are closed. When filing a new issue please include ALL of the following, unless -you're certain that they're not useful for the particular issue being reported. +Before filing an issue: +----------------------- -- A description of the issue. -- Steps describing how the issue can be reproduced, ideally in the ExoPlayer - demo app. -- A link to content that reproduces the issue. If you don't wish to post it - publicly, please submit the issue, then email the link to - dev.exoplayer@gmail.com including the issue number in the subject line. -- The version of ExoPlayer being used. -- The device(s) and version(s) of Android on which the issue can be reproduced, - and how easily it reproduces. If possible, please test on multiple devices and - Android versions. -- A bug report taken from the device just after the issue occurs, attached as a - file. A bug report can be captured using "adb bugreport". Output from "adb - logcat" or a log snippet is not sufficient. +- Search existing issues, including issues that are closed. If an existing issue + exists, please do not file a new one. + +- Consult our FAQs, supported devices and supported formats pages. These can be + found at https://google.github.io/ExoPlayer/. + +- Rule out issues in your own code. A good way to do this is to try and + reproduce the issue in the ExoPlayer demo app. + +- This issue tracker is intended for bugs, feature requests and ExoPlayer + specific questions. If you're asking a general Android development question, + please do so on Stack Overflow. + +When reporting a bug: +----------------------- + +Please fill out the sections below, leaving the section headers but replacing the +content. If you're unable to provide certain information, please explain why in the +relevant section. We may close issues without investigation if they do not include +sufficient information. + +### Issue description +Describe the issue in detail, including observed and expected behavior. + +### Reproduction steps +Describe how the issue can be reproduced, ideally using the ExoPlayer demo app. + +### Link to test content +Provide a link to media that reproduces the issue. If you don't wish to post it +publicly, please submit the issue, then email the link to +dev.exoplayer@gmail.com including the issue number in the subject line. + +### Version of ExoPlayer being used +Specify the absolute version number. Avoid using terms such as "latest". + +### Device(s) and version(s) of Android being used +Specify the devices and versions of Android on which the issue can be +reproduced, and how easily it reproduces. If possible, please test on multiple +devices and Android versions. + +### A full bug report captured from the device +Capture a full bug report using "adb bugreport". Output from "adb logcat" or a +log snippet is NOT sufficient. Please attach the captured bug report as a file. +If you don't wish to post it publicly, please submit the issue, then email the +bug report to dev.exoplayer@gmail.com including the issue number in the subject +line. From c50b570d463a2214e252ebca4b8840cd15d16768 Mon Sep 17 00:00:00 2001 From: ojw28 Date: Tue, 7 Mar 2017 15:03:01 +0000 Subject: [PATCH 0002/2472] Update ISSUE_TEMPLATE --- ISSUE_TEMPLATE | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ISSUE_TEMPLATE b/ISSUE_TEMPLATE index 8c9263b8d1..b1e6687374 100644 --- a/ISSUE_TEMPLATE +++ b/ISSUE_TEMPLATE @@ -5,13 +5,13 @@ Before filing an issue: - Search existing issues, including issues that are closed. If an existing issue exists, please do not file a new one. - + - Consult our FAQs, supported devices and supported formats pages. These can be found at https://google.github.io/ExoPlayer/. - + - Rule out issues in your own code. A good way to do this is to try and reproduce the issue in the ExoPlayer demo app. - + - This issue tracker is intended for bugs, feature requests and ExoPlayer specific questions. If you're asking a general Android development question, please do so on Stack Overflow. From aaaa23e5e4a6771b06067bd28d728781990bd454 Mon Sep 17 00:00:00 2001 From: ojw28 Date: Tue, 7 Mar 2017 15:11:51 +0000 Subject: [PATCH 0003/2472] Update ISSUE_TEMPLATE --- ISSUE_TEMPLATE | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/ISSUE_TEMPLATE b/ISSUE_TEMPLATE index b1e6687374..1b912312d1 100644 --- a/ISSUE_TEMPLATE +++ b/ISSUE_TEMPLATE @@ -2,27 +2,20 @@ Before filing an issue: ----------------------- - -- Search existing issues, including issues that are closed. If an existing issue - exists, please do not file a new one. - +- Search existing issues, including issues that are closed. - Consult our FAQs, supported devices and supported formats pages. These can be found at https://google.github.io/ExoPlayer/. - - Rule out issues in your own code. A good way to do this is to try and reproduce the issue in the ExoPlayer demo app. - - This issue tracker is intended for bugs, feature requests and ExoPlayer specific questions. If you're asking a general Android development question, please do so on Stack Overflow. When reporting a bug: ----------------------- - -Please fill out the sections below, leaving the section headers but replacing the -content. If you're unable to provide certain information, please explain why in the -relevant section. We may close issues without investigation if they do not include -sufficient information. +Fill out the sections below, leaving the headers but replacing the content. If +you're unable to provide certain information, please explain why in the relevant +section. We may close issues if they do not include sufficient information. ### Issue description Describe the issue in detail, including observed and expected behavior. From 3c447130a5aeb323fcf0c76816b02e2ae6777283 Mon Sep 17 00:00:00 2001 From: ojw28 Date: Fri, 17 Mar 2017 20:53:09 +0000 Subject: [PATCH 0004/2472] Update RELEASENOTES.md --- RELEASENOTES.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index f45cb9aff6..4f99f4175d 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -6,8 +6,8 @@ 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 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 From 2966ea71f050347152ca0f8a52e4186e5a5892f9 Mon Sep 17 00:00:00 2001 From: ojw28 Date: Fri, 17 Mar 2017 20:55:45 +0000 Subject: [PATCH 0005/2472] Fix RELEASENOTES.md nesting. --- RELEASENOTES.md | 194 ++++++++++++++++++++++++------------------------ 1 file changed, 97 insertions(+), 97 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 4f99f4175d..1030cbdee4 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -8,29 +8,29 @@ * 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 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)). @@ -125,15 +125,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 @@ -143,19 +143,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)). -* 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 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. * 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 @@ -227,62 +227,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 # From 569cec7fe81250db8a7aa49447c5b96ddd57d7a2 Mon Sep 17 00:00:00 2001 From: ojw28 Date: Fri, 17 Mar 2017 21:00:45 +0000 Subject: [PATCH 0006/2472] Cleanup RELEASENOTES.md --- RELEASENOTES.md | 48 +++++++++++++++++++++++++----------------------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 1030cbdee4..2b0936e0d9 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -17,8 +17,9 @@ * 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)). + * 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 @@ -30,7 +31,7 @@ * MPEG-TS improvements: * Support for multiple programs. * Support for multiple closed captions and caption service descriptors - ([#2161](https://github.com/google/ExoPlayer/issues/2161)). + ([#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)). @@ -151,7 +152,7 @@ ([#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. +* 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. @@ -227,25 +228,25 @@ 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. + * 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. + * 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 + 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). @@ -264,19 +265,20 @@ some of the motivations behind ExoPlayer 2.x [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) + * 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). + * 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)). + * 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 From 646f6a74c94e4ffb228ea018b61fff1eeb59e966 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 23 Mar 2017 17:44:47 +0000 Subject: [PATCH 0007/2472] Fix NullPointerException enabling WebVtt subtitles in DASH Issue: #2596 --- .../source/dash/DefaultDashChunkSource.java | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java b/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java index 7ccea8a2a6..a6e909ddac 100644 --- a/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java @@ -180,23 +180,24 @@ public class DefaultDashChunkSource implements DashChunkSource { RepresentationHolder representationHolder = representationHolders[trackSelection.getSelectedIndex()]; - Representation selectedRepresentation = representationHolder.representation; - DashSegmentIndex segmentIndex = representationHolder.segmentIndex; - RangedUri pendingInitializationUri = null; - RangedUri pendingIndexUri = null; - if (representationHolder.extractorWrapper.getSampleFormats() == null) { - pendingInitializationUri = selectedRepresentation.getInitializationUri(); - } - if (segmentIndex == null) { - pendingIndexUri = selectedRepresentation.getIndexUri(); - } - if (pendingInitializationUri != null || pendingIndexUri != null) { - // We have initialization and/or index requests to make. - out.chunk = newInitializationChunk(representationHolder, dataSource, - trackSelection.getSelectedFormat(), trackSelection.getSelectionReason(), - trackSelection.getSelectionData(), pendingInitializationUri, pendingIndexUri); - return; + if (representationHolder.extractorWrapper != null) { + Representation selectedRepresentation = representationHolder.representation; + RangedUri pendingInitializationUri = null; + RangedUri pendingIndexUri = null; + if (representationHolder.extractorWrapper.getSampleFormats() == null) { + pendingInitializationUri = selectedRepresentation.getInitializationUri(); + } + if (representationHolder.segmentIndex == null) { + pendingIndexUri = selectedRepresentation.getIndexUri(); + } + if (pendingInitializationUri != null || pendingIndexUri != null) { + // We have initialization and/or index requests to make. + out.chunk = newInitializationChunk(representationHolder, dataSource, + trackSelection.getSelectedFormat(), trackSelection.getSelectionReason(), + trackSelection.getSelectionData(), pendingInitializationUri, pendingIndexUri); + return; + } } long nowUs = getNowUnixTimeUs(); From 23b5921b828be728a345bcf4088edcd736f52ade Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 23 Mar 2017 17:46:12 +0000 Subject: [PATCH 0008/2472] Fix typo on CEA-708 decoder Issue: #2595 --- .../com/google/android/exoplayer2/text/cea/Cea708Decoder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java b/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java index 740fd17013..8fd70f7a67 100644 --- a/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java +++ b/library/src/main/java/com/google/android/exoplayer2/text/cea/Cea708Decoder.java @@ -483,7 +483,7 @@ public final class Cea708Decoder extends CeaDecoder { private void handleC2Command(int command) { // C2 Table doesn't contain any commands in CEA-708-B, but we do need to skip bytes - if (command <= 0x0F) { + if (command <= 0x07) { // Do nothing. } else if (command <= 0x0F) { serviceBlockPacket.skipBits(8); From c96f18f5ce4264752c39482a4b2a3b996663fc22 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 23 Mar 2017 17:46:43 +0000 Subject: [PATCH 0009/2472] Fix skipping to keyframe to use correct position Issue: #2575 --- .../main/java/com/google/android/exoplayer2/BaseRenderer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/src/main/java/com/google/android/exoplayer2/BaseRenderer.java b/library/src/main/java/com/google/android/exoplayer2/BaseRenderer.java index f65be3afcd..f6aae200dd 100644 --- a/library/src/main/java/com/google/android/exoplayer2/BaseRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer2/BaseRenderer.java @@ -311,7 +311,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { * @param timeUs The specified time. */ protected void skipToKeyframeBefore(long timeUs) { - stream.skipToKeyframeBefore(timeUs); + stream.skipToKeyframeBefore(timeUs - streamOffsetUs); } } From 6503f016e9b54b6818f04d364727f5d6d3407443 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 23 Mar 2017 17:59:20 +0000 Subject: [PATCH 0010/2472] Update release notes + bump versions --- RELEASENOTES.md | 9 +++++++++ build.gradle | 2 +- demo/src/main/AndroidManifest.xml | 4 ++-- .../google/android/exoplayer2/ExoPlayerLibraryInfo.java | 4 ++-- 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 2b0936e0d9..a0c750660d 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,14 @@ # Release notes # +### r2.3.1 ### + +* Fix NPE enabling WebVTT subtitles in DASH streams + ([#2596](https://github.com/google/ExoPlayer/issues/2596)). +* Fix skipping to keyframes when MediaCodecVideoRenderer is enabled but without + a Surface ([#2575](https://github.com/google/ExoPlayer/issues/2575)). +* Minor fix for CEA-708 decoder + ([#2595](https://github.com/google/ExoPlayer/issues/2595)). + ### r2.3.0 ### * GVR extension: Wraps the Google VR Audio SDK to provide spatial audio diff --git a/build.gradle b/build.gradle index f1901a1270..9883c04e54 100644 --- a/build.gradle +++ b/build.gradle @@ -37,7 +37,7 @@ allprojects { releaseRepoName = getBintrayRepo() releaseUserOrg = 'google' releaseGroupId = 'com.google.android.exoplayer' - releaseVersion = 'r2.3.0' + releaseVersion = 'r2.3.1' releaseWebsite = 'https://github.com/google/ExoPlayer' } } diff --git a/demo/src/main/AndroidManifest.xml b/demo/src/main/AndroidManifest.xml index a834c5df19..9a6e1a4d3a 100644 --- a/demo/src/main/AndroidManifest.xml +++ b/demo/src/main/AndroidManifest.xml @@ -16,8 +16,8 @@ + android:versionCode="2301" + android:versionName="2.3.1"> diff --git a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java index 5ec7fac5dd..bee9904590 100644 --- a/library/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java +++ b/library/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java @@ -23,7 +23,7 @@ public interface ExoPlayerLibraryInfo { /** * The version of the library, expressed as a string. */ - String VERSION = "2.3.0"; + String VERSION = "2.3.1"; /** * The version of the library, expressed as an integer. @@ -32,7 +32,7 @@ public interface ExoPlayerLibraryInfo { * corresponding integer version 1002003 (001-002-003), and "123.45.6" has the corresponding * integer version 123045006 (123-045-006). */ - int VERSION_INT = 2003000; + int VERSION_INT = 2003001; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} From dc80be0ca1c8c87ea4522f9efbb55dd346c836bd Mon Sep 17 00:00:00 2001 From: Andrew Orobator Date: Wed, 3 May 2017 10:49:36 -0400 Subject: [PATCH 0011/2472] Improved Documentation Added missing coma and fixed typo for EventListener#onTimelineChanged --- .../main/java/com/google/android/exoplayer2/ExoPlayer.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java index ab521e3733..82ec610be7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java @@ -113,8 +113,8 @@ public interface ExoPlayer { * Called when the timeline and/or manifest has been refreshed. *

* Note that if the timeline has changed then a position discontinuity may also have occurred. - * For example the current period index may have changed as a result of periods being added or - * removed from the timeline. The will not be reported via a separate call to + * For example, the current period index may have changed as a result of periods being added or + * removed from the timeline. This will not be reported via a separate call to * {@link #onPositionDiscontinuity()}. * * @param timeline The latest timeline. Never null, but may be empty. From 2e2b0085c1de98acfe054b3797de298a767566b1 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 27 Apr 2017 07:49:14 -0700 Subject: [PATCH 0012/2472] Prevent text tracks with no language being selected by default ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=154421706 --- .../exoplayer2/trackselection/DefaultTrackSelector.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index 9db77fd7ad..941df66e4d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -867,7 +867,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { } protected static boolean formatHasLanguage(Format format, String language) { - return TextUtils.equals(language, Util.normalizeLanguageCode(format.language)); + return language != null + && TextUtils.equals(language, Util.normalizeLanguageCode(format.language)); } // Viewport size util methods. From e90e2caec4fe94d7fbb706c7da827c9f0ad3a1c7 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 2 May 2017 06:38:57 -0700 Subject: [PATCH 0013/2472] Improve DefaultTimeBar color customization Add attributes for the scrubber handle color and unplayed color. If attributes are missing, derive defaults from the played color. Issue: #2740 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=154825736 --- .../android/exoplayer2/ui/DefaultTimeBar.java | 50 +++++++++++++------ library/ui/src/main/res/values/attrs.xml | 2 + 2 files changed, 36 insertions(+), 16 deletions(-) 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 index d40da451a2..12f31f5da1 100644 --- 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 @@ -61,22 +61,21 @@ public class DefaultTimeBar extends View implements TimeBar { 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_PLAYED_COLOR = 0xFFFFFFFF; 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 playedPaint; private final Paint scrubberPaint; + private final Paint bufferedPaint; + private final Paint unplayedPaint; private final Paint adMarkerPaint; private final int barHeight; private final int touchTargetHeight; @@ -115,9 +114,10 @@ public class DefaultTimeBar extends View implements TimeBar { progressBar = new Rect(); bufferedBar = new Rect(); scrubberBar = new Rect(); - progressPaint = new Paint(); - bufferedPaint = new Paint(); + playedPaint = new Paint(); scrubberPaint = new Paint(); + bufferedPaint = new Paint(); + unplayedPaint = new Paint(); adMarkerPaint = new Paint(); // Calculate the dimensions and paints for drawn elements. @@ -147,13 +147,18 @@ public class DefaultTimeBar extends View implements TimeBar { scrubberDraggedSize = a.getDimensionPixelSize( R.styleable.DefaultTimeBar_scrubber_dragged_size, defaultScrubberDraggedSize); int playedColor = a.getInt(R.styleable.DefaultTimeBar_played_color, DEFAULT_PLAYED_COLOR); + int scrubberColor = a.getInt(R.styleable.DefaultTimeBar_scrubber_color, + getDefaultScrubberColor(playedColor)); int bufferedColor = a.getInt(R.styleable.DefaultTimeBar_buffered_color, - DEFAULT_BUFFERED_COLOR); + getDefaultBufferedColor(playedColor)); + int unplayedColor = a.getInt(R.styleable.DefaultTimeBar_unplayed_color, + getDefaultUnplayedColor(playedColor)); int adMarkerColor = a.getInt(R.styleable.DefaultTimeBar_ad_marker_color, DEFAULT_AD_MARKER_COLOR); - progressPaint.setColor(playedColor); - scrubberPaint.setColor(OPAQUE_COLOR | playedColor); + playedPaint.setColor(playedColor); + scrubberPaint.setColor(scrubberColor); bufferedPaint.setColor(bufferedColor); + unplayedPaint.setColor(unplayedColor); adMarkerPaint.setColor(adMarkerColor); } finally { a.recycle(); @@ -165,9 +170,10 @@ public class DefaultTimeBar extends View implements TimeBar { scrubberEnabledSize = defaultScrubberEnabledSize; scrubberDisabledSize = defaultScrubberDisabledSize; scrubberDraggedSize = defaultScrubberDraggedSize; - scrubberPaint.setColor(OPAQUE_COLOR | DEFAULT_PLAYED_COLOR); - progressPaint.setColor(DEFAULT_PLAYED_COLOR); - bufferedPaint.setColor(DEFAULT_BUFFERED_COLOR); + playedPaint.setColor(DEFAULT_PLAYED_COLOR); + scrubberPaint.setColor(getDefaultScrubberColor(DEFAULT_PLAYED_COLOR)); + bufferedPaint.setColor(getDefaultBufferedColor(DEFAULT_PLAYED_COLOR)); + unplayedPaint.setColor(getDefaultUnplayedColor(DEFAULT_PLAYED_COLOR)); adMarkerPaint.setColor(DEFAULT_AD_MARKER_COLOR); } formatBuilder = new StringBuilder(); @@ -502,21 +508,21 @@ public class DefaultTimeBar extends View implements TimeBar { int barTop = progressBar.centerY() - progressBarHeight / 2; int barBottom = barTop + progressBarHeight; if (duration <= 0) { - canvas.drawRect(progressBar.left, barTop, progressBar.right, barBottom, progressPaint); + canvas.drawRect(progressBar.left, barTop, progressBar.right, barBottom, unplayedPaint); 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); + canvas.drawRect(progressLeft, barTop, progressBar.right, barBottom, unplayedPaint); } 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); + canvas.drawRect(scrubberBar.left, barTop, scrubberBar.right, barBottom, playedPaint); } int adMarkerOffset = adMarkerWidth / 2; for (int i = 0; i < adBreakCount; i++) { @@ -577,4 +583,16 @@ public class DefaultTimeBar extends View implements TimeBar { return (int) (dps * displayMetrics.density + 0.5f); } + private static int getDefaultScrubberColor(int playedColor) { + return 0xFF000000 | playedColor; + } + + private static int getDefaultUnplayedColor(int playedColor) { + return 0x33000000 | (playedColor & 0x00FFFFFF); + } + + private static int getDefaultBufferedColor(int playedColor) { + return 0xCC000000 | (playedColor & 0x00FFFFFF); + } + } diff --git a/library/ui/src/main/res/values/attrs.xml b/library/ui/src/main/res/values/attrs.xml index 521e535ce3..d8340c21cd 100644 --- a/library/ui/src/main/res/values/attrs.xml +++ b/library/ui/src/main/res/values/attrs.xml @@ -68,7 +68,9 @@ + + From 60bf31ff24f492e8c0bbd798b76d3081e270a4ba Mon Sep 17 00:00:00 2001 From: falhassen Date: Tue, 2 May 2017 10:09:55 -0700 Subject: [PATCH 0014/2472] Use Looper.getMainLooper() in Handler constructors in ExoPlayer when needed. Looper.myLooper(), the default looper, may be null in background threads. This adds a fallback to use the main app looper. This will allow ExoPlayer instances to be built in background threads in Photos. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=154845446 --- .../google/android/exoplayer2/ExoPlayer.java | 9 ++++-- .../android/exoplayer2/ExoPlayerFactory.java | 28 ++++++------------- .../android/exoplayer2/ExoPlayerImpl.java | 3 +- .../android/exoplayer2/SimpleExoPlayer.java | 7 +++-- 4 files changed, 23 insertions(+), 24 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java index 82ec610be7..18bf9eeb8c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2; +import android.os.Looper; +import android.support.annotation.IntDef; import android.support.annotation.Nullable; import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer; import com.google.android.exoplayer2.metadata.MetadataRenderer; @@ -88,7 +90,9 @@ import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; * thread. The application's main thread is ideal. Accessing an instance from multiple threads is * discouraged, however if an application does wish to do this then it may do so provided that it * ensures accesses are synchronized. - *

  • Registered listeners are called on the thread that created the ExoPlayer instance.
  • + *
  • Registered listeners are called on the thread that created the ExoPlayer instance, unless + * the thread that created the ExoPlayer instance does not have a {@link Looper}. In that case, + * registered listeners will be called on the application's main thread.
  • *
  • An internal playback thread is responsible for playback. Injected player components such as * Renderers, MediaSources, TrackSelectors and LoadControls are called by the player on this * thread.
  • @@ -253,7 +257,8 @@ public interface ExoPlayer { /** * Register a listener to receive events from the player. The listener's methods will be called on - * the thread that was used to construct the player. + * the thread that was used to construct the player. However, if the thread used to construct the + * player does not have a {@link Looper}, then the listener will be called on the main thread. * * @param listener The listener to register. */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java index 7aecd20d4e..97a310c3da 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java @@ -16,7 +16,6 @@ package com.google.android.exoplayer2; import android.content.Context; -import android.os.Looper; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.trackselection.TrackSelector; @@ -29,8 +28,7 @@ public final class ExoPlayerFactory { private ExoPlayerFactory() {} /** - * Creates a {@link SimpleExoPlayer} instance. Must be called from a thread that has an associated - * {@link Looper}. + * Creates a {@link SimpleExoPlayer} instance. * * @param context A {@link Context}. * @param trackSelector The {@link TrackSelector} that will be used by the instance. @@ -45,8 +43,7 @@ public final class ExoPlayerFactory { } /** - * Creates a {@link SimpleExoPlayer} instance. Must be called from a thread that has an associated - * {@link Looper}. Available extension renderers are not used. + * Creates a {@link SimpleExoPlayer} instance. Available extension renderers are not used. * * @param context A {@link Context}. * @param trackSelector The {@link TrackSelector} that will be used by the instance. @@ -63,8 +60,7 @@ public final class ExoPlayerFactory { } /** - * Creates a {@link SimpleExoPlayer} instance. Must be called from a thread that has an associated - * {@link Looper}. + * Creates a {@link SimpleExoPlayer} instance. * * @param context A {@link Context}. * @param trackSelector The {@link TrackSelector} that will be used by the instance. @@ -86,8 +82,7 @@ public final class ExoPlayerFactory { } /** - * Creates a {@link SimpleExoPlayer} instance. Must be called from a thread that has an associated - * {@link Looper}. + * Creates a {@link SimpleExoPlayer} instance. * * @param context A {@link Context}. * @param trackSelector The {@link TrackSelector} that will be used by the instance. @@ -112,8 +107,7 @@ public final class ExoPlayerFactory { } /** - * Creates a {@link SimpleExoPlayer} instance. Must be called from a thread that has an associated - * {@link Looper}. + * Creates a {@link SimpleExoPlayer} instance. * * @param context A {@link Context}. * @param trackSelector The {@link TrackSelector} that will be used by the instance. @@ -123,8 +117,7 @@ public final class ExoPlayerFactory { } /** - * Creates a {@link SimpleExoPlayer} instance. Must be called from a thread that has an associated - * {@link Looper}. + * Creates a {@link SimpleExoPlayer} instance. * * @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. @@ -135,8 +128,7 @@ public final class ExoPlayerFactory { } /** - * Creates a {@link SimpleExoPlayer} instance. Must be called from a thread that has an associated - * {@link Looper}. + * Creates a {@link SimpleExoPlayer} instance. * * @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. @@ -148,8 +140,7 @@ public final class ExoPlayerFactory { } /** - * Creates an {@link ExoPlayer} instance. Must be called from a thread that has an associated - * {@link Looper}. + * Creates an {@link ExoPlayer} instance. * * @param renderers The {@link Renderer}s that will be used by the instance. * @param trackSelector The {@link TrackSelector} that will be used by the instance. @@ -159,8 +150,7 @@ public final class ExoPlayerFactory { } /** - * Creates an {@link ExoPlayer} instance. Must be called from a thread that has an associated - * {@link Looper}. + * Creates an {@link ExoPlayer} instance. * * @param renderers The {@link Renderer}s that will be used by the instance. * @param trackSelector The {@link TrackSelector} that will be used by the instance. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index 4131b97954..cb0958a3b1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -92,7 +92,8 @@ import java.util.concurrent.CopyOnWriteArraySet; trackGroups = TrackGroupArray.EMPTY; trackSelections = emptyTrackSelections; playbackParameters = PlaybackParameters.DEFAULT; - eventHandler = new Handler() { + Looper eventLooper = Looper.myLooper() != null ? Looper.myLooper() : Looper.getMainLooper(); + eventHandler = new Handler(eventLooper) { @Override public void handleMessage(Message msg) { ExoPlayerImpl.this.handleEvent(msg); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 28ba8cf9d7..6094513913 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -20,6 +20,7 @@ import android.graphics.SurfaceTexture; import android.media.MediaCodec; import android.media.PlaybackParams; import android.os.Handler; +import android.os.Looper; import android.support.annotation.Nullable; import android.util.Log; import android.view.Surface; @@ -111,8 +112,10 @@ public class SimpleExoPlayer implements ExoPlayer { protected SimpleExoPlayer(RenderersFactory renderersFactory, TrackSelector trackSelector, LoadControl loadControl) { componentListener = new ComponentListener(); - renderers = renderersFactory.createRenderers(new Handler(), componentListener, - componentListener, componentListener, componentListener); + Looper eventLooper = Looper.myLooper() != null ? Looper.myLooper() : Looper.getMainLooper(); + Handler eventHandler = new Handler(eventLooper); + renderers = renderersFactory.createRenderers(eventHandler, componentListener, componentListener, + componentListener, componentListener); // Obtain counts of video and audio renderers. int videoRendererCount = 0; From 8bffe5d11ef5bbdbaa892226b4cba4273351967a Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 3 May 2017 01:32:22 -0700 Subject: [PATCH 0015/2472] Add DummySurface for use with MediaCodec A DummySurface is useful with MediaCodec on API levels 23+. Rather than having to release a MediaCodec instance when the app no longer has a real surface to output to, it's possible to retain the MediaCodec, using MediaCodec.setOutputSurface to target a DummySurface instance instead. When the app has a real surface to output to again, it can call swap this surface back in instantaneously. Without DummySurface a new MediaCodec has to be instantiated at this point, and decoding can only start from a key-frame in the media. A future change may hook this up internally in MediaCodecRenderer for supported use cases, although this looks a little awkward. If this approach isn't viable, we can require applications wanting this to set a DummySurface themselves. This isn't easy to do with the way SimpleExoPlayerView.setPlayer works at the moment, however, so some changes will be needed either way. Issue: #677 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=154931778 --- .../exoplayer2/video/DummySurface.java | 306 ++++++++++++++++++ 1 file changed, 306 insertions(+) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java new file mode 100644 index 0000000000..5298c82f61 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java @@ -0,0 +1,306 @@ +/* + * 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 static android.opengl.EGL14.EGL_ALPHA_SIZE; +import static android.opengl.EGL14.EGL_BLUE_SIZE; +import static android.opengl.EGL14.EGL_CONFIG_CAVEAT; +import static android.opengl.EGL14.EGL_CONTEXT_CLIENT_VERSION; +import static android.opengl.EGL14.EGL_DEFAULT_DISPLAY; +import static android.opengl.EGL14.EGL_DEPTH_SIZE; +import static android.opengl.EGL14.EGL_GREEN_SIZE; +import static android.opengl.EGL14.EGL_HEIGHT; +import static android.opengl.EGL14.EGL_NONE; +import static android.opengl.EGL14.EGL_OPENGL_ES2_BIT; +import static android.opengl.EGL14.EGL_RED_SIZE; +import static android.opengl.EGL14.EGL_RENDERABLE_TYPE; +import static android.opengl.EGL14.EGL_SURFACE_TYPE; +import static android.opengl.EGL14.EGL_TRUE; +import static android.opengl.EGL14.EGL_WIDTH; +import static android.opengl.EGL14.EGL_WINDOW_BIT; +import static android.opengl.EGL14.eglChooseConfig; +import static android.opengl.EGL14.eglCreateContext; +import static android.opengl.EGL14.eglCreatePbufferSurface; +import static android.opengl.EGL14.eglGetDisplay; +import static android.opengl.EGL14.eglInitialize; +import static android.opengl.EGL14.eglMakeCurrent; +import static android.opengl.GLES20.glDeleteTextures; +import static android.opengl.GLES20.glGenTextures; + +import android.annotation.TargetApi; +import android.graphics.SurfaceTexture; +import android.graphics.SurfaceTexture.OnFrameAvailableListener; +import android.opengl.EGL14; +import android.opengl.EGLConfig; +import android.opengl.EGLContext; +import android.opengl.EGLDisplay; +import android.opengl.EGLSurface; +import android.os.Handler; +import android.os.Handler.Callback; +import android.os.HandlerThread; +import android.os.Message; +import android.util.Log; +import android.view.Surface; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; +import javax.microedition.khronos.egl.EGL10; + +/** + * A dummy {@link Surface}. + */ +@TargetApi(17) +public final class DummySurface extends Surface { + + private static final String TAG = "DummySurface"; + + private static final int EGL_PROTECTED_CONTENT_EXT = 0x32C0; + + /** + * Whether the device supports secure dummy surfaces. + */ + public static final boolean SECURE_SUPPORTED; + static { + if (Util.SDK_INT >= 17) { + EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY); + String extensions = EGL14.eglQueryString(display, EGL10.EGL_EXTENSIONS); + SECURE_SUPPORTED = extensions.contains("EGL_EXT_protected_content"); + } else { + SECURE_SUPPORTED = false; + } + } + + /** + * Whether the surface is secure. + */ + public final boolean secure; + + private final DummySurfaceThread thread; + private boolean threadReleased; + + /** + * Returns a newly created dummy surface. The surface must be released by calling {@link #release} + * when it's no longer required. + *

    + * Must only be called if {@link Util#SDK_INT} is 17 or higher. + * + * @param secure Whether a secure surface is required. Must only be requested if + * {@link #SECURE_SUPPORTED} is {@code true}. + */ + public static DummySurface newInstanceV17(boolean secure) { + assertApiLevel17OrHigher(); + Assertions.checkState(!secure || SECURE_SUPPORTED); + DummySurfaceThread thread = new DummySurfaceThread(); + return thread.init(secure); + } + + private DummySurface(DummySurfaceThread thread, SurfaceTexture surfaceTexture, boolean secure) { + super(surfaceTexture); + this.thread = thread; + this.secure = secure; + } + + @Override + public void release() { + super.release(); + // The Surface may be released multiple times (explicitly and by Surface.finalize()). The + // implementation of super.release() has its own deduplication logic. Below we need to + // deduplicate ourselves. Synchronization is required as we don't control the thread on which + // Surface.finalize() is called. + synchronized (thread) { + if (!threadReleased) { + thread.release(); + threadReleased = true; + } + } + } + + private static void assertApiLevel17OrHigher() { + if (Util.SDK_INT < 17) { + throw new UnsupportedOperationException("Unsupported prior to API level 17"); + } + } + + private static class DummySurfaceThread extends HandlerThread implements OnFrameAvailableListener, + Callback { + + private static final int MSG_INIT = 1; + private static final int MSG_UPDATE_TEXTURE = 2; + private static final int MSG_RELEASE = 3; + + private final int[] textureIdHolder; + private Handler handler; + private SurfaceTexture surfaceTexture; + + private Error initError; + private RuntimeException initException; + private DummySurface surface; + + public DummySurfaceThread() { + super("dummySurface"); + textureIdHolder = new int[1]; + } + + public DummySurface init(boolean secure) { + start(); + handler = new Handler(getLooper(), this); + boolean wasInterrupted = false; + synchronized (this) { + handler.obtainMessage(MSG_INIT, secure ? 1 : 0, 0).sendToTarget(); + while (surface == null && initException == null && initError == null) { + try { + wait(); + } catch (InterruptedException e) { + wasInterrupted = true; + } + } + } + if (wasInterrupted) { + // Restore the interrupted status. + Thread.currentThread().interrupt(); + } + if (initException != null) { + throw initException; + } else if (initError != null) { + throw initError; + } else { + return surface; + } + } + + public void release() { + handler.sendEmptyMessage(MSG_RELEASE); + } + + @Override + public void onFrameAvailable(SurfaceTexture surfaceTexture) { + handler.sendEmptyMessage(MSG_UPDATE_TEXTURE); + } + + @Override + public boolean handleMessage(Message msg) { + switch (msg.what) { + case MSG_INIT: + try { + initInternal(msg.arg1 != 0); + } catch (RuntimeException e) { + Log.e(TAG, "Failed to initialize dummy surface", e); + initException = e; + } catch (Error e) { + Log.e(TAG, "Failed to initialize dummy surface", e); + initError = e; + } finally { + synchronized (this) { + notify(); + } + } + return true; + case MSG_UPDATE_TEXTURE: + surfaceTexture.updateTexImage(); + return true; + case MSG_RELEASE: + try { + releaseInternal(); + } catch (Throwable e) { + Log.e(TAG, "Failed to release dummy surface", e); + } finally { + quit(); + } + return true; + default: + return true; + } + } + + private void initInternal(boolean secure) { + EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY); + Assertions.checkState(display != null, "eglGetDisplay failed"); + + int[] version = new int[2]; + boolean eglInitialized = eglInitialize(display, version, 0, version, 1); + Assertions.checkState(eglInitialized, "eglInitialize failed"); + + int[] eglAttributes = new int[] { + EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, + EGL_RED_SIZE, 8, + EGL_GREEN_SIZE, 8, + EGL_BLUE_SIZE, 8, + EGL_ALPHA_SIZE, 8, + EGL_DEPTH_SIZE, 0, + EGL_CONFIG_CAVEAT, EGL_NONE, + EGL_SURFACE_TYPE, EGL_WINDOW_BIT, + EGL_NONE + }; + EGLConfig[] configs = new EGLConfig[1]; + int[] numConfigs = new int[1]; + boolean eglChooseConfigSuccess = eglChooseConfig(display, eglAttributes, 0, configs, 0, 1, + numConfigs, 0); + Assertions.checkState(eglChooseConfigSuccess && numConfigs[0] > 0 && configs[0] != null, + "eglChooseConfig failed"); + + EGLConfig config = configs[0]; + int[] glAttributes; + if (secure) { + glAttributes = new int[] { + EGL_CONTEXT_CLIENT_VERSION, 2, + EGL_PROTECTED_CONTENT_EXT, + EGL_TRUE, EGL_NONE}; + } else { + glAttributes = new int[] { + EGL_CONTEXT_CLIENT_VERSION, 2, + EGL_NONE}; + } + EGLContext context = eglCreateContext(display, config, android.opengl.EGL14.EGL_NO_CONTEXT, + glAttributes, 0); + Assertions.checkState(context != null, "eglCreateContext failed"); + + int[] pbufferAttributes; + if (secure) { + pbufferAttributes = new int[] { + EGL_WIDTH, 1, + EGL_HEIGHT, 1, + EGL_PROTECTED_CONTENT_EXT, EGL_TRUE, + EGL_NONE}; + } else { + pbufferAttributes = new int[] { + EGL_WIDTH, 1, + EGL_HEIGHT, 1, + EGL_NONE}; + } + EGLSurface pbuffer = eglCreatePbufferSurface(display, config, pbufferAttributes, 0); + Assertions.checkState(pbuffer != null, "eglCreatePbufferSurface failed"); + + boolean eglMadeCurrent = eglMakeCurrent(display, pbuffer, pbuffer, context); + Assertions.checkState(eglMadeCurrent, "eglMakeCurrent failed"); + + glGenTextures(1, textureIdHolder, 0); + surfaceTexture = new SurfaceTexture(textureIdHolder[0]); + surfaceTexture.setOnFrameAvailableListener(this); + surface = new DummySurface(this, surfaceTexture, secure); + } + + private void releaseInternal() { + try { + surfaceTexture.release(); + } finally { + surface = null; + surfaceTexture = null; + glDeleteTextures(1, textureIdHolder, 0); + } + } + + } + +} From ed836d31e28867fc81005b4d2dff4891289e5a74 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 4 May 2017 03:24:19 -0700 Subject: [PATCH 0016/2472] Propagate EXT-X-DATERANGE tags with media playlists ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=155062718 --- .../exoplayer2/source/hls/playlist/HlsMediaPlaylist.java | 9 ++++++--- .../source/hls/playlist/HlsPlaylistParser.java | 7 ++++++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/library/hls/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 index c7708a1d2f..69b95e6d3d 100644 --- a/library/hls/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 @@ -91,12 +91,14 @@ public final class HlsMediaPlaylist extends HlsPlaylist { public final boolean hasProgramDateTime; public final Segment initializationSegment; public final List segments; + public final List dateRanges; public final long durationUs; public HlsMediaPlaylist(@PlaylistType int playlistType, String baseUri, long startOffsetUs, long startTimeUs, boolean hasDiscontinuitySequence, int discontinuitySequence, int mediaSequence, int version, long targetDurationUs, boolean hasEndTag, - boolean hasProgramDateTime, Segment initializationSegment, List segments) { + boolean hasProgramDateTime, Segment initializationSegment, List segments, + List dateRanges) { super(baseUri); this.playlistType = playlistType; this.startTimeUs = startTimeUs; @@ -117,6 +119,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { } this.startOffsetUs = startOffsetUs == C.TIME_UNSET ? C.TIME_UNSET : startOffsetUs >= 0 ? startOffsetUs : durationUs + startOffsetUs; + this.dateRanges = Collections.unmodifiableList(dateRanges); } /** @@ -155,7 +158,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { public HlsMediaPlaylist copyWith(long startTimeUs, int discontinuitySequence) { return new HlsMediaPlaylist(playlistType, baseUri, startOffsetUs, startTimeUs, true, discontinuitySequence, mediaSequence, version, targetDurationUs, hasEndTag, - hasProgramDateTime, initializationSegment, segments); + hasProgramDateTime, initializationSegment, segments, dateRanges); } /** @@ -170,7 +173,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { } return new HlsMediaPlaylist(playlistType, baseUri, startOffsetUs, startTimeUs, hasDiscontinuitySequence, discontinuitySequence, mediaSequence, version, targetDurationUs, - true, hasProgramDateTime, initializationSegment, segments); + true, hasProgramDateTime, initializationSegment, segments, dateRanges); } } diff --git a/library/hls/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 index d24264cae6..8e01dec6fe 100644 --- a/library/hls/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 @@ -57,6 +57,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser segments = new ArrayList<>(); + List dateRanges = new ArrayList<>(); long segmentDurationUs = 0; boolean hasDiscontinuitySequence = false; @@ -343,6 +345,8 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser Date: Thu, 4 May 2017 03:29:10 -0700 Subject: [PATCH 0017/2472] Fix javadocs typos Issue:#2773 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=155062917 --- .../google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java | 2 +- .../com/google/android/exoplayer2/upstream/cache/CacheUtil.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 c44c703bb1..4b629c8d2a 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 @@ -286,7 +286,7 @@ public final class LibvpxVideoRenderer extends BaseRenderer { * * @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. + * {@link C#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. 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 index f6251dbbf1..bb1f88e5ea 100644 --- 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 @@ -64,7 +64,7 @@ public final class CacheUtil { } /** - * Returns already cached and missing bytes in the {@cache} for the data defined by {@code + * Returns already cached and missing bytes in the {@code cache} for the data defined by {@code * dataSpec}. * * @param dataSpec Defines the data to be checked. From 943141fb076a4aeb7e7c10d4830aef32caa46aca Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 4 May 2017 11:30:25 -0700 Subject: [PATCH 0018/2472] Some minor cleanup related to track selection and caching ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=155103828 --- .../exoplayer2/demo/PlayerActivity.java | 6 ++-- .../trackselection/DefaultTrackSelector.java | 16 ++++++++-- .../upstream/cache/CacheDataSource.java | 30 ++++++++++++++----- .../cache/CacheDataSourceFactory.java | 18 +++++++---- 4 files changed, 53 insertions(+), 17 deletions(-) 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 2542f23e95..d01a64a051 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 @@ -261,10 +261,10 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(this, drmSessionManager, extensionRendererMode); - TrackSelection.Factory videoTrackSelectionFactory = + TrackSelection.Factory adaptiveTrackSelectionFactory = new AdaptiveTrackSelection.Factory(BANDWIDTH_METER); - trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory); - trackSelectionHelper = new TrackSelectionHelper(trackSelector, videoTrackSelectionFactory); + trackSelector = new DefaultTrackSelector(adaptiveTrackSelectionFactory); + trackSelectionHelper = new TrackSelectionHelper(trackSelector, adaptiveTrackSelectionFactory); lastSeenTrackGroupArray = null; player = ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index 941df66e4d..361fcf0b57 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -24,6 +24,7 @@ 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; +import com.google.android.exoplayer2.upstream.BandwidthMeter; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.util.ArrayList; @@ -376,10 +377,21 @@ public class DefaultTrackSelector extends MappingTrackSelector { private final AtomicReference paramsReference; /** - * Constructs an instance that does not support adaptive tracks. + * Constructs an instance that does not support adaptive track selection. */ public DefaultTrackSelector() { - this(null); + this((TrackSelection.Factory) null); + } + + /** + * Constructs an instance that supports adaptive track selection. Adaptive track selections use + * the provided {@link BandwidthMeter} to determine which individual track should be used during + * playback. + * + * @param bandwidthMeter The {@link BandwidthMeter}. + */ + public DefaultTrackSelector(BandwidthMeter bandwidthMeter) { + this(new AdaptiveTrackSelection.Factory(bandwidthMeter)); } /** diff --git a/library/core/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 index 86dc5cfedf..bb2a952b11 100644 --- a/library/core/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 @@ -54,8 +54,8 @@ public final class CacheDataSource implements DataSource { FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS}) public @interface Flags {} /** - * A flag indicating whether we will block reads if the cache key is locked. If this flag is - * set, then we will read from upstream if the cache key is locked. + * A flag indicating whether we will block reads if the cache key is locked. If unset then data is + * read from upstream if the cache key is locked, regardless of whether the data is cached. */ public static final int FLAG_BLOCK_ON_CACHE = 1 << 0; @@ -110,7 +110,23 @@ public final class CacheDataSource implements DataSource { /** * Constructs an instance with default {@link DataSource} and {@link DataSink} instances for - * reading and writing the cache and with {@link #DEFAULT_MAX_CACHE_FILE_SIZE}. + * reading and writing the cache. + * + * @param cache The cache. + * @param upstream A {@link DataSource} for reading data not in the cache. + */ + public CacheDataSource(Cache cache, DataSource upstream) { + this(cache, upstream, 0, DEFAULT_MAX_CACHE_FILE_SIZE); + } + + /** + * Constructs an instance with default {@link DataSource} and {@link DataSink} instances for + * reading and writing the cache. + * + * @param cache The cache. + * @param upstream A {@link DataSource} for reading data not in the cache. + * @param flags A combination of {@link #FLAG_BLOCK_ON_CACHE}, {@link #FLAG_IGNORE_CACHE_ON_ERROR} + * and {@link #FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS}, or 0. */ public CacheDataSource(Cache cache, DataSource upstream, @Flags int flags) { this(cache, upstream, flags, DEFAULT_MAX_CACHE_FILE_SIZE); @@ -123,8 +139,8 @@ public final class CacheDataSource implements DataSource { * * @param cache The cache. * @param upstream A {@link DataSource} for reading data not in the cache. - * @param flags A combination of {@link #FLAG_BLOCK_ON_CACHE} and {@link - * #FLAG_IGNORE_CACHE_ON_ERROR} or 0. + * @param flags A combination of {@link #FLAG_BLOCK_ON_CACHE}, {@link #FLAG_IGNORE_CACHE_ON_ERROR} + * and {@link #FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS}, or 0. * @param maxCacheFileSize The maximum size of a cache file, in bytes. If the cached data size * exceeds this value, then the data will be fragmented into multiple cache files. The * finer-grained this is the finer-grained the eviction policy can be. @@ -145,8 +161,8 @@ public final class CacheDataSource implements DataSource { * @param cacheReadDataSource A {@link DataSource} for reading data from the cache. * @param cacheWriteDataSink A {@link DataSink} for writing data to the cache. If null, cache is * accessed read-only. - * @param flags A combination of {@link #FLAG_BLOCK_ON_CACHE} and {@link - * #FLAG_IGNORE_CACHE_ON_ERROR} or 0. + * @param flags A combination of {@link #FLAG_BLOCK_ON_CACHE}, {@link #FLAG_IGNORE_CACHE_ON_ERROR} + * and {@link #FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS}, or 0. * @param eventListener An optional {@link EventListener} to receive events. */ public CacheDataSource(Cache cache, DataSource upstream, DataSource cacheReadDataSource, diff --git a/library/core/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 index b6fa3b4e2c..f0285da274 100644 --- a/library/core/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 @@ -33,18 +33,26 @@ public final class CacheDataSourceFactory implements DataSource.Factory { private final int flags; private final EventListener eventListener; + /** + * @see CacheDataSource#CacheDataSource(Cache, DataSource) + */ + public CacheDataSourceFactory(Cache cache, DataSource.Factory upstreamFactory) { + this(cache, upstreamFactory, 0); + } + /** * @see CacheDataSource#CacheDataSource(Cache, DataSource, int) */ - public CacheDataSourceFactory(Cache cache, DataSource.Factory upstreamFactory, int flags) { + public CacheDataSourceFactory(Cache cache, DataSource.Factory upstreamFactory, + @CacheDataSource.Flags int flags) { this(cache, upstreamFactory, flags, CacheDataSource.DEFAULT_MAX_CACHE_FILE_SIZE); } /** * @see CacheDataSource#CacheDataSource(Cache, DataSource, int, long) */ - public CacheDataSourceFactory(Cache cache, DataSource.Factory upstreamFactory, int flags, - long maxCacheFileSize) { + public CacheDataSourceFactory(Cache cache, DataSource.Factory upstreamFactory, + @CacheDataSource.Flags int flags, long maxCacheFileSize) { this(cache, upstreamFactory, new FileDataSourceFactory(), new CacheDataSinkFactory(cache, maxCacheFileSize), flags, null); } @@ -54,8 +62,8 @@ public final class CacheDataSourceFactory implements DataSource.Factory { * EventListener) */ public CacheDataSourceFactory(Cache cache, Factory upstreamFactory, - Factory cacheReadDataSourceFactory, - DataSink.Factory cacheWriteDataSinkFactory, int flags, EventListener eventListener) { + Factory cacheReadDataSourceFactory, DataSink.Factory cacheWriteDataSinkFactory, + @CacheDataSource.Flags int flags, EventListener eventListener) { this.cache = cache; this.upstreamFactory = upstreamFactory; this.cacheReadDataSourceFactory = cacheReadDataSourceFactory; From 68b3e0b54d0c34842c7ec35174e431b2d8f0284b Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Fri, 5 May 2017 03:49:22 -0700 Subject: [PATCH 0019/2472] Expose no CC tracks if CLOSED-CAPTIONS=NONE is present Issue:#2743 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=155182859 --- .../playlist/HlsMasterPlaylistParserTest.java | 14 ++++- .../exoplayer2/source/hls/HlsChunkSource.java | 3 +- .../exoplayer2/source/hls/HlsMediaChunk.java | 11 +++- .../hls/playlist/HlsMasterPlaylist.java | 62 ++++++++++++++++--- .../hls/playlist/HlsPlaylistParser.java | 13 +++- 5 files changed, 90 insertions(+), 13 deletions(-) diff --git a/library/hls/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 index aa279f23f4..912dcb28b2 100644 --- a/library/hls/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 @@ -23,6 +23,7 @@ import com.google.android.exoplayer2.util.MimeTypes; import java.io.ByteArrayInputStream; import java.io.IOException; import java.nio.charset.Charset; +import java.util.Collections; import java.util.List; import junit.framework.TestCase; @@ -56,16 +57,22 @@ public class HlsMasterPlaylistParserTest extends TestCase { private static final String MASTER_PLAYLIST_WITH_CC = " #EXTM3U \n" + "#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,LANGUAGE=\"es\",NAME=\"Eng\",INSTREAM-ID=\"SERVICE4\"\n" - + "\n" + "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n" + "http://example.com/low.m3u8\n"; + private static final String MASTER_PLAYLIST_WITHOUT_CC = " #EXTM3U \n" + + "#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,LANGUAGE=\"es\",NAME=\"Eng\",INSTREAM-ID=\"SERVICE4\"\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128," + + "CLOSED-CAPTIONS=NONE\n" + + "http://example.com/low.m3u8\n"; + public void testParseMasterPlaylist() throws IOException{ HlsMasterPlaylist masterPlaylist = parseMasterPlaylist(PLAYLIST_URI, MASTER_PLAYLIST); List variants = masterPlaylist.variants; assertNotNull(variants); assertEquals(5, variants.size()); + assertNull(masterPlaylist.muxedCaptionFormats); assertEquals(1280000, variants.get(0).format.bitrate); assertNotNull(variants.get(0).format.codecs); @@ -117,6 +124,11 @@ public class HlsMasterPlaylistParserTest extends TestCase { assertEquals("es", closedCaptionFormat.language); } + public void testPlaylistWithoutClosedCaptions() throws IOException { + HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, MASTER_PLAYLIST_WITHOUT_CC); + assertEquals(Collections.emptyList(), playlist.muxedCaptionFormats); + } + private static HlsMasterPlaylist parseMasterPlaylist(String uri, String playlistString) throws IOException { Uri playlistUri = Uri.parse(uri); diff --git a/library/hls/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 index ea99dae345..49c4d04abc 100644 --- a/library/hls/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 @@ -111,7 +111,8 @@ import java.util.Locale; * @param timestampAdjusterProvider A provider of {@link TimestampAdjuster} instances. If * multiple {@link HlsChunkSource}s are used for a single playback, they should all share the * same provider. - * @param muxedCaptionFormats List of muxed caption {@link Format}s. + * @param muxedCaptionFormats List of muxed caption {@link Format}s. Null if no closed caption + * information is available in the master playlist. */ public HlsChunkSource(HlsPlaylistTracker playlistTracker, HlsUrl[] variants, HlsDataSourceFactory dataSourceFactory, TimestampAdjusterProvider timestampAdjusterProvider, diff --git a/library/hls/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 index 6f516923f9..6997324f02 100644 --- a/library/hls/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 @@ -39,6 +39,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.TimestampAdjuster; import com.google.android.exoplayer2.util.Util; import java.io.IOException; +import java.util.Collections; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; @@ -104,7 +105,8 @@ import java.util.concurrent.atomic.AtomicInteger; * @param dataSpec Defines the data to be loaded. * @param initDataSpec Defines the initialization data to be fed to new extractors. May be null. * @param hlsUrl The url of the playlist from which this chunk was obtained. - * @param muxedCaptionFormats List of muxed caption {@link Format}s. + * @param muxedCaptionFormats List of muxed caption {@link Format}s. Null if no closed caption + * information is available in the master playlist. * @param trackSelectionReason See {@link #trackSelectionReason}. * @param trackSelectionData See {@link #trackSelectionData}. * @param startTimeUs The start time of the chunk in microseconds. @@ -356,9 +358,12 @@ import java.util.concurrent.atomic.AtomicInteger; // This flag ensures the change of pid between streams does not affect the sample queues. @DefaultTsPayloadReaderFactory.Flags int esReaderFactoryFlags = DefaultTsPayloadReaderFactory.FLAG_IGNORE_SPLICE_INFO_STREAM; - if (!muxedCaptionFormats.isEmpty()) { + List closedCaptionFormats = muxedCaptionFormats; + if (closedCaptionFormats != null) { // The playlist declares closed caption renditions, we should ignore descriptors. esReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_OVERRIDE_CAPTION_DESCRIPTORS; + } else { + closedCaptionFormats = Collections.emptyList(); } String codecs = trackFormat.codecs; if (!TextUtils.isEmpty(codecs)) { @@ -373,7 +378,7 @@ import java.util.concurrent.atomic.AtomicInteger; } } extractor = new TsExtractor(TsExtractor.MODE_HLS, timestampAdjuster, - new DefaultTsPayloadReaderFactory(esReaderFactoryFlags, muxedCaptionFormats)); + new DefaultTsPayloadReaderFactory(esReaderFactoryFlags, closedCaptionFormats)); } if (usingNewExtractor) { extractor.init(extractorOutput); diff --git a/library/hls/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 index 5a8c63f609..874c865049 100644 --- a/library/hls/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 @@ -30,15 +30,31 @@ public final class HlsMasterPlaylist extends HlsPlaylist { */ public static final class HlsUrl { + /** + * The http url from which the media playlist can be obtained. + */ public final String url; + /** + * Format information associated with the HLS url. + */ public final Format format; - public static HlsUrl createMediaPlaylistHlsUrl(String baseUri) { + /** + * Creates an HLS url from a given http url. + * + * @param url The url. + * @return An HLS url. + */ + public static HlsUrl createMediaPlaylistHlsUrl(String url) { Format format = Format.createContainerFormat("0", MimeTypes.APPLICATION_M3U8, null, null, Format.NO_VALUE, 0, null); - return new HlsUrl(baseUri, format); + return new HlsUrl(url, format); } + /** + * @param url See {@link #url}. + * @param format See {@link #format}. + */ public HlsUrl(String url, Format format) { this.url = url; this.format = format; @@ -46,13 +62,39 @@ public final class HlsMasterPlaylist extends HlsPlaylist { } + /** + * The list of variants declared by the playlist. + */ public final List variants; + /** + * The list of demuxed audios declared by the playlist. + */ public final List audios; + /** + * The list of subtitles declared by the playlist. + */ public final List subtitles; + /** + * The format of the audio muxed in the variants. May be null if the playlist does not declare any + * muxed audio. + */ public final Format muxedAudioFormat; + /** + * The format of the closed captions declared by the playlist. May be empty if the playlist + * explicitly declares no captions are available, or null if the playlist does not declare any + * captions information. + */ public final List muxedCaptionFormats; + /** + * @param baseUri The base uri. Used to resolve relative paths. + * @param variants See {@link #variants}. + * @param audios See {@link #audios}. + * @param subtitles See {@link #subtitles}. + * @param muxedAudioFormat See {@link #muxedAudioFormat}. + * @param muxedCaptionFormats See {@link #muxedCaptionFormats}. + */ public HlsMasterPlaylist(String baseUri, List variants, List audios, List subtitles, Format muxedAudioFormat, List muxedCaptionFormats) { super(baseUri); @@ -60,14 +102,20 @@ public final class HlsMasterPlaylist extends HlsPlaylist { this.audios = Collections.unmodifiableList(audios); this.subtitles = Collections.unmodifiableList(subtitles); this.muxedAudioFormat = muxedAudioFormat; - this.muxedCaptionFormats = Collections.unmodifiableList(muxedCaptionFormats); + this.muxedCaptionFormats = muxedCaptionFormats != null + ? Collections.unmodifiableList(muxedCaptionFormats) : null; } - public static HlsMasterPlaylist createSingleVariantMasterPlaylist(String variantUri) { - List variant = Collections.singletonList(HlsUrl.createMediaPlaylistHlsUrl(variantUri)); + /** + * Creates a playlist with a single variant. + * + * @param variantUrl The url of the single variant. + * @return A master playlist with a single variant for the provided url. + */ + public static HlsMasterPlaylist createSingleVariantMasterPlaylist(String variantUrl) { + List variant = Collections.singletonList(HlsUrl.createMediaPlaylistHlsUrl(variantUrl)); List emptyList = Collections.emptyList(); - return new HlsMasterPlaylist(null, variant, emptyList, emptyList, null, - Collections.emptyList()); + return new HlsMasterPlaylist(null, variant, emptyList, emptyList, null, null); } } diff --git a/library/hls/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 index 8e01dec6fe..664306baff 100644 --- a/library/hls/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 @@ -29,6 +29,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.ArrayList; +import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.Queue; @@ -70,6 +71,8 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser audios = new ArrayList<>(); ArrayList subtitles = new ArrayList<>(); Format muxedAudioFormat = null; - ArrayList muxedCaptionFormats = new ArrayList<>(); + List muxedCaptionFormats = null; + boolean noClosedCaptions = false; String line; while (iterator.hasNext()) { @@ -210,6 +214,9 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser(); + } muxedCaptionFormats.add(Format.createTextContainerFormat(id, null, mimeType, null, Format.NO_VALUE, selectionFlags, language, accessibilityChannel)); break; @@ -221,6 +228,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser Date: Fri, 5 May 2017 07:21:48 -0700 Subject: [PATCH 0020/2472] Don't set MAX_INPUT_SIZE to unnecessarily large values If the codec isn't adaptive, there's no need to accommodate the width/height/input-size of streams that don't have the same resolution as the current stream. This is because we'll always need to instantiate a new codec anyway. Issue: #2607 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=155194458 --- .../video/MediaCodecVideoRenderer.java | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index ac4bb36035..473d61e3fa 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -408,11 +408,9 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { @Override protected boolean canReconfigureCodec(MediaCodec codec, boolean codecIsAdaptive, Format oldFormat, Format newFormat) { - return areAdaptationCompatible(oldFormat, newFormat) + return areAdaptationCompatible(codecIsAdaptive, oldFormat, newFormat) && newFormat.width <= codecMaxValues.width && newFormat.height <= codecMaxValues.height - && newFormat.maxInputSize <= codecMaxValues.inputSize - && (codecIsAdaptive - || (oldFormat.width == newFormat.width && oldFormat.height == newFormat.height)); + && newFormat.maxInputSize <= codecMaxValues.inputSize; } @Override @@ -664,7 +662,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } boolean haveUnknownDimensions = false; for (Format streamFormat : streamFormats) { - if (areAdaptationCompatible(format, streamFormat)) { + if (areAdaptationCompatible(codecInfo.adaptive, format, streamFormat)) { haveUnknownDimensions |= (streamFormat.width == Format.NO_VALUE || streamFormat.height == Format.NO_VALUE); maxWidth = Math.max(maxWidth, streamFormat.width); @@ -817,17 +815,19 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } /** - * Returns whether an adaptive codec with suitable {@link CodecMaxValues} will support adaptation - * between two {@link Format}s. + * Returns whether a codec with suitable {@link CodecMaxValues} will support adaptation between + * two {@link Format}s. * + * @param codecIsAdaptive Whether the codec supports seamless resolution switches. * @param first The first format. * @param second The second format. - * @return Whether an adaptive codec with suitable {@link CodecMaxValues} will support adaptation - * between two {@link Format}s. + * @return Whether the codec will support adaptation between the two {@link Format}s. */ - private static boolean areAdaptationCompatible(Format first, Format second) { + private static boolean areAdaptationCompatible(boolean codecIsAdaptive, Format first, + Format second) { return first.sampleMimeType.equals(second.sampleMimeType) - && getRotationDegrees(first) == getRotationDegrees(second); + && getRotationDegrees(first) == getRotationDegrees(second) + && (codecIsAdaptive || (first.width == second.width && first.height == second.height)); } private static float getPixelWidthHeightRatio(Format format) { From 65607b2736d3e51c59b66fa1a76712c98576b854 Mon Sep 17 00:00:00 2001 From: arakawa_yusuke Date: Mon, 8 May 2017 20:05:10 +0900 Subject: [PATCH 0021/2472] Remove fully-qualified name --- .../android/exoplayer2/video/MediaCodecVideoRenderer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index 473d61e3fa..dd0c5356ea 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -376,7 +376,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } @Override - protected void onOutputFormatChanged(MediaCodec codec, android.media.MediaFormat outputFormat) { + protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputFormat) { boolean hasCrop = outputFormat.containsKey(KEY_CROP_RIGHT) && outputFormat.containsKey(KEY_CROP_LEFT) && outputFormat.containsKey(KEY_CROP_BOTTOM) && outputFormat.containsKey(KEY_CROP_TOP); From 2d2fcf1510be02a2315d41dd1a49e01aa0f71db4 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Sun, 7 May 2017 08:18:25 -0700 Subject: [PATCH 0022/2472] Use native byte order for SimpleOutputBuffers The default byte order for ByteBuffers is big endian, but platform decoder output buffers use native byte order. AudioProcessors handle native byte order input/output. When using a software audio decoding extension the Sonic audio processor would receive big endian input but was outputting to a native byte order buffer, which could be little endian. This mismatch caused audio output to be distorted. After this change both platform decoder and extension decoder output buffers should be in native byte order. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=155320973 --- .../google/android/exoplayer2/decoder/SimpleOutputBuffer.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/decoder/SimpleOutputBuffer.java b/library/core/src/main/java/com/google/android/exoplayer2/decoder/SimpleOutputBuffer.java index 309c7fd144..49c7dafbd6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/decoder/SimpleOutputBuffer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/decoder/SimpleOutputBuffer.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.decoder; import java.nio.ByteBuffer; +import java.nio.ByteOrder; /** * Buffer for {@link SimpleDecoder} output. @@ -40,7 +41,7 @@ public class SimpleOutputBuffer extends OutputBuffer { public ByteBuffer init(long timeUs, int size) { this.timeUs = timeUs; if (data == null || data.capacity() < size) { - data = ByteBuffer.allocateDirect(size); + data = ByteBuffer.allocateDirect(size).order(ByteOrder.nativeOrder()); } data.position(0); data.limit(size); From 963b7cbf72104e1fe1d61d8b218f620c4a39f5c1 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 8 May 2017 00:25:59 -0700 Subject: [PATCH 0023/2472] Fix interpolation for rate/pitch adjustment Based on https://github.com/waywardgeek/sonic/commit/7b441933. Issue: #2774 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=155349817 --- .../java/com/google/android/exoplayer2/audio/Sonic.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 index 5d6f01b6e0..ef7877ae1e 100644 --- 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 @@ -374,8 +374,8 @@ import java.util.Arrays; } private short interpolate(short[] in, int inPos, int oldSampleRate, int newSampleRate) { - short left = in[inPos * numChannels]; - short right = in[inPos * numChannels + numChannels]; + short left = in[inPos]; + short right = in[inPos + numChannels]; int position = newRatePosition * oldSampleRate; int leftPosition = oldRatePosition * newSampleRate; int rightPosition = (oldRatePosition + 1) * newSampleRate; @@ -402,7 +402,7 @@ import java.util.Arrays; enlargeOutputBufferIfNeeded(1); for (int i = 0; i < numChannels; i++) { outputBuffer[numOutputSamples * numChannels + i] = - interpolate(pitchBuffer, position + i, oldSampleRate, newSampleRate); + interpolate(pitchBuffer, position * numChannels + i, oldSampleRate, newSampleRate); } newRatePosition++; numOutputSamples++; From 1d6816d94b3eb3238abfaa38c2edda1a1e4d9e0f Mon Sep 17 00:00:00 2001 From: olly Date: Sat, 7 Jan 2017 11:59:20 +0000 Subject: [PATCH 0024/2472] Avoid process death if OOM occurs on a loading thread This is most commonly caused by malformed media, where the media indicates that something we need to make an allocation for is *really huge*. Failing playback is appropriate for this case; killing the process is not. Issue: #2780 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=155408062 --- .../google/android/exoplayer2/upstream/Loader.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/Loader.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/Loader.java index bca90ddc5c..1bdebf7c17 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/Loader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/Loader.java @@ -33,11 +33,11 @@ import java.util.concurrent.ExecutorService; public final class Loader implements LoaderErrorThrower { /** - * Thrown when an unexpected exception is encountered during loading. + * Thrown when an unexpected exception or error is encountered during loading. */ public static final class UnexpectedLoaderException extends IOException { - public UnexpectedLoaderException(Exception cause) { + public UnexpectedLoaderException(Throwable cause) { super("Unexpected " + cause.getClass().getSimpleName() + ": " + cause.getMessage(), cause); } @@ -316,6 +316,14 @@ public final class Loader implements LoaderErrorThrower { if (!released) { obtainMessage(MSG_IO_EXCEPTION, new UnexpectedLoaderException(e)).sendToTarget(); } + } catch (OutOfMemoryError e) { + // This can occur if a stream is malformed in a way that causes an extractor to think it + // needs to allocate a large amount of memory. We don't want the process to die in this + // case, but we do want the playback to fail. + Log.e(TAG, "OutOfMemory error loading stream", e); + if (!released) { + obtainMessage(MSG_IO_EXCEPTION, new UnexpectedLoaderException(e)).sendToTarget(); + } } catch (Error e) { // We'd hope that the platform would kill the process if an Error is thrown here, but the // executor may catch the error (b/20616433). Throw it here, but also pass and throw it from From 51132f58f9e08cd247a9d2f7149c860037cdb590 Mon Sep 17 00:00:00 2001 From: tasnimsunny Date: Mon, 8 May 2017 12:46:09 -0700 Subject: [PATCH 0025/2472] Make removal of non-existent cache span a no-op ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=155413733 --- .../google/android/exoplayer2/upstream/cache/SimpleCache.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/library/core/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 index 14f006c850..bbff7dc4a2 100644 --- a/library/core/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 @@ -286,7 +286,9 @@ public final class SimpleCache implements Cache { private void removeSpan(CacheSpan span, boolean removeEmptyCachedContent) throws CacheException { CachedContent cachedContent = index.get(span.key); - Assertions.checkState(cachedContent.removeSpan(span)); + if (cachedContent == null || !cachedContent.removeSpan(span)) { + return; + } totalSpace -= span.length; if (removeEmptyCachedContent && cachedContent.isEmpty()) { index.removeEmpty(cachedContent.key); From 70b628526a742a433fdf5d1fccbb9e1995e4297d Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 9 May 2017 03:14:58 -0700 Subject: [PATCH 0026/2472] Propagate playlist loading error if it prevents playback This imitates DashMediaSource's behavior. Issue:#2623 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=155485738 --- .../exoplayer2/source/hls/HlsChunkSource.java | 7 +++++++ .../exoplayer2/source/hls/HlsMediaSource.java | 2 +- .../hls/playlist/HlsPlaylistTracker.java | 19 +++++++++++++++---- 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/library/hls/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 index 49c4d04abc..795e2f0eaa 100644 --- a/library/hls/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 @@ -92,6 +92,7 @@ import java.util.Locale; private boolean isTimestampMaster; private byte[] scratchSpace; private IOException fatalError; + private HlsUrl expectedPlaylistUrl; private Uri encryptionKeyUri; private byte[] encryptionKey; @@ -143,6 +144,9 @@ import java.util.Locale; if (fatalError != null) { throw fatalError; } + if (expectedPlaylistUrl != null) { + playlistTracker.maybeThrowPlaylistRefreshError(expectedPlaylistUrl); + } } /** @@ -195,6 +199,7 @@ import java.util.Locale; public void getNextChunk(HlsMediaChunk previous, long playbackPositionUs, HlsChunkHolder out) { int oldVariantIndex = previous == null ? C.INDEX_UNSET : trackGroup.indexOf(previous.trackFormat); + expectedPlaylistUrl = null; // Use start time of the previous chunk rather than its end time because switching format will // require downloading overlapping segments. long bufferedDurationUs = previous == null ? 0 @@ -208,6 +213,7 @@ import java.util.Locale; HlsUrl selectedUrl = variants[selectedVariantIndex]; if (!playlistTracker.isSnapshotValid(selectedUrl)) { out.playlist = selectedUrl; + expectedPlaylistUrl = selectedUrl; // Retry when playlist is refreshed. return; } @@ -247,6 +253,7 @@ import java.util.Locale; out.endOfStream = true; } else /* Live */ { out.playlist = selectedUrl; + expectedPlaylistUrl = selectedUrl; } return; } diff --git a/library/hls/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 index 3cd9f19522..1bfb8371a0 100644 --- a/library/hls/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 @@ -84,7 +84,7 @@ public final class HlsMediaSource implements MediaSource, @Override public void maybeThrowSourceInfoRefreshError() throws IOException { - playlistTracker.maybeThrowPlaylistRefreshError(); + playlistTracker.maybeThrowPrimaryPlaylistRefreshError(); } @Override diff --git a/library/hls/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 index 02a8e3f098..62b77a0575 100644 --- a/library/hls/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 @@ -200,18 +200,29 @@ public final class HlsPlaylistTracker implements Loader.Callback Date: Tue, 9 May 2017 03:20:40 -0700 Subject: [PATCH 0027/2472] Make MODE_SINGLE_PMT the default mode Even though this is not strictly spec compliant, this will make exoplayer behave like it used to before multiple program support. Developers who want to take advantage of the multiple program support are probably less than the ones who only want their stream to "just work". This is particularly useful for streams obtained after a filtering component, like a tv tuner. Issue:#2757 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=155486122 --- .../extractor/ts/TsExtractorTest.java | 4 ++-- .../extractor/DefaultExtractorsFactory.java | 21 ++++++++++++++++++- .../exoplayer2/extractor/ts/TsExtractor.java | 21 ++++++++++++++----- 3 files changed, 38 insertions(+), 8 deletions(-) diff --git a/library/core/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 index 7bf722cd8f..efd653b8d9 100644 --- a/library/core/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 @@ -75,7 +75,7 @@ public final class TsExtractorTest extends InstrumentationTestCase { public void testCustomPesReader() throws Exception { CustomTsPayloadReaderFactory factory = new CustomTsPayloadReaderFactory(true, false); - TsExtractor tsExtractor = new TsExtractor(TsExtractor.MODE_NORMAL, new TimestampAdjuster(0), + TsExtractor tsExtractor = new TsExtractor(TsExtractor.MODE_MULTI_PMT, new TimestampAdjuster(0), factory); FakeExtractorInput input = new FakeExtractorInput.Builder() .setData(TestUtil.getByteArray(getInstrumentation(), "ts/sample.ts")) @@ -100,7 +100,7 @@ public final class TsExtractorTest extends InstrumentationTestCase { public void testCustomInitialSectionReader() throws Exception { CustomTsPayloadReaderFactory factory = new CustomTsPayloadReaderFactory(false, true); - TsExtractor tsExtractor = new TsExtractor(TsExtractor.MODE_NORMAL, new TimestampAdjuster(0), + TsExtractor tsExtractor = new TsExtractor(TsExtractor.MODE_MULTI_PMT, new TimestampAdjuster(0), factory); FakeExtractorInput input = new FakeExtractorInput.Builder() .setData(TestUtil.getByteArray(getInstrumentation(), "ts/sample_with_sdt.ts")) 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 index 022ca1277d..c47a91b176 100644 --- 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 @@ -26,7 +26,9 @@ 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.ts.TsPayloadReader; import com.google.android.exoplayer2.extractor.wav.WavExtractor; +import com.google.android.exoplayer2.util.TimestampAdjuster; import java.lang.reflect.Constructor; /** @@ -67,8 +69,13 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory { private @MatroskaExtractor.Flags int matroskaFlags; private @FragmentedMp4Extractor.Flags int fragmentedMp4Flags; private @Mp3Extractor.Flags int mp3Flags; + private @TsExtractor.Mode int tsMode; private @DefaultTsPayloadReaderFactory.Flags int tsFlags; + public DefaultExtractorsFactory() { + tsMode = TsExtractor.MODE_SINGLE_PMT; + } + /** * Sets flags for {@link MatroskaExtractor} instances created by the factory. * @@ -107,6 +114,18 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory { return this; } + /** + * Sets the mode for {@link TsExtractor} instances created by the factory. + * + * @see TsExtractor#TsExtractor(int, TimestampAdjuster, TsPayloadReader.Factory). + * @param mode The mode to use. + * @return The factory, for convenience. + */ + public synchronized DefaultExtractorsFactory setTsExtractorMode(@TsExtractor.Mode int mode) { + tsMode = mode; + return this; + } + /** * Sets flags for {@link DefaultTsPayloadReaderFactory}s used by {@link TsExtractor} instances * created by the factory. @@ -130,7 +149,7 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory { extractors[3] = new Mp3Extractor(mp3Flags); extractors[4] = new AdtsExtractor(); extractors[5] = new Ac3Extractor(); - extractors[6] = new TsExtractor(tsFlags); + extractors[6] = new TsExtractor(tsMode, tsFlags); extractors[7] = new FlvExtractor(); extractors[8] = new OggExtractor(); extractors[9] = new PsExtractor(); diff --git a/library/core/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 index df6efb722c..71b8375bd8 100644 --- a/library/core/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 @@ -65,13 +65,13 @@ public final class TsExtractor implements Extractor { * Modes for the extractor. */ @Retention(RetentionPolicy.SOURCE) - @IntDef({MODE_NORMAL, MODE_SINGLE_PMT, MODE_HLS}) + @IntDef({MODE_MULTI_PMT, MODE_SINGLE_PMT, MODE_HLS}) public @interface Mode {} /** * Behave as defined in ISO/IEC 13818-1. */ - public static final int MODE_NORMAL = 0; + public static final int MODE_MULTI_PMT = 0; /** * Assume only one PMT will be contained in the stream, even if more are declared by the PAT. */ @@ -132,12 +132,23 @@ public final class TsExtractor implements Extractor { * {@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)); + this(MODE_SINGLE_PMT, defaultTsPayloadReaderFlags); } /** - * @param mode Mode for the extractor. One of {@link #MODE_NORMAL}, {@link #MODE_SINGLE_PMT} + * @param mode Mode for the extractor. One of {@link #MODE_MULTI_PMT}, {@link #MODE_SINGLE_PMT} + * and {@link #MODE_HLS}. + * @param defaultTsPayloadReaderFlags A combination of {@link DefaultTsPayloadReaderFactory} + * {@code FLAG_*} values that control the behavior of the payload readers. + */ + public TsExtractor(@Mode int mode, @Flags int defaultTsPayloadReaderFlags) { + this(mode, new TimestampAdjuster(0), + new DefaultTsPayloadReaderFactory(defaultTsPayloadReaderFlags)); + } + + + /** + * @param mode Mode for the extractor. One of {@link #MODE_MULTI_PMT}, {@link #MODE_SINGLE_PMT} * and {@link #MODE_HLS}. * @param timestampAdjuster A timestamp adjuster for offsetting and scaling sample timestamps. * @param payloadReaderFactory Factory for injecting a custom set of payload readers. From 7eadfb1ff019f61f53cfb7d323cd77ba96c20cd6 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 10 May 2017 19:36:06 -0700 Subject: [PATCH 0028/2472] Fix wrap_content handling in DefaultTimeBar Issue: #2788 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=155705318 --- .../google/android/exoplayer2/ui/DefaultTimeBar.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) 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 index 12f31f5da1..06ecb1aa69 100644 --- 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 @@ -343,16 +343,18 @@ public class DefaultTimeBar extends View implements TimeBar { @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - int measureWidth = MeasureSpec.getSize(widthMeasureSpec); - int measureHeight = MeasureSpec.getSize(heightMeasureSpec); - setMeasuredDimension(measureWidth, measureHeight); + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + int heightSize = MeasureSpec.getSize(heightMeasureSpec); + int height = heightMode == MeasureSpec.UNSPECIFIED ? touchTargetHeight + : heightMode == MeasureSpec.EXACTLY ? heightSize : Math.min(touchTargetHeight, heightSize); + setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), height); } @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 barY = (height - touchTargetHeight) / 2; int seekLeft = getPaddingLeft(); int seekRight = width - getPaddingRight(); int progressY = barY + (touchTargetHeight - barHeight) / 2; From 9a2cac7f741101c58900da7c0070d66c4a578740 Mon Sep 17 00:00:00 2001 From: arakawa_yusuke Date: Thu, 11 May 2017 21:11:21 +0900 Subject: [PATCH 0029/2472] Refactor: Change the position of variable. --- .../java/com/google/android/exoplayer2/demo/PlayerActivity.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 d01a64a051..34e0365933 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 @@ -234,7 +234,6 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay Intent intent = getIntent(); boolean needNewPlayer = player == null; if (needNewPlayer) { - boolean preferExtensionDecoders = intent.getBooleanExtra(PREFER_EXTENSION_DECODERS, false); UUID drmSchemeUuid = intent.hasExtra(DRM_SCHEME_UUID_EXTRA) ? UUID.fromString(intent.getStringExtra(DRM_SCHEME_UUID_EXTRA)) : null; DrmSessionManager drmSessionManager = null; @@ -253,6 +252,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay } } + boolean preferExtensionDecoders = intent.getBooleanExtra(PREFER_EXTENSION_DECODERS, false); @DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode = ((DemoApplication) getApplication()).useExtensionRenderers() ? (preferExtensionDecoders ? DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER From 1cda6e025e38608263572fb5a65b125c58b4334f Mon Sep 17 00:00:00 2001 From: sillywab8 Date: Fri, 12 May 2017 08:24:40 -0500 Subject: [PATCH 0030/2472] Add support for AVC Level 5.2 --- .../com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java | 1 + 1 file changed, 1 insertion(+) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java index a09f6e26dd..2bb3603df9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java @@ -429,6 +429,7 @@ public final class MediaCodecUtil { case CodecProfileLevel.AVCLevel42: return 8704 * 16 * 16; case CodecProfileLevel.AVCLevel5: return 22080 * 16 * 16; case CodecProfileLevel.AVCLevel51: return 36864 * 16 * 16; + case CodecProfileLevel.AVCLevel52: return 36864 * 16 * 16; default: return -1; } } From e7ec4e6755e83162d1e2b3b44b6dce863348d01c Mon Sep 17 00:00:00 2001 From: bachinger Date: Sun, 14 May 2017 22:26:39 -0700 Subject: [PATCH 0031/2472] Enlarge size of data array of parsable packetArray if ogg packet size exceeds the current size. https://github.com/google/ExoPlayer/issues/2782 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=156018137 --- .../assets/ogg/bear_vorbis.ogg.0.dump | 2 +- .../assets/ogg/bear_vorbis.ogg.1.dump | 2 +- .../assets/ogg/bear_vorbis.ogg.2.dump | 2 +- .../assets/ogg/bear_vorbis.ogg.3.dump | 2 +- .../assets/ogg/bear_vorbis.ogg.unklen.dump | 2 +- .../exoplayer2/extractor/ogg/OggPacket.java | 19 +++++++++++++++++-- .../extractor/ogg/StreamReader.java | 5 ++--- .../extractor/ogg/VorbisReader.java | 2 +- 8 files changed, 25 insertions(+), 11 deletions(-) diff --git a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.0.dump b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.0.dump index 536f76adad..8e2c5125a3 100644 --- a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.0.dump +++ b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.0.dump @@ -9,7 +9,7 @@ track 0: id = null containerMimeType = null sampleMimeType = audio/vorbis - maxInputSize = 65025 + maxInputSize = -1 width = -1 height = -1 frameRate = -1.0 diff --git a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.1.dump b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.1.dump index 7490773bd5..aa25303ac3 100644 --- a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.1.dump +++ b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.1.dump @@ -9,7 +9,7 @@ track 0: id = null containerMimeType = null sampleMimeType = audio/vorbis - maxInputSize = 65025 + maxInputSize = -1 width = -1 height = -1 frameRate = -1.0 diff --git a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.2.dump b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.2.dump index 82ad16e701..58969058fa 100644 --- a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.2.dump +++ b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.2.dump @@ -9,7 +9,7 @@ track 0: id = null containerMimeType = null sampleMimeType = audio/vorbis - maxInputSize = 65025 + maxInputSize = -1 width = -1 height = -1 frameRate = -1.0 diff --git a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.3.dump b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.3.dump index 810b66901c..4c789a8431 100644 --- a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.3.dump +++ b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.3.dump @@ -9,7 +9,7 @@ track 0: id = null containerMimeType = null sampleMimeType = audio/vorbis - maxInputSize = 65025 + maxInputSize = -1 width = -1 height = -1 frameRate = -1.0 diff --git a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.unklen.dump b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.unklen.dump index 8e86ca340d..2f163572bf 100644 --- a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.unklen.dump +++ b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.unklen.dump @@ -9,7 +9,7 @@ track 0: id = null containerMimeType = null sampleMimeType = audio/vorbis - maxInputSize = 65025 + maxInputSize = -1 width = -1 height = -1 frameRate = -1.0 diff --git a/library/core/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 index 892f0a68af..c7f4e9489b 100644 --- a/library/core/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 @@ -20,6 +20,7 @@ import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.ParsableByteArray; import java.io.IOException; +import java.util.Arrays; /** * OGG packet class. @@ -27,8 +28,8 @@ import java.io.IOException; /* package */ final class OggPacket { private final OggPageHeader pageHeader = new OggPageHeader(); - private final ParsableByteArray packetArray = - new ParsableByteArray(new byte[OggPageHeader.MAX_PAGE_PAYLOAD], 0); + private final ParsableByteArray packetArray = new ParsableByteArray( + new byte[OggPageHeader.MAX_PAGE_PAYLOAD], 0); private int currentSegmentIndex = C.INDEX_UNSET; private int segmentCount; @@ -85,6 +86,9 @@ import java.io.IOException; int size = calculatePacketSize(currentSegmentIndex); int segmentIndex = currentSegmentIndex + segmentCount; if (size > 0) { + if (packetArray.capacity() < packetArray.limit() + size) { + packetArray.data = Arrays.copyOf(packetArray.data, packetArray.limit() + size); + } input.readFully(packetArray.data, packetArray.limit(), size); packetArray.setLimit(packetArray.limit() + size); populated = pageHeader.laces[segmentIndex - 1] != 255; @@ -118,6 +122,17 @@ import java.io.IOException; return packetArray; } + /** + * Trims the packet data array. + */ + public void trimPayload() { + if (packetArray.data.length == OggPageHeader.MAX_PAGE_PAYLOAD) { + return; + } + packetArray.data = Arrays.copyOf(packetArray.data, Math.max(OggPageHeader.MAX_PAGE_PAYLOAD, + packetArray.limit())); + } + /** * Calculates the size of the packet starting from {@code startSegmentIndex}. * diff --git a/library/core/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 index 6424155bd9..c203b0c6bd 100644 --- a/library/core/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 @@ -103,15 +103,12 @@ import java.io.IOException; switch (state) { case STATE_READ_HEADERS: return readHeaders(input); - case STATE_SKIP_HEADERS: input.skipFully((int) payloadStartPosition); state = STATE_READ_PAYLOAD; return Extractor.RESULT_CONTINUE; - case STATE_READ_PAYLOAD: return readPayload(input, seekPosition); - default: // Never happens. throw new IllegalStateException(); @@ -152,6 +149,8 @@ import java.io.IOException; setupData = null; state = STATE_READ_PAYLOAD; + // First payload packet. Trim the payload array of the ogg packet after headers have been read. + oggPacket.trimPayload(); return Extractor.RESULT_CONTINUE; } diff --git a/library/core/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 index ae0a69ef7d..31ac6858be 100644 --- a/library/core/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 @@ -101,7 +101,7 @@ import java.util.ArrayList; codecInitialisationData.add(vorbisSetup.setupHeaderData); setupData.format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_VORBIS, null, - this.vorbisSetup.idHeader.bitrateNominal, OggPageHeader.MAX_PAGE_PAYLOAD, + this.vorbisSetup.idHeader.bitrateNominal, Format.NO_VALUE, this.vorbisSetup.idHeader.channels, (int) this.vorbisSetup.idHeader.sampleRate, codecInitialisationData, null, 0, null); return true; From ecc4184e6c6ac8242fc796962f7eba936990077d Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 15 May 2017 11:14:04 -0700 Subject: [PATCH 0032/2472] Enable neon for libvpx on arm64-v8a ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=156077889 --- extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 15dbabdb1f..5f058d0551 100755 --- a/extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh +++ b/extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh @@ -51,8 +51,7 @@ config[3]+=" --disable-sse3 --disable-ssse3 --disable-sse4_1 --disable-avx" config[3]+=" --disable-avx2 --enable-pic" arch[4]="arm64-v8a" -config[4]="--force-target=armv8-android-gcc --sdk-path=$ndk --disable-neon" -config[4]+=" --disable-neon-asm" +config[4]="--force-target=armv8-android-gcc --sdk-path=$ndk --enable-neon" arch[5]="x86_64" config[5]="--force-target=x86_64-android-gcc --sdk-path=$ndk --disable-sse2" From a0f4bf0ee18a5f883530202a2aad0f53604b99cb Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 15 May 2017 15:36:20 -0700 Subject: [PATCH 0033/2472] Constrain DefaultTimeBar maximum positions ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=156113616 --- .../google/android/exoplayer2/ui/DefaultTimeBar.java | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) 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 index 06ecb1aa69..fd05fdd5d0 100644 --- 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 @@ -465,12 +465,10 @@ public class DefaultTimeBar extends View implements TimeBar { 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; + int bufferedPixelWidth = (int) ((progressBar.width() * bufferedPosition) / duration); + bufferedBar.right = Math.min(progressBar.left + bufferedPixelWidth, progressBar.right); + int scrubberPixelPosition = (int) ((progressBar.width() * newScrubberTime) / duration); + scrubberBar.right = Math.min(progressBar.left + scrubberPixelPosition, progressBar.right); } else { bufferedBar.right = progressBar.left; scrubberBar.right = progressBar.left; From a4a31546047a6bea63e41775fb8f4c8ff88cf9be Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 15 May 2017 18:06:20 -0700 Subject: [PATCH 0034/2472] Clear up BaseRenderer.disable - Call onDisabled last. onDisabled really shouldn't be doing anything with the stream, so pretty sure this is fine (and guarantees the stream is cleared properly even if onDisabled throws a RTE). - Remove super.onDisabled calls from Text/Metadata renderers. This is just for consistency; we don't make such calls in other direct descendants of BaseRenderer. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=156130640 --- .../main/java/com/google/android/exoplayer2/BaseRenderer.java | 4 +--- .../google/android/exoplayer2/metadata/MetadataRenderer.java | 1 - .../java/com/google/android/exoplayer2/text/TextRenderer.java | 1 - 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java index 44fb6d68ae..396584a39e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java @@ -142,9 +142,9 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { public final void disable() { Assertions.checkState(state == STATE_ENABLED); state = STATE_DISABLED; - onDisabled(); stream = null; streamIsFinal = false; + onDisabled(); } // RendererCapabilities implementation. @@ -300,8 +300,6 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { /** * Returns whether the upstream source is ready. - * - * @return Whether the source is ready. */ protected final boolean isSourceReady() { return readEndOfStream ? streamIsFinal : stream.isReady(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java index 814238970b..70b2d8aab9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java @@ -153,7 +153,6 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { protected void onDisabled() { flushPendingMetadata(); decoder = null; - super.onDisabled(); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java index 2f07fe5294..4950549b19 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java @@ -254,7 +254,6 @@ public final class TextRenderer extends BaseRenderer implements Callback { streamFormat = null; clearOutput(); releaseDecoder(); - super.onDisabled(); } @Override From fb3c19055691ed835c9594d9a92bdf76117b835f Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 15 May 2017 18:11:45 -0700 Subject: [PATCH 0035/2472] Clear the correct buffer in MediaCodecRenderer ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=156131086 --- .../android/exoplayer2/mediacodec/MediaCodecRenderer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java index 8fb9bc9271..25a5aa4dd3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java @@ -488,7 +488,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } if (format == null) { // We don't have a format yet, so try and read one. - buffer.clear(); + flagsOnlyBuffer.clear(); int result = readSource(formatHolder, flagsOnlyBuffer, true); if (result == C.RESULT_FORMAT_READ) { onInputFormatChanged(formatHolder.format); From bf71ad48838169326b7f5f95c6a4a3f5723c8870 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 22 May 2017 13:52:51 -0700 Subject: [PATCH 0036/2472] Fix TTML positioning Issue: #2824 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=156781252 --- .../exoplayer2/text/ttml/TtmlDecoderTest.java | 34 +++--- .../exoplayer2/text/ttml/TtmlDecoder.java | 100 +++++++++++++----- .../exoplayer2/text/ttml/TtmlNode.java | 7 +- .../exoplayer2/text/ttml/TtmlRegion.java | 14 ++- 4 files changed, 101 insertions(+), 54 deletions(-) diff --git a/library/core/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 index 381aaa34ae..496e3f87de 100644 --- a/library/core/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 @@ -157,39 +157,39 @@ public final class TtmlDecoderTest extends InstrumentationTestCase { assertEquals(2, output.size()); Cue ttmlCue = output.get(0); assertEquals("lorem", ttmlCue.text.toString()); - assertEquals(10.f / 100.f, ttmlCue.position); - assertEquals(10.f / 100.f, ttmlCue.line); - assertEquals(20.f / 100.f, ttmlCue.size); + assertEquals(10f / 100f, ttmlCue.position); + assertEquals(10f / 100f, ttmlCue.line); + assertEquals(20f / 100f, ttmlCue.size); ttmlCue = output.get(1); assertEquals("amet", ttmlCue.text.toString()); - assertEquals(60.f / 100.f, ttmlCue.position); - assertEquals(10.f / 100.f, ttmlCue.line); - assertEquals(20.f / 100.f, ttmlCue.size); + assertEquals(60f / 100f, ttmlCue.position); + assertEquals(10f / 100f, ttmlCue.line); + assertEquals(20f / 100f, ttmlCue.size); output = subtitle.getCues(5000000); assertEquals(1, output.size()); ttmlCue = output.get(0); assertEquals("ipsum", ttmlCue.text.toString()); - assertEquals(40.f / 100.f, ttmlCue.position); - assertEquals(40.f / 100.f, ttmlCue.line); - assertEquals(20.f / 100.f, ttmlCue.size); + assertEquals(40f / 100f, ttmlCue.position); + assertEquals(40f / 100f, ttmlCue.line); + assertEquals(20f / 100f, ttmlCue.size); output = subtitle.getCues(9000000); assertEquals(1, output.size()); ttmlCue = output.get(0); assertEquals("dolor", ttmlCue.text.toString()); - assertEquals(10.f / 100.f, ttmlCue.position); - assertEquals(80.f / 100.f, ttmlCue.line); - assertEquals(Cue.DIMEN_UNSET, ttmlCue.size); + assertEquals(10f / 100f, ttmlCue.position); + assertEquals(80f / 100f, ttmlCue.line); + assertEquals(1f, ttmlCue.size); output = subtitle.getCues(21000000); assertEquals(1, output.size()); ttmlCue = output.get(0); assertEquals("She first said this", ttmlCue.text.toString()); - assertEquals(45.f / 100.f, ttmlCue.position); - assertEquals(45.f / 100.f, ttmlCue.line); - assertEquals(35.f / 100.f, ttmlCue.size); + assertEquals(45f / 100f, ttmlCue.position); + assertEquals(45f / 100f, ttmlCue.line); + assertEquals(35f / 100f, ttmlCue.size); output = subtitle.getCues(25000000); ttmlCue = output.get(0); assertEquals("She first said this\nThen this", ttmlCue.text.toString()); @@ -197,8 +197,8 @@ public final class TtmlDecoderTest extends InstrumentationTestCase { assertEquals(1, output.size()); ttmlCue = output.get(0); assertEquals("She first said this\nThen this\nFinally this", ttmlCue.text.toString()); - assertEquals(45.f / 100.f, ttmlCue.position); - assertEquals(45.f / 100.f, ttmlCue.line); + assertEquals(45f / 100f, ttmlCue.position); + assertEquals(45f / 100f, ttmlCue.line); } public void testEmptyStyleAttribute() throws IOException, SubtitleDecoderException { diff --git a/library/core/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 index 71ce17eeed..0012ce2c22 100644 --- a/library/core/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 @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.text.ttml; import android.text.Layout; import android.util.Log; -import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.SimpleSubtitleDecoder; @@ -100,7 +99,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { XmlPullParser xmlParser = xmlParserFactory.newPullParser(); Map globalStyles = new HashMap<>(); Map regionMap = new HashMap<>(); - regionMap.put(TtmlNode.ANONYMOUS_REGION_ID, new TtmlRegion()); + regionMap.put(TtmlNode.ANONYMOUS_REGION_ID, new TtmlRegion(null)); ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes, 0, length); xmlParser.setInput(inputStream, null); TtmlSubtitle ttmlSubtitle = null; @@ -211,9 +210,9 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { globalStyles.put(style.getId(), style); } } else if (XmlPullParserUtil.isStartTag(xmlParser, TtmlNode.TAG_REGION)) { - Pair ttmlRegionInfo = parseRegionAttributes(xmlParser); - if (ttmlRegionInfo != null) { - globalRegions.put(ttmlRegionInfo.first, ttmlRegionInfo.second); + TtmlRegion ttmlRegion = parseRegionAttributes(xmlParser); + if (ttmlRegion != null) { + globalRegions.put(ttmlRegion.id, ttmlRegion); } } } while (!XmlPullParserUtil.isEndTag(xmlParser, TtmlNode.TAG_HEAD)); @@ -221,41 +220,84 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { } /** - * Parses a region declaration. Supports origin and extent definition but only when defined in - * terms of percentage of the viewport. Regions that do not correctly declare origin are ignored. + * Parses a region declaration. + *

    + * If the region defines an origin and/or extent, it is required that they're defined as + * percentages of the viewport. Region declarations that define origin and/or extent in other + * formats are unsupported, and null is returned. */ - private Pair parseRegionAttributes(XmlPullParser xmlParser) { + private TtmlRegion parseRegionAttributes(XmlPullParser xmlParser) { String regionId = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_ID); - String regionOrigin = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_TTS_ORIGIN); - String regionExtent = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_TTS_EXTENT); - if (regionOrigin == null || regionId == null) { + if (regionId == null) { return null; } - float position = Cue.DIMEN_UNSET; - float line = Cue.DIMEN_UNSET; - Matcher originMatcher = PERCENTAGE_COORDINATES.matcher(regionOrigin); - if (originMatcher.matches()) { - try { - position = Float.parseFloat(originMatcher.group(1)) / 100.f; - line = Float.parseFloat(originMatcher.group(2)) / 100.f; - } catch (NumberFormatException e) { - Log.w(TAG, "Ignoring region with malformed origin: '" + regionOrigin + "'", e); - position = Cue.DIMEN_UNSET; + + float position; + float line; + String regionOrigin = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_TTS_ORIGIN); + if (regionOrigin != null) { + Matcher originMatcher = PERCENTAGE_COORDINATES.matcher(regionOrigin); + if (originMatcher.matches()) { + try { + position = Float.parseFloat(originMatcher.group(1)) / 100f; + line = Float.parseFloat(originMatcher.group(2)) / 100f; + } catch (NumberFormatException e) { + Log.w(TAG, "Ignoring region with malformed origin: " + regionOrigin); + return null; + } + } else { + Log.w(TAG, "Ignoring region with unsupported origin: " + regionOrigin); + return null; } + } else { + // Origin is omitted. Default to top left. + position = 0; + line = 0; } - float width = Cue.DIMEN_UNSET; + + float width; + float height; + String regionExtent = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_TTS_EXTENT); if (regionExtent != null) { Matcher extentMatcher = PERCENTAGE_COORDINATES.matcher(regionExtent); if (extentMatcher.matches()) { try { - width = Float.parseFloat(extentMatcher.group(1)) / 100.f; + width = Float.parseFloat(extentMatcher.group(1)) / 100f; + height = Float.parseFloat(extentMatcher.group(2)) / 100f; } catch (NumberFormatException e) { - Log.w(TAG, "Ignoring malformed region extent: '" + regionExtent + "'", e); + Log.w(TAG, "Ignoring region with malformed extent: " + regionOrigin); + return null; } + } else { + Log.w(TAG, "Ignoring region with unsupported extent: " + regionOrigin); + return null; + } + } else { + // Extent is omitted. Default to extent of parent. + width = 1; + height = 1; + } + + @Cue.AnchorType int lineAnchor = Cue.ANCHOR_TYPE_START; + String displayAlign = XmlPullParserUtil.getAttributeValue(xmlParser, + TtmlNode.ATTR_TTS_DISPLAY_ALIGN); + if (displayAlign != null) { + switch (displayAlign.toLowerCase()) { + case "center": + lineAnchor = Cue.ANCHOR_TYPE_MIDDLE; + line += height / 2; + break; + case "after": + lineAnchor = Cue.ANCHOR_TYPE_END; + line += height; + break; + default: + // Default "before" case. Do nothing. + break; } } - return position != Cue.DIMEN_UNSET ? new Pair<>(regionId, new TtmlRegion(position, line, - Cue.LINE_TYPE_FRACTION, width)) : null; + + return new TtmlRegion(regionId, position, line, Cue.LINE_TYPE_FRACTION, lineAnchor, width); } private String[] parseStyleIds(String parentStyleIds) { @@ -277,7 +319,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { try { style.setBackgroundColor(ColorParser.parseTtmlColor(attributeValue)); } catch (IllegalArgumentException e) { - Log.w(TAG, "failed parsing background value: '" + attributeValue + "'"); + Log.w(TAG, "Failed parsing background value: " + attributeValue); } break; case TtmlNode.ATTR_TTS_COLOR: @@ -285,7 +327,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { try { style.setFontColor(ColorParser.parseTtmlColor(attributeValue)); } catch (IllegalArgumentException e) { - Log.w(TAG, "failed parsing color value: '" + attributeValue + "'"); + Log.w(TAG, "Failed parsing color value: " + attributeValue); } break; case TtmlNode.ATTR_TTS_FONT_FAMILY: @@ -296,7 +338,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { style = createIfNull(style); parseFontSize(attributeValue, style); } catch (SubtitleDecoderException e) { - Log.w(TAG, "failed parsing fontSize value: '" + attributeValue + "'"); + Log.w(TAG, "Failed parsing fontSize value: " + attributeValue); } break; case TtmlNode.ATTR_TTS_FONT_WEIGHT: diff --git a/library/core/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 index 18378df445..43fa7a1bd9 100644 --- a/library/core/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 @@ -50,14 +50,15 @@ import java.util.TreeSet; public static final String ANONYMOUS_REGION_ID = ""; public static final String ATTR_ID = "id"; - public static final String ATTR_TTS_BACKGROUND_COLOR = "backgroundColor"; + public static final String ATTR_TTS_ORIGIN = "origin"; public static final String ATTR_TTS_EXTENT = "extent"; + public static final String ATTR_TTS_DISPLAY_ALIGN = "displayAlign"; + public static final String ATTR_TTS_BACKGROUND_COLOR = "backgroundColor"; public static final String ATTR_TTS_FONT_STYLE = "fontStyle"; public static final String ATTR_TTS_FONT_SIZE = "fontSize"; public static final String ATTR_TTS_FONT_FAMILY = "fontFamily"; public static final String ATTR_TTS_FONT_WEIGHT = "fontWeight"; public static final String ATTR_TTS_COLOR = "color"; - public static final String ATTR_TTS_ORIGIN = "origin"; public static final String ATTR_TTS_TEXT_DECORATION = "textDecoration"; public static final String ATTR_TTS_TEXT_ALIGN = "textAlign"; @@ -179,7 +180,7 @@ import java.util.TreeSet; for (Entry entry : regionOutputs.entrySet()) { TtmlRegion region = regionMap.get(entry.getKey()); cues.add(new Cue(cleanUpText(entry.getValue()), null, region.line, region.lineType, - Cue.TYPE_UNSET, region.position, Cue.TYPE_UNSET, region.width)); + region.lineAnchor, region.position, Cue.TYPE_UNSET, region.width)); } return cues; } diff --git a/library/core/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 index 5f30834b4d..98823d7a84 100644 --- a/library/core/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 @@ -22,20 +22,24 @@ import com.google.android.exoplayer2.text.Cue; */ /* package */ final class TtmlRegion { + public final String id; public final float position; public final float line; - @Cue.LineType - public final int lineType; + @Cue.LineType public final int lineType; + @Cue.AnchorType public final int lineAnchor; public final float width; - public TtmlRegion() { - this(Cue.DIMEN_UNSET, Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.DIMEN_UNSET); + public TtmlRegion(String id) { + this(id, Cue.DIMEN_UNSET, Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.TYPE_UNSET, Cue.DIMEN_UNSET); } - public TtmlRegion(float position, float line, @Cue.LineType int lineType, float width) { + public TtmlRegion(String id, float position, float line, @Cue.LineType int lineType, + @Cue.AnchorType int lineAnchor, float width) { + this.id = id; this.position = position; this.line = line; this.lineType = lineType; + this.lineAnchor = lineAnchor; this.width = width; } From c14a14e51d5c9a6d29f101892fdf184d61f546f9 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 23 May 2017 07:45:53 -0700 Subject: [PATCH 0037/2472] Update gradle + bintray-release ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=156860658 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index cbc34cecd6..da75def90b 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:2.3.0' + classpath 'com.android.tools.build:gradle:2.3.1' classpath 'com.novoda:bintray-release:0.4.0' } // Workaround for the following test coverage issue. Remove when fixed: From fe50459449413ae3a9f3a9bb720812547fbd63a8 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 23 May 2017 09:41:37 -0700 Subject: [PATCH 0038/2472] Bump version and update release notes ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=156872383 --- RELEASENOTES.md | 23 +++++++++++++++++++ build.gradle | 2 +- demo/src/main/AndroidManifest.xml | 4 ++-- .../exoplayer2/ExoPlayerLibraryInfo.java | 6 ++--- 4 files changed, 29 insertions(+), 6 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index d216e767b0..6a1defa809 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,28 @@ # Release notes # +### r2.4.1 ### + +* Stability: Avoid OutOfMemoryError in extractors when parsing malformed media + ([#2780](https://github.com/google/ExoPlayer/issues/2780)). +* Stability: Avoid native crash on Galaxy Nexus. Avoid unnecessarily large codec + input buffer allocations on all devices + ([#2607](https://github.com/google/ExoPlayer/issues/2607)). +* Variable speed playback: Fix interpolation for rate/pitch adjustment + ([#2774](https://github.com/google/ExoPlayer/issues/2774)). +* HLS: Include EXT-X-DATERANGE tags in HlsMediaPlaylist. +* HLS: Don't expose CEA-608 track if CLOSED-CAPTIONS=NONE + ([#2743](https://github.com/google/ExoPlayer/issues/2743)). +* HLS: Correctly propagate errors loading the media playlist + ([#2623](https://github.com/google/ExoPlayer/issues/2623)). +* UI: DefaultTimeBar enhancements and bug fixes + ([#2740](https://github.com/google/ExoPlayer/issues/2740)). +* Ogg: Fix failure to play some Ogg files + ([#2782](https://github.com/google/ExoPlayer/issues/2782)). +* Captions: Don't select text tack with no language by default. +* Captions: TTML positioning fixes + ([#2824](https://github.com/google/ExoPlayer/issues/2824)). +* Misc bugfixes. + ### r2.4.0 ### * New modular library structure. You can read more about depending on individual diff --git a/build.gradle b/build.gradle index da75def90b..258b11d2e6 100644 --- a/build.gradle +++ b/build.gradle @@ -48,7 +48,7 @@ allprojects { releaseRepoName = getBintrayRepo() releaseUserOrg = 'google' releaseGroupId = 'com.google.android.exoplayer' - releaseVersion = 'r2.4.0' + releaseVersion = 'r2.4.1' releaseWebsite = 'https://github.com/google/ExoPlayer' } if (it.hasProperty('externalBuildDir')) { diff --git a/demo/src/main/AndroidManifest.xml b/demo/src/main/AndroidManifest.xml index 6580e687cc..1bb859028d 100644 --- a/demo/src/main/AndroidManifest.xml +++ b/demo/src/main/AndroidManifest.xml @@ -16,8 +16,8 @@ + android:versionCode="2401" + android:versionName="2.4.1"> diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java index 13cf35d449..23c2ddbde9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java @@ -24,13 +24,13 @@ public interface ExoPlayerLibraryInfo { * The version of the library expressed as a string, for example "1.2.3". */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. - String VERSION = "2.4.0"; + String VERSION = "2.4.1"; /** * 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"; + String VERSION_SLASHY = "ExoPlayerLib/2.4.1"; /** * The version of the library expressed as an integer, for example 1002003. @@ -40,7 +40,7 @@ public interface ExoPlayerLibraryInfo { * integer version 123045006 (123-045-006). */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - int VERSION_INT = 2004000; + int VERSION_INT = 2004001; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} From 10f8944060356d575815c6570db9995a1a0d1e01 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 24 May 2017 07:38:42 -0700 Subject: [PATCH 0039/2472] Don't select more than one audio/video/text track by default Issue: #2618 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=156986606 --- .../trackselection/DefaultTrackSelector.java | 43 ++++++++++++------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index 361fcf0b57..b37088e588 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -436,35 +436,48 @@ public class DefaultTrackSelector extends MappingTrackSelector { int rendererCount = rendererCapabilities.length; TrackSelection[] rendererTrackSelections = new TrackSelection[rendererCount]; Parameters params = paramsReference.get(); - boolean videoTrackAndRendererPresent = false; + boolean seenVideoRendererWithMappedTracks = false; + boolean selectedVideoTracks = false; for (int i = 0; i < rendererCount; i++) { if (C.TRACK_TYPE_VIDEO == rendererCapabilities[i].getTrackType()) { - rendererTrackSelections[i] = selectVideoTrack(rendererCapabilities[i], - rendererTrackGroupArrays[i], rendererFormatSupports[i], params.maxVideoWidth, - params.maxVideoHeight, params.maxVideoBitrate, params.allowNonSeamlessAdaptiveness, - params.allowMixedMimeAdaptiveness, params.viewportWidth, params.viewportHeight, - params.orientationMayChange, adaptiveTrackSelectionFactory, - params.exceedVideoConstraintsIfNecessary, params.exceedRendererCapabilitiesIfNecessary); - videoTrackAndRendererPresent |= rendererTrackGroupArrays[i].length > 0; + if (!selectedVideoTracks) { + rendererTrackSelections[i] = selectVideoTrack(rendererCapabilities[i], + rendererTrackGroupArrays[i], rendererFormatSupports[i], params.maxVideoWidth, + params.maxVideoHeight, params.maxVideoBitrate, params.allowNonSeamlessAdaptiveness, + params.allowMixedMimeAdaptiveness, params.viewportWidth, params.viewportHeight, + params.orientationMayChange, adaptiveTrackSelectionFactory, + params.exceedVideoConstraintsIfNecessary, + params.exceedRendererCapabilitiesIfNecessary); + selectedVideoTracks = rendererTrackSelections[i] != null; + } + seenVideoRendererWithMappedTracks |= rendererTrackGroupArrays[i].length > 0; } } + boolean selectedAudioTracks = false; + boolean selectedTextTracks = false; for (int i = 0; i < rendererCount; i++) { switch (rendererCapabilities[i].getTrackType()) { case C.TRACK_TYPE_VIDEO: // Already done. Do nothing. break; case C.TRACK_TYPE_AUDIO: - rendererTrackSelections[i] = selectAudioTrack(rendererTrackGroupArrays[i], - rendererFormatSupports[i], params.preferredAudioLanguage, - params.exceedRendererCapabilitiesIfNecessary, params.allowMixedMimeAdaptiveness, - videoTrackAndRendererPresent ? null : adaptiveTrackSelectionFactory); + if (!selectedAudioTracks) { + rendererTrackSelections[i] = selectAudioTrack(rendererTrackGroupArrays[i], + rendererFormatSupports[i], params.preferredAudioLanguage, + params.exceedRendererCapabilitiesIfNecessary, params.allowMixedMimeAdaptiveness, + seenVideoRendererWithMappedTracks ? null : adaptiveTrackSelectionFactory); + selectedAudioTracks = rendererTrackSelections[i] != null; + } break; case C.TRACK_TYPE_TEXT: - rendererTrackSelections[i] = selectTextTrack(rendererTrackGroupArrays[i], - rendererFormatSupports[i], params.preferredTextLanguage, - params.preferredAudioLanguage, params.exceedRendererCapabilitiesIfNecessary); + if (!selectedTextTracks) { + rendererTrackSelections[i] = selectTextTrack(rendererTrackGroupArrays[i], + rendererFormatSupports[i], params.preferredTextLanguage, + params.preferredAudioLanguage, params.exceedRendererCapabilitiesIfNecessary); + selectedTextTracks = rendererTrackSelections[i] != null; + } break; default: rendererTrackSelections[i] = selectOtherTrack(rendererCapabilities[i].getTrackType(), From 22137f621535fd765eea411ac31fbf00af805667 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 24 May 2017 09:48:39 -0700 Subject: [PATCH 0040/2472] Flexibilize Util.parseXsDateTime to allow single digit hour ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=156999955 --- .../java/com/google/android/exoplayer2/util/UtilTest.java | 1 + .../src/main/java/com/google/android/exoplayer2/util/Util.java | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/util/UtilTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/util/UtilTest.java index 923d1d8aaa..1d9aff0723 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/util/UtilTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/util/UtilTest.java @@ -146,6 +146,7 @@ public class UtilTest extends TestCase { assertEquals(1411161535000L, Util.parseXsDateTime("2014-09-19T13:18:55-08:00")); assertEquals(1411161535000L, Util.parseXsDateTime("2014-09-19T13:18:55-0800")); assertEquals(1411161535000L, Util.parseXsDateTime("2014-09-19T13:18:55.000-0800")); + assertEquals(1411161535000L, Util.parseXsDateTime("2014-09-19T13:18:55.000-800")); } public void testUnescapeInvalidFileName() { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java index 206349fa07..50932cdf48 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -98,7 +98,7 @@ public final class Util { private static final Pattern XS_DATE_TIME_PATTERN = Pattern.compile( "(\\d\\d\\d\\d)\\-(\\d\\d)\\-(\\d\\d)[Tt]" + "(\\d\\d):(\\d\\d):(\\d\\d)([\\.,](\\d+))?" - + "([Zz]|((\\+|\\-)(\\d\\d):?(\\d\\d)))?"); + + "([Zz]|((\\+|\\-)(\\d?\\d):?(\\d\\d)))?"); private static final Pattern XS_DURATION_PATTERN = Pattern.compile("^(-)?P(([0-9]*)Y)?(([0-9]*)M)?(([0-9]*)D)?" + "(T(([0-9]*)H)?(([0-9]*)M)?(([0-9.]*)S)?)?$"); From 67cb7d8da5019af5f273b7be8885ef1281f302f7 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 25 May 2017 06:52:39 -0700 Subject: [PATCH 0041/2472] Fix SmoothStreaming Timeline There were a few things wrong. Specifically the case in the ref'd issue. Also, the timeline was being marked as non-dynamic in the empty-but-live case (it should be marked dynamic as segments may be added later). Issue: #2760 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=157103727 --- .../source/smoothstreaming/SsMediaSource.java | 60 ++++++++++--------- 1 file changed, 31 insertions(+), 29 deletions(-) diff --git a/library/smoothstreaming/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 index e3fb8b606c..d16620d5b2 100644 --- a/library/smoothstreaming/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 @@ -287,39 +287,41 @@ public final class SsMediaSource implements MediaSource, for (int i = 0; i < mediaPeriods.size(); i++) { mediaPeriods.get(i).updateManifest(manifest); } + + long startTimeUs = Long.MAX_VALUE; + long endTimeUs = Long.MIN_VALUE; + for (StreamElement element : manifest.streamElements) { + if (element.chunkCount > 0) { + startTimeUs = Math.min(startTimeUs, element.getStartTimeUs(0)); + endTimeUs = Math.max(endTimeUs, element.getStartTimeUs(element.chunkCount - 1) + + element.getChunkDurationUs(element.chunkCount - 1)); + } + } + Timeline timeline; - if (manifest.isLive) { - long startTimeUs = Long.MAX_VALUE; - long endTimeUs = Long.MIN_VALUE; - for (int i = 0; i < manifest.streamElements.length; i++) { - StreamElement element = manifest.streamElements[i]; - if (element.chunkCount > 0) { - startTimeUs = Math.min(startTimeUs, element.getStartTimeUs(0)); - endTimeUs = Math.max(endTimeUs, element.getStartTimeUs(element.chunkCount - 1) - + element.getChunkDurationUs(element.chunkCount - 1)); - } + if (startTimeUs == Long.MAX_VALUE) { + long periodDurationUs = manifest.isLive ? C.TIME_UNSET : 0; + timeline = new SinglePeriodTimeline(periodDurationUs, 0, 0, 0, true /* isSeekable */, + manifest.isLive /* isDynamic */); + } else if (manifest.isLive) { + if (manifest.dvrWindowLengthUs != C.TIME_UNSET && manifest.dvrWindowLengthUs > 0) { + startTimeUs = Math.max(startTimeUs, endTimeUs - manifest.dvrWindowLengthUs); } - if (startTimeUs == Long.MAX_VALUE) { - timeline = new SinglePeriodTimeline(C.TIME_UNSET, false); - } else { - if (manifest.dvrWindowLengthUs != C.TIME_UNSET - && manifest.dvrWindowLengthUs > 0) { - startTimeUs = Math.max(startTimeUs, endTimeUs - manifest.dvrWindowLengthUs); - } - long durationUs = endTimeUs - startTimeUs; - long defaultStartPositionUs = durationUs - C.msToUs(livePresentationDelayMs); - if (defaultStartPositionUs < MIN_LIVE_DEFAULT_START_POSITION_US) { - // The default start position is too close to the start of the live window. Set it to the - // minimum default start position provided the window is at least twice as big. Else set - // it to the middle of the window. - defaultStartPositionUs = Math.min(MIN_LIVE_DEFAULT_START_POSITION_US, durationUs / 2); - } - timeline = new SinglePeriodTimeline(C.TIME_UNSET, durationUs, startTimeUs, - defaultStartPositionUs, true /* isSeekable */, true /* isDynamic */); + long durationUs = endTimeUs - startTimeUs; + long defaultStartPositionUs = durationUs - C.msToUs(livePresentationDelayMs); + if (defaultStartPositionUs < MIN_LIVE_DEFAULT_START_POSITION_US) { + // The default start position is too close to the start of the live window. Set it to the + // minimum default start position provided the window is at least twice as big. Else set + // it to the middle of the window. + defaultStartPositionUs = Math.min(MIN_LIVE_DEFAULT_START_POSITION_US, durationUs / 2); } + timeline = new SinglePeriodTimeline(C.TIME_UNSET, durationUs, startTimeUs, + defaultStartPositionUs, true /* isSeekable */, true /* isDynamic */); } else { - boolean isSeekable = manifest.durationUs != C.TIME_UNSET; - timeline = new SinglePeriodTimeline(manifest.durationUs, isSeekable); + long durationUs = manifest.durationUs != C.TIME_UNSET ? manifest.durationUs + : endTimeUs - startTimeUs; + timeline = new SinglePeriodTimeline(startTimeUs + durationUs, durationUs, startTimeUs, 0, + true /* isSeekable */, false /* isDynamic */); } sourceListener.onSourceInfoRefreshed(timeline, manifest); } From 5a754fe7611856d333a74bc2c9dfb9f2c4abab28 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 26 May 2017 04:49:44 -0700 Subject: [PATCH 0042/2472] Don't fail if we find a track is unsupported Use AUDIO_UNKNOWN instead. This is in line with our handling of video tracks with VIDEO_UNKNOWN. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=157209428 --- .../extractor/mkv/MatroskaExtractor.java | 32 ++++++++++++++----- .../android/exoplayer2/util/MimeTypes.java | 1 + 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/library/core/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 index 8f3abf4688..2313392fcf 100644 --- a/library/core/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 @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.extractor.mkv; import android.support.annotation.IntDef; +import android.util.Log; import android.util.SparseArray; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; @@ -84,6 +85,8 @@ public final class MatroskaExtractor implements Extractor { */ public static final int FLAG_DISABLE_SEEK_FOR_CUES = 1; + private static final String TAG = "MatroskaExtractor"; + private static final int UNSET_ENTRY_ID = -1; private static final int BLOCK_STATE_START = 0; @@ -1558,7 +1561,12 @@ public final class MatroskaExtractor implements Extractor { break; case CODEC_ID_FOURCC: initializationData = parseFourCcVc1Private(new ParsableByteArray(codecPrivate)); - mimeType = initializationData == null ? MimeTypes.VIDEO_UNKNOWN : MimeTypes.VIDEO_VC1; + if (initializationData != null) { + mimeType = MimeTypes.VIDEO_VC1; + } else { + Log.w(TAG, "Unsupported FourCC. Setting mimeType to " + MimeTypes.VIDEO_UNKNOWN); + mimeType = MimeTypes.VIDEO_UNKNOWN; + } break; case CODEC_ID_THEORA: // TODO: This can be set to the real mimeType if/when we work out what initializationData @@ -1614,19 +1622,27 @@ public final class MatroskaExtractor implements Extractor { break; case CODEC_ID_ACM: mimeType = MimeTypes.AUDIO_RAW; - if (!parseMsAcmCodecPrivate(new ParsableByteArray(codecPrivate))) { - throw new ParserException("Non-PCM MS/ACM is unsupported"); - } - pcmEncoding = Util.getPcmEncoding(audioBitDepth); - if (pcmEncoding == C.ENCODING_INVALID) { - throw new ParserException("Unsupported PCM bit depth: " + audioBitDepth); + if (parseMsAcmCodecPrivate(new ParsableByteArray(codecPrivate))) { + pcmEncoding = Util.getPcmEncoding(audioBitDepth); + if (pcmEncoding == C.ENCODING_INVALID) { + pcmEncoding = Format.NO_VALUE; + mimeType = MimeTypes.AUDIO_UNKNOWN; + Log.w(TAG, "Unsupported PCM bit depth: " + audioBitDepth + ". Setting mimeType to " + + mimeType); + } + } else { + mimeType = MimeTypes.AUDIO_UNKNOWN; + Log.w(TAG, "Non-PCM MS/ACM is unsupported. Setting mimeType to " + mimeType); } break; case CODEC_ID_PCM_INT_LIT: mimeType = MimeTypes.AUDIO_RAW; pcmEncoding = Util.getPcmEncoding(audioBitDepth); if (pcmEncoding == C.ENCODING_INVALID) { - throw new ParserException("Unsupported PCM bit depth: " + audioBitDepth); + pcmEncoding = Format.NO_VALUE; + mimeType = MimeTypes.AUDIO_UNKNOWN; + Log.w(TAG, "Unsupported PCM bit depth: " + audioBitDepth + ". Setting mimeType to " + + mimeType); } break; case CODEC_ID_SUBRIP: diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java b/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java index ea669e6f2a..e227ea1068 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java @@ -61,6 +61,7 @@ public final class MimeTypes { public static final String AUDIO_AMR_WB = BASE_TYPE_AUDIO + "/amr-wb"; public static final String AUDIO_FLAC = BASE_TYPE_AUDIO + "/x-flac"; public static final String AUDIO_ALAC = BASE_TYPE_AUDIO + "/alac"; + public static final String AUDIO_UNKNOWN = BASE_TYPE_AUDIO + "/x-unknown"; public static final String TEXT_VTT = BASE_TYPE_TEXT + "/vtt"; From 2795269daee2270fc2bda1512cefd86ef021f905 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Fri, 26 May 2017 07:48:43 -0700 Subject: [PATCH 0043/2472] Use AVERAGE-BANDWIDTH instead of BANDWIDTH when available Also prevent BANDWIDTH's regex from matching the AVERAGE-BANDWIDTH attribute. Issue:#2863 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=157219453 --- .../playlist/HlsMasterPlaylistParserTest.java | 27 ++++++++++++++----- .../hls/playlist/HlsPlaylistParser.java | 26 ++++++++++-------- 2 files changed, 36 insertions(+), 17 deletions(-) diff --git a/library/hls/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 index 912dcb28b2..f835c87466 100644 --- a/library/hls/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 @@ -51,6 +51,15 @@ public class HlsMasterPlaylistParserTest extends TestCase { + "#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"mp4a.40.5\"\n" + "http://example.com/audio-only.m3u8"; + private static final String AVG_BANDWIDTH_MASTER_PLAYLIST = " #EXTM3U \n" + + "\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n" + + "http://example.com/low.m3u8\n" + + "\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=1280000,AVERAGE-BANDWIDTH=1270000," + + "CODECS=\"mp4a.40.2 , avc1.66.30 \"\n" + + "http://example.com/spaces_in_codecs.m3u8\n"; + private static final String PLAYLIST_WITH_INVALID_HEADER = "#EXTMU3\n" + "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n" + "http://example.com/low.m3u8\n"; @@ -70,42 +79,48 @@ public class HlsMasterPlaylistParserTest extends TestCase { HlsMasterPlaylist masterPlaylist = parseMasterPlaylist(PLAYLIST_URI, MASTER_PLAYLIST); List variants = masterPlaylist.variants; - assertNotNull(variants); assertEquals(5, variants.size()); assertNull(masterPlaylist.muxedCaptionFormats); assertEquals(1280000, variants.get(0).format.bitrate); - assertNotNull(variants.get(0).format.codecs); assertEquals("mp4a.40.2,avc1.66.30", variants.get(0).format.codecs); assertEquals(304, variants.get(0).format.width); assertEquals(128, variants.get(0).format.height); assertEquals("http://example.com/low.m3u8", variants.get(0).url); assertEquals(1280000, variants.get(1).format.bitrate); - assertNotNull(variants.get(1).format.codecs); assertEquals("mp4a.40.2 , avc1.66.30 ", variants.get(1).format.codecs); assertEquals("http://example.com/spaces_in_codecs.m3u8", variants.get(1).url); assertEquals(2560000, variants.get(2).format.bitrate); - assertEquals(null, variants.get(2).format.codecs); + assertNull(variants.get(2).format.codecs); assertEquals(384, variants.get(2).format.width); assertEquals(160, variants.get(2).format.height); assertEquals("http://example.com/mid.m3u8", variants.get(2).url); assertEquals(7680000, variants.get(3).format.bitrate); - assertEquals(null, variants.get(3).format.codecs); + assertNull(variants.get(3).format.codecs); assertEquals(Format.NO_VALUE, variants.get(3).format.width); assertEquals(Format.NO_VALUE, variants.get(3).format.height); assertEquals("http://example.com/hi.m3u8", variants.get(3).url); assertEquals(65000, variants.get(4).format.bitrate); - assertNotNull(variants.get(4).format.codecs); assertEquals("mp4a.40.5", variants.get(4).format.codecs); assertEquals(Format.NO_VALUE, variants.get(4).format.width); assertEquals(Format.NO_VALUE, variants.get(4).format.height); assertEquals("http://example.com/audio-only.m3u8", variants.get(4).url); } + public void testMasterPlaylistWithBandwdithAverage() throws IOException { + HlsMasterPlaylist masterPlaylist = parseMasterPlaylist(PLAYLIST_URI, + AVG_BANDWIDTH_MASTER_PLAYLIST); + + List variants = masterPlaylist.variants; + + assertEquals(1280000, variants.get(0).format.bitrate); + assertEquals(1270000, variants.get(1).format.bitrate); + } + public void testPlaylistWithInvalidHeader() throws IOException { try { parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_INVALID_HEADER); diff --git a/library/hls/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 index 664306baff..9552f5b8c8 100644 --- a/library/hls/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 @@ -73,7 +73,9 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser Date: Wed, 31 May 2017 01:46:49 -0700 Subject: [PATCH 0044/2472] Fix DefaultTimeBar invalidation Issue: #2871 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=157562792 --- .../java/com/google/android/exoplayer2/ui/DefaultTimeBar.java | 4 ++++ 1 file changed, 4 insertions(+) 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 index fd05fdd5d0..d9754420bf 100644 --- 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 @@ -220,11 +220,13 @@ public class DefaultTimeBar extends View implements TimeBar { public void setPosition(long position) { this.position = position; setContentDescription(getProgressText()); + update(); } @Override public void setBufferedPosition(long bufferedPosition) { this.bufferedPosition = bufferedPosition; + update(); } @Override @@ -235,6 +237,7 @@ public class DefaultTimeBar extends View implements TimeBar { } else { updateScrubberState(); } + update(); } @Override @@ -242,6 +245,7 @@ public class DefaultTimeBar extends View implements TimeBar { Assertions.checkArgument(adBreakCount == 0 || adBreakTimesMs != null); this.adBreakCount = adBreakCount; this.adBreakTimesMs = adBreakTimesMs; + update(); } @Override From c80b60f4ac571c7ca22607f7049d90db2629e98a Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 31 May 2017 03:16:47 -0700 Subject: [PATCH 0045/2472] Constraint seeks within bounds for ConstantBitrateSeeker We do this everywhere for index based seeking already. Issue: #2876 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=157568788 --- .../exoplayer2/extractor/mp3/ConstantBitrateSeeker.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/library/core/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 index c5de8d8284..df7748a910 100644 --- a/library/core/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 @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.extractor.mp3; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.util.Util; /** * MP3 seeker that doesn't rely on metadata and seeks assuming the source has a constant bitrate. @@ -41,8 +42,11 @@ import com.google.android.exoplayer2.C; @Override public long getPosition(long timeUs) { - return durationUs == C.TIME_UNSET ? 0 - : firstFramePosition + (timeUs * bitrate) / (C.MICROS_PER_SECOND * BITS_PER_BYTE); + if (durationUs == C.TIME_UNSET) { + return 0; + } + timeUs = Util.constrainValue(timeUs, 0, durationUs); + return firstFramePosition + (timeUs * bitrate) / (C.MICROS_PER_SECOND * BITS_PER_BYTE); } @Override From 80600ffe1be73e83631c8cc38f03db8e3cd0dfce Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 31 May 2017 03:59:24 -0700 Subject: [PATCH 0046/2472] Ignore invalid EXT-X-PLAYLIST-TYPE values Issue:#2889 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=157571216 --- .../exoplayer2/source/hls/playlist/HlsPlaylistParser.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/library/hls/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 index 9552f5b8c8..a867659838 100644 --- a/library/hls/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 @@ -307,8 +307,6 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser Date: Wed, 31 May 2017 07:23:13 -0700 Subject: [PATCH 0047/2472] Move adaptation disabling workaround into MediaCodecUtil This is necessary to make sure that the correct thing happens where MediaCodecInfo.adaptive is queried directly (for example, MediaCodecVideoRenderer uses the field to determine how to size input buffers). Also disable adaptive on Nexus 10. Issue: #2806 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=157583473 --- .../exoplayer2/mediacodec/MediaCodecInfo.java | 23 +++- .../mediacodec/MediaCodecRenderer.java | 16 +-- .../exoplayer2/mediacodec/MediaCodecUtil.java | 105 ++++++++++-------- 3 files changed, 79 insertions(+), 65 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java index 6914b2f52c..3c788a60a4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java @@ -71,7 +71,7 @@ public final class MediaCodecInfo { * @return The created instance. */ public static MediaCodecInfo newPassthroughInstance(String name) { - return new MediaCodecInfo(name, null, null); + return new MediaCodecInfo(name, null, null, false); } /** @@ -84,18 +84,29 @@ public final class MediaCodecInfo { */ public static MediaCodecInfo newInstance(String name, String mimeType, CodecCapabilities capabilities) { - return new MediaCodecInfo(name, mimeType, capabilities); + return new MediaCodecInfo(name, mimeType, capabilities, false); } /** - * @param name The name of the decoder. - * @param capabilities The capabilities of the decoder. + * Creates an instance. + * + * @param name The name of the {@link MediaCodec}. + * @param mimeType A mime type supported by the {@link MediaCodec}. + * @param capabilities The capabilities of the {@link MediaCodec} for the specified mime type. + * @param forceDisableAdaptive Whether {@link #adaptive} should be forced to {@code false}. + * @return The created instance. */ - private MediaCodecInfo(String name, String mimeType, CodecCapabilities capabilities) { + public static MediaCodecInfo newInstance(String name, String mimeType, + CodecCapabilities capabilities, boolean forceDisableAdaptive) { + return new MediaCodecInfo(name, mimeType, capabilities, forceDisableAdaptive); + } + + private MediaCodecInfo(String name, String mimeType, CodecCapabilities capabilities, + boolean forceDisableAdaptive) { this.name = Assertions.checkNotNull(name); this.mimeType = mimeType; this.capabilities = capabilities; - adaptive = capabilities != null && isAdaptive(capabilities); + adaptive = !forceDisableAdaptive && capabilities != null && isAdaptive(capabilities); tunneling = capabilities != null && isTunneling(capabilities); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java index 25a5aa4dd3..f0e23ebfeb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java @@ -339,7 +339,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } String codecName = decoderInfo.name; - codecIsAdaptive = decoderInfo.adaptive && !codecNeedsDisableAdaptationWorkaround(codecName); + codecIsAdaptive = decoderInfo.adaptive; codecNeedsDiscardToSpsWorkaround = codecNeedsDiscardToSpsWorkaround(codecName, format); codecNeedsFlushWorkaround = codecNeedsFlushWorkaround(codecName); codecNeedsAdaptationWorkaround = codecNeedsAdaptationWorkaround(codecName); @@ -1188,18 +1188,4 @@ 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/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java index 2bb3603df9..5369dffeb6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java @@ -160,6 +160,55 @@ public final class MediaCodecUtil { return decoderInfos; } + /** + * Returns the maximum frame size supported by the default H264 decoder. + * + * @return The maximum frame size for an H264 stream that can be decoded on the device. + */ + public static int maxH264DecodableFrameSize() throws DecoderQueryException { + if (maxH264DecodableFrameSize == -1) { + int result = 0; + MediaCodecInfo decoderInfo = getDecoderInfo(MimeTypes.VIDEO_H264, false); + if (decoderInfo != null) { + for (CodecProfileLevel profileLevel : decoderInfo.getProfileLevels()) { + result = Math.max(avcLevelToMaxFrameSize(profileLevel.level), result); + } + // We assume support for at least 480p (SDK_INT >= 21) or 360p (SDK_INT < 21), which are + // the levels mandated by the Android CDD. + result = Math.max(result, Util.SDK_INT >= 21 ? (720 * 480) : (480 * 360)); + } + maxH264DecodableFrameSize = result; + } + return maxH264DecodableFrameSize; + } + + /** + * Returns profile and level (as defined by {@link CodecProfileLevel}) corresponding to the given + * codec description string (as defined by RFC 6381). + * + * @param codec A codec description string, as defined by RFC 6381. + * @return A pair (profile constant, level constant) if {@code codec} is well-formed and + * recognized, or null otherwise + */ + public static Pair getCodecProfileAndLevel(String codec) { + if (codec == null) { + return null; + } + String[] parts = codec.split("\\."); + switch (parts[0]) { + case CODEC_ID_HEV1: + case CODEC_ID_HVC1: + return getHevcProfileAndLevel(codec, parts); + case CODEC_ID_AVC1: + case CODEC_ID_AVC2: + return getAvcProfileAndLevel(codec, parts); + default: + return null; + } + } + + // Internal methods. + private static List getDecoderInfosInternal( CodecKey key, MediaCodecListCompat mediaCodecList) throws DecoderQueryException { try { @@ -177,12 +226,14 @@ public final class MediaCodecUtil { try { CodecCapabilities capabilities = codecInfo.getCapabilitiesForType(supportedType); boolean secure = mediaCodecList.isSecurePlaybackSupported(mimeType, capabilities); + boolean forceDisableAdaptive = codecNeedsDisableAdaptationWorkaround(codecName); if ((secureDecodersExplicit && key.secure == secure) || (!secureDecodersExplicit && !key.secure)) { - decoderInfos.add(MediaCodecInfo.newInstance(codecName, mimeType, capabilities)); + decoderInfos.add(MediaCodecInfo.newInstance(codecName, mimeType, capabilities, + forceDisableAdaptive)); } else if (!secureDecodersExplicit && secure) { decoderInfos.add(MediaCodecInfo.newInstance(codecName + ".secure", mimeType, - capabilities)); + capabilities, forceDisableAdaptive)); // It only makes sense to have one synthesized secure decoder, return immediately. return decoderInfos; } @@ -289,50 +340,16 @@ public final class MediaCodecUtil { } /** - * Returns the maximum frame size supported by the default H264 decoder. + * Returns whether the decoder is known to fail when adapting, despite advertising itself as an + * adaptive decoder. * - * @return The maximum frame size for an H264 stream that can be decoded on the device. + * @param name The decoder name. + * @return True if the decoder is known to fail when adapting. */ - public static int maxH264DecodableFrameSize() throws DecoderQueryException { - if (maxH264DecodableFrameSize == -1) { - int result = 0; - MediaCodecInfo decoderInfo = getDecoderInfo(MimeTypes.VIDEO_H264, false); - if (decoderInfo != null) { - for (CodecProfileLevel profileLevel : decoderInfo.getProfileLevels()) { - result = Math.max(avcLevelToMaxFrameSize(profileLevel.level), result); - } - // We assume support for at least 480p (SDK_INT >= 21) or 360p (SDK_INT < 21), which are - // the levels mandated by the Android CDD. - result = Math.max(result, Util.SDK_INT >= 21 ? (720 * 480) : (480 * 360)); - } - maxH264DecodableFrameSize = result; - } - return maxH264DecodableFrameSize; - } - - /** - * Returns profile and level (as defined by {@link CodecProfileLevel}) corresponding to the given - * codec description string (as defined by RFC 6381). - * - * @param codec A codec description string, as defined by RFC 6381. - * @return A pair (profile constant, level constant) if {@code codec} is well-formed and - * recognized, or null otherwise - */ - public static Pair getCodecProfileAndLevel(String codec) { - if (codec == null) { - return null; - } - String[] parts = codec.split("\\."); - switch (parts[0]) { - case CODEC_ID_HEV1: - case CODEC_ID_HVC1: - return getHevcProfileAndLevel(codec, parts); - case CODEC_ID_AVC1: - case CODEC_ID_AVC2: - return getAvcProfileAndLevel(codec, parts); - default: - return null; - } + private static boolean codecNeedsDisableAdaptationWorkaround(String name) { + return Util.SDK_INT <= 22 + && (Util.MODEL.equals("ODROID-XU3") || Util.MODEL.equals("Nexus 10")) + && ("OMX.Exynos.AVC.Decoder".equals(name) || "OMX.Exynos.AVC.Decoder.secure".equals(name)); } private static Pair getHevcProfileAndLevel(String codec, String[] parts) { From 9796a096983a55d95e29bc5f661489bca98bc9d6 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 2 Jun 2017 09:48:58 -0700 Subject: [PATCH 0048/2472] Assume CBR for MP3s with Info headers Issue: #2895 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=157841519 --- .../extractor/mp3/Mp3Extractor.java | 118 +++++++++++------- 1 file changed, 73 insertions(+), 45 deletions(-) diff --git a/library/core/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 index b0faad71c0..6e114137f1 100644 --- a/library/core/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 @@ -87,10 +87,12 @@ public final class Mp3Extractor implements Extractor { /** * Mask that includes the audio header values that must match between frames. */ - private static final int HEADER_MASK = 0xFFFE0C00; - private static final int XING_HEADER = Util.getIntegerCodeForString("Xing"); - private static final int INFO_HEADER = Util.getIntegerCodeForString("Info"); - private static final int VBRI_HEADER = Util.getIntegerCodeForString("VBRI"); + private static final int MPEG_AUDIO_HEADER_MASK = 0xFFFE0C00; + + private static final int SEEK_HEADER_XING = Util.getIntegerCodeForString("Xing"); + private static final int SEEK_HEADER_INFO = Util.getIntegerCodeForString("Info"); + private static final int SEEK_HEADER_VBRI = Util.getIntegerCodeForString("VBRI"); + private static final int SEEK_HEADER_UNSET = 0; @Flags private final int flags; private final long forcedFirstSampleTimestampUs; @@ -178,7 +180,11 @@ public final class Mp3Extractor implements Extractor { } } if (seeker == null) { - seeker = setupSeeker(input); + seeker = maybeReadSeekFrame(input); + if (seeker == null + || (!seeker.isSeekable() && (flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING) != 0)) { + seeker = getConstantBitrateSeeker(input); + } extractorOutput.seekMap(seeker); trackOutput.format(Format.createAudioSampleFormat(null, synchronizedHeader.mimeType, null, Format.NO_VALUE, MpegAudioHeader.MAX_FRAME_SIZE_BYTES, synchronizedHeader.channels, @@ -197,7 +203,7 @@ public final class Mp3Extractor implements Extractor { } scratch.setPosition(0); int sampleHeaderData = scratch.readInt(); - if ((sampleHeaderData & HEADER_MASK) != (synchronizedHeaderData & HEADER_MASK) + if (!headersMatch(sampleHeaderData, synchronizedHeaderData) || MpegAudioHeader.getFrameSize(sampleHeaderData) == C.LENGTH_UNSET) { // We have lost synchronization, so attempt to resynchronize starting at the next byte. extractorInput.skipFully(1); @@ -254,7 +260,7 @@ public final class Mp3Extractor implements Extractor { int headerData = scratch.readInt(); int frameSize; if ((candidateSynchronizedHeaderData != 0 - && (headerData & HEADER_MASK) != (candidateSynchronizedHeaderData & HEADER_MASK)) + && !headersMatch(headerData, candidateSynchronizedHeaderData)) || (frameSize = MpegAudioHeader.getFrameSize(headerData)) == C.LENGTH_UNSET) { // The header doesn't match the candidate header or is invalid. Try the next byte offset. if (searchedBytes++ == searchLimitBytes) { @@ -337,37 +343,27 @@ public final class Mp3Extractor implements Extractor { } /** - * Returns a {@link Seeker} to seek using metadata read from {@code input}, which should provide - * data from the start of the first frame in the stream. On returning, the input's position will - * be set to the start of the first frame of audio. + * Consumes the next frame from the {@code input} if it contains VBRI or Xing seeking metadata, + * returning a {@link Seeker} if the metadata was present and valid, or {@code null} otherwise. + * After this method returns, the input position is the start of the first frame of audio. * * @param input The {@link ExtractorInput} from which to read. + * @return A {@link Seeker} if seeking metadata was present and valid, or {@code null} otherwise. * @throws IOException Thrown if there was an error reading from the stream. Not expected if the * next two frames were already peeked during synchronization. * @throws InterruptedException Thrown if reading from the stream was interrupted. Not expected if * the next two frames were already peeked during synchronization. - * @return a {@link Seeker}. */ - private Seeker setupSeeker(ExtractorInput input) throws IOException, InterruptedException { - // Read the first frame which may contain a Xing or VBRI header with seeking metadata. + private Seeker maybeReadSeekFrame(ExtractorInput input) throws IOException, InterruptedException { ParsableByteArray frame = new ParsableByteArray(synchronizedHeader.frameSize); input.peekFully(frame.data, 0, synchronizedHeader.frameSize); - - long position = input.getPosition(); - long length = input.getLength(); - int headerData = 0; - Seeker seeker = null; - - // Check if there is a Xing header. int xingBase = (synchronizedHeader.version & 1) != 0 ? (synchronizedHeader.channels != 1 ? 36 : 21) // MPEG 1 : (synchronizedHeader.channels != 1 ? 21 : 13); // MPEG 2 or 2.5 - if (frame.limit() >= xingBase + 4) { - frame.setPosition(xingBase); - headerData = frame.readInt(); - } - if (headerData == XING_HEADER || headerData == INFO_HEADER) { - seeker = XingSeeker.create(synchronizedHeader, frame, position, length); + int seekHeader = getSeekFrameHeader(frame, xingBase); + Seeker seeker; + if (seekHeader == SEEK_HEADER_XING || seekHeader == SEEK_HEADER_INFO) { + seeker = XingSeeker.create(synchronizedHeader, frame, input.getPosition(), input.getLength()); if (seeker != null && !gaplessInfoHolder.hasGaplessInfo()) { // If there is a Xing header, read gapless playback metadata at a fixed offset. input.resetPeekPosition(); @@ -377,28 +373,60 @@ public final class Mp3Extractor implements Extractor { gaplessInfoHolder.setFromXingHeaderValue(scratch.readUnsignedInt24()); } input.skipFully(synchronizedHeader.frameSize); - } else if (frame.limit() >= 40) { - // Check if there is a VBRI header. - frame.setPosition(36); // MPEG audio header (4 bytes) + 32 bytes. - headerData = frame.readInt(); - if (headerData == VBRI_HEADER) { - seeker = VbriSeeker.create(synchronizedHeader, frame, position, length); - input.skipFully(synchronizedHeader.frameSize); + if (seeker != null && !seeker.isSeekable() && seekHeader == SEEK_HEADER_INFO) { + // Fall back to constant bitrate seeking for Info headers missing a table of contents. + return getConstantBitrateSeeker(input); + } + } else if (seekHeader == SEEK_HEADER_VBRI) { + seeker = VbriSeeker.create(synchronizedHeader, frame, input.getPosition(), input.getLength()); + input.skipFully(synchronizedHeader.frameSize); + } else { // seekerHeader == SEEK_HEADER_UNSET + // This frame doesn't contain seeking information, so reset the peek position. + seeker = null; + input.resetPeekPosition(); + } + return seeker; + } + + /** + * Peeks the next frame and returns a {@link ConstantBitrateSeeker} based on its bitrate. + */ + private Seeker getConstantBitrateSeeker(ExtractorInput input) + throws IOException, InterruptedException { + input.peekFully(scratch.data, 0, 4); + scratch.setPosition(0); + MpegAudioHeader.populateHeader(scratch.readInt(), synchronizedHeader); + return new ConstantBitrateSeeker(input.getPosition(), synchronizedHeader.bitrate, + input.getLength()); + } + + /** + * Returns whether the headers match in those bits masked by {@link #MPEG_AUDIO_HEADER_MASK}. + */ + private static boolean headersMatch(int headerA, long headerB) { + return (headerA & MPEG_AUDIO_HEADER_MASK) == (headerB & MPEG_AUDIO_HEADER_MASK); + } + + /** + * Returns {@link #SEEK_HEADER_XING}, {@link #SEEK_HEADER_INFO} or {@link #SEEK_HEADER_VBRI} if + * the provided {@code frame} may have seeking metadata, or {@link #SEEK_HEADER_UNSET} otherwise. + * If seeking metadata is present, {@code frame}'s position is advanced past the header. + */ + private static int getSeekFrameHeader(ParsableByteArray frame, int xingBase) { + if (frame.limit() >= xingBase + 4) { + frame.setPosition(xingBase); + int headerData = frame.readInt(); + if (headerData == SEEK_HEADER_XING || headerData == SEEK_HEADER_INFO) { + return headerData; } } - - if (seeker == null || (!seeker.isSeekable() - && (flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING) != 0)) { - // Repopulate the synchronized header in case we had to skip an invalid seeking header, which - // would give an invalid CBR bitrate. - input.resetPeekPosition(); - input.peekFully(scratch.data, 0, 4); - scratch.setPosition(0); - MpegAudioHeader.populateHeader(scratch.readInt(), synchronizedHeader); - seeker = new ConstantBitrateSeeker(input.getPosition(), synchronizedHeader.bitrate, length); + if (frame.limit() >= 40) { + frame.setPosition(36); // MPEG audio header (4 bytes) + 32 bytes. + if (frame.readInt() == SEEK_HEADER_VBRI) { + return SEEK_HEADER_VBRI; + } } - - return seeker; + return SEEK_HEADER_UNSET; } /** From cc748c30c72eb5b5c421de157358c8da8be999e9 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Sun, 4 Jun 2017 07:02:13 -0700 Subject: [PATCH 0049/2472] Add a null check in DummySurface static initializer ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=157958694 --- .../java/com/google/android/exoplayer2/video/DummySurface.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java index 5298c82f61..23d9941cf3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java @@ -76,7 +76,7 @@ public final class DummySurface extends Surface { if (Util.SDK_INT >= 17) { EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY); String extensions = EGL14.eglQueryString(display, EGL10.EGL_EXTENSIONS); - SECURE_SUPPORTED = extensions.contains("EGL_EXT_protected_content"); + SECURE_SUPPORTED = extensions != null && extensions.contains("EGL_EXT_protected_content"); } else { SECURE_SUPPORTED = false; } From df5e75b76c236e941c24dbc4f769cdc1fd369a9a Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Mon, 5 Jun 2017 05:28:01 -0700 Subject: [PATCH 0050/2472] Pick the lowest quality video when capabilities are exceeded Issue:#2901 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=158006727 --- .../exoplayer2/trackselection/DefaultTrackSelector.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index b37088e588..2a426c9c52 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -639,7 +639,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { continue; } int trackScore = isWithinConstraints ? 2 : 1; - if (isSupported(trackFormatSupport[trackIndex], false)) { + boolean isWithinCapabilities = isSupported(trackFormatSupport[trackIndex], false); + if (isWithinCapabilities) { trackScore += WITHIN_RENDERER_CAPABILITIES_BONUS; } boolean selectTrack = trackScore > selectedTrackScore; @@ -655,7 +656,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { } else { comparisonResult = compareFormatValues(format.bitrate, selectedBitrate); } - selectTrack = isWithinConstraints ? comparisonResult > 0 : comparisonResult < 0; + selectTrack = isWithinCapabilities && isWithinConstraints + ? comparisonResult > 0 : comparisonResult < 0; } if (selectTrack) { selectedGroup = trackGroup; From 8241bb8a6e5f91e0dfa70800f8e187cfd9318c23 Mon Sep 17 00:00:00 2001 From: hoangtc Date: Tue, 6 Jun 2017 06:58:07 -0700 Subject: [PATCH 0051/2472] Fix a minor bug with AdaptiveTrackSelection. When updating track selection, we should only revert back from ideal track selection to current track selection if the currently selected track is not black-listed. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=158135644 --- .../exoplayer2/trackselection/AdaptiveTrackSelection.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java index dc78e28e56..50eaaa02e3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java @@ -156,13 +156,13 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { long nowMs = SystemClock.elapsedRealtime(); // Get the current and ideal selections. int currentSelectedIndex = selectedIndex; - Format currentFormat = getSelectedFormat(); int idealSelectedIndex = determineIdealSelectedIndex(nowMs); - Format idealFormat = getFormat(idealSelectedIndex); // Assume we can switch to the ideal selection. selectedIndex = idealSelectedIndex; // Revert back to the current selection if conditions are not suitable for switching. - if (currentFormat != null && !isBlacklisted(selectedIndex, nowMs)) { + if (!isBlacklisted(currentSelectedIndex, nowMs)) { + Format currentFormat = getFormat(currentSelectedIndex); + Format idealFormat = getFormat(idealSelectedIndex); if (idealFormat.bitrate > currentFormat.bitrate && bufferedDurationUs < minDurationForQualityIncreaseUs) { // The ideal track is a higher quality, but we have insufficient buffer to safely switch From 79048ffae67c44e08330bd3b9328ca0f95267cc9 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 6 Jun 2017 07:31:01 -0700 Subject: [PATCH 0052/2472] Further cleanup of updateSelectedTrack - Return early if the selection is unchanged. - Remove unnecessary variables. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=158138187 --- .../AdaptiveTrackSelection.java | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java index 50eaaa02e3..12f5952dd0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java @@ -154,23 +154,24 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { @Override public void updateSelectedTrack(long bufferedDurationUs) { long nowMs = SystemClock.elapsedRealtime(); - // Get the current and ideal selections. + // Stash the current selection, then make a new one. int currentSelectedIndex = selectedIndex; - int idealSelectedIndex = determineIdealSelectedIndex(nowMs); - // Assume we can switch to the ideal selection. - selectedIndex = idealSelectedIndex; - // Revert back to the current selection if conditions are not suitable for switching. + selectedIndex = determineIdealSelectedIndex(nowMs); + if (selectedIndex == currentSelectedIndex) { + return; + } if (!isBlacklisted(currentSelectedIndex, nowMs)) { + // Revert back to the current selection if conditions are not suitable for switching. Format currentFormat = getFormat(currentSelectedIndex); - Format idealFormat = getFormat(idealSelectedIndex); - if (idealFormat.bitrate > currentFormat.bitrate + Format selectedFormat = getFormat(selectedIndex); + if (selectedFormat.bitrate > currentFormat.bitrate && bufferedDurationUs < minDurationForQualityIncreaseUs) { - // The ideal track is a higher quality, but we have insufficient buffer to safely switch + // The selected track is a higher quality, but we have insufficient buffer to safely switch // up. Defer switching up for now. selectedIndex = currentSelectedIndex; - } else if (idealFormat.bitrate < currentFormat.bitrate + } else if (selectedFormat.bitrate < currentFormat.bitrate && bufferedDurationUs >= maxDurationForQualityDecreaseUs) { - // The ideal track is a lower quality, but we have sufficient buffer to defer switching + // The selected track is a lower quality, but we have sufficient buffer to defer switching // down for now. selectedIndex = currentSelectedIndex; } From a0c884849e501e110b04cd9cfc363119985791fc Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 6 Jun 2017 08:03:04 -0700 Subject: [PATCH 0053/2472] For HLS mode, pick the lowest PID track for each track type This prevents strange behaviors for streams that changes the track declaration order in the PMT. NOTE: This should not change ANY behavior other than the one described above. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=158140890 --- .../exoplayer2/extractor/ts/TsExtractor.java | 35 +++++++++++++------ 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/library/core/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 index 71b8375bd8..7b63ce813c 100644 --- a/library/core/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 @@ -382,10 +382,14 @@ public final class TsExtractor implements Extractor { private static final int TS_PMT_DESC_DVBSUBS = 0x59; private final ParsableBitArray pmtScratch; + private final SparseArray trackIdToReaderScratch; + private final SparseIntArray trackIdToPidScratch; private final int pid; public PmtReader(int pid) { pmtScratch = new ParsableBitArray(new byte[5]); + trackIdToReaderScratch = new SparseArray<>(); + trackIdToPidScratch = new SparseIntArray(); this.pid = pid; } @@ -436,6 +440,8 @@ public final class TsExtractor implements Extractor { new TrackIdGenerator(programNumber, TS_STREAM_TYPE_ID3, MAX_PID_PLUS_ONE)); } + trackIdToReaderScratch.clear(); + trackIdToPidScratch.clear(); int remainingEntriesLength = sectionData.bytesLeft(); while (remainingEntriesLength > 0) { sectionData.readBytes(pmtScratch, 5); @@ -454,23 +460,30 @@ public final class TsExtractor implements Extractor { if (trackIds.get(trackId)) { continue; } - trackIds.put(trackId, true); - TsPayloadReader reader; - if (mode == MODE_HLS && streamType == TS_STREAM_TYPE_ID3) { - reader = id3Reader; - } else { - reader = payloadReaderFactory.createPayloadReader(streamType, esInfo); - if (reader != null) { + TsPayloadReader reader = mode == MODE_HLS && streamType == TS_STREAM_TYPE_ID3 ? id3Reader + : payloadReaderFactory.createPayloadReader(streamType, esInfo); + if (mode != MODE_HLS + || elementaryPid < trackIdToPidScratch.get(trackId, MAX_PID_PLUS_ONE)) { + trackIdToPidScratch.put(trackId, elementaryPid); + trackIdToReaderScratch.put(trackId, reader); + } + } + + int trackIdCount = trackIdToPidScratch.size(); + for (int i = 0; i < trackIdCount; i++) { + int trackId = trackIdToPidScratch.keyAt(i); + trackIds.put(trackId, true); + TsPayloadReader reader = trackIdToReaderScratch.valueAt(i); + if (reader != null) { + if (reader != id3Reader) { reader.init(timestampAdjuster, output, new TrackIdGenerator(programNumber, trackId, MAX_PID_PLUS_ONE)); } - } - - if (reader != null) { - tsPayloadReaders.put(elementaryPid, reader); + tsPayloadReaders.put(trackIdToPidScratch.valueAt(i), reader); } } + if (mode == MODE_HLS) { if (!tracksEnded) { output.endTracks(); From 643083194d2e520a443c24778f9a570d14d3787d Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 6 Jun 2017 08:15:25 -0700 Subject: [PATCH 0054/2472] Pass non-null logger into DefaultDrmSessionManager Issue: #2903 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=158142226 --- .../android/exoplayer2/demo/PlayerActivity.java | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) 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 34e0365933..d0703f3496 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 @@ -234,6 +234,13 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay Intent intent = getIntent(); boolean needNewPlayer = player == null; if (needNewPlayer) { + TrackSelection.Factory adaptiveTrackSelectionFactory = + new AdaptiveTrackSelection.Factory(BANDWIDTH_METER); + trackSelector = new DefaultTrackSelector(adaptiveTrackSelectionFactory); + trackSelectionHelper = new TrackSelectionHelper(trackSelector, adaptiveTrackSelectionFactory); + lastSeenTrackGroupArray = null; + eventLogger = new EventLogger(trackSelector); + UUID drmSchemeUuid = intent.hasExtra(DRM_SCHEME_UUID_EXTRA) ? UUID.fromString(intent.getStringExtra(DRM_SCHEME_UUID_EXTRA)) : null; DrmSessionManager drmSessionManager = null; @@ -261,16 +268,8 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(this, drmSessionManager, extensionRendererMode); - TrackSelection.Factory adaptiveTrackSelectionFactory = - new AdaptiveTrackSelection.Factory(BANDWIDTH_METER); - trackSelector = new DefaultTrackSelector(adaptiveTrackSelectionFactory); - trackSelectionHelper = new TrackSelectionHelper(trackSelector, adaptiveTrackSelectionFactory); - lastSeenTrackGroupArray = null; - player = ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector); player.addListener(this); - - eventLogger = new EventLogger(trackSelector); player.addListener(eventLogger); player.setAudioDebugListener(eventLogger); player.setVideoDebugListener(eventLogger); From 1ac8420b7fea5e695bbf4dce3b373b7c00b34d6d Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 6 Jun 2017 08:21:38 -0700 Subject: [PATCH 0055/2472] Constraint buffered percentage to [0,100] Issue: #2902 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=158142754 --- .../java/com/google/android/exoplayer2/ExoPlayerImpl.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index cb0958a3b1..c70d729fc3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -305,10 +305,10 @@ import java.util.concurrent.CopyOnWriteArraySet; if (timeline.isEmpty()) { return 0; } - long bufferedPosition = getBufferedPosition(); + long position = getBufferedPosition(); long duration = getDuration(); - return (bufferedPosition == C.TIME_UNSET || duration == C.TIME_UNSET) ? 0 - : (int) (duration == 0 ? 100 : (bufferedPosition * 100) / duration); + return position == C.TIME_UNSET || duration == C.TIME_UNSET ? 0 + : (duration == 0 ? 100 : Util.constrainValue((int) ((position * 100) / duration), 0, 100)); } @Override From fb7cb507ea6440a12b674a2e38a3d94fca833cf1 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 6 Jun 2017 09:29:24 -0700 Subject: [PATCH 0056/2472] Expose current scrubber position through onScrubStart Issue: #2910 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=158149904 --- .../java/com/google/android/exoplayer2/ui/DefaultTimeBar.java | 2 +- .../com/google/android/exoplayer2/ui/PlaybackControlView.java | 2 +- .../main/java/com/google/android/exoplayer2/ui/TimeBar.java | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) 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 index d9754420bf..4ede786175 100644 --- 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 @@ -442,7 +442,7 @@ public class DefaultTimeBar extends View implements TimeBar { parent.requestDisallowInterceptTouchEvent(true); } if (listener != null) { - listener.onScrubStart(this); + listener.onScrubStart(this, getScrubberPosition()); } } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java index ce2e81020f..5f88f3a241 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java @@ -875,7 +875,7 @@ public class PlaybackControlView extends FrameLayout { OnClickListener { @Override - public void onScrubStart(TimeBar timeBar) { + public void onScrubStart(TimeBar timeBar, long position) { removeCallbacks(hideAction); scrubbing = true; } 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 index aeb8e0255e..2fd5bff5eb 100644 --- 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 @@ -95,8 +95,9 @@ public interface TimeBar { * Called when the user starts moving the scrubber. * * @param timeBar The time bar. + * @param position The position of the scrubber, in milliseconds. */ - void onScrubStart(TimeBar timeBar); + void onScrubStart(TimeBar timeBar, long position); /** * Called when the user moves the scrubber. From df99922ac13d273ac51d982b733ea5c771f2e155 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 6 Jun 2017 10:03:08 -0700 Subject: [PATCH 0057/2472] Bump version + update release notes ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=158153988 --- RELEASENOTES.md | 14 ++++++++++++++ build.gradle | 2 +- demo/src/main/AndroidManifest.xml | 4 ++-- .../android/exoplayer2/ExoPlayerLibraryInfo.java | 6 +++--- 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 6a1defa809..4f147e2bbd 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,19 @@ # Release notes # +### r2.4.2 ### + +* Stability: Work around Nexus 10 reboot when playing certain content + ([2806](https://github.com/google/ExoPlayer/issues/2806)). +* MP3: Correctly treat MP3s with INFO headers as constant bitrate + ([2895](https://github.com/google/ExoPlayer/issues/2895)). +* HLS: Use average rather than peak bandwidth when available + ([#2863](https://github.com/google/ExoPlayer/issues/2863)). +* SmoothStreaming: Fix timeline for live streams + ([#2760](https://github.com/google/ExoPlayer/issues/2760)). +* UI: Fix DefaultTimeBar invalidation + ([#2871](https://github.com/google/ExoPlayer/issues/2871)). +* Misc bugfixes. + ### r2.4.1 ### * Stability: Avoid OutOfMemoryError in extractors when parsing malformed media diff --git a/build.gradle b/build.gradle index 258b11d2e6..4f18e7c801 100644 --- a/build.gradle +++ b/build.gradle @@ -48,7 +48,7 @@ allprojects { releaseRepoName = getBintrayRepo() releaseUserOrg = 'google' releaseGroupId = 'com.google.android.exoplayer' - releaseVersion = 'r2.4.1' + releaseVersion = 'r2.4.2' releaseWebsite = 'https://github.com/google/ExoPlayer' } if (it.hasProperty('externalBuildDir')) { diff --git a/demo/src/main/AndroidManifest.xml b/demo/src/main/AndroidManifest.xml index 1bb859028d..34256d41c1 100644 --- a/demo/src/main/AndroidManifest.xml +++ b/demo/src/main/AndroidManifest.xml @@ -16,8 +16,8 @@ + android:versionCode="2402" + android:versionName="2.4.2"> diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java index 23c2ddbde9..c6fc139208 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java @@ -24,13 +24,13 @@ public interface ExoPlayerLibraryInfo { * The version of the library expressed as a string, for example "1.2.3". */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. - String VERSION = "2.4.1"; + String VERSION = "2.4.2"; /** * 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.1"; + String VERSION_SLASHY = "ExoPlayerLib/2.4.2"; /** * The version of the library expressed as an integer, for example 1002003. @@ -40,7 +40,7 @@ public interface ExoPlayerLibraryInfo { * integer version 123045006 (123-045-006). */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - int VERSION_INT = 2004001; + int VERSION_INT = 2004002; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} From 4510abf278bd5a8bf8eb2e8b310e701080cdce07 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 7 Jun 2017 08:01:28 -0700 Subject: [PATCH 0058/2472] Update handled schemes for timing element resolution. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=158269487 --- .../exoplayer2/source/dash/DashMediaSource.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/library/dash/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 index 5ab04ea7be..cd995f3739 100644 --- a/library/dash/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 @@ -410,12 +410,14 @@ public final class DashMediaSource implements MediaSource { private void resolveUtcTimingElement(UtcTimingElement timingElement) { String scheme = timingElement.schemeIdUri; - if (Util.areEqual(scheme, "urn:mpeg:dash:utc:direct:2012")) { + if (Util.areEqual(scheme, "urn:mpeg:dash:utc:direct:2014") + || Util.areEqual(scheme, "urn:mpeg:dash:utc:direct:2012")) { resolveUtcTimingElementDirect(timingElement); - } else if (Util.areEqual(scheme, "urn:mpeg:dash:utc:http-iso:2014")) { + } else if (Util.areEqual(scheme, "urn:mpeg:dash:utc:http-iso:2014") + || Util.areEqual(scheme, "urn:mpeg:dash:utc:http-iso:2012")) { resolveUtcTimingElementHttp(timingElement, new Iso8601Parser()); - } else if (Util.areEqual(scheme, "urn:mpeg:dash:utc:http-xsdate:2012") - || Util.areEqual(scheme, "urn:mpeg:dash:utc:http-xsdate:2014")) { + } else if (Util.areEqual(scheme, "urn:mpeg:dash:utc:http-xsdate:2014") + || Util.areEqual(scheme, "urn:mpeg:dash:utc:http-xsdate:2012")) { resolveUtcTimingElementHttp(timingElement, new XsDateTimeParser()); } else { // Unsupported scheme. From 80be637dcc6c13110f157e07e7c28ba8d2d05b26 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 12 Jun 2017 01:37:01 -0700 Subject: [PATCH 0059/2472] Allow overriding of getCodecMaxValues ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=158686545 --- .../android/exoplayer2/video/MediaCodecVideoRenderer.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index dd0c5356ea..0304c33b3c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -650,7 +650,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { * @return Suitable {@link CodecMaxValues}. * @throws DecoderQueryException If an error occurs querying {@code codecInfo}. */ - private static CodecMaxValues getCodecMaxValues(MediaCodecInfo codecInfo, Format format, + protected CodecMaxValues getCodecMaxValues(MediaCodecInfo codecInfo, Format format, Format[] streamFormats) throws DecoderQueryException { int maxWidth = format.width; int maxHeight = format.height; @@ -838,7 +838,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { return format.rotationDegrees == Format.NO_VALUE ? 0 : format.rotationDegrees; } - private static final class CodecMaxValues { + protected static final class CodecMaxValues { public final int width; public final int height; From 4e578a1b2164c4c0a358f6ae28153383e133473f Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 14 Jun 2017 03:34:19 -0700 Subject: [PATCH 0060/2472] Increase MP3 sniffing distance Issue: #2951 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=158960483 --- .../google/android/exoplayer2/extractor/mp3/Mp3Extractor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/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 index 6e114137f1..8d33f95640 100644 --- a/library/core/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 @@ -78,7 +78,7 @@ public final class Mp3Extractor implements Extractor { /** * The maximum number of bytes to peek when sniffing, excluding the ID3 header, before giving up. */ - private static final int MAX_SNIFF_BYTES = MpegAudioHeader.MAX_FRAME_SIZE_BYTES; + private static final int MAX_SNIFF_BYTES = 16 * 1024; /** * Maximum length of data read into {@link #scratch}. */ From facfa5267787fd51e3ab04b89ad13c763e8cbc41 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 14 Jun 2017 07:56:37 -0700 Subject: [PATCH 0061/2472] Log frame counts when we see a spurious audio timestamp ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=158977741 --- .../java/com/google/android/exoplayer2/audio/AudioTrack.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java index 44a96373f3..92838e34b0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java @@ -1292,7 +1292,7 @@ public final class AudioTrack { // The timestamp time base is probably wrong. String message = "Spurious audio timestamp (system clock mismatch): " + audioTimestampFramePosition + ", " + audioTimestampUs + ", " + systemClockUs + ", " - + playbackPositionUs; + + playbackPositionUs + ", " + getSubmittedFrames() + ", " + getWrittenFrames(); if (failOnSpuriousAudioTimestamp) { throw new InvalidAudioTrackTimestampException(message); } @@ -1303,7 +1303,7 @@ public final class AudioTrack { // The timestamp frame position is probably wrong. String message = "Spurious audio timestamp (frame position mismatch): " + audioTimestampFramePosition + ", " + audioTimestampUs + ", " + systemClockUs + ", " - + playbackPositionUs; + + playbackPositionUs + ", " + getSubmittedFrames() + ", " + getWrittenFrames(); if (failOnSpuriousAudioTimestamp) { throw new InvalidAudioTrackTimestampException(message); } From e8ee868a9f65a1e9013dee2729b46df9aa54a83b Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 15 Jun 2017 02:36:46 -0700 Subject: [PATCH 0062/2472] Add support for mono input to the GVR extension Issue: #2710 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=159082518 --- extensions/gvr/build.gradle | 2 +- .../google/android/exoplayer2/ext/gvr/GvrAudioProcessor.java | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/extensions/gvr/build.gradle b/extensions/gvr/build.gradle index f622a73758..e15c8b1ad8 100644 --- a/extensions/gvr/build.gradle +++ b/extensions/gvr/build.gradle @@ -25,7 +25,7 @@ android { dependencies { compile project(':library-core') - compile 'com.google.vr:sdk-audio:1.30.0' + compile 'com.google.vr:sdk-audio:1.60.1' } ext { 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 980424904d..a56bc7f0a9 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 @@ -82,6 +82,9 @@ public final class GvrAudioProcessor implements AudioProcessor { maybeReleaseGvrAudioSurround(); int surroundFormat; switch (channelCount) { + case 1: + surroundFormat = GvrAudioSurround.SurroundFormat.SURROUND_MONO; + break; case 2: surroundFormat = GvrAudioSurround.SurroundFormat.SURROUND_STEREO; break; From e618080c7385e9ddf7217f668fd5635c86e34dec Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 7 Apr 2017 07:08:04 +0100 Subject: [PATCH 0063/2472] Adjust incorrect looking max-channel counts Issue: #2940 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=159099602 --- .../extractor/flv/AudioTagPayloadReader.java | 2 +- .../exoplayer2/mediacodec/MediaCodecInfo.java | 38 ++++++++++++++++++- .../android/exoplayer2/util/MimeTypes.java | 5 ++- 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/library/core/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 index 8e3bd08375..2f21898007 100644 --- a/library/core/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 @@ -67,7 +67,7 @@ import java.util.Collections; hasOutputFormat = true; } else if (audioFormat == AUDIO_FORMAT_ALAW || audioFormat == AUDIO_FORMAT_ULAW) { String type = audioFormat == AUDIO_FORMAT_ALAW ? MimeTypes.AUDIO_ALAW - : MimeTypes.AUDIO_ULAW; + : MimeTypes.AUDIO_MLAW; int pcmEncoding = (header & 0x01) == 1 ? C.ENCODING_PCM_16BIT : C.ENCODING_PCM_8BIT; Format format = Format.createAudioSampleFormat(null, type, null, Format.NO_VALUE, Format.NO_VALUE, 1, 8000, pcmEncoding, null, null, 0, null); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java index 3c788a60a4..a7c237edb2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java @@ -264,7 +264,9 @@ public final class MediaCodecInfo { logNoSupport("channelCount.aCaps"); return false; } - if (audioCapabilities.getMaxInputChannelCount() < channelCount) { + int maxInputChannelCount = adjustMaxInputChannelCount(name, mimeType, + audioCapabilities.getMaxInputChannelCount()); + if (maxInputChannelCount < channelCount) { logNoSupport("channelCount.support, " + channelCount); return false; } @@ -281,6 +283,40 @@ public final class MediaCodecInfo { + Util.DEVICE_DEBUG_INFO + "]"); } + private static int adjustMaxInputChannelCount(String name, String mimeType, int maxChannelCount) { + if (maxChannelCount > 1 || (Util.SDK_INT >= 26 && maxChannelCount > 0)) { + // The maximum channel count looks like it's been set correctly. + return maxChannelCount; + } + if (MimeTypes.AUDIO_MPEG.equals(mimeType) + || MimeTypes.AUDIO_AMR_NB.equals(mimeType) + || MimeTypes.AUDIO_AMR_WB.equals(mimeType) + || MimeTypes.AUDIO_AAC.equals(mimeType) + || MimeTypes.AUDIO_VORBIS.equals(mimeType) + || MimeTypes.AUDIO_OPUS.equals(mimeType) + || MimeTypes.AUDIO_RAW.equals(mimeType) + || MimeTypes.AUDIO_FLAC.equals(mimeType) + || MimeTypes.AUDIO_ALAW.equals(mimeType) + || MimeTypes.AUDIO_MLAW.equals(mimeType) + || MimeTypes.AUDIO_MSGSM.equals(mimeType)) { + // Platform code should have set a default. + return maxChannelCount; + } + // The maximum channel count looks incorrect. Adjust it to an assumed default. + int assumedMaxChannelCount; + if (MimeTypes.AUDIO_AC3.equals(mimeType)) { + assumedMaxChannelCount = 6; + } else if (MimeTypes.AUDIO_E_AC3.equals(mimeType)) { + assumedMaxChannelCount = 16; + } else { + // Default to the platform limit, which is 30. + assumedMaxChannelCount = 30; + } + Log.w(TAG, "AssumedMaxChannelAdjustment: " + name + ", [" + maxChannelCount + " to " + + assumedMaxChannelCount + "]"); + return assumedMaxChannelCount; + } + private static boolean isAdaptive(CodecCapabilities capabilities) { return Util.SDK_INT >= 19 && isAdaptiveV19(capabilities); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java b/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java index e227ea1068..db1122dbe7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java @@ -48,7 +48,7 @@ public final class MimeTypes { public static final String AUDIO_MPEG_L2 = BASE_TYPE_AUDIO + "/mpeg-L2"; public static final String AUDIO_RAW = BASE_TYPE_AUDIO + "/raw"; public static final String AUDIO_ALAW = BASE_TYPE_AUDIO + "/g711-alaw"; - public static final String AUDIO_ULAW = BASE_TYPE_AUDIO + "/g711-mlaw"; + public static final String AUDIO_MLAW = BASE_TYPE_AUDIO + "/g711-mlaw"; public static final String AUDIO_AC3 = BASE_TYPE_AUDIO + "/ac3"; public static final String AUDIO_E_AC3 = BASE_TYPE_AUDIO + "/eac3"; public static final String AUDIO_TRUEHD = BASE_TYPE_AUDIO + "/true-hd"; @@ -59,8 +59,9 @@ public final class MimeTypes { public static final String AUDIO_OPUS = BASE_TYPE_AUDIO + "/opus"; public static final String AUDIO_AMR_NB = BASE_TYPE_AUDIO + "/3gpp"; public static final String AUDIO_AMR_WB = BASE_TYPE_AUDIO + "/amr-wb"; - public static final String AUDIO_FLAC = BASE_TYPE_AUDIO + "/x-flac"; + public static final String AUDIO_FLAC = BASE_TYPE_AUDIO + "/flac"; public static final String AUDIO_ALAC = BASE_TYPE_AUDIO + "/alac"; + public static final String AUDIO_MSGSM = BASE_TYPE_AUDIO + "/gsm"; public static final String AUDIO_UNKNOWN = BASE_TYPE_AUDIO + "/x-unknown"; public static final String TEXT_VTT = BASE_TYPE_TEXT + "/vtt"; From 92206b9fc2434027cd524be1901c13b8131d5eeb Mon Sep 17 00:00:00 2001 From: olly Date: Sun, 7 May 2017 05:37:26 +0100 Subject: [PATCH 0064/2472] TTML: Ignore regions that don't declare origin and extent Issue: #2953 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=159218386 --- .../exoplayer2/text/ttml/TtmlDecoderTest.java | 10 ++++++--- .../exoplayer2/text/ttml/TtmlDecoder.java | 22 +++++++++++++------ 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/library/core/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 index 496e3f87de..492cf036b4 100644 --- a/library/core/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 @@ -179,9 +179,13 @@ public final class TtmlDecoderTest extends InstrumentationTestCase { assertEquals(1, output.size()); ttmlCue = output.get(0); assertEquals("dolor", ttmlCue.text.toString()); - assertEquals(10f / 100f, ttmlCue.position); - assertEquals(80f / 100f, ttmlCue.line); - assertEquals(1f, ttmlCue.size); + assertEquals(Cue.DIMEN_UNSET, ttmlCue.position); + assertEquals(Cue.DIMEN_UNSET, ttmlCue.line); + assertEquals(Cue.DIMEN_UNSET, ttmlCue.size); + // TODO: Should be as below, once https://github.com/google/ExoPlayer/issues/2953 is fixed. + // assertEquals(10f / 100f, ttmlCue.position); + // assertEquals(80f / 100f, ttmlCue.line); + // assertEquals(1f, ttmlCue.size); output = subtitle.getCues(21000000); assertEquals(1, output.size()); diff --git a/library/core/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 index 0012ce2c22..e438aa1837 100644 --- a/library/core/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 @@ -222,9 +222,9 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { /** * Parses a region declaration. *

    - * If the region defines an origin and/or extent, it is required that they're defined as - * percentages of the viewport. Region declarations that define origin and/or extent in other - * formats are unsupported, and null is returned. + * If the region defines an origin and extent, it is required that they're defined as percentages + * of the viewport. Region declarations that define origin and extent in other formats are + * unsupported, and null is returned. */ private TtmlRegion parseRegionAttributes(XmlPullParser xmlParser) { String regionId = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_ID); @@ -250,9 +250,13 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { return null; } } else { + Log.w(TAG, "Ignoring region without an origin"); + return null; + // TODO: Should default to top left as below in this case, but need to fix + // https://github.com/google/ExoPlayer/issues/2953 first. // Origin is omitted. Default to top left. - position = 0; - line = 0; + // position = 0; + // line = 0; } float width; @@ -273,9 +277,13 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { return null; } } else { + Log.w(TAG, "Ignoring region without an extent"); + return null; + // TODO: Should default to extent of parent as below in this case, but need to fix + // https://github.com/google/ExoPlayer/issues/2953 first. // Extent is omitted. Default to extent of parent. - width = 1; - height = 1; + // width = 1; + // height = 1; } @Cue.AnchorType int lineAnchor = Cue.ANCHOR_TYPE_START; From 76faa5b6d2d271474bcae6ee106177aa53de3ed3 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 19 Jun 2017 06:48:18 -0700 Subject: [PATCH 0065/2472] Move clearing of joining deadline back to onStopped ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=159421000 --- .../google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java | 2 +- .../android/exoplayer2/video/MediaCodecVideoRenderer.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 4b629c8d2a..29423547b6 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 @@ -480,11 +480,11 @@ public final class LibvpxVideoRenderer extends BaseRenderer { protected void onStarted() { droppedFrames = 0; droppedFrameAccumulationStartTimeMs = SystemClock.elapsedRealtime(); - joiningDeadlineMs = C.TIME_UNSET; } @Override protected void onStopped() { + joiningDeadlineMs = C.TIME_UNSET; maybeNotifyDroppedFrames(); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index 0304c33b3c..990a29dc4e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -260,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(); } From 4b8cddbefd7f212b65b78c612ae7b12053bdb882 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karol=20Wr=C3=B3tniak?= Date: Sat, 17 Jun 2017 16:18:43 +0200 Subject: [PATCH 0066/2472] Introduced failing unit test for ContentDataSource --- .../core/src/androidTest/AndroidManifest.xml | 3 + .../upstream/AndroidDataSourceTest.java | 31 +++++++++ .../exoplayer2/upstream/TestDataProvider.java | 64 +++++++++++++++++++ 3 files changed, 98 insertions(+) create mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/AndroidDataSourceTest.java create mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/TestDataProvider.java diff --git a/library/core/src/androidTest/AndroidManifest.xml b/library/core/src/androidTest/AndroidManifest.xml index 9eab386b51..8968ae59d6 100644 --- a/library/core/src/androidTest/AndroidManifest.xml +++ b/library/core/src/androidTest/AndroidManifest.xml @@ -24,6 +24,9 @@ android:allowBackup="false" tools:ignore="MissingApplicationIcon,HardcodedDebugMode"> + Date: Sat, 17 Jun 2017 16:26:29 +0200 Subject: [PATCH 0067/2472] InputStream creation for ContentDataSource changed --- .../google/android/exoplayer2/upstream/ContentDataSource.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java index f806f47410..5d0d9a80e9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java @@ -71,7 +71,7 @@ public final class ContentDataSource implements DataSource { try { uri = dataSpec.uri; assetFileDescriptor = resolver.openAssetFileDescriptor(uri, "r"); - inputStream = new FileInputStream(assetFileDescriptor.getFileDescriptor()); + inputStream = assetFileDescriptor.createInputStream(); long skipped = inputStream.skip(dataSpec.position); if (skipped < dataSpec.position) { // We expect the skip to be satisfied in full. If it isn't then we're probably trying to From 66c461e65b57e289189846580ba42672539210e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karol=20Wr=C3=B3tniak?= Date: Mon, 19 Jun 2017 12:46:09 +0200 Subject: [PATCH 0068/2472] Comments from https://github.com/google/ExoPlayer/pull/2963#discussion_r122669328 applied --- .../upstream/AndroidDataSourceConstants.java | 8 +++++ .../upstream/AndroidDataSourceTest.java | 31 ------------------- .../upstream/AssetDataSourceTest.java | 21 +++++++++++++ .../upstream/ContentDataSourceTest.java | 21 +++++++++++++ .../upstream/ContentDataSource.java | 18 ++++++----- 5 files changed, 61 insertions(+), 38 deletions(-) create mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/AndroidDataSourceConstants.java delete mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/AndroidDataSourceTest.java create mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/AssetDataSourceTest.java create mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/AndroidDataSourceConstants.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/AndroidDataSourceConstants.java new file mode 100644 index 0000000000..ad19b7a824 --- /dev/null +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/AndroidDataSourceConstants.java @@ -0,0 +1,8 @@ +package com.google.android.exoplayer2.upstream; + +final class AndroidDataSourceConstants { + static final long SAMPLE_MP4_BYTES = 101597; + static final String SAMPLE_MP4_PATH = "/mp4/sample.mp4"; + + private AndroidDataSourceConstants() {} +} diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/AndroidDataSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/AndroidDataSourceTest.java deleted file mode 100644 index 42dadaf379..0000000000 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/AndroidDataSourceTest.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.google.android.exoplayer2.upstream; - -import android.content.Context; -import android.net.Uri; -import android.test.InstrumentationTestCase; - -public class AndroidDataSourceTest extends InstrumentationTestCase { - - private static final long SAMPLE_MP4_BYTES = 101597; - private static final String SAMPLE_MP4_PATH = "/mp4/sample.mp4"; - - public void testAssetDataSource() throws Exception { - final Context context = getInstrumentation().getContext(); - AssetDataSource dataSource = new AssetDataSource(context); - Uri assetUri = Uri.parse("file:///android_asset" + SAMPLE_MP4_PATH); - DataSpec dataSpec = new DataSpec(assetUri); - long sourceLengthBytes = dataSource.open(dataSpec); - - assertEquals(SAMPLE_MP4_BYTES, sourceLengthBytes); - } - - public void testContentDataSource() throws Exception { - Context context = getInstrumentation().getContext(); - ContentDataSource dataSource = new ContentDataSource(context); - Uri contentUri = Uri.parse("content://exoplayer" + SAMPLE_MP4_PATH); - DataSpec dataSpec = new DataSpec(contentUri); - long sourceLengthBytes = dataSource.open(dataSpec); - - assertEquals(SAMPLE_MP4_BYTES, sourceLengthBytes); - } -} diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/AssetDataSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/AssetDataSourceTest.java new file mode 100644 index 0000000000..178842bd4d --- /dev/null +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/AssetDataSourceTest.java @@ -0,0 +1,21 @@ +package com.google.android.exoplayer2.upstream; + +import android.content.Context; +import android.net.Uri; +import android.test.InstrumentationTestCase; + +import static com.google.android.exoplayer2.upstream.AndroidDataSourceConstants.SAMPLE_MP4_BYTES; +import static com.google.android.exoplayer2.upstream.AndroidDataSourceConstants.SAMPLE_MP4_PATH; + +public class AssetDataSourceTest extends InstrumentationTestCase { + + public void testAssetDataSource() throws Exception { + final Context context = getInstrumentation().getContext(); + AssetDataSource dataSource = new AssetDataSource(context); + Uri assetUri = Uri.parse("file:///android_asset" + SAMPLE_MP4_PATH); + DataSpec dataSpec = new DataSpec(assetUri); + long sourceLengthBytes = dataSource.open(dataSpec); + + assertEquals(SAMPLE_MP4_BYTES, sourceLengthBytes); + } +} diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java new file mode 100644 index 0000000000..b2edeea0cc --- /dev/null +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java @@ -0,0 +1,21 @@ +package com.google.android.exoplayer2.upstream; + +import android.content.Context; +import android.net.Uri; +import android.test.InstrumentationTestCase; + +import static com.google.android.exoplayer2.upstream.AndroidDataSourceConstants.SAMPLE_MP4_BYTES; +import static com.google.android.exoplayer2.upstream.AndroidDataSourceConstants.SAMPLE_MP4_PATH; + +public class ContentDataSourceTest extends InstrumentationTestCase { + + public void testContentDataSource() throws Exception { + Context context = getInstrumentation().getContext(); + ContentDataSource dataSource = new ContentDataSource(context); + Uri contentUri = Uri.parse("content://exoplayer" + SAMPLE_MP4_PATH); + DataSpec dataSpec = new DataSpec(contentUri); + long sourceLengthBytes = dataSource.open(dataSpec); + + assertEquals(SAMPLE_MP4_BYTES, sourceLengthBytes); + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java index 5d0d9a80e9..9421b7ba03 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java @@ -71,7 +71,7 @@ public final class ContentDataSource implements DataSource { try { uri = dataSpec.uri; assetFileDescriptor = resolver.openAssetFileDescriptor(uri, "r"); - inputStream = assetFileDescriptor.createInputStream(); + inputStream = new FileInputStream(assetFileDescriptor.getFileDescriptor()); long skipped = inputStream.skip(dataSpec.position); if (skipped < dataSpec.position) { // We expect the skip to be satisfied in full. If it isn't then we're probably trying to @@ -81,12 +81,16 @@ public final class ContentDataSource implements DataSource { if (dataSpec.length != C.LENGTH_UNSET) { bytesRemaining = dataSpec.length; } else { - bytesRemaining = inputStream.available(); - if (bytesRemaining == 0) { - // FileInputStream.available() returns 0 if the remaining length cannot be determined, or - // if it's greater than Integer.MAX_VALUE. We don't know the true length in either case, - // so treat as unbounded. - bytesRemaining = C.LENGTH_UNSET; + bytesRemaining = assetFileDescriptor.getLength(); + if (bytesRemaining == AssetFileDescriptor.UNKNOWN_LENGTH) { + // The asset must extend to the end of the file. + bytesRemaining = inputStream.available(); + if (bytesRemaining == 0) { + // FileInputStream.available() returns 0 if the remaining length cannot be determined, or + // if it's greater than Integer.MAX_VALUE. We don't know the true length in either case, + // so treat as unbounded. + bytesRemaining = C.LENGTH_UNSET; + } } } } catch (IOException e) { From 795e3be44029878b5772014f92a5fb435b335299 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karol=20Wr=C3=B3tniak?= Date: Mon, 19 Jun 2017 16:09:54 +0200 Subject: [PATCH 0069/2472] null AssetFileDescriptors support added in `ContentDataSource` --- .../upstream/AndroidDataSourceConstants.java | 8 +++++++ .../upstream/ContentDataSourceTest.java | 23 +++++++++++++++++-- .../exoplayer2/upstream/TestDataProvider.java | 4 ++++ .../upstream/ContentDataSource.java | 4 ++++ 4 files changed, 37 insertions(+), 2 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/AndroidDataSourceConstants.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/AndroidDataSourceConstants.java index ad19b7a824..d11202ccf2 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/AndroidDataSourceConstants.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/AndroidDataSourceConstants.java @@ -1,8 +1,16 @@ package com.google.android.exoplayer2.upstream; +import android.content.ContentResolver; +import android.net.Uri; + final class AndroidDataSourceConstants { static final long SAMPLE_MP4_BYTES = 101597; static final String SAMPLE_MP4_PATH = "/mp4/sample.mp4"; + static final String TEST_DATA_PROVIDER_AUTHORITY = "exoplayer"; + static final Uri NULL_DESCRIPTOR_URI = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(TEST_DATA_PROVIDER_AUTHORITY) + .build(); private AndroidDataSourceConstants() {} } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java index b2edeea0cc..1cd14b45e1 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java @@ -1,21 +1,40 @@ package com.google.android.exoplayer2.upstream; +import android.content.ContentResolver; import android.content.Context; import android.net.Uri; import android.test.InstrumentationTestCase; +import static com.google.android.exoplayer2.upstream.AndroidDataSourceConstants.NULL_DESCRIPTOR_URI; import static com.google.android.exoplayer2.upstream.AndroidDataSourceConstants.SAMPLE_MP4_BYTES; import static com.google.android.exoplayer2.upstream.AndroidDataSourceConstants.SAMPLE_MP4_PATH; +import static com.google.android.exoplayer2.upstream.AndroidDataSourceConstants.TEST_DATA_PROVIDER_AUTHORITY; public class ContentDataSourceTest extends InstrumentationTestCase { - public void testContentDataSource() throws Exception { + public void testValidContentDataSource() throws Exception { Context context = getInstrumentation().getContext(); ContentDataSource dataSource = new ContentDataSource(context); - Uri contentUri = Uri.parse("content://exoplayer" + SAMPLE_MP4_PATH); + Uri contentUri = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(TEST_DATA_PROVIDER_AUTHORITY) + .path(SAMPLE_MP4_PATH).build(); DataSpec dataSpec = new DataSpec(contentUri); long sourceLengthBytes = dataSource.open(dataSpec); assertEquals(SAMPLE_MP4_BYTES, sourceLengthBytes); } + + public void testNullContentDataSource() throws Exception { + Context context = getInstrumentation().getContext(); + ContentDataSource dataSource = new ContentDataSource(context); + DataSpec dataSpec = new DataSpec(NULL_DESCRIPTOR_URI); + + try { + dataSource.open(dataSpec); + fail("Expected exception not thrown."); + } catch (ContentDataSource.ContentDataSourceException e) { + // Expected. + } + } } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/TestDataProvider.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/TestDataProvider.java index 851e7b4b0c..f6e09a7067 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/TestDataProvider.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/TestDataProvider.java @@ -29,6 +29,10 @@ public class TestDataProvider extends ContentProvider { @Nullable @Override public AssetFileDescriptor openAssetFile(@NonNull final Uri uri, @NonNull final String mode) throws FileNotFoundException { + if (uri.equals(AndroidDataSourceConstants.NULL_DESCRIPTOR_URI)) { + return null; + } + try { Context context = getContext(); assertNotNull(context); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java index 9421b7ba03..3a9be552d2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java @@ -22,6 +22,7 @@ import android.net.Uri; import com.google.android.exoplayer2.C; import java.io.EOFException; import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; @@ -71,6 +72,9 @@ public final class ContentDataSource implements DataSource { try { uri = dataSpec.uri; assetFileDescriptor = resolver.openAssetFileDescriptor(uri, "r"); + if (assetFileDescriptor == null) { + throw new FileNotFoundException("Could not open file descriptor for: " + uri); + } inputStream = new FileInputStream(assetFileDescriptor.getFileDescriptor()); long skipped = inputStream.skip(dataSpec.position); if (skipped < dataSpec.position) { From 5eb64dbe1208a61c157d87f1e33fc33180ad85c3 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 23 Jun 2017 17:20:51 +0100 Subject: [PATCH 0070/2472] Fix ContentDataSource and enhance tests to validate read data --- .../core/src/androidTest/AndroidManifest.xml | 4 +- .../assets/binary/1024_incrementing_bytes.mp3 | Bin 0 -> 1024 bytes .../upstream/AndroidDataSourceConstants.java | 16 -- .../upstream/AssetDataSourceTest.java | 59 ++++++-- .../upstream/ContentDataSourceTest.java | 139 ++++++++++++++---- .../exoplayer2/upstream/TestDataProvider.java | 68 --------- .../upstream/ContentDataSource.java | 5 +- .../android/exoplayer2/testutil/TestUtil.java | 17 +++ 8 files changed, 185 insertions(+), 123 deletions(-) create mode 100644 library/core/src/androidTest/assets/binary/1024_incrementing_bytes.mp3 delete mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/AndroidDataSourceConstants.java delete mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/TestDataProvider.java diff --git a/library/core/src/androidTest/AndroidManifest.xml b/library/core/src/androidTest/AndroidManifest.xml index 8968ae59d6..a50de35b62 100644 --- a/library/core/src/androidTest/AndroidManifest.xml +++ b/library/core/src/androidTest/AndroidManifest.xml @@ -25,8 +25,8 @@ tools:ignore="MissingApplicationIcon,HardcodedDebugMode"> + android:authorities="com.google.android.exoplayer2.core.test" + android:name="com.google.android.exoplayer2.upstream.ContentDataSourceTest$TestContentProvider"/> MC+6cQE@6%&_`l#-T_m6KOcR8m$^Ra4i{)Y8_`)zddH zG%_|ZH8Z!cw6eCbwX=6{baHlab#wRd^z!!c_45x13RUz zF>}`JIdkXDU$Ah|;w4L$Enl&6)#^2C*R9{Mant54TeofBv2)k%J$v` Date: Fri, 23 Jun 2017 17:37:28 +0100 Subject: [PATCH 0071/2472] Mini cleanup --- .../exoplayer2/upstream/ContentDataSourceTest.java | 10 ++++++---- .../android/exoplayer2/upstream/ContentDataSource.java | 6 +++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java index 10a408c578..d8743a0a2c 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java @@ -33,6 +33,7 @@ import java.io.IOException; */ public final class ContentDataSourceTest extends InstrumentationTestCase { + private static final String AUTHORITY = "com.google.android.exoplayer2.core.test"; private static final String DATA_PATH = "binary/1024_incrementing_bytes.mp3"; private static final long DATA_LENGTH = 1024; @@ -40,7 +41,7 @@ public final class ContentDataSourceTest extends InstrumentationTestCase { ContentDataSource dataSource = new ContentDataSource(getInstrumentation().getContext()); Uri contentUri = new Uri.Builder() .scheme(ContentResolver.SCHEME_CONTENT) - .authority(TestContentProvider.AUTHORITY) + .authority(AUTHORITY) .path(DATA_PATH).build(); DataSpec dataSpec = new DataSpec(contentUri); try { @@ -57,7 +58,7 @@ public final class ContentDataSourceTest extends InstrumentationTestCase { ContentDataSource dataSource = new ContentDataSource(getInstrumentation().getContext()); Uri contentUri = new Uri.Builder() .scheme(ContentResolver.SCHEME_CONTENT) - .authority(TestContentProvider.AUTHORITY) + .authority(AUTHORITY) .build(); DataSpec dataSpec = new DataSpec(contentUri); try { @@ -70,10 +71,11 @@ public final class ContentDataSourceTest extends InstrumentationTestCase { } } + /** + * A {@link ContentProvider} for the test. + */ public static final class TestContentProvider extends ContentProvider { - private static final String AUTHORITY = "com.google.android.exoplayer2.core.test"; - @Override public boolean onCreate() { return true; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java index 507162519a..d118b91378 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java @@ -91,9 +91,9 @@ public final class ContentDataSource implements DataSource { // The asset must extend to the end of the file. bytesRemaining = inputStream.available(); if (bytesRemaining == 0) { - // FileInputStream.available() returns 0 if the remaining length cannot be determined, or - // if it's greater than Integer.MAX_VALUE. We don't know the true length in either case, - // so treat as unbounded. + // FileInputStream.available() returns 0 if the remaining length cannot be determined, + // or if it's greater than Integer.MAX_VALUE. We don't know the true length in either + // case, so treat as unbounded. bytesRemaining = C.LENGTH_UNSET; } } From fd9b162d0fe635e9dbbbcffd0ff1a63ee45cfc08 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 23 Jun 2017 09:44:27 -0700 Subject: [PATCH 0072/2472] Fix setSelectionOverride(index, tracks, null) Issue: #2988 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=159958591 --- .../MappingTrackSelectorTest.java | 196 ++++++++++++++++++ .../android/exoplayer2/source/TrackGroup.java | 2 +- .../trackselection/MappingTrackSelector.java | 8 +- 3 files changed, 201 insertions(+), 5 deletions(-) create mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/trackselection/MappingTrackSelectorTest.java diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/trackselection/MappingTrackSelectorTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/trackselection/MappingTrackSelectorTest.java new file mode 100644 index 0000000000..c31c651384 --- /dev/null +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/trackselection/MappingTrackSelectorTest.java @@ -0,0 +1,196 @@ +/* + * 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.trackselection; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlaybackException; +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; +import com.google.android.exoplayer2.util.MimeTypes; +import junit.framework.TestCase; + +/** + * Unit tests for {@link MappingTrackSelector}. + */ +public final class MappingTrackSelectorTest extends TestCase { + + private static final RendererCapabilities VIDEO_CAPABILITIES = + new FakeRendererCapabilities(C.TRACK_TYPE_VIDEO); + private static final RendererCapabilities AUDIO_CAPABILITIES = + new FakeRendererCapabilities(C.TRACK_TYPE_AUDIO); + private static final RendererCapabilities[] RENDERER_CAPABILITIES = new RendererCapabilities[] { + VIDEO_CAPABILITIES, AUDIO_CAPABILITIES + }; + + private static final TrackGroup VIDEO_TRACK_GROUP = new TrackGroup( + Format.createVideoSampleFormat("video", MimeTypes.VIDEO_H264, null, Format.NO_VALUE, + Format.NO_VALUE, 1024, 768, Format.NO_VALUE, null, null)); + private static final TrackGroup AUDIO_TRACK_GROUP = new TrackGroup( + Format.createAudioSampleFormat("audio", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, + Format.NO_VALUE, 2, 44100, null, null, 0, null)); + private static final TrackGroupArray TRACK_GROUPS = new TrackGroupArray( + VIDEO_TRACK_GROUP, AUDIO_TRACK_GROUP); + + private static final TrackSelection[] TRACK_SELECTIONS = new TrackSelection[] { + new FixedTrackSelection(VIDEO_TRACK_GROUP, 0), + new FixedTrackSelection(AUDIO_TRACK_GROUP, 0) + }; + + /** + * Tests that the video and audio track groups are mapped onto the correct renderers. + */ + public void testMapping() throws ExoPlaybackException { + FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector(); + trackSelector.selectTracks(RENDERER_CAPABILITIES, TRACK_GROUPS); + trackSelector.assertMappedTrackGroups(0, VIDEO_TRACK_GROUP); + trackSelector.assertMappedTrackGroups(1, AUDIO_TRACK_GROUP); + } + + /** + * Tests that the video and audio track groups are mapped onto the correct renderers when the + * renderer ordering is reversed. + */ + public void testMappingReverseOrder() throws ExoPlaybackException { + FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector(); + RendererCapabilities[] reverseOrderRendererCapabilities = new RendererCapabilities[] { + AUDIO_CAPABILITIES, VIDEO_CAPABILITIES}; + trackSelector.selectTracks(reverseOrderRendererCapabilities, TRACK_GROUPS); + trackSelector.assertMappedTrackGroups(0, AUDIO_TRACK_GROUP); + trackSelector.assertMappedTrackGroups(1, VIDEO_TRACK_GROUP); + } + + /** + * Tests video and audio track groups are mapped onto the correct renderers when there are + * multiple track groups of the same type. + */ + public void testMappingMulti() throws ExoPlaybackException { + FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector(); + TrackGroupArray multiTrackGroups = new TrackGroupArray(VIDEO_TRACK_GROUP, AUDIO_TRACK_GROUP, + VIDEO_TRACK_GROUP); + trackSelector.selectTracks(RENDERER_CAPABILITIES, multiTrackGroups); + trackSelector.assertMappedTrackGroups(0, VIDEO_TRACK_GROUP, VIDEO_TRACK_GROUP); + trackSelector.assertMappedTrackGroups(1, AUDIO_TRACK_GROUP); + } + + /** + * Tests the result of {@link MappingTrackSelector#selectTracks(RendererCapabilities[], + * TrackGroupArray[], int[][][])} is propagated correctly to the result of + * {@link MappingTrackSelector#selectTracks(RendererCapabilities[], TrackGroupArray)}. + */ + public void testSelectTracks() throws ExoPlaybackException { + FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector(TRACK_SELECTIONS); + TrackSelectorResult result = trackSelector.selectTracks(RENDERER_CAPABILITIES, TRACK_GROUPS); + assertEquals(TRACK_SELECTIONS[0], result.selections.get(0)); + assertEquals(TRACK_SELECTIONS[1], result.selections.get(1)); + } + + /** + * Tests that a null override clears a track selection. + */ + public void testSelectTracksWithNullOverride() throws ExoPlaybackException { + FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector(TRACK_SELECTIONS); + trackSelector.setSelectionOverride(0, new TrackGroupArray(VIDEO_TRACK_GROUP), null); + TrackSelectorResult result = trackSelector.selectTracks(RENDERER_CAPABILITIES, TRACK_GROUPS); + assertNull(result.selections.get(0)); + assertEquals(TRACK_SELECTIONS[1], result.selections.get(1)); + } + + /** + * Tests that a null override can be cleared. + */ + public void testSelectTracksWithClearedNullOverride() throws ExoPlaybackException { + FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector(TRACK_SELECTIONS); + trackSelector.setSelectionOverride(0, new TrackGroupArray(VIDEO_TRACK_GROUP), null); + trackSelector.clearSelectionOverride(0, new TrackGroupArray(VIDEO_TRACK_GROUP)); + TrackSelectorResult result = trackSelector.selectTracks(RENDERER_CAPABILITIES, TRACK_GROUPS); + assertEquals(TRACK_SELECTIONS[0], result.selections.get(0)); + assertEquals(TRACK_SELECTIONS[1], result.selections.get(1)); + } + + /** + * Tests that an override is not applied for a different set of available track groups. + */ + public void testSelectTracksWithNullOverrideForDifferentTracks() throws ExoPlaybackException { + FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector(TRACK_SELECTIONS); + trackSelector.setSelectionOverride(0, new TrackGroupArray(VIDEO_TRACK_GROUP), null); + TrackSelectorResult result = trackSelector.selectTracks(RENDERER_CAPABILITIES, + new TrackGroupArray(VIDEO_TRACK_GROUP, AUDIO_TRACK_GROUP, VIDEO_TRACK_GROUP)); + assertEquals(TRACK_SELECTIONS[0], result.selections.get(0)); + assertEquals(TRACK_SELECTIONS[1], result.selections.get(1)); + } + + /** + * A {@link MappingTrackSelector} that returns a fixed result from + * {@link #selectTracks(RendererCapabilities[], TrackGroupArray[], int[][][])}. + */ + private static final class FakeMappingTrackSelector extends MappingTrackSelector { + + private final TrackSelection[] result; + private TrackGroupArray[] lastRendererTrackGroupArrays; + + public FakeMappingTrackSelector(TrackSelection... result) { + this.result = result.length == 0 ? null : result; + } + + @Override + protected TrackSelection[] selectTracks(RendererCapabilities[] rendererCapabilities, + TrackGroupArray[] rendererTrackGroupArrays, int[][][] rendererFormatSupports) + throws ExoPlaybackException { + lastRendererTrackGroupArrays = rendererTrackGroupArrays; + return result == null ? new TrackSelection[rendererCapabilities.length] : result; + } + + public void assertMappedTrackGroups(int rendererIndex, TrackGroup... expected) { + assertEquals(expected.length, lastRendererTrackGroupArrays[rendererIndex].length); + for (int i = 0; i < expected.length; i++) { + assertEquals(expected[i], lastRendererTrackGroupArrays[rendererIndex].get(i)); + } + } + + } + + /** + * A {@link RendererCapabilities} that advertises adaptive support for all tracks of a given type. + */ + private static final class FakeRendererCapabilities implements RendererCapabilities { + + private final int trackType; + + public FakeRendererCapabilities(int trackType) { + this.trackType = trackType; + } + + @Override + public int getTrackType() { + return trackType; + } + + @Override + public int supportsFormat(Format format) throws ExoPlaybackException { + return MimeTypes.getTrackType(format.sampleMimeType) == trackType + ? (FORMAT_HANDLED | ADAPTIVE_SEAMLESS) : FORMAT_UNSUPPORTED_TYPE; + } + + @Override + public int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException { + return ADAPTIVE_SEAMLESS; + } + + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroup.java b/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroup.java index 393ac1988a..06410d5426 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroup.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroup.java @@ -42,7 +42,7 @@ public final class TrackGroup { private int hashCode; /** - * @param formats The track formats. Must not be null or contain null elements. + * @param formats The track formats. Must not be null, contain null elements or be of length 0. */ public TrackGroup(Format... formats) { Assertions.checkState(formats.length > 0); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java index 690723cf15..3499efdb16 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java @@ -304,10 +304,10 @@ public abstract class MappingTrackSelector extends TrackSelector { trackSelections[i] = null; } else { TrackGroupArray rendererTrackGroup = rendererTrackGroupArrays[i]; - Map overrides = selectionOverrides.get(i); - SelectionOverride override = overrides == null ? null : overrides.get(rendererTrackGroup); - if (override != null) { - trackSelections[i] = override.createTrackSelection(rendererTrackGroup); + if (hasSelectionOverride(i, rendererTrackGroup)) { + SelectionOverride override = selectionOverrides.get(i).get(rendererTrackGroup); + trackSelections[i] = override == null ? null + : override.createTrackSelection(rendererTrackGroup); } } } From d4e598f4172352e1ab6eb916aed9de1c03686957 Mon Sep 17 00:00:00 2001 From: Alex Birkett Date: Fri, 23 Jun 2017 18:56:40 +0200 Subject: [PATCH 0073/2472] Make OkHttpDataSource userAgent parameter optional --- .../ext/okhttp/OkHttpDataSource.java | 21 ++++++++++++------- .../ext/okhttp/OkHttpDataSourceFactory.java | 11 ++++++---- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java index 47850c0637..fac35bd427 100644 --- a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java +++ b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java @@ -16,6 +16,8 @@ package com.google.android.exoplayer2.ext.okhttp; import android.net.Uri; +import android.support.annotation.Nullable; + import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.upstream.DataSourceException; import com.google.android.exoplayer2.upstream.DataSpec; @@ -67,11 +69,11 @@ public class OkHttpDataSource implements HttpDataSource { /** * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use * by the source. - * @param userAgent The User-Agent string that should be used. + * @param userAgent An optional User-Agent string that should be used. * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the * predicate then a InvalidContentTypeException} is thrown from {@link #open(DataSpec)}. */ - public OkHttpDataSource(Call.Factory callFactory, String userAgent, + public OkHttpDataSource(Call.Factory callFactory, @Nullable String userAgent, Predicate contentTypePredicate) { this(callFactory, userAgent, contentTypePredicate, null); } @@ -79,13 +81,13 @@ public class OkHttpDataSource implements HttpDataSource { /** * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use * by the source. - * @param userAgent The User-Agent string that should be used. + * @param userAgent An optional User-Agent string that should be used. * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the * predicate then a {@link InvalidContentTypeException} is thrown from * {@link #open(DataSpec)}. * @param listener An optional listener. */ - public OkHttpDataSource(Call.Factory callFactory, String userAgent, + public OkHttpDataSource(Call.Factory callFactory, @Nullable String userAgent, Predicate contentTypePredicate, TransferListener listener) { this(callFactory, userAgent, contentTypePredicate, listener, null, null); } @@ -93,7 +95,7 @@ public class OkHttpDataSource implements HttpDataSource { /** * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use * by the source. - * @param userAgent The User-Agent string that should be used. + * @param userAgent An optional User-Agent string that should be used. * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the * predicate then a {@link InvalidContentTypeException} is thrown from * {@link #open(DataSpec)}. @@ -102,11 +104,11 @@ public class OkHttpDataSource implements HttpDataSource { * @param defaultRequestProperties The optional default {@link RequestProperties} to be sent to * the server as HTTP headers on every request. */ - public OkHttpDataSource(Call.Factory callFactory, String userAgent, + public OkHttpDataSource(Call.Factory callFactory, @Nullable String userAgent, Predicate contentTypePredicate, TransferListener listener, CacheControl cacheControl, RequestProperties defaultRequestProperties) { this.callFactory = Assertions.checkNotNull(callFactory); - this.userAgent = Assertions.checkNotEmpty(userAgent); + this.userAgent = userAgent; this.contentTypePredicate = contentTypePredicate; this.listener = listener; this.cacheControl = cacheControl; @@ -280,7 +282,10 @@ public class OkHttpDataSource implements HttpDataSource { } builder.addHeader("Range", rangeRequest); } - builder.addHeader("User-Agent", userAgent); + if (userAgent != null) { + builder.addHeader("User-Agent", userAgent); + } + if (!allowGzip) { builder.addHeader("Accept-Encoding", "identity"); } diff --git a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSourceFactory.java b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSourceFactory.java index 5228065db1..6ee09df7de 100644 --- a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSourceFactory.java +++ b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSourceFactory.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.ext.okhttp; +import android.support.annotation.Nullable; + import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory; @@ -36,10 +38,10 @@ public final class OkHttpDataSourceFactory extends BaseFactory { /** * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use * by the sources created by the factory. - * @param userAgent The User-Agent string that should be used. + * @param userAgent An optional User-Agent string that should be used. * @param listener An optional listener. */ - public OkHttpDataSourceFactory(Call.Factory callFactory, String userAgent, + public OkHttpDataSourceFactory(Call.Factory callFactory, @Nullable String userAgent, TransferListener listener) { this(callFactory, userAgent, listener, null); } @@ -47,11 +49,12 @@ public final class OkHttpDataSourceFactory extends BaseFactory { /** * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use * by the sources created by the factory. - * @param userAgent The User-Agent string that should be used. + * @param userAgent An optional User-Agent string that should be used. * @param listener An optional listener. * @param cacheControl An optional {@link CacheControl} for setting the Cache-Control header. */ - public OkHttpDataSourceFactory(Call.Factory callFactory, String userAgent, + public OkHttpDataSourceFactory(Call.Factory callFactory, + @Nullable String userAgent, TransferListener listener, CacheControl cacheControl) { this.callFactory = callFactory; this.userAgent = userAgent; From 045a153cb7afdffc69b46adb9de1aad8b14b909f Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Sun, 25 Jun 2017 15:16:11 +0100 Subject: [PATCH 0074/2472] Clean up okhttp datasource. --- .../ext/okhttp/OkHttpDataSource.java | 39 ++++++++++--------- .../ext/okhttp/OkHttpDataSourceFactory.java | 24 ++++++------ 2 files changed, 33 insertions(+), 30 deletions(-) diff --git a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java index fac35bd427..167fc68e86 100644 --- a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java +++ b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java @@ -16,8 +16,8 @@ package com.google.android.exoplayer2.ext.okhttp; import android.net.Uri; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; - import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.upstream.DataSourceException; import com.google.android.exoplayer2.upstream.DataSpec; @@ -47,13 +47,14 @@ public class OkHttpDataSource implements HttpDataSource { private static final AtomicReference skipBufferReference = new AtomicReference<>(); - private final Call.Factory callFactory; - private final String userAgent; - private final Predicate contentTypePredicate; - private final TransferListener listener; - private final CacheControl cacheControl; - private final RequestProperties defaultRequestProperties; - private final RequestProperties requestProperties; + @NonNull private final Call.Factory callFactory; + @NonNull private final RequestProperties requestProperties; + + @Nullable private final String userAgent; + @Nullable private final Predicate contentTypePredicate; + @Nullable private final TransferListener listener; + @Nullable private final CacheControl cacheControl; + @Nullable private final RequestProperties defaultRequestProperties; private DataSpec dataSpec; private Response response; @@ -69,33 +70,34 @@ public class OkHttpDataSource implements HttpDataSource { /** * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use * by the source. - * @param userAgent An optional User-Agent string that should be used. + * @param userAgent An optional User-Agent string. * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the * predicate then a InvalidContentTypeException} is thrown from {@link #open(DataSpec)}. */ - public OkHttpDataSource(Call.Factory callFactory, @Nullable String userAgent, - Predicate contentTypePredicate) { + public OkHttpDataSource(@NonNull Call.Factory callFactory, @Nullable String userAgent, + @Nullable Predicate contentTypePredicate) { this(callFactory, userAgent, contentTypePredicate, null); } /** * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use * by the source. - * @param userAgent An optional User-Agent string that should be used. + * @param userAgent An optional User-Agent string. * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the * predicate then a {@link InvalidContentTypeException} is thrown from * {@link #open(DataSpec)}. * @param listener An optional listener. */ - public OkHttpDataSource(Call.Factory callFactory, @Nullable String userAgent, - Predicate contentTypePredicate, TransferListener listener) { + public OkHttpDataSource(@NonNull Call.Factory callFactory, @Nullable String userAgent, + @Nullable Predicate contentTypePredicate, + @Nullable TransferListener listener) { this(callFactory, userAgent, contentTypePredicate, listener, null, null); } /** * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use * by the source. - * @param userAgent An optional User-Agent string that should be used. + * @param userAgent An optional User-Agent string. * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the * predicate then a {@link InvalidContentTypeException} is thrown from * {@link #open(DataSpec)}. @@ -104,9 +106,10 @@ public class OkHttpDataSource implements HttpDataSource { * @param defaultRequestProperties The optional default {@link RequestProperties} to be sent to * the server as HTTP headers on every request. */ - public OkHttpDataSource(Call.Factory callFactory, @Nullable String userAgent, - Predicate contentTypePredicate, TransferListener listener, - CacheControl cacheControl, RequestProperties defaultRequestProperties) { + public OkHttpDataSource(@NonNull Call.Factory callFactory, @Nullable String userAgent, + @Nullable Predicate contentTypePredicate, + @Nullable TransferListener listener, + @Nullable CacheControl cacheControl, @Nullable RequestProperties defaultRequestProperties) { this.callFactory = Assertions.checkNotNull(callFactory); this.userAgent = userAgent; this.contentTypePredicate = contentTypePredicate; diff --git a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSourceFactory.java b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSourceFactory.java index 6ee09df7de..32fc5a58cb 100644 --- a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSourceFactory.java +++ b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSourceFactory.java @@ -15,8 +15,8 @@ */ package com.google.android.exoplayer2.ext.okhttp; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; - import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory; @@ -30,32 +30,32 @@ import okhttp3.Call; */ public final class OkHttpDataSourceFactory extends BaseFactory { - private final Call.Factory callFactory; - private final String userAgent; - private final TransferListener listener; - private final CacheControl cacheControl; + @NonNull private final Call.Factory callFactory; + @Nullable private final String userAgent; + @Nullable private final TransferListener listener; + @Nullable private final CacheControl cacheControl; /** * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use * by the sources created by the factory. - * @param userAgent An optional User-Agent string that should be used. + * @param userAgent An optional User-Agent string. * @param listener An optional listener. */ - public OkHttpDataSourceFactory(Call.Factory callFactory, @Nullable String userAgent, - TransferListener listener) { + public OkHttpDataSourceFactory(@NonNull Call.Factory callFactory, @Nullable String userAgent, + @Nullable TransferListener listener) { this(callFactory, userAgent, listener, null); } /** * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use * by the sources created by the factory. - * @param userAgent An optional User-Agent string that should be used. + * @param userAgent An optional User-Agent string. * @param listener An optional listener. * @param cacheControl An optional {@link CacheControl} for setting the Cache-Control header. */ - public OkHttpDataSourceFactory(Call.Factory callFactory, - @Nullable String userAgent, - TransferListener listener, CacheControl cacheControl) { + public OkHttpDataSourceFactory(@NonNull Call.Factory callFactory, @Nullable String userAgent, + @Nullable TransferListener listener, + @Nullable CacheControl cacheControl) { this.callFactory = callFactory; this.userAgent = userAgent; this.listener = listener; From a5eba0162b08f786d5a57b28746737a9ed83504e Mon Sep 17 00:00:00 2001 From: hoangtc Date: Mon, 26 Jun 2017 02:46:42 -0700 Subject: [PATCH 0075/2472] Update DrmSessionException. Make DrmSessionException takes in Throwable cause instead of Exception cause, which is more limiting and doesn't add any benefit. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=160121486 --- .../java/com/google/android/exoplayer2/drm/DrmSession.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java index 538db9e1d9..cd694396b7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java @@ -28,11 +28,11 @@ import java.util.Map; @TargetApi(16) public interface DrmSession { - /** Wraps the exception which is the cause of the error state. */ + /** Wraps the throwable which is the cause of the error state. */ class DrmSessionException extends Exception { - public DrmSessionException(Exception e) { - super(e); + public DrmSessionException(Throwable cause) { + super(cause); } } From 2f7de7d3e8a7b29f29d429d1deaffee7a33a5883 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 26 Jun 2017 04:02:03 -0700 Subject: [PATCH 0076/2472] Merge ContentDataSource fixes + tests from GitHub https://github.com/google/ExoPlayer/pull/2963/files https://github.com/google/ExoPlayer/commit/8bb643976fe20d1ec684291aa7bf5337e474bec4 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=160128047 --- .../upstream/AssetDataSourceTest.java | 35 +++++-------------- .../upstream/ContentDataSourceTest.java | 13 ++----- .../android/exoplayer2/testutil/TestUtil.java | 22 ++++++++++++ 3 files changed, 33 insertions(+), 37 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/AssetDataSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/AssetDataSourceTest.java index d8e61eb94c..102c89ec2b 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/AssetDataSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/AssetDataSourceTest.java @@ -15,10 +15,8 @@ */ package com.google.android.exoplayer2.upstream; -import android.content.Context; import android.net.Uri; import android.test.InstrumentationTestCase; -import android.test.MoreAsserts; import com.google.android.exoplayer2.testutil.TestUtil; /** @@ -27,36 +25,19 @@ import com.google.android.exoplayer2.testutil.TestUtil; public final class AssetDataSourceTest extends InstrumentationTestCase { private static final String DATA_PATH = "binary/1024_incrementing_bytes.mp3"; - private static final long DATA_LENGTH = 1024; public void testReadFileUri() throws Exception { - Context context = getInstrumentation().getContext(); - AssetDataSource dataSource = new AssetDataSource(context); - Uri assetUri = Uri.parse("file:///android_asset/" + DATA_PATH); - DataSpec dataSpec = new DataSpec(assetUri); - try { - long length = dataSource.open(dataSpec); - assertEquals(DATA_LENGTH, length); - byte[] readData = TestUtil.readToEnd(dataSource); - MoreAsserts.assertEquals(TestUtil.getByteArray(getInstrumentation(), DATA_PATH), readData); - } finally { - dataSource.close(); - } + AssetDataSource dataSource = new AssetDataSource(getInstrumentation().getContext()); + DataSpec dataSpec = new DataSpec(Uri.parse("file:///android_asset/" + DATA_PATH)); + TestUtil.assertDataSourceContent(dataSource, dataSpec, + TestUtil.getByteArray(getInstrumentation(), DATA_PATH)); } public void testReadAssetUri() throws Exception { - Context context = getInstrumentation().getContext(); - AssetDataSource dataSource = new AssetDataSource(context); - Uri assetUri = Uri.parse("asset:///" + DATA_PATH); - DataSpec dataSpec = new DataSpec(assetUri); - try { - long length = dataSource.open(dataSpec); - assertEquals(DATA_LENGTH, length); - byte[] readData = TestUtil.readToEnd(dataSource); - MoreAsserts.assertEquals(TestUtil.getByteArray(getInstrumentation(), DATA_PATH), readData); - } finally { - dataSource.close(); - } + AssetDataSource dataSource = new AssetDataSource(getInstrumentation().getContext()); + DataSpec dataSpec = new DataSpec(Uri.parse("asset:///" + DATA_PATH)); + TestUtil.assertDataSourceContent(dataSource, dataSpec, + TestUtil.getByteArray(getInstrumentation(), DATA_PATH)); } } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java index d8743a0a2c..834e7e1374 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java @@ -23,7 +23,6 @@ import android.database.Cursor; import android.net.Uri; import android.support.annotation.NonNull; import android.test.InstrumentationTestCase; -import android.test.MoreAsserts; import com.google.android.exoplayer2.testutil.TestUtil; import java.io.FileNotFoundException; import java.io.IOException; @@ -35,7 +34,6 @@ public final class ContentDataSourceTest extends InstrumentationTestCase { private static final String AUTHORITY = "com.google.android.exoplayer2.core.test"; private static final String DATA_PATH = "binary/1024_incrementing_bytes.mp3"; - private static final long DATA_LENGTH = 1024; public void testReadValidUri() throws Exception { ContentDataSource dataSource = new ContentDataSource(getInstrumentation().getContext()); @@ -44,14 +42,8 @@ public final class ContentDataSourceTest extends InstrumentationTestCase { .authority(AUTHORITY) .path(DATA_PATH).build(); DataSpec dataSpec = new DataSpec(contentUri); - try { - long length = dataSource.open(dataSpec); - assertEquals(DATA_LENGTH, length); - byte[] readData = TestUtil.readToEnd(dataSource); - MoreAsserts.assertEquals(TestUtil.getByteArray(getInstrumentation(), DATA_PATH), readData); - } finally { - dataSource.close(); - } + TestUtil.assertDataSourceContent(dataSource, dataSpec, + TestUtil.getByteArray(getInstrumentation(), DATA_PATH)); } public void testReadInvalidUri() throws Exception { @@ -66,6 +58,7 @@ public final class ContentDataSourceTest extends InstrumentationTestCase { fail(); } catch (ContentDataSource.ContentDataSourceException e) { // Expected. + assertTrue(e.getCause() instanceof FileNotFoundException); } finally { dataSource.close(); } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java index f75239318a..363f60b10d 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java @@ -17,12 +17,14 @@ package com.google.android.exoplayer2.testutil; import android.app.Instrumentation; import android.test.InstrumentationTestCase; +import android.test.MoreAsserts; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.testutil.FakeExtractorInput.SimulatedIOException; 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.Util; import java.io.IOException; @@ -390,4 +392,24 @@ public class TestUtil { } } + /** + * Asserts that data read from a {@link DataSource} matches {@code expected}. + * + * @param dataSource The {@link DataSource} through which to read. + * @param dataSpec The {@link DataSpec} to use when opening the {@link DataSource}. + * @param expectedData The expected data. + * @throws IOException If an error occurs reading fom the {@link DataSource}. + */ + public static void assertDataSourceContent(DataSource dataSource, DataSpec dataSpec, + byte[] expectedData) throws IOException { + try { + long length = dataSource.open(dataSpec); + Assert.assertEquals(length, expectedData.length); + byte[] readData = TestUtil.readToEnd(dataSource); + MoreAsserts.assertEquals(expectedData, readData); + } finally { + dataSource.close(); + } + } + } From 6c24d9380560adfc7c2577cdadf49d770541c383 Mon Sep 17 00:00:00 2001 From: eguven Date: Mon, 26 Jun 2017 06:51:52 -0700 Subject: [PATCH 0077/2472] Fix FLAC extension native part compilation In the latest NDK version (r15) compilation fails because 'memset' isn't defined. Included cstring header. Issue: #2977 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=160139022 --- extensions/flac/src/main/jni/flac_parser.cc | 1 + 1 file changed, 1 insertion(+) diff --git a/extensions/flac/src/main/jni/flac_parser.cc b/extensions/flac/src/main/jni/flac_parser.cc index e4925cb462..6c6e57f5f7 100644 --- a/extensions/flac/src/main/jni/flac_parser.cc +++ b/extensions/flac/src/main/jni/flac_parser.cc @@ -22,6 +22,7 @@ #include #include +#include #define LOG_TAG "FLACParser" #define ALOGE(...) \ From efd17f86c5507a68e2568b2d4a30f9280eb53bab Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 27 Jun 2017 09:38:31 -0700 Subject: [PATCH 0078/2472] Add URLs EXT-X-STREAM-INF uris only once This prevents ExoPlayer from thinking there are many more video tracks than there actually are. And will prevent downloading multiple times the same rendition once offline support for HLS is added. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=160285777 --- .../source/hls/playlist/HlsPlaylistParser.java | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/library/hls/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 index a867659838..c64b98b5f3 100644 --- a/library/hls/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 @@ -30,6 +30,7 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.Collections; +import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Queue; @@ -174,6 +175,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser variantUrls = new HashSet<>(); ArrayList variants = new ArrayList<>(); ArrayList audios = new ArrayList<>(); ArrayList subtitles = new ArrayList<>(); @@ -251,11 +253,13 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser Date: Thu, 29 Jun 2017 00:37:28 -0700 Subject: [PATCH 0079/2472] Show larger scrubber handle when focused Also remove updateScrubberState as it doesn't do anything useful. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=160496133 --- .../android/exoplayer2/ui/DefaultTimeBar.java | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) 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 index 4ede786175..2b699c8957 100644 --- 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 @@ -89,7 +89,6 @@ public class DefaultTimeBar extends View implements TimeBar { private final Formatter formatter; private final Runnable stopScrubbingRunnable; - private int scrubberSize; private OnScrubListener listener; private int keyCountIncrement; private long keyTimeIncrement; @@ -184,7 +183,6 @@ public class DefaultTimeBar extends View implements TimeBar { stopScrubbing(false); } }; - scrubberSize = scrubberEnabledSize; scrubberPadding = (Math.max(scrubberDisabledSize, Math.max(scrubberEnabledSize, scrubberDraggedSize)) + 1) / 2; @@ -234,8 +232,6 @@ public class DefaultTimeBar extends View implements TimeBar { this.duration = duration; if (scrubbing && duration == C.TIME_UNSET) { stopScrubbing(true); - } else { - updateScrubberState(); } update(); } @@ -251,7 +247,6 @@ public class DefaultTimeBar extends View implements TimeBar { @Override public void setEnabled(boolean enabled) { super.setEnabled(enabled); - updateScrubberState(); if (scrubbing && !enabled) { stopScrubbing(true); } @@ -436,7 +431,6 @@ public class DefaultTimeBar extends View implements TimeBar { private void startScrubbing() { scrubbing = true; - updateScrubberState(); ViewParent parent = getParent(); if (parent != null) { parent.requestDisallowInterceptTouchEvent(true); @@ -452,18 +446,12 @@ public class DefaultTimeBar extends View implements TimeBar { 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); @@ -543,6 +531,8 @@ public class DefaultTimeBar extends View implements TimeBar { if (duration <= 0) { return; } + int scrubberSize = (scrubbing || isFocused()) ? scrubberDraggedSize + : (isEnabled() ? scrubberEnabledSize : scrubberDisabledSize); int playheadRadius = scrubberSize / 2; int playheadCenter = Util.constrainValue(scrubberBar.right, scrubberBar.left, progressBar.right); From df84f2930cdb1f75d273668a96953c0aae102943 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 29 Jun 2017 04:16:16 -0700 Subject: [PATCH 0080/2472] Clarify JavaDoc of MediaPeriod. Two of MediaPeriod's methods are only called after the media period has been prepared. Added this to JavaDoc of these method to simplify implementations. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=160510373 --- .../com/google/android/exoplayer2/source/MediaPeriod.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java index 3b06542855..aaf4c89ff7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java @@ -106,6 +106,8 @@ public interface MediaPeriod extends SequenceableLoader { /** * Discards buffered media up to the specified position. + *

    + * This method should only be called after the period has been prepared. * * @param positionUs The position in microseconds. */ @@ -116,6 +118,8 @@ public interface MediaPeriod extends SequenceableLoader { *

    * After this method has returned a value other than {@link C#TIME_UNSET}, all * {@link SampleStream}s provided by the period are guaranteed to start from a key frame. + *

    + * This method should only be called after the period has been prepared. * * @return If a discontinuity was read then the playback position in microseconds after the * discontinuity. Else {@link C#TIME_UNSET}. From 79f7db7fcd1285e1248045fd486a109dade941e4 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 29 Jun 2017 06:49:30 -0700 Subject: [PATCH 0081/2472] Prefer Google over MediaTek for PCM decoding pre-O. Issue: #2873 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=160520136 --- .../exoplayer2/mediacodec/MediaCodecUtil.java | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java index 5369dffeb6..392162f607 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java @@ -56,8 +56,10 @@ public final class MediaCodecUtil { } private static final String TAG = "MediaCodecUtil"; + private static final String GOOGLE_RAW_DECODER_NAME = "OMX.google.raw.decoder"; + private static final String MTK_RAW_DECODER_NAME = "OMX.MTK.AUDIO.DECODER.RAW"; private static final MediaCodecInfo PASSTHROUGH_DECODER_INFO = - MediaCodecInfo.newPassthroughInstance("OMX.google.raw.decoder"); + MediaCodecInfo.newPassthroughInstance(GOOGLE_RAW_DECODER_NAME); private static final Pattern PROFILE_PATTERN = Pattern.compile("^\\D?(\\d+)$"); private static final HashMap> decoderInfosCache = new HashMap<>(); @@ -155,6 +157,7 @@ public final class MediaCodecUtil { + ". Assuming: " + decoderInfos.get(0).name); } } + applyWorkarounds(decoderInfos); decoderInfos = Collections.unmodifiableList(decoderInfos); decoderInfosCache.put(key, decoderInfos); return decoderInfos; @@ -339,6 +342,27 @@ public final class MediaCodecUtil { return true; } + /** + * Modifies a list of {@link MediaCodecInfo}s to apply workarounds where we know better than the + * platform. + * + * @param decoderInfos The list to modify. + */ + private static void applyWorkarounds(List decoderInfos) { + if (Util.SDK_INT < 26 && decoderInfos.size() > 1 + && MTK_RAW_DECODER_NAME.equals(decoderInfos.get(0).name)) { + // Prefer the Google raw decoder over the MediaTek one [Internal: b/62337687]. + for (int i = 1; i < decoderInfos.size(); i++) { + MediaCodecInfo decoderInfo = decoderInfos.get(i); + if (GOOGLE_RAW_DECODER_NAME.equals(decoderInfo.name)) { + decoderInfos.remove(i); + decoderInfos.add(0, decoderInfo); + break; + } + } + } + } + /** * Returns whether the decoder is known to fail when adapting, despite advertising itself as an * adaptive decoder. From a579b8d82dd74dca8a4e6b666c0df693aa14aa80 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 30 Jun 2017 02:52:20 -0700 Subject: [PATCH 0082/2472] Fix DvbParser bug Issue: #2957 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=160628086 --- .../com/google/android/exoplayer2/text/dvb/DvbParser.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 index 96c8a89801..c0caf1e57a 100644 --- 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 @@ -667,13 +667,15 @@ import java.util.List; int runLength = 0; int clutIndex = 0; int peek = data.readBits(2); - if (!data.readBit()) { + if (peek != 0x00) { runLength = 1; clutIndex = peek; } else if (data.readBit()) { runLength = 3 + data.readBits(3); clutIndex = data.readBits(2); - } else if (!data.readBit()) { + } else if (data.readBit()) { + runLength = 1; + } else { switch (data.readBits(2)) { case 0x00: endOfPixelCodeString = true; From 43daf0f2bb96bb514d22561dd4821f6ce7562aa9 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 15 Jun 2017 08:06:31 -0700 Subject: [PATCH 0083/2472] Update MIME type in FLAC test data ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=159104188 --- library/core/src/androidTest/assets/ogg/bear_flac.ogg.0.dump | 2 +- library/core/src/androidTest/assets/ogg/bear_flac.ogg.1.dump | 2 +- library/core/src/androidTest/assets/ogg/bear_flac.ogg.2.dump | 2 +- library/core/src/androidTest/assets/ogg/bear_flac.ogg.3.dump | 2 +- .../core/src/androidTest/assets/ogg/bear_flac.ogg.unklen.dump | 2 +- .../src/androidTest/assets/ogg/bear_flac_noseektable.ogg.0.dump | 2 +- .../src/androidTest/assets/ogg/bear_flac_noseektable.ogg.1.dump | 2 +- .../src/androidTest/assets/ogg/bear_flac_noseektable.ogg.2.dump | 2 +- .../src/androidTest/assets/ogg/bear_flac_noseektable.ogg.3.dump | 2 +- .../assets/ogg/bear_flac_noseektable.ogg.unklen.dump | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.0.dump b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.0.dump index 16816917b7..5ba8cc29ae 100644 --- a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.0.dump +++ b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.0.dump @@ -8,7 +8,7 @@ track 0: bitrate = -1 id = null containerMimeType = null - sampleMimeType = audio/x-flac + sampleMimeType = audio/flac maxInputSize = 768000 width = -1 height = -1 diff --git a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.1.dump b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.1.dump index fec523f971..f698fd28cf 100644 --- a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.1.dump +++ b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.1.dump @@ -8,7 +8,7 @@ track 0: bitrate = -1 id = null containerMimeType = null - sampleMimeType = audio/x-flac + sampleMimeType = audio/flac maxInputSize = 768000 width = -1 height = -1 diff --git a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.2.dump b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.2.dump index a4a60989ed..8d803d0bac 100644 --- a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.2.dump +++ b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.2.dump @@ -8,7 +8,7 @@ track 0: bitrate = -1 id = null containerMimeType = null - sampleMimeType = audio/x-flac + sampleMimeType = audio/flac maxInputSize = 768000 width = -1 height = -1 diff --git a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.3.dump b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.3.dump index a77575bb0c..09f6267270 100644 --- a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.3.dump +++ b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.3.dump @@ -8,7 +8,7 @@ track 0: bitrate = -1 id = null containerMimeType = null - sampleMimeType = audio/x-flac + sampleMimeType = audio/flac maxInputSize = 768000 width = -1 height = -1 diff --git a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.unklen.dump b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.unklen.dump index 16816917b7..5ba8cc29ae 100644 --- a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.unklen.dump +++ b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.unklen.dump @@ -8,7 +8,7 @@ track 0: bitrate = -1 id = null containerMimeType = null - sampleMimeType = audio/x-flac + sampleMimeType = audio/flac maxInputSize = 768000 width = -1 height = -1 diff --git a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.0.dump b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.0.dump index 7be7d02493..73e537f8c8 100644 --- a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.0.dump +++ b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.0.dump @@ -8,7 +8,7 @@ track 0: bitrate = -1 id = null containerMimeType = null - sampleMimeType = audio/x-flac + sampleMimeType = audio/flac maxInputSize = 768000 width = -1 height = -1 diff --git a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.1.dump b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.1.dump index 34f19c6bce..3b7dc3fd1e 100644 --- a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.1.dump +++ b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.1.dump @@ -8,7 +8,7 @@ track 0: bitrate = -1 id = null containerMimeType = null - sampleMimeType = audio/x-flac + sampleMimeType = audio/flac maxInputSize = 768000 width = -1 height = -1 diff --git a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.2.dump b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.2.dump index 68484d2cf4..b6a6741fcc 100644 --- a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.2.dump +++ b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.2.dump @@ -8,7 +8,7 @@ track 0: bitrate = -1 id = null containerMimeType = null - sampleMimeType = audio/x-flac + sampleMimeType = audio/flac maxInputSize = 768000 width = -1 height = -1 diff --git a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.3.dump b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.3.dump index 8b2e7858b0..738002f7ef 100644 --- a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.3.dump +++ b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.3.dump @@ -8,7 +8,7 @@ track 0: bitrate = -1 id = null containerMimeType = null - sampleMimeType = audio/x-flac + sampleMimeType = audio/flac maxInputSize = 768000 width = -1 height = -1 diff --git a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.unklen.dump b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.unklen.dump index 8d398efdb8..a237fd0dfc 100644 --- a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.unklen.dump +++ b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.unklen.dump @@ -8,7 +8,7 @@ track 0: bitrate = -1 id = null containerMimeType = null - sampleMimeType = audio/x-flac + sampleMimeType = audio/flac maxInputSize = 768000 width = -1 height = -1 From 1b64d62e61dd68635df0df3a1a83d303db4d0b12 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 30 Jun 2017 10:48:03 -0700 Subject: [PATCH 0084/2472] Update release notes + bump version number ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=160663100 --- RELEASENOTES.md | 22 +++++++++++++++++-- build.gradle | 2 +- demo/src/main/AndroidManifest.xml | 4 ++-- .../exoplayer2/ExoPlayerLibraryInfo.java | 6 ++--- 4 files changed, 26 insertions(+), 8 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 4f147e2bbd..f9f6b02c19 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,11 +1,29 @@ # Release notes # +### r2.4.3 ### + +* Audio: Workaround custom audio decoders misreporting their maximum supported + channel counts ([#2940](https://github.com/google/ExoPlayer/issues/2940)). +* Audio: Workaround for broken MediaTek raw decoder on some devices + ([#2873](https://github.com/google/ExoPlayer/issues/2873)). +* Captions: Fix TTML captions appearing at the top of the screen + ([#2953](https://github.com/google/ExoPlayer/issues/2953)). +* Captions: Fix handling of some DVB subtitles + ([#2957](https://github.com/google/ExoPlayer/issues/2957)). +* Track selection: Fix setSelectionOverride(index, tracks, null) + ([#2988](https://github.com/google/ExoPlayer/issues/2988)). +* GVR extension: Add support for mono input + ([#2710](https://github.com/google/ExoPlayer/issues/2710)). +* FLAC extension: Fix failing build + ([#2977](https://github.com/google/ExoPlayer/pull/2977)). +* Misc bugfixes. + ### r2.4.2 ### * Stability: Work around Nexus 10 reboot when playing certain content - ([2806](https://github.com/google/ExoPlayer/issues/2806)). + ([#2806](https://github.com/google/ExoPlayer/issues/2806)). * MP3: Correctly treat MP3s with INFO headers as constant bitrate - ([2895](https://github.com/google/ExoPlayer/issues/2895)). + ([#2895](https://github.com/google/ExoPlayer/issues/2895)). * HLS: Use average rather than peak bandwidth when available ([#2863](https://github.com/google/ExoPlayer/issues/2863)). * SmoothStreaming: Fix timeline for live streams diff --git a/build.gradle b/build.gradle index 4f18e7c801..01d8b6616c 100644 --- a/build.gradle +++ b/build.gradle @@ -48,7 +48,7 @@ allprojects { releaseRepoName = getBintrayRepo() releaseUserOrg = 'google' releaseGroupId = 'com.google.android.exoplayer' - releaseVersion = 'r2.4.2' + releaseVersion = 'r2.4.3' releaseWebsite = 'https://github.com/google/ExoPlayer' } if (it.hasProperty('externalBuildDir')) { diff --git a/demo/src/main/AndroidManifest.xml b/demo/src/main/AndroidManifest.xml index 34256d41c1..addce60cad 100644 --- a/demo/src/main/AndroidManifest.xml +++ b/demo/src/main/AndroidManifest.xml @@ -16,8 +16,8 @@ + android:versionCode="2403" + android:versionName="2.4.3"> diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java index c6fc139208..650ce727cd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java @@ -24,13 +24,13 @@ public interface ExoPlayerLibraryInfo { * The version of the library expressed as a string, for example "1.2.3". */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. - String VERSION = "2.4.2"; + String VERSION = "2.4.3"; /** * 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.2"; + String VERSION_SLASHY = "ExoPlayerLib/2.4.3"; /** * The version of the library expressed as an integer, for example 1002003. @@ -40,7 +40,7 @@ public interface ExoPlayerLibraryInfo { * integer version 123045006 (123-045-006). */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - int VERSION_INT = 2004002; + int VERSION_INT = 2004003; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} From f94db63366f8649fab29978d008fea35c761b837 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 4 Jul 2017 03:32:23 -0700 Subject: [PATCH 0085/2472] Fix reporting of width/height 1. maybeRenotifyVideoSizeChanged should report reported* variables 2. Add check into maybeNotifyVideoSizeChanged to suppress reporting in the case that the width and height are still unknown. Issue: #3007 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=160879625 --- .../exoplayer2/video/MediaCodecVideoRenderer.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index 990a29dc4e..fbbcd9a99a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -578,9 +578,10 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } private void maybeNotifyVideoSizeChanged() { - if (reportedWidth != currentWidth || reportedHeight != currentHeight + if ((currentWidth != Format.NO_VALUE || currentHeight != Format.NO_VALUE) + && (reportedWidth != currentWidth || reportedHeight != currentHeight || reportedUnappliedRotationDegrees != currentUnappliedRotationDegrees - || reportedPixelWidthHeightRatio != currentPixelWidthHeightRatio) { + || reportedPixelWidthHeightRatio != currentPixelWidthHeightRatio)) { eventDispatcher.videoSizeChanged(currentWidth, currentHeight, currentUnappliedRotationDegrees, currentPixelWidthHeightRatio); reportedWidth = currentWidth; @@ -592,8 +593,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { private void maybeRenotifyVideoSizeChanged() { if (reportedWidth != Format.NO_VALUE || reportedHeight != Format.NO_VALUE) { - eventDispatcher.videoSizeChanged(currentWidth, currentHeight, currentUnappliedRotationDegrees, - currentPixelWidthHeightRatio); + eventDispatcher.videoSizeChanged(reportedWidth, reportedHeight, + reportedUnappliedRotationDegrees, reportedPixelWidthHeightRatio); } } From 4233f81ed7d95d3f40d277f93820b03f6c50890d Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 10 Jul 2017 16:00:12 -0700 Subject: [PATCH 0086/2472] Misc tweaks to UI components ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=161454491 --- .../google/android/exoplayer2/ui/PlaybackControlView.java | 7 ++++++- .../google/android/exoplayer2/ui/SimpleExoPlayerView.java | 6 ++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java index 5f88f3a241..250c237772 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java @@ -712,7 +712,12 @@ public class PlaybackControlView extends FrameLayout { if (fastForwardMs <= 0) { return; } - seekTo(Math.min(player.getCurrentPosition() + fastForwardMs, player.getDuration())); + long durationMs = player.getDuration(); + long seekPositionMs = player.getCurrentPosition() + fastForwardMs; + if (durationMs != C.TIME_UNSET) { + seekPositionMs = Math.min(seekPositionMs, durationMs); + } + seekTo(seekPositionMs); } private void seekTo(long positionMs) { diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index fce05f5bc4..245999f8b5 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -574,7 +574,8 @@ public final class SimpleExoPlayerView extends FrameLayout { /** * Sets the rewind increment in milliseconds. * - * @param rewindMs The rewind increment in milliseconds. + * @param rewindMs The rewind increment in milliseconds. A non-positive value will cause the + * rewind button to be disabled. */ public void setRewindIncrementMs(int rewindMs) { Assertions.checkState(controller != null); @@ -584,7 +585,8 @@ public final class SimpleExoPlayerView extends FrameLayout { /** * Sets the fast forward increment in milliseconds. * - * @param fastForwardMs The fast forward increment in milliseconds. + * @param fastForwardMs The fast forward increment in milliseconds. A non-positive value will + * cause the fast forward button to be disabled. */ public void setFastForwardIncrementMs(int fastForwardMs) { Assertions.checkState(controller != null); From 2665e42f85b5c64dd38439b49247bfbfa983cda2 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 11 Jul 2017 04:36:06 -0700 Subject: [PATCH 0087/2472] Correctly propagate format identifier for CEA-608 in HLS Issue: #3033 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=161512537 --- .../google/android/exoplayer2/extractor/ts/SeiReader.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/library/core/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 index 1e5d480ea1..907419f8fc 100644 --- a/library/core/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 @@ -51,9 +51,10 @@ import java.util.List; Assertions.checkArgument(MimeTypes.APPLICATION_CEA608.equals(channelMimeType) || MimeTypes.APPLICATION_CEA708.equals(channelMimeType), "Invalid closed caption mime type provided: " + channelMimeType); - output.format(Format.createTextSampleFormat(idGenerator.getFormatId(), channelMimeType, null, - Format.NO_VALUE, channelFormat.selectionFlags, channelFormat.language, - channelFormat.accessibilityChannel, null)); + String formatId = channelFormat.id != null ? channelFormat.id : idGenerator.getFormatId(); + output.format(Format.createTextSampleFormat(formatId, channelMimeType, null, Format.NO_VALUE, + channelFormat.selectionFlags, channelFormat.language, channelFormat.accessibilityChannel, + null)); outputs[i] = output; } } From 4cb5b349777ed6a71ab704200e7ccc6a8d1d3fe2 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 12 Jul 2017 04:47:31 -0700 Subject: [PATCH 0088/2472] Don't use ParsableBitArray to parse TS packet headers Really low hanging fruit optimization for TS extraction. ParsableBitArray is quite expensive. In particular readBits contains at least 2 if blocks and a for loop, and was being called 5 times per 188 byte packet (4 times via readBit). A separate change will follow that optimizes readBit, but for this particular case there's no real value to using a ParsableBitArray anyway; use of ParsableBitArray IMO only really becomes useful when you need to parse a bitstream more than 4 bytes long, or where parsing the bitstream requires some control flow (if/for) to parse. There are probably other places where we're using ParsableBitArray over-zealously. I'll roll that into a tracking bug for looking in more detail at all extractors. Issue: #3040 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=161650940 --- .../exoplayer2/extractor/ts/TsExtractor.java | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/library/core/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 index 7b63ce813c..1149856649 100644 --- a/library/core/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 @@ -111,7 +111,6 @@ public final class TsExtractor implements Extractor { @Mode private final int mode; private final List timestampAdjusters; private final ParsableByteArray tsPacketBuffer; - private final ParsableBitArray tsScratch; private final SparseIntArray continuityCounters; private final TsPayloadReader.Factory payloadReaderFactory; private final SparseArray tsPayloadReaders; // Indexed by pid @@ -164,7 +163,6 @@ public final class TsExtractor implements Extractor { timestampAdjusters.add(timestampAdjuster); } tsPacketBuffer = new ParsableByteArray(BUFFER_SIZE); - tsScratch = new ParsableBitArray(new byte[3]); trackIds = new SparseBooleanArray(); tsPayloadReaders = new SparseArray<>(); continuityCounters = new SparseIntArray(); @@ -250,24 +248,23 @@ public final class TsExtractor implements Extractor { return RESULT_CONTINUE; } - tsPacketBuffer.skipBytes(1); - tsPacketBuffer.readBytes(tsScratch, 3); - if (tsScratch.readBit()) { // transport_error_indicator + int tsPacketHeader = tsPacketBuffer.readInt(); + if ((tsPacketHeader & 0x800000) != 0) { // transport_error_indicator // There are uncorrectable errors in this packet. tsPacketBuffer.setPosition(endOfPacket); return RESULT_CONTINUE; } - boolean payloadUnitStartIndicator = tsScratch.readBit(); - tsScratch.skipBits(1); // transport_priority - int pid = tsScratch.readBits(13); - tsScratch.skipBits(2); // transport_scrambling_control - boolean adaptationFieldExists = tsScratch.readBit(); - boolean payloadExists = tsScratch.readBit(); + boolean payloadUnitStartIndicator = (tsPacketHeader & 0x400000) != 0; + // Ignoring transport_priority (tsPacketHeader & 0x200000) + int pid = (tsPacketHeader & 0x1FFF00) >> 8; + // Ignoring transport_scrambling_control (tsPacketHeader & 0xC0) + boolean adaptationFieldExists = (tsPacketHeader & 0x20) != 0; + boolean payloadExists = (tsPacketHeader & 0x10) != 0; // Discontinuity check. boolean discontinuityFound = false; - int continuityCounter = tsScratch.readBits(4); if (mode != MODE_HLS) { + int continuityCounter = tsPacketHeader & 0xF; int previousCounter = continuityCounters.get(pid, continuityCounter - 1); continuityCounters.put(pid, continuityCounter); if (previousCounter == continuityCounter) { @@ -276,7 +273,7 @@ public final class TsExtractor implements Extractor { tsPacketBuffer.setPosition(endOfPacket); return RESULT_CONTINUE; } - } else if (continuityCounter != (previousCounter + 1) % 16) { + } else if (continuityCounter != ((previousCounter + 1) & 0xF)) { discontinuityFound = true; } } @@ -296,7 +293,6 @@ public final class TsExtractor implements Extractor { } tsPacketBuffer.setLimit(endOfPacket); payloadReader.consume(tsPacketBuffer, payloadUnitStartIndicator); - Assertions.checkState(tsPacketBuffer.getPosition() <= endOfPacket); tsPacketBuffer.setLimit(limit); } } From fba0546774133c0d7b78b575e96ba8208a88cab5 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 12 Jul 2017 08:12:11 -0700 Subject: [PATCH 0089/2472] Optimize ParsableBitArray ParsableBitArray.readBit in particular was doing an excessive amount of work. The new implementation is ~20% faster on desktop. Issue: #3040 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=161666420 --- .../exoplayer2/util/ParsableBitArrayTest.java | 139 ++++++++++++++++++ .../exoplayer2/util/ParsableBitArray.java | 73 ++++----- 2 files changed, 167 insertions(+), 45 deletions(-) create mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/util/ParsableBitArrayTest.java diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ParsableBitArrayTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ParsableBitArrayTest.java new file mode 100644 index 0000000000..cfb9cd78be --- /dev/null +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ParsableBitArrayTest.java @@ -0,0 +1,139 @@ +/* + * 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.util; + +import android.test.MoreAsserts; + +import junit.framework.TestCase; + +/** + * Tests for {@link ParsableBitArray}. + */ +public final class ParsableBitArrayTest extends TestCase { + + private static final byte[] TEST_DATA = new byte[] {0x3C, (byte) 0xD2, (byte) 0x5F, (byte) 0x01, + (byte) 0xFF, (byte) 0x14, (byte) 0x60, (byte) 0x99}; + + public void testReadAllBytes() { + ParsableBitArray testArray = new ParsableBitArray(TEST_DATA); + byte[] bytesRead = new byte[TEST_DATA.length]; + testArray.readBytes(bytesRead, 0, TEST_DATA.length); + MoreAsserts.assertEquals(TEST_DATA, bytesRead); + assertEquals(TEST_DATA.length * 8, testArray.getPosition()); + assertEquals(TEST_DATA.length, testArray.getBytePosition()); + } + + public void testReadBit() { + ParsableBitArray testArray = new ParsableBitArray(TEST_DATA); + assertReadBitsToEnd(0, testArray); + } + + public void testReadBits() { + ParsableBitArray testArray = new ParsableBitArray(TEST_DATA); + assertEquals(getTestDataBits(0, 5), testArray.readBits(5)); + assertEquals(getTestDataBits(5, 3), testArray.readBits(3)); + assertEquals(getTestDataBits(8, 16), testArray.readBits(16)); + assertEquals(getTestDataBits(24, 3), testArray.readBits(3)); + assertEquals(getTestDataBits(27, 18), testArray.readBits(18)); + assertEquals(getTestDataBits(45, 5), testArray.readBits(5)); + assertEquals(getTestDataBits(50, 14), testArray.readBits(14)); + } + + public void testRead32BitsByteAligned() { + ParsableBitArray testArray = new ParsableBitArray(TEST_DATA); + assertEquals(getTestDataBits(0, 32), testArray.readBits(32)); + assertEquals(getTestDataBits(32, 32), testArray.readBits(32)); + } + + public void testRead32BitsNonByteAligned() { + ParsableBitArray testArray = new ParsableBitArray(TEST_DATA); + assertEquals(getTestDataBits(0, 5), testArray.readBits(5)); + assertEquals(getTestDataBits(5, 32), testArray.readBits(32)); + } + + public void testSkipBytes() { + ParsableBitArray testArray = new ParsableBitArray(TEST_DATA); + testArray.skipBytes(2); + assertReadBitsToEnd(16, testArray); + } + + public void testSkipBitsByteAligned() { + ParsableBitArray testArray = new ParsableBitArray(TEST_DATA); + testArray.skipBits(16); + assertReadBitsToEnd(16, testArray); + } + + public void testSkipBitsNonByteAligned() { + ParsableBitArray testArray = new ParsableBitArray(TEST_DATA); + testArray.skipBits(5); + assertReadBitsToEnd(5, testArray); + } + + public void testSetPositionByteAligned() { + ParsableBitArray testArray = new ParsableBitArray(TEST_DATA); + testArray.setPosition(16); + assertReadBitsToEnd(16, testArray); + } + + public void testSetPositionNonByteAligned() { + ParsableBitArray testArray = new ParsableBitArray(TEST_DATA); + testArray.setPosition(5); + assertReadBitsToEnd(5, testArray); + } + + public void testByteAlignFromNonByteAligned() { + ParsableBitArray testArray = new ParsableBitArray(TEST_DATA); + testArray.setPosition(11); + testArray.byteAlign(); + assertEquals(2, testArray.getBytePosition()); + assertEquals(16, testArray.getPosition()); + assertReadBitsToEnd(16, testArray); + } + + public void testByteAlignFromByteAligned() { + ParsableBitArray testArray = new ParsableBitArray(TEST_DATA); + testArray.setPosition(16); + testArray.byteAlign(); // Should be a no-op. + assertEquals(2, testArray.getBytePosition()); + assertEquals(16, testArray.getPosition()); + assertReadBitsToEnd(16, testArray); + } + + private static void assertReadBitsToEnd(int expectedStartPosition, ParsableBitArray testArray) { + int position = testArray.getPosition(); + assertEquals(expectedStartPosition, position); + for (int i = position; i < TEST_DATA.length * 8; i++) { + assertEquals(getTestDataBit(i), testArray.readBit()); + assertEquals(i + 1, testArray.getPosition()); + } + } + + private static int getTestDataBits(int bitPosition, int length) { + int result = 0; + for (int i = 0; i < length; i++) { + result = result << 1; + if (getTestDataBit(bitPosition++)) { + result |= 0x1; + } + } + return result; + } + + private static boolean getTestDataBit(int bitPosition) { + return (TEST_DATA[bitPosition / 8] & (0x80 >>> (bitPosition % 8))) != 0; + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java b/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java index df9f04f067..0456bcb879 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java @@ -110,14 +110,26 @@ public final class ParsableBitArray { assertValidOffset(); } + /** + * Skips a single bit. + */ + public void skipBit() { + if (++bitOffset == 8) { + bitOffset = 0; + byteOffset++; + } + assertValidOffset(); + } + /** * Skips bits and moves current reading position forward. * - * @param n The number of bits to skip. + * @param numBits The number of bits to skip. */ - public void skipBits(int n) { - byteOffset += (n / 8); - bitOffset += (n % 8); + public void skipBits(int numBits) { + int numBytes = numBits / 8; + byteOffset += numBytes; + bitOffset += numBits - (numBytes * 8); if (bitOffset > 7) { byteOffset++; bitOffset -= 8; @@ -131,7 +143,9 @@ public final class ParsableBitArray { * @return Whether the bit is set. */ public boolean readBit() { - return readBits(1) == 1; + boolean returnValue = (data[byteOffset] & (0x80 >> bitOffset)) != 0; + skipBit(); + return returnValue; } /** @@ -141,48 +155,18 @@ public final class ParsableBitArray { * @return An integer whose bottom n bits hold the read data. */ public int readBits(int numBits) { - if (numBits == 0) { - return 0; - } - int returnValue = 0; - - // Read as many whole bytes as we can. - int wholeBytes = (numBits / 8); - for (int i = 0; i < wholeBytes; i++) { - int byteValue; - if (bitOffset != 0) { - byteValue = ((data[byteOffset] & 0xFF) << bitOffset) - | ((data[byteOffset + 1] & 0xFF) >>> (8 - bitOffset)); - } else { - byteValue = data[byteOffset]; - } - numBits -= 8; - returnValue |= (byteValue & 0xFF) << numBits; + bitOffset += numBits; + while (bitOffset > 8) { + bitOffset -= 8; + returnValue |= (data[byteOffset++] & 0xFF) << bitOffset; + } + returnValue |= (data[byteOffset] & 0xFF) >> 8 - bitOffset; + returnValue &= 0xFFFFFFFF >>> (32 - numBits); + if (bitOffset == 8) { + bitOffset = 0; byteOffset++; } - - // Read any remaining bits. - if (numBits > 0) { - int nextBit = bitOffset + numBits; - byte writeMask = (byte) (0xFF >> (8 - numBits)); - - if (nextBit > 8) { - // Combine bits from current byte and next byte. - returnValue |= ((((data[byteOffset] & 0xFF) << (nextBit - 8) - | ((data[byteOffset + 1] & 0xFF) >> (16 - nextBit))) & writeMask)); - byteOffset++; - } else { - // Bits to be read only within current byte. - returnValue |= (((data[byteOffset] & 0xFF) >> (8 - nextBit)) & writeMask); - if (nextBit == 8) { - byteOffset++; - } - } - - bitOffset = nextBit % 8; - } - assertValidOffset(); return returnValue; } @@ -231,7 +215,6 @@ public final class ParsableBitArray { private void assertValidOffset() { // It is fine for position to be at the end of the array, but no further. Assertions.checkState(byteOffset >= 0 - && (bitOffset >= 0 && bitOffset < 8) && (byteOffset < byteLimit || (byteOffset == byteLimit && bitOffset == 0))); } From 17e73bdc7859852cab8821501da6c61bbcb7dc29 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 12 Jul 2017 09:25:08 -0700 Subject: [PATCH 0090/2472] Optimize ParsableNalUnitBitArray Apply the same learnings as in ParsableBitArray. Issue: #3040 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=161674119 --- .../util/ParsableNalUnitBitArrayTest.java | 2 +- .../exoplayer2/extractor/ts/H264Reader.java | 2 +- .../exoplayer2/extractor/ts/H265Reader.java | 14 +-- .../android/exoplayer2/util/NalUnitUtil.java | 10 +-- .../util/ParsableNalUnitBitArray.java | 85 ++++++++----------- 5 files changed, 48 insertions(+), 65 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ParsableNalUnitBitArrayTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ParsableNalUnitBitArrayTest.java index b62aff46f5..294d3d352a 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ParsableNalUnitBitArrayTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ParsableNalUnitBitArrayTest.java @@ -95,7 +95,7 @@ public final class ParsableNalUnitBitArrayTest extends TestCase { ParsableNalUnitBitArray array = new ParsableNalUnitBitArray(createByteArray(0, 0, 3, 128, 0), 0, 5); assertFalse(array.canReadExpGolombCodedNum()); - array.skipBits(1); + array.skipBit(); assertTrue(array.canReadExpGolombCodedNum()); assertEquals(32767, array.readUnsignedExpGolombCodedInt()); assertFalse(array.canReadBits(1)); diff --git a/library/core/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 index 8206ed7d6d..3cde946ce3 100644 --- a/library/core/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 @@ -316,7 +316,7 @@ public final class H264Reader implements ElementaryStreamReader { if (!bitArray.canReadBits(8)) { return; } - bitArray.skipBits(1); // forbidden_zero_bit + bitArray.skipBit(); // forbidden_zero_bit int nalRefIdc = bitArray.readBits(2); bitArray.skipBits(5); // nal_unit_type diff --git a/library/core/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 index 712ca8d69c..f6ae80ba56 100644 --- a/library/core/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 @@ -225,7 +225,7 @@ public final class H265Reader implements ElementaryStreamReader { ParsableNalUnitBitArray bitArray = new ParsableNalUnitBitArray(sps.nalData, 0, sps.nalLength); bitArray.skipBits(40 + 4); // NAL header, sps_video_parameter_set_id int maxSubLayersMinus1 = bitArray.readBits(3); - bitArray.skipBits(1); // sps_temporal_id_nesting_flag + bitArray.skipBit(); // sps_temporal_id_nesting_flag // profile_tier_level(1, sps_max_sub_layers_minus1) bitArray.skipBits(88); // if (profilePresentFlag) {...} @@ -247,7 +247,7 @@ public final class H265Reader implements ElementaryStreamReader { bitArray.readUnsignedExpGolombCodedInt(); // sps_seq_parameter_set_id int chromaFormatIdc = bitArray.readUnsignedExpGolombCodedInt(); if (chromaFormatIdc == 3) { - bitArray.skipBits(1); // separate_colour_plane_flag + bitArray.skipBit(); // separate_colour_plane_flag } int picWidthInLumaSamples = bitArray.readUnsignedExpGolombCodedInt(); int picHeightInLumaSamples = bitArray.readUnsignedExpGolombCodedInt(); @@ -288,7 +288,7 @@ public final class H265Reader implements ElementaryStreamReader { bitArray.skipBits(8); bitArray.readUnsignedExpGolombCodedInt(); // log2_min_pcm_luma_coding_block_size_minus3 bitArray.readUnsignedExpGolombCodedInt(); // log2_diff_max_min_pcm_luma_coding_block_size - bitArray.skipBits(1); // pcm_loop_filter_disabled_flag + bitArray.skipBit(); // pcm_loop_filter_disabled_flag } // Skips all short term reference picture sets. skipShortTermRefPicSets(bitArray); @@ -365,11 +365,11 @@ public final class H265Reader implements ElementaryStreamReader { interRefPicSetPredictionFlag = bitArray.readBit(); } if (interRefPicSetPredictionFlag) { - bitArray.skipBits(1); // delta_rps_sign + bitArray.skipBit(); // delta_rps_sign bitArray.readUnsignedExpGolombCodedInt(); // abs_delta_rps_minus1 for (int j = 0; j <= previousNumDeltaPocs; j++) { if (bitArray.readBit()) { // used_by_curr_pic_flag[j] - bitArray.skipBits(1); // use_delta_flag[j] + bitArray.skipBit(); // use_delta_flag[j] } } } else { @@ -378,11 +378,11 @@ public final class H265Reader implements ElementaryStreamReader { previousNumDeltaPocs = numNegativePics + numPositivePics; for (int i = 0; i < numNegativePics; i++) { bitArray.readUnsignedExpGolombCodedInt(); // delta_poc_s0_minus1[i] - bitArray.skipBits(1); // used_by_curr_pic_s0_flag[i] + bitArray.skipBit(); // used_by_curr_pic_s0_flag[i] } for (int i = 0; i < numPositivePics; i++) { bitArray.readUnsignedExpGolombCodedInt(); // delta_poc_s1_minus1[i] - bitArray.skipBits(1); // used_by_curr_pic_s1_flag[i] + bitArray.skipBit(); // used_by_curr_pic_s1_flag[i] } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/NalUnitUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/util/NalUnitUtil.java index ab2fec0db7..c4ed20546d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/NalUnitUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/NalUnitUtil.java @@ -265,7 +265,7 @@ public final class NalUnitUtil { } data.readUnsignedExpGolombCodedInt(); // bit_depth_luma_minus8 data.readUnsignedExpGolombCodedInt(); // bit_depth_chroma_minus8 - data.skipBits(1); // qpprime_y_zero_transform_bypass_flag + data.skipBit(); // qpprime_y_zero_transform_bypass_flag boolean seqScalingMatrixPresentFlag = data.readBit(); if (seqScalingMatrixPresentFlag) { int limit = (chromaFormatIdc != 3) ? 8 : 12; @@ -295,17 +295,17 @@ public final class NalUnitUtil { } } data.readUnsignedExpGolombCodedInt(); // max_num_ref_frames - data.skipBits(1); // gaps_in_frame_num_value_allowed_flag + data.skipBit(); // gaps_in_frame_num_value_allowed_flag int picWidthInMbs = data.readUnsignedExpGolombCodedInt() + 1; int picHeightInMapUnits = data.readUnsignedExpGolombCodedInt() + 1; boolean frameMbsOnlyFlag = data.readBit(); int frameHeightInMbs = (2 - (frameMbsOnlyFlag ? 1 : 0)) * picHeightInMapUnits; if (!frameMbsOnlyFlag) { - data.skipBits(1); // mb_adaptive_frame_field_flag + data.skipBit(); // mb_adaptive_frame_field_flag } - data.skipBits(1); // direct_8x8_inference_flag + data.skipBit(); // direct_8x8_inference_flag int frameWidth = picWidthInMbs * 16; int frameHeight = frameHeightInMbs * 16; boolean frameCroppingFlag = data.readBit(); @@ -368,7 +368,7 @@ public final class NalUnitUtil { data.skipBits(8); // nal_unit int picParameterSetId = data.readUnsignedExpGolombCodedInt(); int seqParameterSetId = data.readUnsignedExpGolombCodedInt(); - data.skipBits(1); // entropy_coding_mode_flag + data.skipBit(); // entropy_coding_mode_flag boolean bottomFieldPicOrderInFramePresentFlag = data.readBit(); return new PpsData(picParameterSetId, seqParameterSetId, bottomFieldPicOrderInFramePresentFlag); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableNalUnitBitArray.java b/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableNalUnitBitArray.java index 05d7a9929d..443c69909c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableNalUnitBitArray.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableNalUnitBitArray.java @@ -54,15 +54,27 @@ public final class ParsableNalUnitBitArray { assertValidOffset(); } + /** + * Skips a single bit. + */ + public void skipBit() { + if (++bitOffset == 8) { + bitOffset = 0; + byteOffset += shouldSkipByte(byteOffset + 1) ? 2 : 1; + } + assertValidOffset(); + } + /** * Skips bits and moves current reading position forward. * - * @param n The number of bits to skip. + * @param numBits The number of bits to skip. */ - public void skipBits(int n) { + public void skipBits(int numBits) { int oldByteOffset = byteOffset; - byteOffset += (n / 8); - bitOffset += (n % 8); + int numBytes = numBits / 8; + byteOffset += numBytes; + bitOffset += numBits - (numBytes * 8); if (bitOffset > 7) { byteOffset++; bitOffset -= 8; @@ -81,13 +93,14 @@ public final class ParsableNalUnitBitArray { * Returns whether it's possible to read {@code n} bits starting from the current offset. The * offset is not modified. * - * @param n The number of bits. + * @param numBits The number of bits. * @return Whether it is possible to read {@code n} bits. */ - public boolean canReadBits(int n) { + public boolean canReadBits(int numBits) { int oldByteOffset = byteOffset; - int newByteOffset = byteOffset + (n / 8); - int newBitOffset = bitOffset + (n % 8); + int numBytes = numBits / 8; + int newByteOffset = byteOffset + numBytes; + int newBitOffset = bitOffset + numBits - (numBytes * 8); if (newBitOffset > 7) { newByteOffset++; newBitOffset -= 8; @@ -108,7 +121,9 @@ public final class ParsableNalUnitBitArray { * @return Whether the bit is set. */ public boolean readBit() { - return readBits(1) == 1; + boolean returnValue = (data[byteOffset] & (0x80 >> bitOffset)) != 0; + skipBit(); + return returnValue; } /** @@ -118,50 +133,19 @@ public final class ParsableNalUnitBitArray { * @return An integer whose bottom n bits hold the read data. */ public int readBits(int numBits) { - if (numBits == 0) { - return 0; - } - int returnValue = 0; - - // Read as many whole bytes as we can. - int wholeBytes = (numBits / 8); - for (int i = 0; i < wholeBytes; i++) { - int nextByteOffset = shouldSkipByte(byteOffset + 1) ? byteOffset + 2 : byteOffset + 1; - int byteValue; - if (bitOffset != 0) { - byteValue = ((data[byteOffset] & 0xFF) << bitOffset) - | ((data[nextByteOffset] & 0xFF) >>> (8 - bitOffset)); - } else { - byteValue = data[byteOffset]; - } - numBits -= 8; - returnValue |= (byteValue & 0xFF) << numBits; - byteOffset = nextByteOffset; + bitOffset += numBits; + while (bitOffset > 8) { + bitOffset -= 8; + returnValue |= (data[byteOffset] & 0xFF) << bitOffset; + byteOffset += shouldSkipByte(byteOffset + 1) ? 2 : 1; } - - // Read any remaining bits. - if (numBits > 0) { - int nextBit = bitOffset + numBits; - byte writeMask = (byte) (0xFF >> (8 - numBits)); - int nextByteOffset = shouldSkipByte(byteOffset + 1) ? byteOffset + 2 : byteOffset + 1; - - if (nextBit > 8) { - // Combine bits from current byte and next byte. - returnValue |= ((((data[byteOffset] & 0xFF) << (nextBit - 8) - | ((data[nextByteOffset] & 0xFF) >> (16 - nextBit))) & writeMask)); - byteOffset = nextByteOffset; - } else { - // Bits to be read only within current byte. - returnValue |= (((data[byteOffset] & 0xFF) >> (8 - nextBit)) & writeMask); - if (nextBit == 8) { - byteOffset = nextByteOffset; - } - } - - bitOffset = nextBit % 8; + returnValue |= (data[byteOffset] & 0xFF) >> 8 - bitOffset; + returnValue &= 0xFFFFFFFF >>> (32 - numBits); + if (bitOffset == 8) { + bitOffset = 0; + byteOffset += shouldSkipByte(byteOffset + 1) ? 2 : 1; } - assertValidOffset(); return returnValue; } @@ -220,7 +204,6 @@ public final class ParsableNalUnitBitArray { private void assertValidOffset() { // It is fine for position to be at the end of the array, but no further. Assertions.checkState(byteOffset >= 0 - && (bitOffset >= 0 && bitOffset < 8) && (byteOffset < byteLimit || (byteOffset == byteLimit && bitOffset == 0))); } From a39ab8161e035174c862fb42b0c522baf42f22fb Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 12 Jul 2017 09:53:20 -0700 Subject: [PATCH 0091/2472] Fix FlacStreamInfo to not call readBits with a >32 value ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=161677399 --- .../com/google/android/exoplayer2/util/FlacStreamInfo.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamInfo.java index 8a69a6d095..6382f1130e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamInfo.java @@ -47,7 +47,8 @@ public final class FlacStreamInfo { this.sampleRate = scratch.readBits(20); this.channels = scratch.readBits(3) + 1; this.bitsPerSample = scratch.readBits(5) + 1; - this.totalSamples = scratch.readBits(36); + this.totalSamples = ((scratch.readBits(4) & 0xFL) << 32) + | (scratch.readBits(32) & 0xFFFFFFFFL); // Remaining 16 bytes is md5 value } From 977fb225e3ba58965b16bda99483f5e5eef9313e Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 12 Jul 2017 11:55:49 -0700 Subject: [PATCH 0092/2472] Minor tweak to demo app "Default (none)" is sometimes just wrong, since the track selector may attempt to select a track even if it exceeds the renderer's capabilities. Just "Default", as it used to be, was more accurate. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=161695241 --- .../android/exoplayer2/demo/TrackSelectionHelper.java | 7 +------ demo/src/main/res/values/strings.xml | 2 -- 2 files changed, 1 insertion(+), 8 deletions(-) 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 033b515767..fb7217f8fd 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 @@ -136,7 +136,6 @@ import java.util.Arrays; root.addView(defaultView); // Per-track views. - boolean haveSupportedTracks = false; boolean haveAdaptiveTracks = false; trackViews = new CheckedTextView[trackGroups.length][]; for (int groupIndex = 0; groupIndex < trackGroups.length; groupIndex++) { @@ -159,7 +158,6 @@ import java.util.Arrays; trackView.setFocusable(true); trackView.setTag(Pair.create(groupIndex, trackIndex)); trackView.setOnClickListener(this); - haveSupportedTracks = true; } else { trackView.setFocusable(false); trackView.setEnabled(false); @@ -169,10 +167,7 @@ import java.util.Arrays; } } - if (!haveSupportedTracks) { - // Indicate that the default selection will be nothing. - defaultView.setText(R.string.selection_default_none); - } else if (haveAdaptiveTracks) { + if (haveAdaptiveTracks) { // View for using random adaptation. enableRandomAdaptationView = (CheckedTextView) inflater.inflate( android.R.layout.simple_list_item_multiple_choice, root, false); diff --git a/demo/src/main/res/values/strings.xml b/demo/src/main/res/values/strings.xml index ac17ad4443..4eb2b89324 100644 --- a/demo/src/main/res/values/strings.xml +++ b/demo/src/main/res/values/strings.xml @@ -30,8 +30,6 @@ Default - Default (none) - Unexpected intent action: %1$s Enable random adaptation From 1855423734287861163ccb82258779555159dad8 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 13 Jul 2017 05:59:15 -0700 Subject: [PATCH 0093/2472] Simplify + optimize VorbisBitArray ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=161796758 --- .../extractor/ogg/VorbisBitArrayTest.java | 131 ------------------ .../extractor/ogg/VorbisBitArray.java | 85 ++++-------- 2 files changed, 28 insertions(+), 188 deletions(-) diff --git a/library/core/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 index 9a65cad6a5..a24cb1599b 100644 --- a/library/core/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 @@ -25,36 +25,26 @@ public final class VorbisBitArrayTest extends TestCase { public void testReadBit() { VorbisBitArray bitArray = new VorbisBitArray(TestUtil.createByteArray(0x5c, 0x50)); - assertFalse(bitArray.readBit()); assertFalse(bitArray.readBit()); assertTrue(bitArray.readBit()); assertTrue(bitArray.readBit()); - assertTrue(bitArray.readBit()); assertFalse(bitArray.readBit()); assertTrue(bitArray.readBit()); assertFalse(bitArray.readBit()); - assertFalse(bitArray.readBit()); assertFalse(bitArray.readBit()); assertFalse(bitArray.readBit()); assertFalse(bitArray.readBit()); - assertTrue(bitArray.readBit()); assertFalse(bitArray.readBit()); assertTrue(bitArray.readBit()); assertFalse(bitArray.readBit()); - - try { - assertFalse(bitArray.readBit()); - fail(); - } catch (IllegalStateException e) {/* ignored */} } public void testSkipBits() { VorbisBitArray bitArray = new VorbisBitArray(TestUtil.createByteArray(0xF0, 0x0F)); - bitArray.skipBits(10); assertEquals(10, bitArray.getPosition()); assertTrue(bitArray.readBit()); @@ -64,27 +54,10 @@ public final class VorbisBitArrayTest extends TestCase { assertEquals(14, bitArray.getPosition()); assertFalse(bitArray.readBit()); assertFalse(bitArray.readBit()); - try { - bitArray.readBit(); - fail(); - } catch (IllegalStateException e) { - // ignored - } - } - - - public void testSkipBitsThrowsErrorIfEOB() { - VorbisBitArray bitArray = new VorbisBitArray(TestUtil.createByteArray(0xF0, 0x0F)); - - try { - bitArray.skipBits(17); - fail(); - } catch (IllegalStateException e) {/* ignored */} } public void testGetPosition() throws Exception { VorbisBitArray bitArray = new VorbisBitArray(TestUtil.createByteArray(0xF0, 0x0F)); - assertEquals(0, bitArray.getPosition()); bitArray.readBit(); assertEquals(1, bitArray.getPosition()); @@ -96,35 +69,11 @@ public final class VorbisBitArrayTest extends TestCase { public void testSetPosition() throws Exception { VorbisBitArray bitArray = new VorbisBitArray(TestUtil.createByteArray(0xF0, 0x0F)); - assertEquals(0, bitArray.getPosition()); bitArray.setPosition(4); assertEquals(4, bitArray.getPosition()); - bitArray.setPosition(15); assertFalse(bitArray.readBit()); - try { - bitArray.readBit(); - fail(); - } catch (IllegalStateException e) {/* ignored */} - - } - public void testSetPositionIllegalPositions() throws Exception { - VorbisBitArray bitArray = new VorbisBitArray(TestUtil.createByteArray(0xF0, 0x0F)); - - try { - bitArray.setPosition(16); - fail(); - } catch (IllegalArgumentException e) { - assertEquals(0, bitArray.getPosition()); - } - - try { - bitArray.setPosition(-1); - fail(); - } catch (IllegalArgumentException e) { - assertEquals(0, bitArray.getPosition()); - } } public void testReadInt32() { @@ -136,13 +85,11 @@ public final class VorbisBitArrayTest extends TestCase { public void testReadBits() throws Exception { VorbisBitArray bitArray = new VorbisBitArray(TestUtil.createByteArray(0x03, 0x22)); - assertEquals(3, bitArray.readBits(2)); bitArray.skipBits(6); assertEquals(2, bitArray.readBits(2)); bitArray.skipBits(2); assertEquals(2, bitArray.readBits(2)); - bitArray.reset(); assertEquals(0x2203, bitArray.readBits(16)); } @@ -156,7 +103,6 @@ public final class VorbisBitArrayTest extends TestCase { public void testReadBitsBeyondByteBoundaries() throws Exception { VorbisBitArray bitArray = new VorbisBitArray(TestUtil.createByteArray(0xFF, 0x0F, 0xFF, 0x0F)); - assertEquals(0x0FFF0FFF, bitArray.readBits(32)); bitArray.reset(); @@ -188,83 +134,6 @@ public final class VorbisBitArrayTest extends TestCase { assertEquals(0, bitArray.getPosition()); bitArray.readBit(); assertEquals(1, bitArray.getPosition()); - - try { - bitArray.readBits(24); - fail(); - } catch (IllegalStateException e) { - assertEquals(1, bitArray.getPosition()); - } - } - - public void testLimit() { - VorbisBitArray bitArray = new VorbisBitArray(TestUtil.createByteArray(0xc0, 0x02), 1); - - try { - bitArray.skipBits(9); - fail(); - } catch (IllegalStateException e) { - assertEquals(0, bitArray.getPosition()); - } - - try { - bitArray.readBits(9); - fail(); - } catch (IllegalStateException e) { - assertEquals(0, bitArray.getPosition()); - } - - int byteValue = bitArray.readBits(8); - assertEquals(0xc0, byteValue); - assertEquals(8, bitArray.getPosition()); - try { - bitArray.readBit(); - fail(); - } catch (IllegalStateException e) { - assertEquals(8, bitArray.getPosition()); - } - } - - public void testBitsLeft() { - VorbisBitArray bitArray = new VorbisBitArray(TestUtil.createByteArray(0xc0, 0x02)); - - assertEquals(16, bitArray.bitsLeft()); - assertEquals(bitArray.limit(), bitArray.getPosition() + bitArray.bitsLeft()); - - bitArray.skipBits(1); - assertEquals(15, bitArray.bitsLeft()); - assertEquals(bitArray.limit(), bitArray.getPosition() + bitArray.bitsLeft()); - - bitArray.skipBits(3); - assertEquals(12, bitArray.bitsLeft()); - assertEquals(bitArray.limit(), bitArray.getPosition() + bitArray.bitsLeft()); - - bitArray.setPosition(6); - assertEquals(10, bitArray.bitsLeft()); - assertEquals(bitArray.limit(), bitArray.getPosition() + bitArray.bitsLeft()); - - bitArray.skipBits(1); - assertEquals(9, bitArray.bitsLeft()); - assertEquals(bitArray.limit(), bitArray.getPosition() + bitArray.bitsLeft()); - - bitArray.skipBits(1); - assertEquals(8, bitArray.bitsLeft()); - assertEquals(bitArray.limit(), bitArray.getPosition() + bitArray.bitsLeft()); - - bitArray.readBits(4); - assertEquals(4, bitArray.bitsLeft()); - assertEquals(bitArray.limit(), bitArray.getPosition() + bitArray.bitsLeft()); - - bitArray.readBits(4); - assertEquals(0, bitArray.bitsLeft()); - assertEquals(bitArray.limit(), bitArray.getPosition() + bitArray.bitsLeft()); - - try { - bitArray.readBit(); - fail(); - } catch (IllegalStateException e) { - assertEquals(0, bitArray.bitsLeft()); - } } } diff --git a/library/core/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 index ae52e80299..958a2ef955 100644 --- a/library/core/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 @@ -25,8 +25,9 @@ import com.google.android.exoplayer2.util.Assertions; */ /* package */ final class VorbisBitArray { - public final byte[] data; - private final int limit; + private final byte[] data; + private final int byteLimit; + private int byteOffset; private int bitOffset; @@ -36,18 +37,8 @@ import com.google.android.exoplayer2.util.Assertions; * @param data the array to wrap. */ public VorbisBitArray(byte[] data) { - this(data, data.length); - } - - /** - * Creates a new instance that wraps an existing array. - * - * @param data the array to wrap. - * @param limit the limit in bytes. - */ - public VorbisBitArray(byte[] data, int limit) { this.data = data; - this.limit = limit * 8; + byteLimit = data.length; } /** @@ -64,7 +55,9 @@ import com.google.android.exoplayer2.util.Assertions; * @return {@code true} if the bit is set, {@code false} otherwise. */ public boolean readBit() { - return readBits(1) == 1; + boolean returnValue = (((data[byteOffset] & 0xFF) >> bitOffset) & 0x01) == 1; + skipBits(1); + return returnValue; } /** @@ -74,53 +67,32 @@ import com.google.android.exoplayer2.util.Assertions; * @return An integer whose bottom {@code numBits} bits hold the read data. */ public int readBits(int numBits) { - Assertions.checkState(getPosition() + numBits <= limit); - if (numBits == 0) { - return 0; + int tempByteOffset = byteOffset; + int bitsRead = Math.min(numBits, 8 - bitOffset); + int returnValue = ((data[tempByteOffset++] & 0xFF) >> bitOffset) & (0xFF >> (8 - bitsRead)); + while (bitsRead < numBits) { + returnValue |= (data[tempByteOffset++] & 0xFF) << bitsRead; + bitsRead += 8; } - int result = 0; - int bitCount = 0; - if (bitOffset != 0) { - bitCount = Math.min(numBits, 8 - bitOffset); - int mask = 0xFF >>> (8 - bitCount); - result = (data[byteOffset] >>> bitOffset) & mask; - bitOffset += bitCount; - if (bitOffset == 8) { - byteOffset++; - bitOffset = 0; - } - } - - if (numBits - bitCount > 7) { - int numBytes = (numBits - bitCount) / 8; - for (int i = 0; i < numBytes; i++) { - result |= (data[byteOffset++] & 0xFFL) << bitCount; - bitCount += 8; - } - } - - if (numBits > bitCount) { - int bitsOnNextByte = numBits - bitCount; - int mask = 0xFF >>> (8 - bitsOnNextByte); - result |= (data[byteOffset] & mask) << bitCount; - bitOffset += bitsOnNextByte; - } - return result; + returnValue &= 0xFFFFFFFF >>> (32 - numBits); + skipBits(numBits); + return returnValue; } /** * Skips {@code numberOfBits} bits. * - * @param numberOfBits The number of bits to skip. + * @param numBits The number of bits to skip. */ - public void skipBits(int numberOfBits) { - Assertions.checkState(getPosition() + numberOfBits <= limit); - byteOffset += numberOfBits / 8; - bitOffset += numberOfBits % 8; + public void skipBits(int numBits) { + int numBytes = numBits / 8; + byteOffset += numBytes; + bitOffset += numBits - (numBytes * 8); if (bitOffset > 7) { byteOffset++; bitOffset -= 8; } + assertValidOffset(); } /** @@ -136,23 +108,22 @@ import com.google.android.exoplayer2.util.Assertions; * @param position The new reading position in bits. */ public void setPosition(int position) { - Assertions.checkArgument(position < limit && position >= 0); byteOffset = position / 8; bitOffset = position - (byteOffset * 8); + assertValidOffset(); } /** * Returns the number of remaining bits. */ public int bitsLeft() { - return limit - getPosition(); + return (byteLimit - byteOffset) * 8 - bitOffset; } - /** - * Returns the limit in bits. - **/ - public int limit() { - return limit; + private void assertValidOffset() { + // It is fine for position to be at the end of the array, but no further. + Assertions.checkState(byteOffset >= 0 + && (byteOffset < byteLimit || (byteOffset == byteLimit && bitOffset == 0))); } } From 055abc759269e48dd4a9c7a1b315012d3abf0ac1 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 4 Jul 2017 09:38:57 -0700 Subject: [PATCH 0094/2472] Detect playlist stuck and playlist reset conditions in HLS This CL aims that the player fails upon: - Playlist that don't change in a suspiciously long time, which might mean there are server side issues. - Playlist with a media sequence lower that its last snapshot and no overlapping segments. This two error conditions are propagated through the renderer, but not through MediaSource#maybeThrowSourceInfoRefreshError. Issue:#2872 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=160899995 --- .../hls/playlist/HlsPlaylistTracker.java | 82 ++++++++++++++++--- 1 file changed, 72 insertions(+), 10 deletions(-) diff --git a/library/hls/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 index 62b77a0575..567dbd4af6 100644 --- a/library/hls/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 @@ -40,6 +40,38 @@ import java.util.List; */ public final class HlsPlaylistTracker implements Loader.Callback> { + /** + * Thrown when a playlist is considered to be stuck due to a server side error. + */ + public static final class PlaylistStuckException extends IOException { + + /** + * The url of the stuck playlist. + */ + public final String url; + + private PlaylistStuckException(String url) { + this.url = url; + } + + } + + /** + * Thrown when the media sequence of a new snapshot indicates the server has reset. + */ + public static final class PlaylistResetException extends IOException { + + /** + * The url of the reset playlist. + */ + public final String url; + + private PlaylistResetException(String url) { + this.url = url; + } + + } + /** * Listener for primary playlist changes. */ @@ -75,6 +107,11 @@ public final class HlsPlaylistTracker implements Loader.Callback C.usToMs(playlistSnapshot.targetDurationUs) + * PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT) { + // The playlist seems to be stuck, we blacklist it. + playlistError = new PlaylistStuckException(playlistUrl.url); + blacklistPlaylist(); + } else if (loadedPlaylist.mediaSequence + loadedPlaylist.segments.size() + < playlistSnapshot.mediaSequence) { + // The media sequence has jumped backwards. The server has likely reset. + playlistError = new PlaylistResetException(playlistUrl.url); + } refreshDelayUs = playlistSnapshot.targetDurationUs / 2; } if (refreshDelayUs != C.TIME_UNSET) { @@ -554,6 +610,12 @@ public final class HlsPlaylistTracker implements Loader.Callback Date: Wed, 19 Jul 2017 11:52:12 -0700 Subject: [PATCH 0095/2472] Update release notes + bump version number ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=162514848 --- RELEASENOTES.md | 11 +++++++++++ build.gradle | 2 +- demo/src/main/AndroidManifest.xml | 4 ++-- .../android/exoplayer2/ExoPlayerLibraryInfo.java | 6 +++--- 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index f9f6b02c19..ff1bd42fde 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,16 @@ # Release notes # +### r2.4.4 ### + +* HLS/MPEG-TS: Some initial optimizations of MPEG-TS extractor performance + ([#3040](https://github.com/google/ExoPlayer/issues/3040)). +* HLS: Fix propagation of format identifier for CEA-608 + ([#3033](https://github.com/google/ExoPlayer/issues/3033)). +* HLS: Detect playlist stuck and reset conditions + ([#2872](https://github.com/google/ExoPlayer/issues/2872)). +* Video: Fix video dimension reporting on some devices + ([#3007](https://github.com/google/ExoPlayer/issues/3007)). + ### r2.4.3 ### * Audio: Workaround custom audio decoders misreporting their maximum supported diff --git a/build.gradle b/build.gradle index 01d8b6616c..a4ae1f175e 100644 --- a/build.gradle +++ b/build.gradle @@ -48,7 +48,7 @@ allprojects { releaseRepoName = getBintrayRepo() releaseUserOrg = 'google' releaseGroupId = 'com.google.android.exoplayer' - releaseVersion = 'r2.4.3' + releaseVersion = 'r2.4.4' releaseWebsite = 'https://github.com/google/ExoPlayer' } if (it.hasProperty('externalBuildDir')) { diff --git a/demo/src/main/AndroidManifest.xml b/demo/src/main/AndroidManifest.xml index addce60cad..afcddccac9 100644 --- a/demo/src/main/AndroidManifest.xml +++ b/demo/src/main/AndroidManifest.xml @@ -16,8 +16,8 @@ + android:versionCode="2404" + android:versionName="2.4.4"> diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java index 650ce727cd..73d91f293e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java @@ -24,13 +24,13 @@ public interface ExoPlayerLibraryInfo { * The version of the library expressed as a string, for example "1.2.3". */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. - String VERSION = "2.4.3"; + String VERSION = "2.4.4"; /** * 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.3"; + String VERSION_SLASHY = "ExoPlayerLib/2.4.4"; /** * The version of the library expressed as an integer, for example 1002003. @@ -40,7 +40,7 @@ public interface ExoPlayerLibraryInfo { * integer version 123045006 (123-045-006). */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - int VERSION_INT = 2004003; + int VERSION_INT = 2004004; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} From 54e869cbbdf3bc5abbc08a090839e288e6624945 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 20 Jul 2017 04:51:59 -0700 Subject: [PATCH 0096/2472] Update Timeline Javadoc to include brief mention of ad groups ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=162601778 --- .../google/android/exoplayer2/Timeline.java | 57 ++++++++++++------- .../timeline-single-file-midrolls.svg | 51 +++++++++++++++++ 2 files changed, 86 insertions(+), 22 deletions(-) create mode 100644 library/core/src/main/javadoc/com/google/android/exoplayer2/doc-files/timeline-single-file-midrolls.svg diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java index 60650f9990..e45171fc69 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java @@ -19,17 +19,20 @@ import android.util.Pair; import com.google.android.exoplayer2.util.Assertions; /** - * A representation of media currently available for playback. - *

    - * Timeline instances are immutable. For cases where the available media is changing dynamically - * (e.g. live streams) a timeline provides a snapshot of the media currently available. + * A flexible representation of the structure of media. A timeline is able to represent the + * structure of a wide variety of media, from simple cases like a single media file through to + * complex compositions of media such as playlists and streams with inserted ads. Instances are + * immutable. For cases where media is changing dynamically (e.g. live streams), a timeline provides + * a snapshot of the current state. *

    * A timeline consists of related {@link Period}s and {@link Window}s. A period defines a single - * logical piece of media, for example a media file. A window spans one or more periods, defining - * the region within those periods that's currently available for playback along with additional - * information such as whether seeking is supported within the window. Each window defines a default - * position, which is the position from which playback will start when the player starts playing the - * window. The following examples illustrate timelines for various use cases. + * logical piece of media, for example a media file. It may also define groups of ads inserted into + * the media, along with information about whether those ads have been loaded and played. A window + * spans one or more periods, defining the region within those periods that's currently available + * for playback along with additional information such as whether seeking is supported within the + * window. Each window defines a default position, which is the position from which playback will + * start when the player starts playing the window. The following examples illustrate timelines for + * various use cases. * *

    Single media file or on-demand stream

    *

    @@ -78,28 +81,36 @@ import com.google.android.exoplayer2.util.Assertions; * with multiple periods"> *

    * This case arises when a live stream is explicitly divided into separate periods, for example at - * content and advert boundaries. This case is similar to the Live stream - * with limited availability case, except that the window may span more than one period. - * Multiple periods are also possible in the indefinite availability case. + * content boundaries. This case is similar to the Live stream with limited + * availability case, except that the window may span more than one period. Multiple periods are + * also possible in the indefinite availability case. * - *

    On-demand pre-roll followed by live stream

    + *

    On-demand stream followed by live stream

    *

    - * Example timeline for an on-demand pre-roll
+ *   <img src= *

    * This case is the concatenation of the Single media file or on-demand * stream and Live stream with multiple periods cases. When playback - * of the pre-roll ends, playback of the live stream will start from its default position near the - * live edge. + * of the on-demand stream ends, playback of the live stream will start from its default position + * near the live edge. + * + *

    On-demand stream with mid-roll ads

    + *

    + * Example timeline for an on-demand
+ *       stream with mid-roll ad groups + *

    + * This case includes mid-roll ad groups, which are defined as part of the timeline's single period. + * The period can be queried for information about the ad groups and the ads they contain. */ public abstract class Timeline { /** * Holds information about a window in a {@link Timeline}. A window defines a region of media * currently available for playback along with additional information such as whether seeking is - * supported within the window. See {@link Timeline} for more details. The figure below shows some - * of the information defined by a window, as well as how this information relates to - * corresponding {@link Period}s in the timeline. + * supported within the window. The figure below shows some of the information defined by a + * window, as well as how this information relates to corresponding {@link Period}s in the + * timeline. *

    * Information defined by a timeline window *

    @@ -235,9 +246,11 @@ public abstract class Timeline { /** * Holds information about a period in a {@link Timeline}. A period defines a single logical piece - * of media, for example a media file. See {@link Timeline} for more details. The figure below - * shows some of the information defined by a period, as well as how this information relates to a - * corresponding {@link Window} in the timeline. + * of media, for example a media file. It may also define groups of ads inserted into the media, + * along with information about whether those ads have been loaded and played. + *

    + * The figure below shows some of the information defined by a period, as well as how this + * information relates to a corresponding {@link Window} in the timeline. *

    * Information defined by a period *

    diff --git a/library/core/src/main/javadoc/com/google/android/exoplayer2/doc-files/timeline-single-file-midrolls.svg b/library/core/src/main/javadoc/com/google/android/exoplayer2/doc-files/timeline-single-file-midrolls.svg new file mode 100644 index 0000000000..a364587320 --- /dev/null +++ b/library/core/src/main/javadoc/com/google/android/exoplayer2/doc-files/timeline-single-file-midrolls.svg @@ -0,0 +1,51 @@ + + + + Produced by OmniGraffle 7.4 + 2017-07-19 14:26:00 +0000 + + + + + + + + + + + + + + + Canvas 1 + + + Layer 1 + + + + period1 + + + + + window1 + + + + + + + time + + + + + + + + + + + + From 19087a7fa0481dde8ae266d8b9cfe196477b2aa6 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 20 Jul 2017 04:56:02 -0700 Subject: [PATCH 0097/2472] Fix broken Javadoc ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=162601961 --- .../ext/mediasession/MediaSessionConnector.java | 4 ++-- .../extractor/DefaultExtractorsFactory.java | 2 +- .../source/DynamicConcatenatingMediaSource.java | 12 ++++++------ .../com/google/android/exoplayer2/util/Clock.java | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java index 0f96e5104f..9f3e299e96 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java @@ -164,7 +164,7 @@ public final class MediaSessionConnector { */ long getActiveQueueItemId(@Nullable ExoPlayer player); /** - * See {@link MediaSessionCompat.Callback#onSkipToPrevious()). + * See {@link MediaSessionCompat.Callback#onSkipToPrevious()}. */ void onSkipToPrevious(ExoPlayer player); /** @@ -200,7 +200,7 @@ public final class MediaSessionConnector { */ void onRemoveQueueItem(ExoPlayer player, MediaDescriptionCompat description); /** - * See {@link MediaSessionCompat.Callback#onRemoveQueueItemAt(int index)). + * See {@link MediaSessionCompat.Callback#onRemoveQueueItemAt(int index)}. */ void onRemoveQueueItemAt(ExoPlayer player, int index); /** 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 index c47a91b176..ccc5c0eb3e 100644 --- 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 @@ -117,7 +117,7 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory { /** * Sets the mode for {@link TsExtractor} instances created by the factory. * - * @see TsExtractor#TsExtractor(int, TimestampAdjuster, TsPayloadReader.Factory). + * @see TsExtractor#TsExtractor(int, TimestampAdjuster, TsPayloadReader.Factory) * @param mode The mode to use. * @return The factory, for convenience. */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java index 8ba6b46a47..ad2e154f6d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java @@ -82,7 +82,7 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl * Adds a {@link MediaSource} to the playlist. * * @param index The index at which the new {@link MediaSource} will be inserted. This index must - * be in the range of 0 <= index <= {@link #getSize()}. + * be in the range of 0 <= index <= {@link #getSize()}. * @param mediaSource The {@link MediaSource} to be added to the list. */ public synchronized void addMediaSource(int index, MediaSource mediaSource) { @@ -108,7 +108,7 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl * Adds multiple {@link MediaSource}s to the playlist. * * @param index The index at which the new {@link MediaSource}s will be inserted. This index must - * be in the range of 0 <= index <= {@link #getSize()}. + * be in the range of 0 <= index <= {@link #getSize()}. * @param mediaSources A collection of {@link MediaSource}s to be added to the list. The media * sources are added in the order in which they appear in this collection. */ @@ -128,7 +128,7 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl * Removes a {@link MediaSource} from the playlist. * * @param index The index at which the media source will be removed. This index must be in the - * range of 0 <= index < {@link #getSize()}. + * range of 0 <= index < {@link #getSize()}. */ public synchronized void removeMediaSource(int index) { mediaSourcesPublic.remove(index); @@ -141,9 +141,9 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl * Moves an existing {@link MediaSource} within the playlist. * * @param currentIndex The current index of the media source in the playlist. This index must be - * in the range of 0 <= index < {@link #getSize()}. + * in the range of 0 <= index < {@link #getSize()}. * @param newIndex The target index of the media source in the playlist. This index must be in the - * range of 0 <= index < {@link #getSize()}. + * range of 0 <= index < {@link #getSize()}. */ public synchronized void moveMediaSource(int currentIndex, int newIndex) { if (currentIndex == newIndex) { @@ -166,7 +166,7 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl /** * Returns the {@link MediaSource} at a specified index. * - * @param index A index in the range of 0 <= index <= {@link #getSize()}. + * @param index A index in the range of 0 <= index <= {@link #getSize()}. * @return The {@link MediaSource} at this index. */ public synchronized MediaSource getMediaSource(int index) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Clock.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Clock.java index 044d87d0a4..f8d5759c2c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Clock.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Clock.java @@ -27,12 +27,12 @@ public interface Clock { Clock DEFAULT = new SystemClock(); /** - * @see android.os.SystemClock#elapsedRealtime(). + * @see android.os.SystemClock#elapsedRealtime() */ long elapsedRealtime(); /** - * @see android.os.SystemClock#sleep(long). + * @see android.os.SystemClock#sleep(long) */ void sleep(long sleepTimeMs); From 33f5bd6aed9c75e5e560ccea8369b64f7496eb14 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 20 Jul 2017 04:56:38 -0700 Subject: [PATCH 0098/2472] Start better documenting track selection ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=162601990 --- .../trackselection/MappingTrackSelector.java | 4 +- .../trackselection/TrackSelector.java | 75 ++++++++++++++++--- .../trackselection/TrackSelectorResult.java | 15 ++-- 3 files changed, 77 insertions(+), 17 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java index 3499efdb16..30cc02936a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java @@ -20,6 +20,7 @@ import android.util.SparseArray; import android.util.SparseBooleanArray; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.RendererConfiguration; import com.google.android.exoplayer2.source.TrackGroup; @@ -31,7 +32,8 @@ import java.util.Map; /** * Base class for {@link TrackSelector}s that first establish a mapping between {@link TrackGroup}s - * and renderers, and then from that mapping create a {@link TrackSelection} for each renderer. + * and {@link Renderer}s, and then from that mapping create a {@link TrackSelection} for each + * renderer. */ public abstract class MappingTrackSelector extends TrackSelector { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelector.java index 6c9fbfcb00..a26fee6f78 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelector.java @@ -16,19 +16,74 @@ package com.google.android.exoplayer2.trackselection; import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.RendererCapabilities; +import com.google.android.exoplayer2.RendererConfiguration; import com.google.android.exoplayer2.source.TrackGroupArray; -/** Selects tracks to be consumed by available renderers. */ +/** + * The component of an {@link ExoPlayer} responsible for selecting tracks to be consumed by each of + * the player's {@link Renderer}s. The {@link DefaultTrackSelector} implementation should be + * suitable for most use cases. + * + *

    Interactions with the player

    + * The following interactions occur between the player and its track selector during playback. + *

    + *

      + *
    • When the player is created it will initialize the track selector by calling + * {@link #init(InvalidationListener)}.
    • + *
    • When the player needs to make a track selection it will call + * {@link #selectTracks(RendererCapabilities[], TrackGroupArray)}. This typically occurs at the + * start of playback, when the player starts to buffer a new period of the media being played, + * and when the track selector invalidates its previous selections.
    • + *
    • The player may perform a track selection well in advance of the selected tracks becoming + * active, where active is defined to mean that the renderers are actually consuming media + * corresponding to the selection that was made. For example when playing media containing + * multiple periods, the track selection for a period is made when the player starts to buffer + * that period. Hence if the player's buffering policy is to maintain a 30 second buffer, the + * selection will occur approximately 30 seconds in advance of it becoming active. In fact the + * selection may never become active, for example if the user seeks to some other period of the + * media during the 30 second gap. The player indicates to the track selector when a selection + * it has previously made becomes active by calling {@link #onSelectionActivated(Object)}.
    • + *
    • If the track selector wishes to indicate to the player that selections it has previously + * made are invalid, it can do so by calling + * {@link InvalidationListener#onTrackSelectionsInvalidated()} on the + * {@link InvalidationListener} that was passed to {@link #init(InvalidationListener)}. A + * track selector may wish to do this if its configuration has changed, for example if it now + * wishes to prefer audio tracks in a particular language. This will trigger the player to make + * new track selections. Note that the player will have to re-buffer in the case that the new + * track selection for the currently playing period differs from the one that was invalidated. + *
    • + *
    + * + *

    Renderer configuration

    + * The {@link TrackSelectorResult} returned by + * {@link #selectTracks(RendererCapabilities[], TrackGroupArray)} contains not only + * {@link TrackSelection}s for each renderer, but also {@link RendererConfiguration}s defining + * configuration parameters that the renderers should apply when consuming the corresponding media. + * Whilst it may seem counter-intuitive for a track selector to also specify renderer configuration + * information, in practice the two are tightly bound together. It may only be possible to play a + * certain combination tracks if the renderers are configured in a particular way. Equally, it may + * only be possible to configure renderers in a particular way if certain tracks are selected. Hence + * it makes sense to determined the track selection and corresponding renderer configurations in a + * single step. + * + *

    Threading model

    + * All calls made by the player into the track selector are on the player's internal playback + * thread. The track selector may call {@link InvalidationListener#onTrackSelectionsInvalidated()} + * from any thread. + */ public abstract class TrackSelector { /** - * Notified when previous selections by a {@link TrackSelector} are no longer valid. + * Notified when selections previously made by a {@link TrackSelector} are no longer valid. */ public interface InvalidationListener { /** - * Called by a {@link TrackSelector} when previous selections are no longer valid. + * Called by a {@link TrackSelector} to indicate that selections it has previously made are no + * longer valid. May be called from any thread. */ void onTrackSelectionsInvalidated(); @@ -37,16 +92,17 @@ public abstract class TrackSelector { private InvalidationListener listener; /** - * Initializes the selector. + * Called by the player to initialize the selector. * - * @param listener A listener for the selector. + * @param listener An invalidation listener that the selector can call to indicate that selections + * it has previously made are no longer valid. */ public final void init(InvalidationListener listener) { this.listener = listener; } /** - * Performs a track selection for renderers. + * Called by the player to perform a track selection. * * @param rendererCapabilities The {@link RendererCapabilities} of the renderers for which tracks * are to be selected. @@ -58,15 +114,16 @@ public abstract class TrackSelector { TrackGroupArray trackGroups) throws ExoPlaybackException; /** - * Called when a {@link TrackSelectorResult} previously generated by + * Called by the player when a {@link TrackSelectorResult} previously generated by * {@link #selectTracks(RendererCapabilities[], TrackGroupArray)} is activated. * - * @param info The value of {@link TrackSelectorResult#info} in the activated result. + * @param info The value of {@link TrackSelectorResult#info} in the activated selection. */ public abstract void onSelectionActivated(Object info); /** - * Invalidates all previously generated track selections. + * Calls {@link InvalidationListener#onTrackSelectionsInvalidated()} to invalidate all previously + * generated track selections. */ protected final void invalidate() { if (listener != null) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java index 5cdb157570..cab9a689be 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java @@ -25,11 +25,11 @@ import com.google.android.exoplayer2.util.Util; public final class TrackSelectorResult { /** - * The groups provided to the {@link TrackSelector}. + * The track groups that were provided to the {@link TrackSelector}. */ public final TrackGroupArray groups; /** - * A {@link TrackSelectionArray} containing the selection for each renderer. + * A {@link TrackSelectionArray} containing the track selection for each renderer. */ public final TrackSelectionArray selections; /** @@ -43,10 +43,10 @@ public final class TrackSelectorResult { public final RendererConfiguration[] rendererConfigurations; /** - * @param groups The groups provided to the {@link TrackSelector}. + * @param groups The track groups provided to the {@link TrackSelector}. * @param selections A {@link TrackSelectionArray} containing the selection for each renderer. * @param info An opaque object that will be returned to - * {@link TrackSelector#onSelectionActivated(Object)} should the selections be activated. + * {@link TrackSelector#onSelectionActivated(Object)} should the selection be activated. * @param rendererConfigurations A {@link RendererConfiguration} for each renderer, to be used * with the selections. */ @@ -62,7 +62,7 @@ public final class TrackSelectorResult { * Returns whether this result is equivalent to {@code other} for all renderers. * * @param other The other {@link TrackSelectorResult}. May be null, in which case {@code false} - * will be returned in all cases. + * will be returned. * @return Whether this result is equivalent to {@code other} for all renderers. */ public boolean isEquivalent(TrackSelectorResult other) { @@ -83,9 +83,10 @@ public final class TrackSelectorResult { * renderer. * * @param other The other {@link TrackSelectorResult}. May be null, in which case {@code false} - * will be returned in all cases. + * will be returned. * @param index The renderer index to check for equivalence. - * @return Whether this result is equivalent to {@code other} for all renderers. + * @return Whether this result is equivalent to {@code other} for the renderer at the specified + * index. */ public boolean isEquivalent(TrackSelectorResult other, int index) { if (other == null) { From 8d56f904a068583eed2a387811edcd3643004424 Mon Sep 17 00:00:00 2001 From: bachinger Date: Thu, 20 Jul 2017 05:44:21 -0700 Subject: [PATCH 0099/2472] make MediaSessionConnector depend only to the Player interface ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=162604746 --- .../mediasession/MediaSessionConnector.java | 78 +++++++++---------- .../RepeatModeActionProvider.java | 7 +- .../mediasession/TimelineQueueNavigator.java | 20 ++--- 3 files changed, 50 insertions(+), 55 deletions(-) diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java index 9f3e299e96..d70d1bcaa9 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java @@ -32,11 +32,9 @@ import android.support.v4.media.session.PlaybackStateCompat; import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; -import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; @@ -46,8 +44,8 @@ import java.util.List; import java.util.Map; /** - * Mediates between a {@link MediaSessionCompat} and an {@link SimpleExoPlayer} instance set with - * {@link #setPlayer(SimpleExoPlayer, CustomActionProvider...)}. + * Mediates between a {@link MediaSessionCompat} and an {@link Player} instance set with + * {@link #setPlayer(Player, PlaybackPreparer, CustomActionProvider...)}. *

    * By default the {@code MediaSessionConnector} listens for {@link #DEFAULT_PLAYBACK_ACTIONS} sent * by a media controller and realizes these actions by calling appropriate ExoPlayer methods. @@ -110,28 +108,32 @@ public final class MediaSessionConnector { * Normally preparing playback includes preparing the player with a * {@link com.google.android.exoplayer2.source.MediaSource} and setting up the media session queue * with a corresponding list of queue items. + *

    + * The {@link PlaybackPreparer} handles the media actions {@code ACTION_PREPARE}, + * {@code ACTION_PREPARE_FROM_MEDIA_ID}, {@code ACTION_PREPARE_FROM_URI} and + * {@code ACTION_PREPARE_FROM_SEARCH}. */ public interface PlaybackPreparer extends PlaybackActionSupport { /** * See {@link MediaSessionCompat.Callback#onPrepare()}. */ - void onPrepare(ExoPlayer player); + void onPrepare(); /** * See {@link MediaSessionCompat.Callback#onPrepareFromMediaId(String, Bundle)}. */ - void onPrepareFromMediaId(ExoPlayer player, String mediaId, Bundle extras); + void onPrepareFromMediaId(String mediaId, Bundle extras); /** * See {@link MediaSessionCompat.Callback#onPrepareFromSearch(String, Bundle)}. */ - void onPrepareFromSearch(ExoPlayer player, String query, Bundle extras); + void onPrepareFromSearch(String query, Bundle extras); /** * See {@link MediaSessionCompat.Callback#onPrepareFromUri(Uri, Bundle)}. */ - void onPrepareFromUri(ExoPlayer player, Uri uri, Bundle extras); + void onPrepareFromUri(Uri uri, Bundle extras); /** * See {@link MediaSessionCompat.Callback#onCommand(String, Bundle, ResultReceiver)}. */ - void onCommand(ExoPlayer player, String command, Bundle extras, ResultReceiver cb); + void onCommand(String command, Bundle extras, ResultReceiver cb); } /** @@ -144,13 +146,13 @@ public final class MediaSessionConnector { * * @param player The player of which the timeline has changed. */ - void onTimelineChanged(ExoPlayer player); + void onTimelineChanged(Player player); /** * Called when the current window index changed. * * @param player The player of which the current window index of the timeline has changed. */ - void onCurrentWindowIndexChanged(ExoPlayer player); + void onCurrentWindowIndexChanged(Player player); /** * Gets the id of the currently active queue item or * {@link MediaSessionCompat.QueueItem#UNKNOWN_ID} if the active item is unknown. @@ -162,23 +164,23 @@ public final class MediaSessionConnector { * @param player The player connected to the media session. * @return The id of the active queue item. */ - long getActiveQueueItemId(@Nullable ExoPlayer player); + long getActiveQueueItemId(@Nullable Player player); /** * See {@link MediaSessionCompat.Callback#onSkipToPrevious()}. */ - void onSkipToPrevious(ExoPlayer player); + void onSkipToPrevious(Player player); /** * See {@link MediaSessionCompat.Callback#onSkipToQueueItem(long)}. */ - void onSkipToQueueItem(ExoPlayer player, long id); + void onSkipToQueueItem(Player player, long id); /** * See {@link MediaSessionCompat.Callback#onSkipToNext()}. */ - void onSkipToNext(ExoPlayer player); + void onSkipToNext(Player player); /** * See {@link MediaSessionCompat.Callback#onSetShuffleModeEnabled(boolean)}. */ - void onSetShuffleModeEnabled(ExoPlayer player, boolean enabled); + void onSetShuffleModeEnabled(Player player, boolean enabled); } /** @@ -188,25 +190,25 @@ public final class MediaSessionConnector { /** * See {@link MediaSessionCompat.Callback#onAddQueueItem(MediaDescriptionCompat description)}. */ - void onAddQueueItem(ExoPlayer player, MediaDescriptionCompat description); + void onAddQueueItem(Player player, MediaDescriptionCompat description); /** * See {@link MediaSessionCompat.Callback#onAddQueueItem(MediaDescriptionCompat description, * int index)}. */ - void onAddQueueItem(ExoPlayer player, MediaDescriptionCompat description, int index); + void onAddQueueItem(Player player, MediaDescriptionCompat description, int index); /** * See {@link MediaSessionCompat.Callback#onRemoveQueueItem(MediaDescriptionCompat * description)}. */ - void onRemoveQueueItem(ExoPlayer player, MediaDescriptionCompat description); + void onRemoveQueueItem(Player player, MediaDescriptionCompat description); /** * See {@link MediaSessionCompat.Callback#onRemoveQueueItemAt(int index)}. */ - void onRemoveQueueItemAt(ExoPlayer player, int index); + void onRemoveQueueItemAt(Player player, int index); /** * See {@link MediaSessionCompat.Callback#onSetRating(RatingCompat)}. */ - void onSetRating(ExoPlayer player, RatingCompat rating); + void onSetRating(Player player, RatingCompat rating); } /** @@ -253,7 +255,7 @@ public final class MediaSessionConnector { private final ExoPlayerEventListener exoPlayerEventListener; private final MediaSessionCallback mediaSessionCallback; - private SimpleExoPlayer player; + private Player player; private CustomActionProvider[] customActionProviders; private int currentWindowIndex; private long playbackActions; @@ -319,14 +321,17 @@ public final class MediaSessionConnector { * actions published with the playback state of the session. * * @param player The player to be connected to the {@code MediaSession}. + * @param playbackPreparer The playback preparer for the player. * @param customActionProviders Optional {@link CustomActionProvider}s to publish and handle * custom actions. */ - public void setPlayer(SimpleExoPlayer player, CustomActionProvider... customActionProviders) { + public void setPlayer(Player player, PlaybackPreparer playbackPreparer, + CustomActionProvider... customActionProviders) { if (this.player != null) { this.player.removeListener(exoPlayerEventListener); mediaSession.setCallback(null); } + setPlaybackPreparer(playbackPreparer); this.player = player; this.customActionProviders = (player != null && customActionProviders != null) ? customActionProviders : new CustomActionProvider[0]; @@ -441,16 +446,7 @@ public final class MediaSessionConnector { } } - /** - * Sets the {@link PlaybackPreparer} to which preparation commands sent by a media - * controller are delegated. - *

    - * Required to work properly with Android Auto which requires - * {@link PlaybackStateCompat#ACTION_PREPARE_FROM_MEDIA_ID}. - * - * @param playbackPreparer The preparer to delegate to. - */ - public void setPlaybackPreparer(PlaybackPreparer playbackPreparer) { + private void setPlaybackPreparer(PlaybackPreparer playbackPreparer) { if (this.playbackPreparer != null) { removePlaybackActions(this.playbackPreparer.getSupportedPlaybackActions()); } @@ -740,7 +736,7 @@ public final class MediaSessionConnector { @Override public void onCommand(String command, Bundle extras, ResultReceiver cb) { if (playbackPreparer != null) { - playbackPreparer.onCommand(player, command, extras, cb); + playbackPreparer.onCommand(command, extras, cb); } } @@ -749,7 +745,7 @@ public final class MediaSessionConnector { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE)) { player.stop(); player.setPlayWhenReady(false); - playbackPreparer.onPrepare(player); + playbackPreparer.onPrepare(); } } @@ -758,7 +754,7 @@ public final class MediaSessionConnector { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID)) { player.stop(); player.setPlayWhenReady(false); - playbackPreparer.onPrepareFromMediaId(player, mediaId, extras); + playbackPreparer.onPrepareFromMediaId(mediaId, extras); } } @@ -767,7 +763,7 @@ public final class MediaSessionConnector { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH)) { player.stop(); player.setPlayWhenReady(false); - playbackPreparer.onPrepareFromSearch(player, query, extras); + playbackPreparer.onPrepareFromSearch(query, extras); } } @@ -776,7 +772,7 @@ public final class MediaSessionConnector { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PREPARE_FROM_URI)) { player.stop(); player.setPlayWhenReady(false); - playbackPreparer.onPrepareFromUri(player, uri, extras); + playbackPreparer.onPrepareFromUri(uri, extras); } } @@ -785,7 +781,7 @@ public final class MediaSessionConnector { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID)) { player.stop(); player.setPlayWhenReady(true); - playbackPreparer.onPrepareFromMediaId(player, mediaId, extras); + playbackPreparer.onPrepareFromMediaId(mediaId, extras); } } @@ -794,7 +790,7 @@ public final class MediaSessionConnector { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH)) { player.stop(); player.setPlayWhenReady(true); - playbackPreparer.onPrepareFromSearch(player, query, extras); + playbackPreparer.onPrepareFromSearch(query, extras); } } @@ -803,7 +799,7 @@ public final class MediaSessionConnector { if (canDispatchToPlaybackPreparer(PlaybackStateCompat.ACTION_PLAY_FROM_URI)) { player.stop(); player.setPlayWhenReady(true); - playbackPreparer.onPrepareFromUri(player, uri, extras); + playbackPreparer.onPrepareFromUri(uri, extras); } } diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java index 62dcb29235..1f33245059 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java @@ -18,7 +18,6 @@ package com.google.android.exoplayer2.ext.mediasession; import android.content.Context; import android.os.Bundle; import android.support.v4.media.session.PlaybackStateCompat; -import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.util.RepeatModeUtil; @@ -32,7 +31,7 @@ public final class RepeatModeActionProvider implements MediaSessionConnector.Cus private static final int DEFAULT_REPEAT_MODES = RepeatModeUtil.REPEAT_TOGGLE_MODE_ONE | RepeatModeUtil.REPEAT_TOGGLE_MODE_ALL; - private final ExoPlayer player; + private final Player player; @RepeatModeUtil.RepeatToggleModes private final int repeatToggleModes; private final CharSequence repeatAllDescription; @@ -48,7 +47,7 @@ public final class RepeatModeActionProvider implements MediaSessionConnector.Cus * @param context The context. * @param player The player on which to toggle the repeat mode. */ - public RepeatModeActionProvider(Context context, ExoPlayer player) { + public RepeatModeActionProvider(Context context, Player player) { this(context, player, DEFAULT_REPEAT_MODES); } @@ -59,7 +58,7 @@ public final class RepeatModeActionProvider implements MediaSessionConnector.Cus * @param player The player on which to toggle the repeat mode. * @param repeatToggleModes The toggle modes to enable. */ - public RepeatModeActionProvider(Context context, ExoPlayer player, + public RepeatModeActionProvider(Context context, Player player, @RepeatModeUtil.RepeatToggleModes int repeatToggleModes) { this.player = player; this.repeatToggleModes = repeatToggleModes; diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java index 97959ccf12..21bdaef0f3 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java @@ -20,7 +20,7 @@ import android.support.v4.media.MediaDescriptionCompat; import android.support.v4.media.session.MediaSessionCompat; import android.support.v4.media.session.PlaybackStateCompat; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.util.Util; @@ -30,7 +30,7 @@ import java.util.List; /** * An abstract implementation of the {@link MediaSessionConnector.QueueNavigator} that's based on an - * {@link ExoPlayer}'s current {@link Timeline} and maps the timeline of the player to the media + * {@link Player}'s current {@link Timeline} and maps the timeline of the player to the media * session queue. */ public abstract class TimelineQueueNavigator implements MediaSessionConnector.QueueNavigator { @@ -87,22 +87,22 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu } @Override - public void onTimelineChanged(ExoPlayer player) { + public void onTimelineChanged(Player player) { publishFloatingQueueWindow(player); } @Override - public void onCurrentWindowIndexChanged(ExoPlayer player) { + public void onCurrentWindowIndexChanged(Player player) { publishFloatingQueueWindow(player); } @Override - public final long getActiveQueueItemId(@Nullable ExoPlayer player) { + public final long getActiveQueueItemId(@Nullable Player player) { return activeQueueItemId; } @Override - public final void onSkipToPrevious(ExoPlayer player) { + public final void onSkipToPrevious(Player player) { Timeline timeline = player.getCurrentTimeline(); if (timeline.isEmpty()) { return; @@ -118,7 +118,7 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu } @Override - public final void onSkipToQueueItem(ExoPlayer player, long id) { + public final void onSkipToQueueItem(Player player, long id) { Timeline timeline = player.getCurrentTimeline(); if (timeline.isEmpty()) { return; @@ -130,7 +130,7 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu } @Override - public final void onSkipToNext(ExoPlayer player) { + public final void onSkipToNext(Player player) { Timeline timeline = player.getCurrentTimeline(); if (timeline.isEmpty()) { return; @@ -143,11 +143,11 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu } @Override - public void onSetShuffleModeEnabled(ExoPlayer player, boolean enabled) { + public void onSetShuffleModeEnabled(Player player, boolean enabled) { // TODO: Implement this. } - private void publishFloatingQueueWindow(ExoPlayer player) { + private void publishFloatingQueueWindow(Player player) { if (player.getCurrentTimeline().isEmpty()) { mediaSession.setQueue(Collections.emptyList()); activeQueueItemId = MediaSessionCompat.QueueItem.UNKNOWN_ID; From 554a399407317626acf789637eb186631eb23dc8 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 20 Jul 2017 05:57:25 -0700 Subject: [PATCH 0100/2472] Switch to non-deprecated player constants ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=162605429 --- .../android/exoplayer2/demo/EventLogger.java | 26 +++---- .../exoplayer2/demo/PlayerActivity.java | 6 +- .../exoplayer2/ext/flac/FlacPlaybackTest.java | 7 +- .../exoplayer2/ext/ima/ImaAdsLoader.java | 11 +-- .../exoplayer2/ext/opus/OpusPlaybackTest.java | 7 +- .../exoplayer2/ext/vp9/VpxPlaybackTest.java | 7 +- .../android/exoplayer2/ExoPlayerTest.java | 12 ++-- .../android/exoplayer2/TimelineTest.java | 24 +++---- .../source/ClippingMediaSourceTest.java | 17 +++-- .../source/ConcatenatingMediaSourceTest.java | 72 +++++++++---------- .../DynamicConcatenatingMediaSourceTest.java | 15 ++-- .../source/LoopingMediaSourceTest.java | 41 ++++++----- .../android/exoplayer2/ExoPlayerImpl.java | 4 +- .../exoplayer2/ExoPlayerImplInternal.java | 46 ++++++------ .../google/android/exoplayer2/Timeline.java | 24 +++---- .../source/AbstractConcatenatedTimeline.java | 14 ++-- .../source/ConcatenatingMediaSource.java | 16 ++--- .../exoplayer2/source/LoopingMediaSource.java | 5 +- .../exoplayer2/util/RepeatModeUtil.java | 19 +++-- .../exoplayer2/ui/DebugTextViewHelper.java | 14 ++-- .../exoplayer2/ui/SimpleExoPlayerView.java | 10 +-- .../exoplayer2/testutil/ExoHostedTest.java | 19 ++--- .../exoplayer2/testutil/ExoPlayerWrapper.java | 7 +- .../exoplayer2/testutil/TimelineAsserts.java | 29 ++++---- 24 files changed, 224 insertions(+), 228 deletions(-) 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 87c85f6800..30dfb5140a 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 @@ -20,9 +20,9 @@ import android.util.Log; import android.view.Surface; 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.Player; import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.audio.AudioRendererEventListener; @@ -55,8 +55,8 @@ import java.util.Locale; /** * Logs player events using {@link Log}. */ -/* package */ final class EventLogger implements ExoPlayer.EventListener, - AudioRendererEventListener, VideoRendererEventListener, AdaptiveMediaSourceEventListener, +/* package */ final class EventLogger implements Player.EventListener, AudioRendererEventListener, + VideoRendererEventListener, AdaptiveMediaSourceEventListener, ExtractorMediaSource.EventListener, DefaultDrmSessionManager.EventListener, MetadataRenderer.Output { @@ -82,7 +82,7 @@ import java.util.Locale; startTimeMs = SystemClock.elapsedRealtime(); } - // ExoPlayer.EventListener + // Player.EventListener @Override public void onLoadingChanged(boolean isLoading) { @@ -96,7 +96,7 @@ import java.util.Locale; } @Override - public void onRepeatModeChanged(@ExoPlayer.RepeatMode int repeatMode) { + public void onRepeatModeChanged(@Player.RepeatMode int repeatMode) { Log.d(TAG, "repeatMode [" + getRepeatModeString(repeatMode) + "]"); } @@ -412,13 +412,13 @@ import java.util.Locale; private static String getStateString(int state) { switch (state) { - case ExoPlayer.STATE_BUFFERING: + case Player.STATE_BUFFERING: return "B"; - case ExoPlayer.STATE_ENDED: + case Player.STATE_ENDED: return "E"; - case ExoPlayer.STATE_IDLE: + case Player.STATE_IDLE: return "I"; - case ExoPlayer.STATE_READY: + case Player.STATE_READY: return "R"; default: return "?"; @@ -466,13 +466,13 @@ import java.util.Locale; return enabled ? "[X]" : "[ ]"; } - private static String getRepeatModeString(@ExoPlayer.RepeatMode int repeatMode) { + private static String getRepeatModeString(@Player.RepeatMode int repeatMode) { switch (repeatMode) { - case ExoPlayer.REPEAT_MODE_OFF: + case Player.REPEAT_MODE_OFF: return "OFF"; - case ExoPlayer.REPEAT_MODE_ONE: + case Player.REPEAT_MODE_ONE: return "ONE"; - case ExoPlayer.REPEAT_MODE_ALL: + case Player.REPEAT_MODE_ALL: return "ALL"; default: return "?"; 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 d498b8f0c4..40e77452ea 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 @@ -36,9 +36,9 @@ import android.widget.Toast; import com.google.android.exoplayer2.C; 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.Player; import com.google.android.exoplayer2.Player.EventListener; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; @@ -480,7 +480,7 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi } } - // ExoPlayer.EventListener implementation + // Player.EventListener implementation @Override public void onLoadingChanged(boolean isLoading) { @@ -489,7 +489,7 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi @Override public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { - if (playbackState == ExoPlayer.STATE_ENDED) { + if (playbackState == Player.STATE_ENDED) { showControls(); } updateButtonVisibilities(); 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 a49ae073ef..1fa30bed9d 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 @@ -23,6 +23,7 @@ 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.Player; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; @@ -57,7 +58,7 @@ public class FlacPlaybackTest extends InstrumentationTestCase { } } - private static class TestPlaybackThread extends Thread implements ExoPlayer.EventListener { + private static class TestPlaybackThread extends Thread implements Player.EventListener { private final Context context; private final Uri uri; @@ -120,8 +121,8 @@ public class FlacPlaybackTest extends InstrumentationTestCase { @Override public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { - if (playbackState == ExoPlayer.STATE_ENDED - || (playbackState == ExoPlayer.STATE_IDLE && playbackException != null)) { + if (playbackState == Player.STATE_ENDED + || (playbackState == Player.STATE_IDLE && playbackException != null)) { releasePlayerAndQuitLooper(); } } diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index f64080dbc7..12d58f70cf 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -43,6 +43,7 @@ import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; @@ -54,7 +55,7 @@ import java.util.List; /** * Loads ads using the IMA SDK. All methods are called on the main thread. */ -public final class ImaAdsLoader implements ExoPlayer.EventListener, VideoAdPlayer, +public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, ContentProgressProvider, AdErrorListener, AdsLoadedListener, AdEventListener { /** @@ -107,7 +108,7 @@ public final class ImaAdsLoader implements ExoPlayer.EventListener, VideoAdPlaye private final AdsLoader adsLoader; private EventListener eventListener; - private ExoPlayer player; + private Player player; private VideoProgressUpdate lastContentProgress; private VideoProgressUpdate lastAdProgress; @@ -485,7 +486,7 @@ public final class ImaAdsLoader implements ExoPlayer.EventListener, VideoAdPlaye throw new IllegalStateException(); } - // ExoPlayer.EventListener implementation. + // Player.EventListener implementation. @Override public void onTimelineChanged(Timeline timeline, Object manifest) { @@ -516,9 +517,9 @@ public final class ImaAdsLoader implements ExoPlayer.EventListener, VideoAdPlaye return; } - if (!imaPlayingAd && playbackState == ExoPlayer.STATE_BUFFERING && playWhenReady) { + if (!imaPlayingAd && playbackState == Player.STATE_BUFFERING && playWhenReady) { checkForContentComplete(); - } else if (imaPlayingAd && playbackState == ExoPlayer.STATE_ENDED) { + } else if (imaPlayingAd && playbackState == Player.STATE_ENDED) { // IMA is waiting for the ad playback to finish so invoke the callback now. // Either CONTENT_RESUME_REQUESTED will be passed next, or playAd will be called again. for (VideoAdPlayerCallback callback : adCallbacks) { 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 76e19b0ebe..4c576b2cc0 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 @@ -23,6 +23,7 @@ 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.Player; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; @@ -57,7 +58,7 @@ public class OpusPlaybackTest extends InstrumentationTestCase { } } - private static class TestPlaybackThread extends Thread implements ExoPlayer.EventListener { + private static class TestPlaybackThread extends Thread implements Player.EventListener { private final Context context; private final Uri uri; @@ -120,8 +121,8 @@ public class OpusPlaybackTest extends InstrumentationTestCase { @Override public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { - if (playbackState == ExoPlayer.STATE_ENDED - || (playbackState == ExoPlayer.STATE_IDLE && playbackException != null)) { + if (playbackState == Player.STATE_ENDED + || (playbackState == Player.STATE_IDLE && playbackException != null)) { releasePlayerAndQuitLooper(); } } 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 669d77cdeb..0bc945174e 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 @@ -24,6 +24,7 @@ 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.Player; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; @@ -86,7 +87,7 @@ public class VpxPlaybackTest extends InstrumentationTestCase { } } - private static class TestPlaybackThread extends Thread implements ExoPlayer.EventListener { + private static class TestPlaybackThread extends Thread implements Player.EventListener { private final Context context; private final Uri uri; @@ -152,8 +153,8 @@ public class VpxPlaybackTest extends InstrumentationTestCase { @Override public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { - if (playbackState == ExoPlayer.STATE_ENDED - || (playbackState == ExoPlayer.STATE_IDLE && playbackException != null)) { + if (playbackState == Player.STATE_ENDED + || (playbackState == Player.STATE_IDLE && playbackException != null)) { releasePlayerAndQuitLooper(); } } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index dbb36eb0ed..bf4ea6e972 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -216,13 +216,13 @@ public final class ExoPlayerTest extends TestCase { new TimelineWindowDefinition(true, false, 100000), new TimelineWindowDefinition(true, false, 100000)); final int[] actionSchedule = { // 0 -> 1 - ExoPlayer.REPEAT_MODE_ONE, // 1 -> 1 - ExoPlayer.REPEAT_MODE_OFF, // 1 -> 2 - ExoPlayer.REPEAT_MODE_ONE, // 2 -> 2 - ExoPlayer.REPEAT_MODE_ALL, // 2 -> 0 - ExoPlayer.REPEAT_MODE_ONE, // 0 -> 0 + Player.REPEAT_MODE_ONE, // 1 -> 1 + Player.REPEAT_MODE_OFF, // 1 -> 2 + Player.REPEAT_MODE_ONE, // 2 -> 2 + Player.REPEAT_MODE_ALL, // 2 -> 0 + Player.REPEAT_MODE_ONE, // 0 -> 0 -1, // 0 -> 0 - ExoPlayer.REPEAT_MODE_OFF, // 0 -> 1 + Player.REPEAT_MODE_OFF, // 0 -> 1 -1, // 1 -> 2 -1 // 2 -> ended }; diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/TimelineTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/TimelineTest.java index d69f40283f..d9ee27bd62 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/TimelineTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/TimelineTest.java @@ -33,23 +33,23 @@ public class TimelineTest extends TestCase { Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(1, 111)); TimelineAsserts.assertWindowIds(timeline, 111); TimelineAsserts.assertPeriodCounts(timeline, 1); - TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, C.INDEX_UNSET); - TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0); - TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 0); - TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0); - TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 0); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, 0); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, 0); } public void testMultiPeriodTimeline() { Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(5, 111)); TimelineAsserts.assertWindowIds(timeline, 111); TimelineAsserts.assertPeriodCounts(timeline, 5); - TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, C.INDEX_UNSET); - TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0); - TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 0); - TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0); - TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 0); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, 0); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, 0); } } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java index 1a15b750ac..66b0337450 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java @@ -17,7 +17,7 @@ package com.google.android.exoplayer2.source; import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline.Period; import com.google.android.exoplayer2.Timeline.Window; @@ -109,14 +109,13 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase { TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US); TimelineAsserts.assertWindowIds(clippedTimeline, 111); TimelineAsserts.assertPeriodCounts(clippedTimeline, 1); - TimelineAsserts.assertPreviousWindowIndices(clippedTimeline, ExoPlayer.REPEAT_MODE_OFF, - C.INDEX_UNSET); - TimelineAsserts.assertPreviousWindowIndices(clippedTimeline, ExoPlayer.REPEAT_MODE_ONE, 0); - TimelineAsserts.assertPreviousWindowIndices(clippedTimeline, ExoPlayer.REPEAT_MODE_ALL, 0); - TimelineAsserts.assertNextWindowIndices(clippedTimeline, ExoPlayer.REPEAT_MODE_OFF, - C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices(clippedTimeline, ExoPlayer.REPEAT_MODE_ONE, 0); - TimelineAsserts.assertNextWindowIndices(clippedTimeline, ExoPlayer.REPEAT_MODE_ALL, 0); + TimelineAsserts.assertPreviousWindowIndices( + clippedTimeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET); + TimelineAsserts.assertPreviousWindowIndices(clippedTimeline, Player.REPEAT_MODE_ONE, 0); + TimelineAsserts.assertPreviousWindowIndices(clippedTimeline, Player.REPEAT_MODE_ALL, 0); + TimelineAsserts.assertNextWindowIndices(clippedTimeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(clippedTimeline, Player.REPEAT_MODE_ONE, 0); + TimelineAsserts.assertNextWindowIndices(clippedTimeline, Player.REPEAT_MODE_ALL, 0); } /** diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java index 49f34f7b2b..3bf89f9bcc 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java @@ -16,7 +16,7 @@ package com.google.android.exoplayer2.source; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeTimeline; @@ -34,22 +34,22 @@ public final class ConcatenatingMediaSourceTest extends TestCase { Timeline timeline = getConcatenatedTimeline(false, createFakeTimeline(3, 111)); TimelineAsserts.assertWindowIds(timeline, 111); TimelineAsserts.assertPeriodCounts(timeline, 3); - TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, C.INDEX_UNSET); - TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0); - TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 0); - TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0); - TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 0); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, 0); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, 0); timeline = getConcatenatedTimeline(true, createFakeTimeline(3, 111)); TimelineAsserts.assertWindowIds(timeline, 111); TimelineAsserts.assertPeriodCounts(timeline, 3); - TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, C.INDEX_UNSET); - TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0); - TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 0); - TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0); - TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 0); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, 0); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, 0); } public void testMultipleMediaSources() { @@ -58,26 +58,24 @@ public final class ConcatenatingMediaSourceTest extends TestCase { Timeline timeline = getConcatenatedTimeline(false, timelines); TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); TimelineAsserts.assertPeriodCounts(timeline, 3, 1, 3); - TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, C.INDEX_UNSET, - 0, 1); - TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0, 1, 2); - TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 2, 0, 1); - TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, - 1, 2, C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0, 1, 2); - TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 1, 2, 0); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0, 1, 2); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, 2, 0, 1); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0, 1, 2); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, 1, 2, 0); timeline = getConcatenatedTimeline(true, timelines); TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); TimelineAsserts.assertPeriodCounts(timeline, 3, 1, 3); - TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, - C.INDEX_UNSET, 0, 1); - TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 2, 0, 1); - TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 2, 0, 1); - TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, - 1, 2, C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 1, 2, 0); - TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 1, 2, 0); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, 2, 0, 1); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, 2, 0, 1); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, 1, 2, 0); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, 1, 2, 0); } public void testNestedMediaSources() { @@ -86,14 +84,14 @@ public final class ConcatenatingMediaSourceTest extends TestCase { getConcatenatedTimeline(true, createFakeTimeline(1, 333), createFakeTimeline(1, 444))); TimelineAsserts.assertWindowIds(timeline, 111, 222, 333, 444); TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1, 1); - TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, - C.INDEX_UNSET, 0, 1, 2); - TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0, 1, 3, 2); - TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 3, 0, 1, 2); - TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, - 1, 2, 3, C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0, 1, 3, 2); - TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 1, 2, 3, 0); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET, 0, 1, 2); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0, 1, 3, 2); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, 3, 0, 1, 2); + TimelineAsserts.assertNextWindowIndices( + timeline, Player.REPEAT_MODE_OFF, 1, 2, 3, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0, 1, 3, 2); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, 1, 2, 3, 0); } /** diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java index daa454ad14..f8636b9990 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java @@ -120,14 +120,13 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { } // Assert correct next and previous indices behavior after some insertions and removals. - TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, - 1, 2, C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0, 1, 2); - TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 1, 2, 0); - TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, - C.INDEX_UNSET, 0, 1); - TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0, 1, 2); - TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 2, 0, 1); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0, 1, 2); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, 1, 2, 0); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0, 1, 2); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, 2, 0, 1); // Remove at front of queue. mediaSource.removeMediaSource(0); diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java index 87e6bb9983..d2045c29a5 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java @@ -16,7 +16,7 @@ package com.google.android.exoplayer2.source; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeTimeline; @@ -42,31 +42,30 @@ public class LoopingMediaSourceTest extends TestCase { Timeline timeline = getLoopingTimeline(multiWindowTimeline, 1); TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1); - TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, - C.INDEX_UNSET, 0, 1); - TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0, 1, 2); - TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 2, 0, 1); - TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, - 1, 2, C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0, 1, 2); - TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 1, 2, 0); + TimelineAsserts.assertPreviousWindowIndices( + timeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0, 1, 2); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, 2, 0, 1); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0, 1, 2); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, 1, 2, 0); } public void testMultiLoop() { Timeline timeline = getLoopingTimeline(multiWindowTimeline, 3); TimelineAsserts.assertWindowIds(timeline, 111, 222, 333, 111, 222, 333, 111, 222, 333); TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1, 1, 1, 1, 1, 1, 1); - TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET, 0, 1, 2, 3, 4, 5, 6, 7, 8); - TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0, 1, 2, 3, 4, 5, 6, 7, 8); - TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, 8, 0, 1, 2, 3, 4, 5, 6, 7); - TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, 1, 2, 3, 4, 5, 6, 7, 8, C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0, 1, 2, 3, 4, 5, 6, 7, 8); - TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, 1, 2, 3, 4, 5, 6, 7, 8, 0); } @@ -74,12 +73,12 @@ public class LoopingMediaSourceTest extends TestCase { Timeline timeline = getLoopingTimeline(multiWindowTimeline, Integer.MAX_VALUE); TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1); - TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, 2, 0, 1); - TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0, 1, 2); - TimelineAsserts.assertPreviousWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 2, 0, 1); - TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_OFF, 1, 2, 0); - TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ONE, 0, 1, 2); - TimelineAsserts.assertNextWindowIndices(timeline, ExoPlayer.REPEAT_MODE_ALL, 1, 2, 0); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, 2, 0, 1); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0, 1, 2); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, 2, 0, 1); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, 1, 2, 0); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0, 1, 2); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, 1, 2, 0); } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index d81e500349..c3a76cd962 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -86,8 +86,8 @@ import java.util.concurrent.CopyOnWriteArraySet; this.renderers = Assertions.checkNotNull(renderers); this.trackSelector = Assertions.checkNotNull(trackSelector); this.playWhenReady = false; - this.repeatMode = REPEAT_MODE_OFF; - this.playbackState = STATE_IDLE; + this.repeatMode = Player.REPEAT_MODE_OFF; + this.playbackState = Player.STATE_IDLE; this.listeners = new CopyOnWriteArraySet<>(); emptyTrackSelections = new TrackSelectionArray(new TrackSelection[renderers.length]); timeline = Timeline.EMPTY; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 8d9720b291..ea1e898e66 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -172,7 +172,7 @@ import java.io.IOException; private boolean rebuffering; private boolean isLoading; private int state; - private @ExoPlayer.RepeatMode int repeatMode; + private @Player.RepeatMode int repeatMode; private int customMessagesSent; private int customMessagesProcessed; private long elapsedRealtimeUs; @@ -188,7 +188,7 @@ import java.io.IOException; private Timeline timeline; public ExoPlayerImplInternal(Renderer[] renderers, TrackSelector trackSelector, - LoadControl loadControl, boolean playWhenReady, @ExoPlayer.RepeatMode int repeatMode, + LoadControl loadControl, boolean playWhenReady, @Player.RepeatMode int repeatMode, Handler eventHandler, PlaybackInfo playbackInfo, ExoPlayer player) { this.renderers = renderers; this.trackSelector = trackSelector; @@ -196,7 +196,7 @@ import java.io.IOException; this.playWhenReady = playWhenReady; this.repeatMode = repeatMode; this.eventHandler = eventHandler; - this.state = ExoPlayer.STATE_IDLE; + this.state = Player.STATE_IDLE; this.playbackInfo = playbackInfo; this.player = player; @@ -230,7 +230,7 @@ import java.io.IOException; handler.obtainMessage(MSG_SET_PLAY_WHEN_READY, playWhenReady ? 1 : 0, 0).sendToTarget(); } - public void setRepeatMode(@ExoPlayer.RepeatMode int repeatMode) { + public void setRepeatMode(@Player.RepeatMode int repeatMode) { handler.obtainMessage(MSG_SET_REPEAT_MODE, repeatMode, 0).sendToTarget(); } @@ -423,7 +423,7 @@ import java.io.IOException; } this.mediaSource = mediaSource; mediaSource.prepareSource(player, true, this); - setState(ExoPlayer.STATE_BUFFERING); + setState(Player.STATE_BUFFERING); handler.sendEmptyMessage(MSG_DO_SOME_WORK); } @@ -434,16 +434,16 @@ import java.io.IOException; stopRenderers(); updatePlaybackPositions(); } else { - if (state == ExoPlayer.STATE_READY) { + if (state == Player.STATE_READY) { startRenderers(); handler.sendEmptyMessage(MSG_DO_SOME_WORK); - } else if (state == ExoPlayer.STATE_BUFFERING) { + } else if (state == Player.STATE_BUFFERING) { handler.sendEmptyMessage(MSG_DO_SOME_WORK); } } } - private void setRepeatModeInternal(@ExoPlayer.RepeatMode int repeatMode) + private void setRepeatModeInternal(@Player.RepeatMode int repeatMode) throws ExoPlaybackException { this.repeatMode = repeatMode; mediaPeriodInfoSequence.setRepeatMode(repeatMode); @@ -594,38 +594,38 @@ import java.io.IOException; && (playingPeriodDurationUs == C.TIME_UNSET || playingPeriodDurationUs <= playbackInfo.positionUs) && playingPeriodHolder.info.isFinal) { - setState(ExoPlayer.STATE_ENDED); + setState(Player.STATE_ENDED); stopRenderers(); - } else if (state == ExoPlayer.STATE_BUFFERING) { + } else if (state == Player.STATE_BUFFERING) { boolean isNewlyReady = enabledRenderers.length > 0 ? (allRenderersReadyOrEnded && loadingPeriodHolder.haveSufficientBuffer(rebuffering, rendererPositionUs)) : isTimelineReady(playingPeriodDurationUs); if (isNewlyReady) { - setState(ExoPlayer.STATE_READY); + setState(Player.STATE_READY); if (playWhenReady) { startRenderers(); } } - } else if (state == ExoPlayer.STATE_READY) { + } else if (state == Player.STATE_READY) { boolean isStillReady = enabledRenderers.length > 0 ? allRenderersReadyOrEnded : isTimelineReady(playingPeriodDurationUs); if (!isStillReady) { rebuffering = playWhenReady; - setState(ExoPlayer.STATE_BUFFERING); + setState(Player.STATE_BUFFERING); stopRenderers(); } } - if (state == ExoPlayer.STATE_BUFFERING) { + if (state == Player.STATE_BUFFERING) { for (Renderer renderer : enabledRenderers) { renderer.maybeThrowStreamError(); } } - if ((playWhenReady && state == ExoPlayer.STATE_READY) || state == ExoPlayer.STATE_BUFFERING) { + if ((playWhenReady && state == Player.STATE_READY) || state == Player.STATE_BUFFERING) { scheduleNextWork(operationStartTimeMs, RENDERING_INTERVAL_MS); - } else if (enabledRenderers.length != 0 && state != ExoPlayer.STATE_ENDED) { + } else if (enabledRenderers.length != 0 && state != Player.STATE_ENDED) { scheduleNextWork(operationStartTimeMs, IDLE_INTERVAL_MS); } else { handler.removeMessages(MSG_DO_SOME_WORK); @@ -661,7 +661,7 @@ import java.io.IOException; // Set the internal position to (0,TIME_UNSET) so that a subsequent seek to (0,0) isn't // ignored. playbackInfo = new PlaybackInfo(0, C.TIME_UNSET); - setState(ExoPlayer.STATE_ENDED); + setState(Player.STATE_ENDED); // Reset, but retain the source so that it can still be used should a seek occur. resetInternal(false); return; @@ -697,7 +697,7 @@ import java.io.IOException; throws ExoPlaybackException { stopRenderers(); rebuffering = false; - setState(ExoPlayer.STATE_BUFFERING); + setState(Player.STATE_BUFFERING); MediaPeriodHolder newPlayingPeriodHolder = null; if (playingPeriodHolder == null) { @@ -787,13 +787,13 @@ import java.io.IOException; private void stopInternal() { resetInternal(true); loadControl.onStopped(); - setState(ExoPlayer.STATE_IDLE); + setState(Player.STATE_IDLE); } private void releaseInternal() { resetInternal(true); loadControl.onReleased(); - setState(ExoPlayer.STATE_IDLE); + setState(Player.STATE_IDLE); synchronized (this) { released = true; notifyAll(); @@ -838,7 +838,7 @@ import java.io.IOException; for (ExoPlayerMessage message : messages) { message.target.handleMessage(message.messageType, message.message); } - if (state == ExoPlayer.STATE_READY || state == ExoPlayer.STATE_BUFFERING) { + if (state == Player.STATE_READY || state == Player.STATE_BUFFERING) { // The message may have caused something to change that now requires us to do work. handler.sendEmptyMessage(MSG_DO_SOME_WORK); } @@ -1114,7 +1114,7 @@ import java.io.IOException; notifySourceInfoRefresh(manifest, processedInitialSeekCount); // Set the internal position to (0,TIME_UNSET) so that a subsequent seek to (0,0) isn't ignored. playbackInfo = new PlaybackInfo(0, C.TIME_UNSET); - setState(ExoPlayer.STATE_ENDED); + setState(Player.STATE_ENDED); // Reset, but retain the source so that it can still be used should a seek occur. resetInternal(false); } @@ -1427,7 +1427,7 @@ import java.io.IOException; RendererConfiguration rendererConfiguration = playingPeriodHolder.trackSelectorResult.rendererConfigurations[i]; // The renderer needs enabling with its new track selection. - boolean playing = playWhenReady && state == ExoPlayer.STATE_READY; + boolean playing = playWhenReady && state == Player.STATE_READY; // Consider as joining only if the renderer was previously disabled. boolean joining = !rendererWasEnabledFlags[i] && playing; // Build an array of formats contained by the selection. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java index e45171fc69..7ce23e67ec 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java @@ -559,13 +559,13 @@ public abstract class Timeline { * @param repeatMode A repeat mode. * @return The index of the next window, or {@link C#INDEX_UNSET} if this is the last window. */ - public int getNextWindowIndex(int windowIndex, @ExoPlayer.RepeatMode int repeatMode) { + public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) { switch (repeatMode) { - case ExoPlayer.REPEAT_MODE_OFF: + case Player.REPEAT_MODE_OFF: return windowIndex == getWindowCount() - 1 ? C.INDEX_UNSET : windowIndex + 1; - case ExoPlayer.REPEAT_MODE_ONE: + case Player.REPEAT_MODE_ONE: return windowIndex; - case ExoPlayer.REPEAT_MODE_ALL: + case Player.REPEAT_MODE_ALL: return windowIndex == getWindowCount() - 1 ? 0 : windowIndex + 1; default: throw new IllegalStateException(); @@ -580,13 +580,13 @@ public abstract class Timeline { * @param repeatMode A repeat mode. * @return The index of the previous window, or {@link C#INDEX_UNSET} if this is the first window. */ - public int getPreviousWindowIndex(int windowIndex, @ExoPlayer.RepeatMode int repeatMode) { + public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) { switch (repeatMode) { - case ExoPlayer.REPEAT_MODE_OFF: + case Player.REPEAT_MODE_OFF: return windowIndex == 0 ? C.INDEX_UNSET : windowIndex - 1; - case ExoPlayer.REPEAT_MODE_ONE: + case Player.REPEAT_MODE_ONE: return windowIndex; - case ExoPlayer.REPEAT_MODE_ALL: + case Player.REPEAT_MODE_ALL: return windowIndex == 0 ? getWindowCount() - 1 : windowIndex - 1; default: throw new IllegalStateException(); @@ -601,7 +601,7 @@ public abstract class Timeline { * @param repeatMode A repeat mode. * @return Whether the window of the given index is the last window of the timeline. */ - public final boolean isLastWindow(int windowIndex, @ExoPlayer.RepeatMode int repeatMode) { + public final boolean isLastWindow(int windowIndex, @Player.RepeatMode int repeatMode) { return getNextWindowIndex(windowIndex, repeatMode) == C.INDEX_UNSET; } @@ -613,7 +613,7 @@ public abstract class Timeline { * @param repeatMode A repeat mode. * @return Whether the window of the given index is the first window of the timeline. */ - public final boolean isFirstWindow(int windowIndex, @ExoPlayer.RepeatMode int repeatMode) { + public final boolean isFirstWindow(int windowIndex, @Player.RepeatMode int repeatMode) { return getPreviousWindowIndex(windowIndex, repeatMode) == C.INDEX_UNSET; } @@ -672,7 +672,7 @@ public abstract class Timeline { * @return The index of the next period, or {@link C#INDEX_UNSET} if this is the last period. */ public final int getNextPeriodIndex(int periodIndex, Period period, Window window, - @ExoPlayer.RepeatMode int repeatMode) { + @Player.RepeatMode int repeatMode) { int windowIndex = getPeriod(periodIndex, period).windowIndex; if (getWindow(windowIndex, window).lastPeriodIndex == periodIndex) { int nextWindowIndex = getNextWindowIndex(windowIndex, repeatMode); @@ -695,7 +695,7 @@ public abstract class Timeline { * @return Whether the period of the given index is the last period of the timeline. */ public final boolean isLastPeriod(int periodIndex, Period period, Window window, - @ExoPlayer.RepeatMode int repeatMode) { + @Player.RepeatMode int repeatMode) { return getNextPeriodIndex(periodIndex, period, window, repeatMode) == C.INDEX_UNSET; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java b/library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java index 714d72104b..3bee3cc73f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java @@ -17,7 +17,7 @@ package com.google.android.exoplayer2.source; import android.util.Pair; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; /** @@ -75,19 +75,19 @@ import com.google.android.exoplayer2.Timeline; } @Override - public int getNextWindowIndex(int windowIndex, @ExoPlayer.RepeatMode int repeatMode) { + public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) { getChildDataByWindowIndex(windowIndex, childDataHolder); int firstWindowIndexInChild = childDataHolder.firstWindowIndexInChild; int nextWindowIndexInChild = childDataHolder.timeline.getNextWindowIndex( windowIndex - firstWindowIndexInChild, - repeatMode == ExoPlayer.REPEAT_MODE_ALL ? ExoPlayer.REPEAT_MODE_OFF : repeatMode); + repeatMode == Player.REPEAT_MODE_ALL ? Player.REPEAT_MODE_OFF : repeatMode); if (nextWindowIndexInChild != C.INDEX_UNSET) { return firstWindowIndexInChild + nextWindowIndexInChild; } else { firstWindowIndexInChild += childDataHolder.timeline.getWindowCount(); if (firstWindowIndexInChild < getWindowCount()) { return firstWindowIndexInChild; - } else if (repeatMode == ExoPlayer.REPEAT_MODE_ALL) { + } else if (repeatMode == Player.REPEAT_MODE_ALL) { return 0; } else { return C.INDEX_UNSET; @@ -96,18 +96,18 @@ import com.google.android.exoplayer2.Timeline; } @Override - public int getPreviousWindowIndex(int windowIndex, @ExoPlayer.RepeatMode int repeatMode) { + public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) { getChildDataByWindowIndex(windowIndex, childDataHolder); int firstWindowIndexInChild = childDataHolder.firstWindowIndexInChild; int previousWindowIndexInChild = childDataHolder.timeline.getPreviousWindowIndex( windowIndex - firstWindowIndexInChild, - repeatMode == ExoPlayer.REPEAT_MODE_ALL ? ExoPlayer.REPEAT_MODE_OFF : repeatMode); + repeatMode == Player.REPEAT_MODE_ALL ? Player.REPEAT_MODE_OFF : repeatMode); if (previousWindowIndexInChild != C.INDEX_UNSET) { return firstWindowIndexInChild + previousWindowIndexInChild; } else { if (firstWindowIndexInChild > 0) { return firstWindowIndexInChild - 1; - } else if (repeatMode == ExoPlayer.REPEAT_MODE_ALL) { + } else if (repeatMode == Player.REPEAT_MODE_ALL) { return getWindowCount() - 1; } else { return C.INDEX_UNSET; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java index cb939fd14a..de42df9a14 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.source; import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.util.Assertions; @@ -51,8 +52,7 @@ public final class ConcatenatingMediaSource implements MediaSource { /** * @param isRepeatOneAtomic Whether the concatenated media source shall be treated as atomic - * (i.e., repeated in its entirety) when repeat mode is set to - * {@code ExoPlayer.REPEAT_MODE_ONE}. + * (i.e., repeated in its entirety) when repeat mode is set to {@code Player.REPEAT_MODE_ONE}. * @param mediaSources The {@link MediaSource}s to concatenate. It is valid for the same * {@link MediaSource} instance to be present more than once in the array. */ @@ -193,17 +193,17 @@ public final class ConcatenatingMediaSource implements MediaSource { } @Override - public int getNextWindowIndex(int windowIndex, @ExoPlayer.RepeatMode int repeatMode) { - if (isRepeatOneAtomic && repeatMode == ExoPlayer.REPEAT_MODE_ONE) { - repeatMode = ExoPlayer.REPEAT_MODE_ALL; + public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) { + if (isRepeatOneAtomic && repeatMode == Player.REPEAT_MODE_ONE) { + repeatMode = Player.REPEAT_MODE_ALL; } return super.getNextWindowIndex(windowIndex, repeatMode); } @Override - public int getPreviousWindowIndex(int windowIndex, @ExoPlayer.RepeatMode int repeatMode) { - if (isRepeatOneAtomic && repeatMode == ExoPlayer.REPEAT_MODE_ONE) { - repeatMode = ExoPlayer.REPEAT_MODE_ALL; + public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) { + if (isRepeatOneAtomic && repeatMode == Player.REPEAT_MODE_ONE) { + repeatMode = Player.REPEAT_MODE_ALL; } return super.getPreviousWindowIndex(windowIndex, repeatMode); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java index da2593ba15..f0032e0ee0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.util.Assertions; @@ -161,13 +162,13 @@ public final class LoopingMediaSource implements MediaSource { } @Override - public int getNextWindowIndex(int windowIndex, @ExoPlayer.RepeatMode int repeatMode) { + public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) { int childNextWindowIndex = childTimeline.getNextWindowIndex(windowIndex, repeatMode); return childNextWindowIndex == C.INDEX_UNSET ? 0 : childNextWindowIndex; } @Override - public int getPreviousWindowIndex(int windowIndex, @ExoPlayer.RepeatMode int repeatMode) { + public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) { int childPreviousWindowIndex = childTimeline.getPreviousWindowIndex(windowIndex, repeatMode); return childPreviousWindowIndex == C.INDEX_UNSET ? getWindowCount() - 1 : childPreviousWindowIndex; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/RepeatModeUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/util/RepeatModeUtil.java index 07850269f9..53cb051230 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/RepeatModeUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/RepeatModeUtil.java @@ -16,9 +16,7 @@ package com.google.android.exoplayer2.util; import android.support.annotation.IntDef; - -import com.google.android.exoplayer2.ExoPlayer; - +import com.google.android.exoplayer2.Player; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -58,10 +56,10 @@ public final class RepeatModeUtil { * @param enabledModes Bitmask of enabled modes. * @return The next repeat mode. */ - public static @ExoPlayer.RepeatMode int getNextRepeatMode( - @ExoPlayer.RepeatMode int currentMode, int enabledModes) { + public static @Player.RepeatMode int getNextRepeatMode(@Player.RepeatMode int currentMode, + int enabledModes) { for (int offset = 1; offset <= 2; offset++) { - @ExoPlayer.RepeatMode int proposedMode = (currentMode + offset) % 3; + @Player.RepeatMode int proposedMode = (currentMode + offset) % 3; if (isRepeatModeEnabled(proposedMode, enabledModes)) { return proposedMode; } @@ -76,14 +74,13 @@ public final class RepeatModeUtil { * @param enabledModes The bitmask representing the enabled modes. * @return {@code true} if enabled. */ - public static boolean isRepeatModeEnabled(@ExoPlayer.RepeatMode int repeatMode, - int enabledModes) { + public static boolean isRepeatModeEnabled(@Player.RepeatMode int repeatMode, int enabledModes) { switch (repeatMode) { - case ExoPlayer.REPEAT_MODE_OFF: + case Player.REPEAT_MODE_OFF: return true; - case ExoPlayer.REPEAT_MODE_ONE: + case Player.REPEAT_MODE_ONE: return (enabledModes & REPEAT_TOGGLE_MODE_ONE) != 0; - case ExoPlayer.REPEAT_MODE_ALL: + case Player.REPEAT_MODE_ALL: return (enabledModes & REPEAT_TOGGLE_MODE_ALL) != 0; default: return false; diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java index 373312b073..2b8705bb74 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java @@ -17,9 +17,9 @@ package com.google.android.exoplayer2.ui; 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.Player; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.decoder.DecoderCounters; @@ -30,7 +30,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectionArray; * A helper class for periodically updating a {@link TextView} with debug information obtained from * a {@link SimpleExoPlayer}. */ -public final class DebugTextViewHelper implements Runnable, ExoPlayer.EventListener { +public final class DebugTextViewHelper implements Runnable, Player.EventListener { private static final int REFRESH_INTERVAL_MS = 1000; @@ -74,7 +74,7 @@ public final class DebugTextViewHelper implements Runnable, ExoPlayer.EventListe textView.removeCallbacks(this); } - // ExoPlayer.EventListener implementation. + // Player.EventListener implementation. @Override public void onLoadingChanged(boolean isLoading) { @@ -135,16 +135,16 @@ public final class DebugTextViewHelper implements Runnable, ExoPlayer.EventListe private String getPlayerStateString() { String text = "playWhenReady:" + player.getPlayWhenReady() + " playbackState:"; switch (player.getPlaybackState()) { - case ExoPlayer.STATE_BUFFERING: + case Player.STATE_BUFFERING: text += "buffering"; break; - case ExoPlayer.STATE_ENDED: + case Player.STATE_ENDED: text += "ended"; break; - case ExoPlayer.STATE_IDLE: + case Player.STATE_IDLE: text += "idle"; break; - case ExoPlayer.STATE_READY: + case Player.STATE_READY: text += "ready"; break; default: diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index b3c79b9fdc..a4083c940f 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -35,8 +35,8 @@ import android.widget.FrameLayout; 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.PlaybackParameters; +import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.metadata.Metadata; @@ -726,8 +726,8 @@ public final class SimpleExoPlayerView extends FrameLayout { return true; } int playbackState = player.getPlaybackState(); - return controllerAutoShow && (playbackState == ExoPlayer.STATE_IDLE - || playbackState == ExoPlayer.STATE_ENDED || !player.getPlayWhenReady()); + return controllerAutoShow && (playbackState == Player.STATE_IDLE + || playbackState == Player.STATE_ENDED || !player.getPlayWhenReady()); } private void showController(boolean showIndefinitely) { @@ -830,7 +830,7 @@ public final class SimpleExoPlayerView extends FrameLayout { } private final class ComponentListener implements SimpleExoPlayer.VideoListener, - TextRenderer.Output, ExoPlayer.EventListener { + TextRenderer.Output, Player.EventListener { // TextRenderer.Output implementation @@ -864,7 +864,7 @@ public final class SimpleExoPlayerView extends FrameLayout { updateForCurrentTrackSelections(); } - // ExoPlayer.EventListener implementation + // Player.EventListener implementation @Override public void onLoadingChanged(boolean isLoading) { diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java index 610b5d1a84..b61b484e32 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java @@ -25,6 +25,7 @@ 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.Player; import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; @@ -51,7 +52,7 @@ import junit.framework.Assert; /** * A {@link HostedTest} for {@link ExoPlayer} playback tests. */ -public abstract class ExoHostedTest implements HostedTest, ExoPlayer.EventListener, +public abstract class ExoHostedTest implements HostedTest, Player.EventListener, AudioRendererEventListener, VideoRendererEventListener { static { @@ -78,7 +79,7 @@ public abstract class ExoHostedTest implements HostedTest, ExoPlayer.EventListen private SimpleExoPlayer player; private Surface surface; private ExoPlaybackException playerError; - private ExoPlayer.EventListener playerEventListener; + private Player.EventListener playerEventListener; private boolean playerWasPrepared; private boolean playerFinished; private boolean playing; @@ -131,9 +132,9 @@ public abstract class ExoHostedTest implements HostedTest, ExoPlayer.EventListen } /** - * Sets an {@link ExoPlayer.EventListener} to listen for ExoPlayer events during the test. + * Sets an {@link Player.EventListener} to listen for ExoPlayer events during the test. */ - public final void setEventListener(ExoPlayer.EventListener eventListener) { + public final void setEventListener(Player.EventListener eventListener) { this.playerEventListener = eventListener; if (player != null) { player.addListener(eventListener); @@ -200,7 +201,7 @@ public abstract class ExoHostedTest implements HostedTest, ExoPlayer.EventListen assertPassed(audioDecoderCounters, videoDecoderCounters); } - // ExoPlayer.EventListener + // Player.EventListener @Override public void onLoadingChanged(boolean isLoading) { @@ -215,12 +216,12 @@ public abstract class ExoHostedTest implements HostedTest, ExoPlayer.EventListen @Override public final void onPlayerStateChanged(boolean playWhenReady, int playbackState) { Log.d(tag, "state [" + playWhenReady + ", " + playbackState + "]"); - playerWasPrepared |= playbackState != ExoPlayer.STATE_IDLE; - if (playbackState == ExoPlayer.STATE_ENDED - || (playbackState == ExoPlayer.STATE_IDLE && playerWasPrepared)) { + playerWasPrepared |= playbackState != Player.STATE_IDLE; + if (playbackState == Player.STATE_ENDED + || (playbackState == Player.STATE_IDLE && playerWasPrepared)) { playerFinished = true; } - boolean playing = playWhenReady && playbackState == ExoPlayer.STATE_READY; + boolean playing = playWhenReady && playbackState == Player.STATE_READY; if (!this.playing && playing) { lastPlayingStartTimeMs = SystemClock.elapsedRealtime(); } else if (this.playing && !playing) { diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerWrapper.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerWrapper.java index ff819d722e..ab247283e6 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerWrapper.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerWrapper.java @@ -22,6 +22,7 @@ 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.Player; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaSource; @@ -37,7 +38,7 @@ import junit.framework.Assert; /** * Wraps a player with its own handler thread. */ -public class ExoPlayerWrapper implements ExoPlayer.EventListener { +public class ExoPlayerWrapper implements Player.EventListener { private final CountDownLatch sourceInfoCountDownLatch; private final CountDownLatch endedCountDownLatch; @@ -142,7 +143,7 @@ public class ExoPlayerWrapper implements ExoPlayer.EventListener { } } - // ExoPlayer.EventListener implementation. + // Player.EventListener implementation. @Override public void onLoadingChanged(boolean isLoading) { @@ -151,7 +152,7 @@ public class ExoPlayerWrapper implements ExoPlayer.EventListener { @Override public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { - if (playbackState == ExoPlayer.STATE_ENDED) { + if (playbackState == Player.STATE_ENDED) { endedCountDownLatch.countDown(); } } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java index afbfbb59db..8357ce70c7 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java @@ -18,7 +18,7 @@ package com.google.android.exoplayer2.testutil; import static junit.framework.Assert.assertEquals; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline.Period; import com.google.android.exoplayer2.Timeline.Window; @@ -71,7 +71,7 @@ public final class TimelineAsserts { * mode. */ public static void assertPreviousWindowIndices(Timeline timeline, - @ExoPlayer.RepeatMode int repeatMode, int... expectedPreviousWindowIndices) { + @Player.RepeatMode int repeatMode, int... expectedPreviousWindowIndices) { for (int i = 0; i < timeline.getWindowCount(); i++) { assertEquals(expectedPreviousWindowIndices[i], timeline.getPreviousWindowIndex(i, repeatMode)); @@ -82,8 +82,8 @@ public final class TimelineAsserts { * Asserts that next window indices for each window are set correctly depending on the repeat * mode. */ - public static void assertNextWindowIndices(Timeline timeline, - @ExoPlayer.RepeatMode int repeatMode, int... expectedNextWindowIndices) { + public static void assertNextWindowIndices(Timeline timeline, @Player.RepeatMode int repeatMode, + int... expectedNextWindowIndices) { for (int i = 0; i < timeline.getWindowCount(); i++) { assertEquals(expectedNextWindowIndices[i], timeline.getNextWindowIndex(i, repeatMode)); @@ -119,19 +119,16 @@ public final class TimelineAsserts { } assertEquals(expectedWindowIndex, period.windowIndex); if (i < accumulatedPeriodCounts[expectedWindowIndex + 1] - 1) { - assertEquals(i + 1, timeline.getNextPeriodIndex(i, period, window, - ExoPlayer.REPEAT_MODE_OFF)); - assertEquals(i + 1, timeline.getNextPeriodIndex(i, period, window, - ExoPlayer.REPEAT_MODE_ONE)); - assertEquals(i + 1, timeline.getNextPeriodIndex(i, period, window, - ExoPlayer.REPEAT_MODE_ALL)); + assertEquals(i + 1, timeline.getNextPeriodIndex(i, period, window, Player.REPEAT_MODE_OFF)); + assertEquals(i + 1, timeline.getNextPeriodIndex(i, period, window, Player.REPEAT_MODE_ONE)); + assertEquals(i + 1, timeline.getNextPeriodIndex(i, period, window, Player.REPEAT_MODE_ALL)); } else { int nextWindowOff = timeline.getNextWindowIndex(expectedWindowIndex, - ExoPlayer.REPEAT_MODE_OFF); + Player.REPEAT_MODE_OFF); int nextWindowOne = timeline.getNextWindowIndex(expectedWindowIndex, - ExoPlayer.REPEAT_MODE_ONE); + Player.REPEAT_MODE_ONE); int nextWindowAll = timeline.getNextWindowIndex(expectedWindowIndex, - ExoPlayer.REPEAT_MODE_ALL); + Player.REPEAT_MODE_ALL); int nextPeriodOff = nextWindowOff == C.INDEX_UNSET ? C.INDEX_UNSET : accumulatedPeriodCounts[nextWindowOff]; int nextPeriodOne = nextWindowOne == C.INDEX_UNSET ? C.INDEX_UNSET @@ -139,11 +136,11 @@ public final class TimelineAsserts { int nextPeriodAll = nextWindowAll == C.INDEX_UNSET ? C.INDEX_UNSET : accumulatedPeriodCounts[nextWindowAll]; assertEquals(nextPeriodOff, timeline.getNextPeriodIndex(i, period, window, - ExoPlayer.REPEAT_MODE_OFF)); + Player.REPEAT_MODE_OFF)); assertEquals(nextPeriodOne, timeline.getNextPeriodIndex(i, period, window, - ExoPlayer.REPEAT_MODE_ONE)); + Player.REPEAT_MODE_ONE)); assertEquals(nextPeriodAll, timeline.getNextPeriodIndex(i, period, window, - ExoPlayer.REPEAT_MODE_ALL)); + Player.REPEAT_MODE_ALL)); } } } From 94b08b27e96efb6886af3602968435f305539e16 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 20 Jul 2017 07:02:52 -0700 Subject: [PATCH 0101/2472] Fix MediaSession gradle file to use modulePrefix ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=162610352 --- extensions/mediasession/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/mediasession/build.gradle b/extensions/mediasession/build.gradle index c439543967..85a8ac46e2 100644 --- a/extensions/mediasession/build.gradle +++ b/extensions/mediasession/build.gradle @@ -25,7 +25,7 @@ android { } dependencies { - compile project(':library-core') + compile project(modulePrefix + 'library-core') compile 'com.android.support:support-media-compat:' + supportLibraryVersion compile 'com.android.support:appcompat-v7:' + supportLibraryVersion } From 07de4d1b2ca593ed917318eec06a75bc6eb8f840 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 20 Jul 2017 07:05:34 -0700 Subject: [PATCH 0102/2472] Handle ad skipping and content resume SKIPPED can't be handled as CONTENT_RESUME_REQUESTED because after skipping an ad there may be further ads to play in its ad group. Remove workaround for handling unexpected playAd without stopAd, as the player can instead recover when IMA sends CONTENT_RESUME_REQUESTED. This in turn fixes handling of the case where playAd is called twice but IMA expects only the first ad to play, when skipping a particular ad. (Add an ad tag where this occurs to internal samples.) Check whether a currently playing ad has been marked as played in ExoPlayerImplInternal, and handle this case as a seek. This ensures that any loaded ad periods are discarded in the case of CONTENT_RESUME_REQUESTED. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=162610621 --- .../exoplayer2/ext/ima/AdPlaybackState.java | 11 +++ .../exoplayer2/ext/ima/ImaAdsLoader.java | 79 ++++++++++--------- .../exoplayer2/ExoPlayerImplInternal.java | 13 +++ 3 files changed, 65 insertions(+), 38 deletions(-) diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/AdPlaybackState.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/AdPlaybackState.java index d05232da2e..0edd7d6558 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/AdPlaybackState.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/AdPlaybackState.java @@ -122,6 +122,17 @@ import java.util.Arrays; adsPlayedCounts[adGroupIndex]++; } + /** + * Marks all ads in the specified ad group as played. + */ + public void playedAdGroup(int adGroupIndex) { + adResumePositionUs = 0; + if (adCounts[adGroupIndex] == C.LENGTH_UNSET) { + adCounts[adGroupIndex] = 0; + } + adsPlayedCounts[adGroupIndex] = adCounts[adGroupIndex]; + } + /** * Sets the position offset in the first unplayed ad at which to begin playback, in microseconds. */ diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index 12d58f70cf..6e2206d6ae 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -337,7 +337,6 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, imaPausedContent = true; pauseContentInternal(); break; - case SKIPPED: // Fall through. case CONTENT_RESUME_REQUESTED: imaPausedContent = false; resumeContentInternal(); @@ -432,9 +431,8 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, } if (imaPlayingAd && !imaPausedInAd) { // Work around an issue where IMA does not always call stopAd before resuming content. - // See [Internal: b/38354028]. + // See [Internal: b/38354028, b/63320878]. Log.w(TAG, "Unexpected playAd without stopAd"); - stopAdInternal(); } if (!imaPlayingAd) { imaPlayingAd = true; @@ -497,8 +495,7 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, Assertions.checkArgument(timeline.getPeriodCount() == 1); this.timeline = timeline; contentDurationMs = C.usToMs(timeline.getPeriod(0, period).durationUs); - playingAd = player.isPlayingAd(); - playingAdIndexInAdGroup = playingAd ? player.getCurrentAdIndexInAdGroup() : C.INDEX_UNSET; + updateImaStateForPlayerState(); } @Override @@ -547,9 +544,7 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, if (adsManager == null) { return; } - boolean wasPlayingAd = playingAd; - playingAd = player.isPlayingAd(); - if (!wasPlayingAd && !playingAd) { + if (!playingAd && !player.isPlayingAd()) { long positionUs = C.msToUs(player.getCurrentPosition()); int adGroupIndex = timeline.getPeriod(0, period).getAdGroupIndexForPositionUs(positionUs); if (adGroupIndex != C.INDEX_UNSET) { @@ -558,32 +553,7 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, } return; } - if (!playingAd && playWhenReadyOverriddenForAds) { - playWhenReadyOverriddenForAds = false; - player.setPlayWhenReady(false); - } - if (!sentContentComplete) { - boolean adFinished = - !playingAd || playingAdIndexInAdGroup != player.getCurrentAdIndexInAdGroup(); - if (adFinished) { - // IMA is waiting for the ad playback to finish so invoke the callback now. - // Either CONTENT_RESUME_REQUESTED will be passed next, or playAd will be called again. - for (VideoAdPlayerCallback callback : adCallbacks) { - callback.onEnded(); - } - } - if (playingAd && !wasPlayingAd) { - int adGroupIndex = player.getCurrentAdGroupIndex(); - // IMA hasn't sent CONTENT_PAUSE_REQUESTED yet, so fake the content position. - Assertions.checkState(fakeContentProgressElapsedRealtimeMs == C.TIME_UNSET); - fakeContentProgressElapsedRealtimeMs = SystemClock.elapsedRealtime(); - fakeContentProgressOffsetMs = C.usToMs(adPlaybackState.adGroupTimesUs[adGroupIndex]); - if (fakeContentProgressOffsetMs == C.TIME_END_OF_SOURCE) { - fakeContentProgressOffsetMs = contentDurationMs; - } - } - } - playingAdIndexInAdGroup = playingAd ? player.getCurrentAdIndexInAdGroup() : C.INDEX_UNSET; + updateImaStateForPlayerState(); } @Override @@ -601,14 +571,47 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, adsLoader.requestAds(request); } + private void updateImaStateForPlayerState() { + boolean wasPlayingAd = playingAd; + playingAd = player.isPlayingAd(); + if (!playingAd && playWhenReadyOverriddenForAds) { + playWhenReadyOverriddenForAds = false; + player.setPlayWhenReady(false); + } + if (!sentContentComplete) { + boolean adFinished = (wasPlayingAd && !playingAd) + || playingAdIndexInAdGroup != player.getCurrentAdIndexInAdGroup(); + if (adFinished) { + // IMA is waiting for the ad playback to finish so invoke the callback now. + // Either CONTENT_RESUME_REQUESTED will be passed next, or playAd will be called again. + for (VideoAdPlayerCallback callback : adCallbacks) { + callback.onEnded(); + } + } + if (!wasPlayingAd && playingAd) { + int adGroupIndex = player.getCurrentAdGroupIndex(); + // IMA hasn't sent CONTENT_PAUSE_REQUESTED yet, so fake the content position. + Assertions.checkState(fakeContentProgressElapsedRealtimeMs == C.TIME_UNSET); + fakeContentProgressElapsedRealtimeMs = SystemClock.elapsedRealtime(); + fakeContentProgressOffsetMs = C.usToMs(adPlaybackState.adGroupTimesUs[adGroupIndex]); + if (fakeContentProgressOffsetMs == C.TIME_END_OF_SOURCE) { + fakeContentProgressOffsetMs = contentDurationMs; + } + } + } + playingAdIndexInAdGroup = playingAd ? player.getCurrentAdIndexInAdGroup() : C.INDEX_UNSET; + } + private void resumeContentInternal() { - if (contentDurationMs != C.TIME_UNSET && imaPlayingAd) { - // Work around an issue where IMA does not always call stopAd before resuming content. - // See [Internal: b/38354028]. + if (imaPlayingAd) { if (DEBUG) { Log.d(TAG, "Unexpected CONTENT_RESUME_REQUESTED without stopAd"); } - stopAdInternal(); + } + if (playingAd && adGroupIndex != C.INDEX_UNSET) { + adPlaybackState.playedAdGroup(adGroupIndex); + adGroupIndex = C.INDEX_UNSET; + updateAdPlaybackState(); } clearFlags(); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index ea1e898e66..633250a784 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -1054,6 +1054,19 @@ import java.io.IOException; return; } + // If playing an ad, check that it hasn't been marked as played. If it has, skip forward. + if (playbackInfo.periodId.isAd()) { + MediaPeriodId periodId = mediaPeriodInfoSequence.resolvePeriodPositionForAds(periodIndex, + playbackInfo.contentPositionUs); + if (!periodId.isAd() || periodId.adIndexInAdGroup != playbackInfo.periodId.adIndexInAdGroup) { + long newPositionUs = seekToPeriodPosition(periodId, playbackInfo.contentPositionUs); + long contentPositionUs = periodId.isAd() ? playbackInfo.contentPositionUs : C.TIME_UNSET; + playbackInfo = new PlaybackInfo(periodId, newPositionUs, contentPositionUs); + notifySourceInfoRefresh(manifest, processedInitialSeekCount); + return; + } + } + // The current period is in the new timeline. Update the holder and playbackInfo. periodHolder = updatePeriodInfo(periodHolder, periodIndex); if (periodIndex != playbackInfo.periodId.periodIndex) { From 89181cf4bccfcfca91205ba95acfa7e47fff0b0b Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 21 Jul 2017 02:43:25 -0700 Subject: [PATCH 0103/2472] Fully document MappingTrackSelector ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=162721489 --- .../trackselection/DefaultTrackSelector.java | 4 +- .../trackselection/MappingTrackSelector.java | 525 ++++++++++-------- 2 files changed, 287 insertions(+), 242 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index 2a426c9c52..2407a2cca9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -33,9 +33,7 @@ import java.util.List; import java.util.concurrent.atomic.AtomicReference; /** - * A {@link MappingTrackSelector} that allows configuration of common parameters. It is safe to call - * the methods of this class from the application thread. See {@link Parameters#Parameters()} for - * default selection parameters. + * A {@link MappingTrackSelector} suitable for most use cases. */ public class DefaultTrackSelector extends MappingTrackSelector { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java index 30cc02936a..45ac9eab6e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java @@ -20,6 +20,7 @@ import android.util.SparseArray; import android.util.SparseBooleanArray; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.RendererConfiguration; @@ -34,9 +35,258 @@ import java.util.Map; * Base class for {@link TrackSelector}s that first establish a mapping between {@link TrackGroup}s * and {@link Renderer}s, and then from that mapping create a {@link TrackSelection} for each * renderer. + * + *

    Track overrides

    + * Mapping track selectors support overriding of track selections for each renderer. To specify an + * override for a renderer it's first necessary to obtain the tracks that have been mapped to it: + *
    + * {@code
    + * MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
    + * TrackGroupArray rendererTrackGroups = mappedTrackInfo == null ? null
    + *     : mappedTrackInfo.getTrackGroups(rendererIndex);}
    + * 
    + * If {@code rendererTrackGroups} is null then there aren't any currently mapped tracks, and so + * setting an override isn't possible. Note that a {@link Player.EventListener} registered on the + * player can be used to determine when the current tracks (and therefore the mapping) changes. If + * {@code rendererTrackGroups} is non-null then an override can be set. The next step is to query + * the properties of the available tracks to determine the {@code groupIndex} of the track group you + * want to select and the {@code trackIndices} within it. You can then create and set the override: + *
    + * {@code
    + * trackSelector.setSelectionOverride(rendererIndex, rendererTrackGroups,
    + *     new SelectionOverride(trackSelectionFactory, groupIndex, trackIndices));}
    + * 
    + * where {@code trackSelectionFactory} is a {@link TrackSelection.Factory} for generating concrete + * {@link TrackSelection} instances for the override. It's also possible to pass {@code null} as the + * selection override if you don't want any tracks to be selected. + *

    + * Note that an override applies only when the track groups available to the renderer match the + * {@link TrackGroupArray} for which the override was specified. Overrides can be cleared using + * the {@code clearSelectionOverride} methods. + * + *

    Disabling renderers

    + * Renderers can be disabled using {@link #setRendererDisabled(int, boolean)}. Disabling a renderer + * differs from setting a {@code null} override because the renderer is disabled unconditionally, + * whereas a {@code null} override is applied only when the track groups available to the renderer + * match the {@link TrackGroupArray} for which it was specified. + * + *

    Tunneling

    + * Tunneled playback can be enabled in cases where the combination of renderers and selected tracks + * support it. See {@link #setTunnelingAudioSessionId(int)} for more details. */ public abstract class MappingTrackSelector extends TrackSelector { + /** + * Provides mapped track information for each renderer. + */ + public static final class MappedTrackInfo { + + /** + * The renderer does not have any associated tracks. + */ + public static final int RENDERER_SUPPORT_NO_TRACKS = 0; + /** + * The renderer has associated tracks, but all are of unsupported types. + */ + public static final int RENDERER_SUPPORT_UNSUPPORTED_TRACKS = 1; + /** + * The renderer has associated tracks and at least one is of a supported type, but all of the + * tracks whose types are supported exceed the renderer's capabilities. + */ + public static final int RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS = 2; + /** + * The renderer has associated tracks and can play at least one of them. + */ + public static final int RENDERER_SUPPORT_PLAYABLE_TRACKS = 3; + + /** + * The number of renderers to which tracks are mapped. + */ + public final int length; + + private final int[] rendererTrackTypes; + private final TrackGroupArray[] trackGroups; + private final int[] mixedMimeTypeAdaptiveSupport; + private final int[][][] formatSupport; + private final TrackGroupArray unassociatedTrackGroups; + + /** + * @param rendererTrackTypes The track type supported by each renderer. + * @param trackGroups The {@link TrackGroup}s mapped to each renderer. + * @param mixedMimeTypeAdaptiveSupport The result of + * {@link RendererCapabilities#supportsMixedMimeTypeAdaptation()} for each renderer. + * @param formatSupport The result of {@link RendererCapabilities#supportsFormat} for each + * mapped track, indexed by renderer index, track group index and track index (in that + * order). + * @param unassociatedTrackGroups Any {@link TrackGroup}s not mapped to any renderer. + */ + /* package */ MappedTrackInfo(int[] rendererTrackTypes, + TrackGroupArray[] trackGroups, int[] mixedMimeTypeAdaptiveSupport, + int[][][] formatSupport, TrackGroupArray unassociatedTrackGroups) { + this.rendererTrackTypes = rendererTrackTypes; + this.trackGroups = trackGroups; + this.formatSupport = formatSupport; + this.mixedMimeTypeAdaptiveSupport = mixedMimeTypeAdaptiveSupport; + this.unassociatedTrackGroups = unassociatedTrackGroups; + this.length = trackGroups.length; + } + + /** + * Returns the {@link TrackGroup}s mapped to the renderer at the specified index. + * + * @param rendererIndex The renderer index. + * @return The corresponding {@link TrackGroup}s. + */ + public TrackGroupArray getTrackGroups(int rendererIndex) { + return trackGroups[rendererIndex]; + } + + /** + * Returns the extent to which a renderer can play the tracks in the track groups mapped to it. + * + * @param rendererIndex The renderer index. + * @return One of {@link #RENDERER_SUPPORT_PLAYABLE_TRACKS}, + * {@link #RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS}, + * {@link #RENDERER_SUPPORT_UNSUPPORTED_TRACKS} and {@link #RENDERER_SUPPORT_NO_TRACKS}. + */ + public int getRendererSupport(int rendererIndex) { + int bestRendererSupport = RENDERER_SUPPORT_NO_TRACKS; + int[][] rendererFormatSupport = formatSupport[rendererIndex]; + for (int i = 0; i < rendererFormatSupport.length; i++) { + for (int j = 0; j < rendererFormatSupport[i].length; j++) { + int trackRendererSupport; + switch (rendererFormatSupport[i][j] & RendererCapabilities.FORMAT_SUPPORT_MASK) { + case RendererCapabilities.FORMAT_HANDLED: + return RENDERER_SUPPORT_PLAYABLE_TRACKS; + case RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES: + trackRendererSupport = RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS; + break; + default: + trackRendererSupport = RENDERER_SUPPORT_UNSUPPORTED_TRACKS; + break; + } + bestRendererSupport = Math.max(bestRendererSupport, trackRendererSupport); + } + } + return bestRendererSupport; + } + + /** + * Returns the best level of support obtained from {@link #getRendererSupport(int)} for all + * renderers of the specified track type. If no renderers exist for the specified type then + * {@link #RENDERER_SUPPORT_NO_TRACKS} is returned. + * + * @param trackType The track type. One of the {@link C} {@code TRACK_TYPE_*} constants. + * @return One of {@link #RENDERER_SUPPORT_PLAYABLE_TRACKS}, + * {@link #RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS}, + * {@link #RENDERER_SUPPORT_UNSUPPORTED_TRACKS} and {@link #RENDERER_SUPPORT_NO_TRACKS}. + */ + public int getTrackTypeRendererSupport(int trackType) { + int bestRendererSupport = RENDERER_SUPPORT_NO_TRACKS; + for (int i = 0; i < length; i++) { + if (rendererTrackTypes[i] == trackType) { + bestRendererSupport = Math.max(bestRendererSupport, getRendererSupport(i)); + } + } + return bestRendererSupport; + } + + /** + * Returns the extent to which an individual track is supported by the renderer. + * + * @param rendererIndex The renderer index. + * @param groupIndex The index of the track group to which the track belongs. + * @param trackIndex The index of the track within the track group. + * @return One of {@link RendererCapabilities#FORMAT_HANDLED}, + * {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}, + * {@link RendererCapabilities#FORMAT_UNSUPPORTED_SUBTYPE} and + * {@link RendererCapabilities#FORMAT_UNSUPPORTED_TYPE}. + */ + public int getTrackFormatSupport(int rendererIndex, int groupIndex, int trackIndex) { + return formatSupport[rendererIndex][groupIndex][trackIndex] + & RendererCapabilities.FORMAT_SUPPORT_MASK; + } + + /** + * Returns the extent to which a renderer supports adaptation between supported tracks in a + * specified {@link TrackGroup}. + *

    + * Tracks for which {@link #getTrackFormatSupport(int, int, int)} returns + * {@link RendererCapabilities#FORMAT_HANDLED} are always considered. + * Tracks for which {@link #getTrackFormatSupport(int, int, int)} returns + * {@link RendererCapabilities#FORMAT_UNSUPPORTED_TYPE} or + * {@link RendererCapabilities#FORMAT_UNSUPPORTED_SUBTYPE} are never considered. + * Tracks for which {@link #getTrackFormatSupport(int, int, int)} returns + * {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES} are considered only if + * {@code includeCapabilitiesExceededTracks} is set to {@code true}. + * + * @param rendererIndex The renderer index. + * @param groupIndex The index of the track group. + * @param includeCapabilitiesExceededTracks True if formats that exceed the capabilities of the + * renderer should be included when determining support. False otherwise. + * @return One of {@link RendererCapabilities#ADAPTIVE_SEAMLESS}, + * {@link RendererCapabilities#ADAPTIVE_NOT_SEAMLESS} and + * {@link RendererCapabilities#ADAPTIVE_NOT_SUPPORTED}. + */ + public int getAdaptiveSupport(int rendererIndex, int groupIndex, + boolean includeCapabilitiesExceededTracks) { + int trackCount = trackGroups[rendererIndex].get(groupIndex).length; + // Iterate over the tracks in the group, recording the indices of those to consider. + int[] trackIndices = new int[trackCount]; + int trackIndexCount = 0; + for (int i = 0; i < trackCount; i++) { + int fixedSupport = getTrackFormatSupport(rendererIndex, groupIndex, i); + if (fixedSupport == RendererCapabilities.FORMAT_HANDLED + || (includeCapabilitiesExceededTracks + && fixedSupport == RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES)) { + trackIndices[trackIndexCount++] = i; + } + } + trackIndices = Arrays.copyOf(trackIndices, trackIndexCount); + return getAdaptiveSupport(rendererIndex, groupIndex, trackIndices); + } + + /** + * Returns the extent to which a renderer supports adaptation between specified tracks within + * a {@link TrackGroup}. + * + * @param rendererIndex The renderer index. + * @param groupIndex The index of the track group. + * @return One of {@link RendererCapabilities#ADAPTIVE_SEAMLESS}, + * {@link RendererCapabilities#ADAPTIVE_NOT_SEAMLESS} and + * {@link RendererCapabilities#ADAPTIVE_NOT_SUPPORTED}. + */ + public int getAdaptiveSupport(int rendererIndex, int groupIndex, int[] trackIndices) { + int handledTrackCount = 0; + int adaptiveSupport = RendererCapabilities.ADAPTIVE_SEAMLESS; + boolean multipleMimeTypes = false; + String firstSampleMimeType = null; + for (int i = 0; i < trackIndices.length; i++) { + int trackIndex = trackIndices[i]; + String sampleMimeType = trackGroups[rendererIndex].get(groupIndex).getFormat(trackIndex) + .sampleMimeType; + if (handledTrackCount++ == 0) { + firstSampleMimeType = sampleMimeType; + } else { + multipleMimeTypes |= !Util.areEqual(firstSampleMimeType, sampleMimeType); + } + adaptiveSupport = Math.min(adaptiveSupport, formatSupport[rendererIndex][groupIndex][i] + & RendererCapabilities.ADAPTIVE_SUPPORT_MASK); + } + return multipleMimeTypes + ? Math.min(adaptiveSupport, mixedMimeTypeAdaptiveSupport[rendererIndex]) + : adaptiveSupport; + } + + /** + * Returns {@link TrackGroup}s not mapped to any renderer. + */ + public TrackGroupArray getUnassociatedTrackGroups() { + return unassociatedTrackGroups; + } + + } + /** * A track selection override. */ @@ -49,8 +299,8 @@ public abstract class MappingTrackSelector extends TrackSelector { /** * @param factory A factory for creating selections from this override. - * @param groupIndex The overriding group index. - * @param tracks The overriding track indices within the group. + * @param groupIndex The overriding track group index. + * @param tracks The overriding track indices within the track group. */ public SelectionOverride(TrackSelection.Factory factory, int groupIndex, int... tracks) { this.factory = factory; @@ -62,7 +312,7 @@ public abstract class MappingTrackSelector extends TrackSelector { /** * Creates an selection from this override. * - * @param groups The groups whose selection is being overridden. + * @param groups The track groups whose selection is being overridden. * @return The selection. */ public TrackSelection createTrackSelection(TrackGroupArray groups) { @@ -96,7 +346,7 @@ public abstract class MappingTrackSelector extends TrackSelector { } /** - * Returns the mapping information associated with the current track selections, or null if no + * Returns the mapping information for the currently active track selection, or null if no * selection is currently active. */ public final MappedTrackInfo getCurrentMappedTrackInfo() { @@ -104,7 +354,8 @@ public abstract class MappingTrackSelector extends TrackSelector { } /** - * Sets whether the renderer at the specified index is disabled. + * Sets whether the renderer at the specified index is disabled. Disabling a renderer prevents the + * selector from selecting any tracks for it. * * @param rendererIndex The renderer index. * @param disabled Whether the renderer is disabled. @@ -129,16 +380,22 @@ public abstract class MappingTrackSelector extends TrackSelector { } /** - * Overrides the track selection for the renderer at a specified index. + * Overrides the track selection for the renderer at the specified index. *

    - * When the {@link TrackGroupArray} available to the renderer at the specified index matches the - * one provided, the override is applied. When the {@link TrackGroupArray} does not match, the - * override has no effect. The override replaces any previous override for the renderer and the - * provided {@link TrackGroupArray}. + * When the {@link TrackGroupArray} mapped to the renderer matches the one provided, the override + * is applied. When the {@link TrackGroupArray} does not match, the override has no effect. The + * override replaces any previous override for the specified {@link TrackGroupArray} for the + * specified {@link Renderer}. *

    - * Passing a {@code null} override will explicitly disable the renderer. To remove overrides use - * {@link #clearSelectionOverride(int, TrackGroupArray)}, {@link #clearSelectionOverrides(int)} - * or {@link #clearSelectionOverrides()}. + * Passing a {@code null} override will cause the renderer to be disabled when the + * {@link TrackGroupArray} mapped to it matches the one provided. When the {@link TrackGroupArray} + * does not match a {@code null} override has no effect. Hence a {@code null} override differs + * from disabling the renderer using {@link #setRendererDisabled(int, boolean)} because the + * renderer is disabled conditionally on the {@link TrackGroupArray} mapped to it, where-as + * {@link #setRendererDisabled(int, boolean)} disables the renderer unconditionally. + *

    + * To remove overrides use {@link #clearSelectionOverride(int, TrackGroupArray)}, + * {@link #clearSelectionOverrides(int)} or {@link #clearSelectionOverrides()}. * * @param rendererIndex The renderer index. * @param groups The {@link TrackGroupArray} for which the override should be applied. @@ -203,7 +460,7 @@ public abstract class MappingTrackSelector extends TrackSelector { } /** - * Clears all track selection override for the specified renderer. + * Clears all track selection overrides for the specified renderer. * * @param rendererIndex The renderer index. */ @@ -218,7 +475,7 @@ public abstract class MappingTrackSelector extends TrackSelector { } /** - * Clears all track selection overrides. + * Clears all track selection overrides for all renderers. */ public final void clearSelectionOverrides() { if (selectionOverrides.size() == 0) { @@ -340,15 +597,15 @@ public abstract class MappingTrackSelector extends TrackSelector { } /** - * Given an array of renderers and a set of {@link TrackGroup}s mapped to each of them, provides a - * {@link TrackSelection} per renderer. + * Given an array of renderer capabilities and the {@link TrackGroupArray}s mapped to each of + * them, provides a {@link TrackSelection} per renderer. * * @param rendererCapabilities The {@link RendererCapabilities} of the renderers for which * {@link TrackSelection}s are to be generated. - * @param rendererTrackGroupArrays An array of {@link TrackGroupArray}s where each entry - * corresponds to the renderer of equal index in {@code renderers}. - * @param rendererFormatSupports Maps every available track to a specific level of support as - * defined by the renderer {@code FORMAT_*} constants. + * @param rendererTrackGroupArrays The {@link TrackGroupArray}s mapped to each of the renderers. + * @param rendererFormatSupports The result of {@link RendererCapabilities#supportsFormat} for + * each mapped track, indexed by renderer index, track group index and track index (in that + * order). * @throws ExoPlaybackException If an error occurs while selecting the tracks. */ protected abstract TrackSelection[] selectTracks(RendererCapabilities[] rendererCapabilities, @@ -356,9 +613,9 @@ public abstract class MappingTrackSelector extends TrackSelector { throws ExoPlaybackException; /** - * Finds the renderer to which the provided {@link TrackGroup} should be associated. + * Finds the renderer to which the provided {@link TrackGroup} should be mapped. *

    - * A {@link TrackGroup} is associated to a renderer that reports + * A {@link TrackGroup} is mapped to the renderer that reports * {@link RendererCapabilities#FORMAT_HANDLED} support for one or more of the tracks in the group, * or {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES} if no such renderer exists, or * {@link RendererCapabilities#FORMAT_UNSUPPORTED_SUBTYPE} if again no such renderer exists. In @@ -366,13 +623,13 @@ public abstract class MappingTrackSelector extends TrackSelector { * lowest index is associated. *

    * If all renderers report {@link RendererCapabilities#FORMAT_UNSUPPORTED_TYPE} for all of the - * tracks in the group, then {@code renderers.length} is returned to indicate that no association - * was made. + * tracks in the group, then {@code renderers.length} is returned to indicate that the group was + * not mapped to any renderer. * * @param rendererCapabilities The {@link RendererCapabilities} of the renderers. - * @param group The {@link TrackGroup} whose associated renderer is to be found. - * @return The index of the associated renderer, or {@code renderers.length} if no - * association was made. + * @param group The track group to map to a renderer. + * @return The index of the renderer to which the track group was mapped, or + * {@code renderers.length} if it was not mapped to any renderer. * @throws ExoPlaybackException If an error occurs finding a renderer. */ private static int findRenderer(RendererCapabilities[] rendererCapabilities, TrackGroup group) @@ -402,7 +659,7 @@ public abstract class MappingTrackSelector extends TrackSelector { * {@link TrackGroup}, returning the results in an array. * * @param rendererCapabilities The {@link RendererCapabilities} of the renderer. - * @param group The {@link TrackGroup} to evaluate. + * @param group The track group to evaluate. * @return An array containing the result of calling * {@link RendererCapabilities#supportsFormat} for each track in the group. * @throws ExoPlaybackException If an error occurs determining the format support. @@ -522,214 +779,4 @@ public abstract class MappingTrackSelector extends TrackSelector { return true; } - /** - * Provides track information for each renderer. - */ - public static final class MappedTrackInfo { - - /** - * The renderer does not have any associated tracks. - */ - public static final int RENDERER_SUPPORT_NO_TRACKS = 0; - /** - * The renderer has associated tracks, but all are of unsupported types. - */ - public static final int RENDERER_SUPPORT_UNSUPPORTED_TRACKS = 1; - /** - * The renderer has associated tracks and at least one is of a supported type, but all of the - * tracks whose types are supported exceed the renderer's capabilities. - */ - public static final int RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS = 2; - /** - * The renderer has associated tracks and can play at least one of them. - */ - public static final int RENDERER_SUPPORT_PLAYABLE_TRACKS = 3; - - /** - * The number of renderers to which tracks are mapped. - */ - public final int length; - - private final int[] rendererTrackTypes; - private final TrackGroupArray[] trackGroups; - private final int[] mixedMimeTypeAdaptiveSupport; - private final int[][][] formatSupport; - private final TrackGroupArray unassociatedTrackGroups; - - /** - * @param rendererTrackTypes The track type supported by each renderer. - * @param trackGroups The {@link TrackGroupArray}s for each renderer. - * @param mixedMimeTypeAdaptiveSupport The result of - * {@link RendererCapabilities#supportsMixedMimeTypeAdaptation()} for each renderer. - * @param formatSupport The result of {@link RendererCapabilities#supportsFormat} for each - * track, indexed by renderer index, group index and track index (in that order). - * @param unassociatedTrackGroups Contains {@link TrackGroup}s not associated with any renderer. - */ - /* package */ MappedTrackInfo(int[] rendererTrackTypes, - TrackGroupArray[] trackGroups, int[] mixedMimeTypeAdaptiveSupport, - int[][][] formatSupport, TrackGroupArray unassociatedTrackGroups) { - this.rendererTrackTypes = rendererTrackTypes; - this.trackGroups = trackGroups; - this.formatSupport = formatSupport; - this.mixedMimeTypeAdaptiveSupport = mixedMimeTypeAdaptiveSupport; - this.unassociatedTrackGroups = unassociatedTrackGroups; - this.length = trackGroups.length; - } - - /** - * Returns the array of {@link TrackGroup}s associated to the renderer at a specified index. - * - * @param rendererIndex The renderer index. - * @return The corresponding {@link TrackGroup}s. - */ - public TrackGroupArray getTrackGroups(int rendererIndex) { - return trackGroups[rendererIndex]; - } - - /** - * Returns the extent to which a renderer can support playback of the tracks associated to it. - * - * @param rendererIndex The renderer index. - * @return One of {@link #RENDERER_SUPPORT_PLAYABLE_TRACKS}, - * {@link #RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS}, - * {@link #RENDERER_SUPPORT_UNSUPPORTED_TRACKS} and {@link #RENDERER_SUPPORT_NO_TRACKS}. - */ - public int getRendererSupport(int rendererIndex) { - int bestRendererSupport = RENDERER_SUPPORT_NO_TRACKS; - int[][] rendererFormatSupport = formatSupport[rendererIndex]; - for (int i = 0; i < rendererFormatSupport.length; i++) { - for (int j = 0; j < rendererFormatSupport[i].length; j++) { - int trackRendererSupport; - switch (rendererFormatSupport[i][j] & RendererCapabilities.FORMAT_SUPPORT_MASK) { - case RendererCapabilities.FORMAT_HANDLED: - return RENDERER_SUPPORT_PLAYABLE_TRACKS; - case RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES: - trackRendererSupport = RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS; - break; - default: - trackRendererSupport = RENDERER_SUPPORT_UNSUPPORTED_TRACKS; - break; - } - bestRendererSupport = Math.max(bestRendererSupport, trackRendererSupport); - } - } - return bestRendererSupport; - } - - /** - * Returns the best level of support obtained from {@link #getRendererSupport(int)} for all - * renderers of the specified track type. If no renderers exist for the specified type then - * {@link #RENDERER_SUPPORT_NO_TRACKS} is returned. - * - * @param trackType The track type. One of the {@link C} {@code TRACK_TYPE_*} constants. - * @return One of {@link #RENDERER_SUPPORT_PLAYABLE_TRACKS}, - * {@link #RENDERER_SUPPORT_EXCEEDS_CAPABILITIES_TRACKS}, - * {@link #RENDERER_SUPPORT_UNSUPPORTED_TRACKS} and {@link #RENDERER_SUPPORT_NO_TRACKS}. - */ - public int getTrackTypeRendererSupport(int trackType) { - int bestRendererSupport = RENDERER_SUPPORT_NO_TRACKS; - for (int i = 0; i < length; i++) { - if (rendererTrackTypes[i] == trackType) { - bestRendererSupport = Math.max(bestRendererSupport, getRendererSupport(i)); - } - } - return bestRendererSupport; - } - - /** - * Returns the extent to which the format of an individual track is supported by the renderer. - * - * @param rendererIndex The renderer index. - * @param groupIndex The index of the group to which the track belongs. - * @param trackIndex The index of the track within the group. - * @return One of {@link RendererCapabilities#FORMAT_HANDLED}, - * {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}, - * {@link RendererCapabilities#FORMAT_UNSUPPORTED_SUBTYPE} and - * {@link RendererCapabilities#FORMAT_UNSUPPORTED_TYPE}. - */ - public int getTrackFormatSupport(int rendererIndex, int groupIndex, int trackIndex) { - return formatSupport[rendererIndex][groupIndex][trackIndex] - & RendererCapabilities.FORMAT_SUPPORT_MASK; - } - - /** - * Returns the extent to which the renderer supports adaptation between supported tracks in a - * specified {@link TrackGroup}. - *

    - * Tracks for which {@link #getTrackFormatSupport(int, int, int)} returns - * {@link RendererCapabilities#FORMAT_HANDLED} are always considered. - * Tracks for which {@link #getTrackFormatSupport(int, int, int)} returns - * {@link RendererCapabilities#FORMAT_UNSUPPORTED_TYPE} or - * {@link RendererCapabilities#FORMAT_UNSUPPORTED_SUBTYPE} are never considered. - * Tracks for which {@link #getTrackFormatSupport(int, int, int)} returns - * {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES} are considered only if - * {@code includeCapabilitiesExceededTracks} is set to {@code true}. - * - * @param rendererIndex The renderer index. - * @param groupIndex The index of the group. - * @param includeCapabilitiesExceededTracks True if formats that exceed the capabilities of the - * renderer should be included when determining support. False otherwise. - * @return One of {@link RendererCapabilities#ADAPTIVE_SEAMLESS}, - * {@link RendererCapabilities#ADAPTIVE_NOT_SEAMLESS} and - * {@link RendererCapabilities#ADAPTIVE_NOT_SUPPORTED}. - */ - public int getAdaptiveSupport(int rendererIndex, int groupIndex, - boolean includeCapabilitiesExceededTracks) { - int trackCount = trackGroups[rendererIndex].get(groupIndex).length; - // Iterate over the tracks in the group, recording the indices of those to consider. - int[] trackIndices = new int[trackCount]; - int trackIndexCount = 0; - for (int i = 0; i < trackCount; i++) { - int fixedSupport = getTrackFormatSupport(rendererIndex, groupIndex, i); - if (fixedSupport == RendererCapabilities.FORMAT_HANDLED - || (includeCapabilitiesExceededTracks - && fixedSupport == RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES)) { - trackIndices[trackIndexCount++] = i; - } - } - trackIndices = Arrays.copyOf(trackIndices, trackIndexCount); - return getAdaptiveSupport(rendererIndex, groupIndex, trackIndices); - } - - /** - * Returns the extent to which the renderer supports adaptation between specified tracks within - * a {@link TrackGroup}. - * - * @param rendererIndex The renderer index. - * @param groupIndex The index of the group. - * @return One of {@link RendererCapabilities#ADAPTIVE_SEAMLESS}, - * {@link RendererCapabilities#ADAPTIVE_NOT_SEAMLESS} and - * {@link RendererCapabilities#ADAPTIVE_NOT_SUPPORTED}. - */ - public int getAdaptiveSupport(int rendererIndex, int groupIndex, int[] trackIndices) { - int handledTrackCount = 0; - int adaptiveSupport = RendererCapabilities.ADAPTIVE_SEAMLESS; - boolean multipleMimeTypes = false; - String firstSampleMimeType = null; - for (int i = 0; i < trackIndices.length; i++) { - int trackIndex = trackIndices[i]; - String sampleMimeType = trackGroups[rendererIndex].get(groupIndex).getFormat(trackIndex) - .sampleMimeType; - if (handledTrackCount++ == 0) { - firstSampleMimeType = sampleMimeType; - } else { - multipleMimeTypes |= !Util.areEqual(firstSampleMimeType, sampleMimeType); - } - adaptiveSupport = Math.min(adaptiveSupport, formatSupport[rendererIndex][groupIndex][i] - & RendererCapabilities.ADAPTIVE_SUPPORT_MASK); - } - return multipleMimeTypes - ? Math.min(adaptiveSupport, mixedMimeTypeAdaptiveSupport[rendererIndex]) - : adaptiveSupport; - } - - /** - * Returns the {@link TrackGroup}s not associated with any renderer. - */ - public TrackGroupArray getUnassociatedTrackGroups() { - return unassociatedTrackGroups; - } - - } - } From 3bc3900dba5b9bfb5b6f9b7e147233631207f498 Mon Sep 17 00:00:00 2001 From: bachinger Date: Fri, 21 Jul 2017 04:48:03 -0700 Subject: [PATCH 0104/2472] Do not update queue when the queue did not actually change to avoid unnecessary updates are broadcasted to all clients. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=162728670 --- .../ext/mediasession/TimelineQueueNavigator.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java index 21bdaef0f3..76dbf40194 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java @@ -93,7 +93,12 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu @Override public void onCurrentWindowIndexChanged(Player player) { - publishFloatingQueueWindow(player); + if (activeQueueItemId == MediaSessionCompat.QueueItem.UNKNOWN_ID + || player.getCurrentTimeline().getWindowCount() > maxQueueSize) { + publishFloatingQueueWindow(player); + } else if (!player.getCurrentTimeline().isEmpty()) { + activeQueueItemId = player.getCurrentWindowIndex(); + } } @Override From 6f600a8fa5486c0177a7b85c92b435b96805ea2f Mon Sep 17 00:00:00 2001 From: bachinger Date: Fri, 21 Jul 2017 06:56:03 -0700 Subject: [PATCH 0105/2472] Take care playback preparer and queue navigator can not register overlapping playback actions. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=162736210 --- .../DefaultPlaybackController.java | 118 ++++++ .../mediasession/MediaSessionConnector.java | 354 +++++++++--------- .../mediasession/TimelineQueueNavigator.java | 41 +- 3 files changed, 330 insertions(+), 183 deletions(-) create mode 100644 extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/DefaultPlaybackController.java diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/DefaultPlaybackController.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/DefaultPlaybackController.java new file mode 100644 index 0000000000..231c1f1ea5 --- /dev/null +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/DefaultPlaybackController.java @@ -0,0 +1,118 @@ +/* + * 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.ext.mediasession; + +import android.support.v4.media.session.PlaybackStateCompat; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Player; + +/** + * A default implementation of the {@link MediaSessionConnector.PlaybackController}. You can safely + * override any method for instance to intercept calls for a given action. + */ +public class DefaultPlaybackController implements MediaSessionConnector.PlaybackController { + + private static final long BASE_ACTIONS = PlaybackStateCompat.ACTION_PLAY_PAUSE + | PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PAUSE + | PlaybackStateCompat.ACTION_STOP; + + protected final long fastForwardIncrementMs; + protected final long rewindIncrementMs; + + /** + * Creates a new {@link DefaultPlaybackController}. This is equivalent to calling + * {@code DefaultPlaybackController(15000L, 5000L)}. + */ + public DefaultPlaybackController() { + this(15000L, 5000L); + } + + /** + * Creates a new {@link DefaultPlaybackController} and sets the fast forward and rewind increments + * in milliseconds. + * + * @param fastForwardIncrementMs A positive value will cause the + * {@link PlaybackStateCompat#ACTION_FAST_FORWARD} playback action to be added. A zero or a + * negative value will cause it to be removed. + * @param rewindIncrementMs A positive value will cause the + * {@link PlaybackStateCompat#ACTION_REWIND} playback action to be added. A zero or a + * negative value will cause it to be removed. + */ + public DefaultPlaybackController(long fastForwardIncrementMs, long rewindIncrementMs) { + this.fastForwardIncrementMs = fastForwardIncrementMs; + this.rewindIncrementMs = rewindIncrementMs; + } + + @Override + public long getSupportedPlaybackActions(Player player) { + if (player == null || player.getCurrentTimeline().isEmpty()) { + return 0; + } + long actions = BASE_ACTIONS; + if (player.isCurrentWindowSeekable()) { + actions |= PlaybackStateCompat.ACTION_SEEK_TO; + } + if (fastForwardIncrementMs > 0) { + actions |= PlaybackStateCompat.ACTION_FAST_FORWARD; + } + if (rewindIncrementMs > 0) { + actions |= PlaybackStateCompat.ACTION_REWIND; + } + return actions; + } + + @Override + public void onPlay(Player player) { + player.setPlayWhenReady(true); + } + + @Override + public void onPause(Player player) { + player.setPlayWhenReady(false); + } + + @Override + public void onSeekTo(Player player, long position) { + long duration = player.getDuration(); + if (duration != C.TIME_UNSET) { + position = Math.min(position, duration); + } + player.seekTo(Math.max(position, 0)); + } + + @Override + public void onFastForward(Player player) { + if (fastForwardIncrementMs <= 0) { + return; + } + onSeekTo(player, player.getCurrentPosition() + fastForwardIncrementMs); + } + + @Override + public void onRewind(Player player) { + if (rewindIncrementMs <= 0) { + return; + } + onSeekTo(player, player.getCurrentPosition() - rewindIncrementMs); + } + + @Override + public void onStop(Player player) { + player.stop(); + } + +} diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java index d70d1bcaa9..a300acfffa 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java @@ -47,11 +47,11 @@ import java.util.Map; * Mediates between a {@link MediaSessionCompat} and an {@link Player} instance set with * {@link #setPlayer(Player, PlaybackPreparer, CustomActionProvider...)}. *

    - * By default the {@code MediaSessionConnector} listens for {@link #DEFAULT_PLAYBACK_ACTIONS} sent - * by a media controller and realizes these actions by calling appropriate ExoPlayer methods. - * Further, the state of ExoPlayer will be synced automatically with the {@link PlaybackStateCompat} - * of the media session to broadcast state transitions to clients. You can optionally extend this - * behaviour by providing various collaborators. + * The {@code MediaSessionConnector} listens for media actions sent by a media controller and + * realizes these actions by calling appropriate ExoPlayer methods. Further, the state of ExoPlayer + * will be synced automatically with the {@link PlaybackStateCompat} of the media session to + * broadcast state transitions to clients. You can optionally extend this behaviour by providing + * various collaborators. *

    * Media actions to initiate media playback ({@code PlaybackStateCompat#ACTION_PREPARE_*} and * {@code PlaybackStateCompat#ACTION_PLAY_*} need to be handled by a {@link PlaybackPreparer} which @@ -75,32 +75,8 @@ public final class MediaSessionConnector { ExoPlayerLibraryInfo.registerModule("goog.exo.mediasession"); } - /** - * Actions that are published to the media session by default - * ({@code PlaybackStateCompat.ACTION_PLAY_PAUSE}, {@code PlaybackStateCompat.ACTION_PLAY}, - * {@code PlaybackStateCompat.ACTION_PAUSE}, {@code PlaybackStateCompat.ACTION_FAST_FORWARD}, - * {@code PlaybackStateCompat.ACTION_REWIND}). - */ - public static final long DEFAULT_PLAYBACK_ACTIONS = PlaybackStateCompat.ACTION_PLAY_PAUSE - | PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PAUSE - | PlaybackStateCompat.ACTION_FAST_FORWARD | PlaybackStateCompat.ACTION_REWIND; - public static final String EXTRAS_PITCH = "EXO_PITCH"; - public static final long DEFAULT_FAST_FORWARD_MS = 15000; - public static final long DEFAULT_REWIND_MS = 5000; - - /** - * Interface of components taking responsibility of a set of media session playback actions - * ({@code PlaybackStateCompat#ACTION_*}). - */ - public interface PlaybackActionSupport { - /** - * Returns the bit mask of the playback actions supported by this component. - */ - long getSupportedPlaybackActions(); - } - /** * Interface to which media controller commands regarding preparing playback for a given media * clip are delegated to. @@ -108,12 +84,30 @@ public final class MediaSessionConnector { * Normally preparing playback includes preparing the player with a * {@link com.google.android.exoplayer2.source.MediaSource} and setting up the media session queue * with a corresponding list of queue items. - *

    - * The {@link PlaybackPreparer} handles the media actions {@code ACTION_PREPARE}, - * {@code ACTION_PREPARE_FROM_MEDIA_ID}, {@code ACTION_PREPARE_FROM_URI} and - * {@code ACTION_PREPARE_FROM_SEARCH}. */ - public interface PlaybackPreparer extends PlaybackActionSupport { + public interface PlaybackPreparer { + + long ACTIONS = PlaybackStateCompat.ACTION_PREPARE + | PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID + | PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH + | PlaybackStateCompat.ACTION_PREPARE_FROM_URI + | PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID + | PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH + | PlaybackStateCompat.ACTION_PLAY_FROM_URI; + + /** + * Returns the actions which are supported by the preparer. The supported actions must be a + * bitmask combined out of {@link PlaybackStateCompat#ACTION_PREPARE}, + * {@link PlaybackStateCompat#ACTION_PREPARE_FROM_MEDIA_ID}, + * {@link PlaybackStateCompat#ACTION_PREPARE_FROM_SEARCH}, + * {@link PlaybackStateCompat#ACTION_PREPARE_FROM_URI}, + * {@link PlaybackStateCompat#ACTION_PLAY_FROM_MEDIA_ID}, + * {@link PlaybackStateCompat#ACTION_PLAY_FROM_SEARCH} and + * {@link PlaybackStateCompat#ACTION_PLAY_FROM_URI}. + * + * @return The bitmask of the supported media actions. + */ + long getSupportedPrepareActions(); /** * See {@link MediaSessionCompat.Callback#onPrepare()}. */ @@ -137,10 +131,73 @@ public final class MediaSessionConnector { } /** - * Navigator to handle queue navigation commands and maintain the media session queue with + * Controller to handle playback actions. + */ + public interface PlaybackController { + + long ACTIONS = PlaybackStateCompat.ACTION_PLAY_PAUSE | PlaybackStateCompat.ACTION_PLAY + | PlaybackStateCompat.ACTION_PAUSE | PlaybackStateCompat.ACTION_SEEK_TO + | PlaybackStateCompat.ACTION_FAST_FORWARD | PlaybackStateCompat.ACTION_REWIND + | PlaybackStateCompat.ACTION_STOP; + + /** + * Returns the actions which are supported by the controller. The supported actions must be a + * bitmask combined out of {@link PlaybackStateCompat#ACTION_PLAY_PAUSE}, + * {@link PlaybackStateCompat#ACTION_PLAY}, {@link PlaybackStateCompat#ACTION_PAUSE}, + * {@link PlaybackStateCompat#ACTION_SEEK_TO}, {@link PlaybackStateCompat#ACTION_FAST_FORWARD}, + * {@link PlaybackStateCompat#ACTION_REWIND} and {@link PlaybackStateCompat#ACTION_STOP}. + * + * @param player The player. + * @return The bitmask of the supported media actions. + */ + long getSupportedPlaybackActions(@Nullable Player player); + /** + * See {@link MediaSessionCompat.Callback#onPlay()}. + */ + void onPlay(Player player); + /** + * See {@link MediaSessionCompat.Callback#onPause()}. + */ + void onPause(Player player); + /** + * See {@link MediaSessionCompat.Callback#onSeekTo(long)}. + */ + void onSeekTo(Player player, long position); + /** + * See {@link MediaSessionCompat.Callback#onFastForward()}. + */ + void onFastForward(Player player); + /** + * See {@link MediaSessionCompat.Callback#onRewind()}. + */ + void onRewind(Player player); + /** + * See {@link MediaSessionCompat.Callback#onStop()}. + */ + void onStop(Player player); + } + + /** + * Navigator to handle queue navigation actions and maintain the media session queue with * {#link MediaSessionCompat#setQueue(List)} to provide the active queue item to the connector. */ - public interface QueueNavigator extends PlaybackActionSupport { + public interface QueueNavigator { + + long ACTIONS = PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM + | PlaybackStateCompat.ACTION_SKIP_TO_NEXT | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS + | PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE_ENABLED; + + /** + * Returns the actions which are supported by the navigator. The supported actions must be a + * bitmask combined out of {@link PlaybackStateCompat#ACTION_SKIP_TO_QUEUE_ITEM}, + * {@link PlaybackStateCompat#ACTION_SKIP_TO_NEXT}, + * {@link PlaybackStateCompat#ACTION_SKIP_TO_PREVIOUS}, + * {@link PlaybackStateCompat#ACTION_SET_SHUFFLE_MODE_ENABLED}. + * + * @param player The {@link Player}. + * @return The bitmask of the supported media actions. + */ + long getSupportedQueueNavigatorActions(@Nullable Player player); /** * Called when the timeline of the player has changed. * @@ -186,7 +243,17 @@ public final class MediaSessionConnector { /** * Editor to manipulate the queue. */ - public interface QueueEditor extends PlaybackActionSupport { + public interface QueueEditor { + + long ACTIONS = PlaybackStateCompat.ACTION_SET_RATING; + + /** + * Returns {@link PlaybackStateCompat#ACTION_SET_RATING} or {@code 0}. The Media API does + * not declare action constants for adding and removing queue items. + * + * @param player The {@link Player}. + */ + long getSupportedQueueEditorActions(@Nullable Player player); /** * See {@link MediaSessionCompat.Callback#onAddQueueItem(MediaDescriptionCompat description)}. */ @@ -254,13 +321,11 @@ public final class MediaSessionConnector { private final boolean doMaintainMetadata; private final ExoPlayerEventListener exoPlayerEventListener; private final MediaSessionCallback mediaSessionCallback; + private final PlaybackController playbackController; private Player player; private CustomActionProvider[] customActionProviders; private int currentWindowIndex; - private long playbackActions; - private long fastForwardIncrementMs; - private long rewindIncrementMs; private Map customActionMap; private ErrorMessageProvider errorMessageProvider; private PlaybackPreparer playbackPreparer; @@ -270,7 +335,7 @@ public final class MediaSessionConnector { /** * Creates a {@code MediaSessionConnector}. This is equivalent to calling - * {@code #MediaSessionConnector(mediaSession, true)}. + * {@code #MediaSessionConnector(mediaSession, new DefaultPlaybackController)}. *

    * Constructing the {@link MediaSessionConnector} needs to be done on the same thread as * constructing the player instance. @@ -278,7 +343,22 @@ public final class MediaSessionConnector { * @param mediaSession The {@link MediaSessionCompat} to connect to. */ public MediaSessionConnector(MediaSessionCompat mediaSession) { - this(mediaSession, true); + this(mediaSession, new DefaultPlaybackController()); + } + + /** + * Creates a {@code MediaSessionConnector}. This is equivalent to calling + * {@code #MediaSessionConnector(mediaSession, playbackController, true)}. + *

    + * Constructing the {@link MediaSessionConnector} needs to be done on the same thread as + * constructing the player instance. + * + * @param mediaSession The {@link MediaSessionCompat} to connect to. + * @param playbackController The {@link PlaybackController}. + */ + public MediaSessionConnector(MediaSessionCompat mediaSession, + PlaybackController playbackController) { + this(mediaSession, playbackController, true); } /** @@ -292,11 +372,14 @@ public final class MediaSessionConnector { * constructing the player instance. * * @param mediaSession The {@link MediaSessionCompat} to connect to. + * @param playbackController The {@link PlaybackController}. * @param doMaintainMetadata Sets whether the connector should maintain the metadata of the * session. */ - public MediaSessionConnector(MediaSessionCompat mediaSession, boolean doMaintainMetadata) { + public MediaSessionConnector(MediaSessionCompat mediaSession, + PlaybackController playbackController, boolean doMaintainMetadata) { this.mediaSession = mediaSession; + this.playbackController = playbackController; this.handler = new Handler(Looper.myLooper() != null ? Looper.myLooper() : Looper.getMainLooper()); this.doMaintainMetadata = doMaintainMetadata; @@ -305,10 +388,7 @@ public final class MediaSessionConnector { mediaController = mediaSession.getController(); mediaSessionCallback = new MediaSessionCallback(); exoPlayerEventListener = new ExoPlayerEventListener(); - playbackActions = DEFAULT_PLAYBACK_ACTIONS; customActionMap = Collections.emptyMap(); - fastForwardIncrementMs = DEFAULT_FAST_FORWARD_MS; - rewindIncrementMs = DEFAULT_REWIND_MS; } /** @@ -343,68 +423,6 @@ public final class MediaSessionConnector { updateMediaSessionMetadata(); } - /** - * Sets the fast forward increment in milliseconds. A positive value will cause the - * {@link PlaybackStateCompat#ACTION_FAST_FORWARD} playback action to be added. A zero or a - * negative value will cause it to be removed. - * - * @param fastForwardIncrementMs The fast forward increment in milliseconds. - */ - public void setFastForwardIncrementMs(long fastForwardIncrementMs) { - this.fastForwardIncrementMs = fastForwardIncrementMs; - if (fastForwardIncrementMs > 0) { - addPlaybackActions(PlaybackStateCompat.ACTION_FAST_FORWARD); - } else { - removePlaybackActions(PlaybackStateCompat.ACTION_FAST_FORWARD); - } - } - - /** - * Sets the rewind increment in milliseconds. A positive value will cause the - * {@link PlaybackStateCompat#ACTION_REWIND} playback action to be added. A zero or a - * negative value will cause it to be removed. - * - * @param rewindIncrementMs The rewind increment in milliseconds. - */ - public void setRewindIncrementMs(long rewindIncrementMs) { - this.rewindIncrementMs = rewindIncrementMs; - if (rewindIncrementMs > 0) { - addPlaybackActions(PlaybackStateCompat.ACTION_REWIND); - } else { - removePlaybackActions(PlaybackStateCompat.ACTION_REWIND); - } - } - - /** - * Adds playback actions. The playback actions that are enabled by default are those in - * {@link MediaSessionConnector#DEFAULT_PLAYBACK_ACTIONS}. See {@link PlaybackStateCompat} for - * available playback action constants. - * - * @param playbackActions The playback actions to add. - */ - public void addPlaybackActions(long playbackActions) { - this.playbackActions |= playbackActions; - } - - /** - * Removes playback actions. The playback actions that are enabled by default are those in - * {@link MediaSessionConnector#DEFAULT_PLAYBACK_ACTIONS}. - * - * @param playbackActions The playback actions to remove. - */ - public void removePlaybackActions(long playbackActions) { - this.playbackActions &= ~playbackActions; - } - - /** - * Sets the playback actions. The playback actions that are enabled by default are overridden. - * - * @param playbackActions The playback actions to publish. - */ - public void setPlaybackActions(long playbackActions) { - this.playbackActions = playbackActions; - } - /** * Sets the optional {@link ErrorMessageProvider}. * @@ -415,20 +433,14 @@ public final class MediaSessionConnector { } /** - * Sets the {@link QueueNavigator} to handle queue navigation for the media actions - * {@code ACTION_SKIP_TO_NEXT}, {@code ACTION_SKIP_TO_PREVIOUS}, - * {@code ACTION_SKIP_TO_QUEUE_ITEM} and {@code ACTION_SET_SHUFFLE_MODE_ENABLED}. + * Sets the {@link QueueNavigator} to handle queue navigation actions {@code ACTION_SKIP_TO_NEXT}, + * {@code ACTION_SKIP_TO_PREVIOUS}, {@code ACTION_SKIP_TO_QUEUE_ITEM} and + * {@code ACTION_SET_SHUFFLE_MODE_ENABLED}. * * @param queueNavigator The navigator to handle queue navigation. */ public void setQueueNavigator(QueueNavigator queueNavigator) { - if (this.queueNavigator != null) { - removePlaybackActions(this.queueNavigator.getSupportedPlaybackActions()); - } this.queueNavigator = queueNavigator; - if (queueNavigator != null) { - addPlaybackActions(queueNavigator.getSupportedPlaybackActions()); - } } /** @@ -437,29 +449,17 @@ public final class MediaSessionConnector { * @param queueEditor The editor to handle queue manipulation actions. */ public void setQueueEditor(QueueEditor queueEditor) { - if (this.queueEditor != null) { - removePlaybackActions(this.queueEditor.getSupportedPlaybackActions()); - } this.queueEditor = queueEditor; - if (queueEditor != null) { - addPlaybackActions(queueEditor.getSupportedPlaybackActions()); - } } private void setPlaybackPreparer(PlaybackPreparer playbackPreparer) { - if (this.playbackPreparer != null) { - removePlaybackActions(this.playbackPreparer.getSupportedPlaybackActions()); - } this.playbackPreparer = playbackPreparer; - if (playbackPreparer != null) { - addPlaybackActions(playbackPreparer.getSupportedPlaybackActions()); - } } private void updateMediaSessionPlaybackState() { PlaybackStateCompat.Builder builder = new PlaybackStateCompat.Builder(); if (player == null) { - builder.setActions(0).setState(PlaybackStateCompat.STATE_NONE, 0, 0, 0); + builder.setActions(buildPlaybackActions()).setState(PlaybackStateCompat.STATE_NONE, 0, 0, 0); mediaSession.setPlaybackState(builder.build()); return; } @@ -487,10 +487,9 @@ public final class MediaSessionConnector { } long activeQueueItemId = queueNavigator != null ? queueNavigator.getActiveQueueItemId(player) : MediaSessionCompat.QueueItem.UNKNOWN_ID; - updatePlaybackActions(activeQueueItemId); Bundle extras = new Bundle(); extras.putFloat(EXTRAS_PITCH, player.getPlaybackParameters().pitch); - builder.setActions(playbackActions) + builder.setActions(buildPlaybackActions()) .setActiveQueueItemId(activeQueueItemId) .setBufferedPosition(player.getBufferedPosition()) .setState(sessionPlaybackState, player.getCurrentPosition(), @@ -499,24 +498,23 @@ public final class MediaSessionConnector { mediaSession.setPlaybackState(builder.build()); } - private void updatePlaybackActions(long activeQueueItemId) { - List queue = mediaController.getQueue(); - if (queue == null || queue.size() < 2) { - removePlaybackActions(PlaybackStateCompat.ACTION_SKIP_TO_NEXT - | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS); - } else if (player.getRepeatMode() != Player.REPEAT_MODE_OFF) { - addPlaybackActions(PlaybackStateCompat.ACTION_SKIP_TO_NEXT - | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS); - } else if (activeQueueItemId == queue.get(0).getQueueId()) { - removePlaybackActions(PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS); - addPlaybackActions(PlaybackStateCompat.ACTION_SKIP_TO_NEXT); - } else if (activeQueueItemId == queue.get(queue.size() - 1).getQueueId()) { - removePlaybackActions(PlaybackStateCompat.ACTION_SKIP_TO_NEXT); - addPlaybackActions(PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS); - } else { - addPlaybackActions(PlaybackStateCompat.ACTION_SKIP_TO_NEXT - | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS); + private long buildPlaybackActions() { + long actions = 0; + if (playbackController != null) { + actions |= (PlaybackController.ACTIONS & playbackController + .getSupportedPlaybackActions(player)); } + if (playbackPreparer != null) { + actions |= (PlaybackPreparer.ACTIONS & playbackPreparer.getSupportedPrepareActions()); + } + if (queueNavigator != null) { + actions |= (QueueNavigator.ACTIONS & queueNavigator.getSupportedQueueNavigatorActions( + player)); + } + if (queueEditor != null) { + actions |= (QueueEditor.ACTIONS & queueEditor.getSupportedQueueEditorActions(player)); + } + return actions; } private void updateMediaSessionMetadata() { @@ -584,16 +582,24 @@ public final class MediaSessionConnector { } } - private boolean isActionPublished(long action) { - return (playbackActions & action) != 0; + private boolean canDispatchToPlaybackPreparer(long action) { + return playbackPreparer != null && (playbackPreparer.getSupportedPrepareActions() + & PlaybackPreparer.ACTIONS & action) != 0; + } + + private boolean canDispatchToPlaybackController(long action) { + return playbackController != null && (playbackController.getSupportedPlaybackActions(player) + & PlaybackController.ACTIONS & action) != 0; } private boolean canDispatchToQueueNavigator(long action) { - return queueNavigator != null && isActionPublished(action); + return queueNavigator != null && (queueNavigator.getSupportedQueueNavigatorActions(player) + & QueueNavigator.ACTIONS & action) != 0; } - private boolean canDispatchToPlaybackPreparer(long action) { - return playbackPreparer != null && isActionPublished(action); + private boolean canDispatchToQueueEditor(long action) { + return queueEditor != null && (queueEditor.getSupportedQueueEditorActions(player) + & QueueEditor.ACTIONS & action) != 0; } private class ExoPlayerEventListener implements Player.EventListener { @@ -658,37 +664,44 @@ public final class MediaSessionConnector { @Override public void onPlay() { - player.setPlayWhenReady(true); + if (canDispatchToPlaybackController(PlaybackStateCompat.ACTION_PLAY)) { + playbackController.onPlay(player); + } } @Override public void onPause() { - player.setPlayWhenReady(false); + if (canDispatchToPlaybackController(PlaybackStateCompat.ACTION_PAUSE)) { + playbackController.onPause(player); + } } @Override public void onSeekTo(long position) { - long duration = player.getDuration(); - if (duration != C.TIME_UNSET) { - position = Math.min(position, duration); + if (canDispatchToPlaybackController(PlaybackStateCompat.ACTION_SEEK_TO)) { + playbackController.onSeekTo(player, position); } - player.seekTo(Math.max(position, 0)); } @Override public void onFastForward() { - if (fastForwardIncrementMs <= 0) { - return; + if (canDispatchToPlaybackController(PlaybackStateCompat.ACTION_FAST_FORWARD)) { + playbackController.onFastForward(player); } - onSeekTo(player.getCurrentPosition() + fastForwardIncrementMs); } @Override public void onRewind() { - if (rewindIncrementMs <= 0) { - return; + if (canDispatchToPlaybackController(PlaybackStateCompat.ACTION_REWIND)) { + playbackController.onRewind(player); + } + } + + @Override + public void onStop() { + if (canDispatchToPlaybackController(PlaybackStateCompat.ACTION_STOP)) { + playbackController.onStop(player); } - onSeekTo(player.getCurrentPosition() - rewindIncrementMs); } @Override @@ -712,13 +725,6 @@ public final class MediaSessionConnector { } } - @Override - public void onStop() { - if (isActionPublished(PlaybackStateCompat.ACTION_STOP)) { - player.stop(); - } - } - @Override public void onSetRepeatMode(int repeatMode) { // implemented as custom action @@ -840,7 +846,7 @@ public final class MediaSessionConnector { @Override public void onSetRating(RatingCompat rating) { - if (queueEditor != null && isActionPublished(PlaybackStateCompat.ACTION_SET_RATING)) { + if (canDispatchToQueueEditor(PlaybackStateCompat.ACTION_SET_RATING)) { queueEditor.onSetRating(player, rating); } } diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java index 76dbf40194..60aa5a5ba0 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java @@ -39,7 +39,7 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu public static final int DEFAULT_MAX_QUEUE_SIZE = 10; private final MediaSessionCompat mediaSession; - private final int maxQueueSize; + protected final int maxQueueSize; private long activeQueueItemId; @@ -80,19 +80,42 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu */ public abstract MediaDescriptionCompat getMediaDescription(int windowIndex); + /** + * Supports the following media actions: {@code PlaybackStateCompat.ACTION_SKIP_TO_NEXT | + * PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS | PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM}. + * + * @return The bit mask of the supported media actions. + */ @Override - public long getSupportedPlaybackActions() { - return PlaybackStateCompat.ACTION_SKIP_TO_NEXT | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS - | PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM; + public long getSupportedQueueNavigatorActions(Player player) { + if (player == null || player.getCurrentTimeline().getWindowCount() < 2) { + return 0; + } + if (player.getRepeatMode() != Player.REPEAT_MODE_OFF) { + return PlaybackStateCompat.ACTION_SKIP_TO_NEXT | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS + | PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM; + } + + int currentWindowIndex = player.getCurrentWindowIndex(); + long actions; + if (currentWindowIndex == 0) { + actions = PlaybackStateCompat.ACTION_SKIP_TO_NEXT; + } else if (currentWindowIndex == player.getCurrentTimeline().getWindowCount() - 1) { + actions = PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS; + } else { + actions = PlaybackStateCompat.ACTION_SKIP_TO_NEXT + | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS; + } + return actions | PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM; } @Override - public void onTimelineChanged(Player player) { + public final void onTimelineChanged(Player player) { publishFloatingQueueWindow(player); } @Override - public void onCurrentWindowIndexChanged(Player player) { + public final void onCurrentWindowIndexChanged(Player player) { if (activeQueueItemId == MediaSessionCompat.QueueItem.UNKNOWN_ID || player.getCurrentTimeline().getWindowCount() > maxQueueSize) { publishFloatingQueueWindow(player); @@ -107,7 +130,7 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu } @Override - public final void onSkipToPrevious(Player player) { + public void onSkipToPrevious(Player player) { Timeline timeline = player.getCurrentTimeline(); if (timeline.isEmpty()) { return; @@ -123,7 +146,7 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu } @Override - public final void onSkipToQueueItem(Player player, long id) { + public void onSkipToQueueItem(Player player, long id) { Timeline timeline = player.getCurrentTimeline(); if (timeline.isEmpty()) { return; @@ -135,7 +158,7 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu } @Override - public final void onSkipToNext(Player player) { + public void onSkipToNext(Player player) { Timeline timeline = player.getCurrentTimeline(); if (timeline.isEmpty()) { return; From 9bb8b240d20a04ebd823c35a273ebc0f490f27b2 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 21 Jul 2017 07:51:27 -0700 Subject: [PATCH 0106/2472] Improve Player/ExoPlayer Javadoc ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=162740451 --- .../google/android/exoplayer2/ExoPlayer.java | 34 +++++++++++-------- .../com/google/android/exoplayer2/Player.java | 33 +++++++++++++----- 2 files changed, 43 insertions(+), 24 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java index e0f3290088..b096b5ae12 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java @@ -18,8 +18,11 @@ package com.google.android.exoplayer2; import android.os.Looper; import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer; import com.google.android.exoplayer2.metadata.MetadataRenderer; +import com.google.android.exoplayer2.source.ClippingMediaSource; import com.google.android.exoplayer2.source.ConcatenatingMediaSource; +import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource; import com.google.android.exoplayer2.source.ExtractorMediaSource; +import com.google.android.exoplayer2.source.LoopingMediaSource; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MergingMediaSource; import com.google.android.exoplayer2.source.SingleSampleMediaSource; @@ -30,11 +33,10 @@ import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; /** - * An extensible media player exposing traditional high-level media player functionality, such as - * the ability to buffer media, play, pause and seek. Instances can be obtained from + * An extensible media player that plays {@link MediaSource}s. Instances can be obtained from * {@link ExoPlayerFactory}. * - *

    Player composition

    + *

    Player components

    *

    ExoPlayer is designed to make few assumptions about (and hence impose few restrictions on) the * type of the media being played, how and where it is stored, and how it is rendered. Rather than * implementing the loading and rendering of media directly, ExoPlayer implementations delegate this @@ -42,18 +44,20 @@ import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; * Components common to all ExoPlayer implementations are: *

      *
    • 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 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.
    • + * which the loaded media can be read. A MediaSource is injected via {@link #prepare(MediaSource)} + * at the start of playback. The library modules provide default implementations for regular media + * files ({@link ExtractorMediaSource}), DASH (DashMediaSource), SmoothStreaming (SsMediaSource) + * and HLS (HlsMediaSource), an implementation for loading single media samples + * ({@link SingleSampleMediaSource}) that's most often used for side-loaded subtitle files, and + * implementations for building more complex MediaSources from simpler ones + * ({@link MergingMediaSource}, {@link ConcatenatingMediaSource}, + * {@link DynamicConcatenatingMediaSource}, {@link LoopingMediaSource} and + * {@link ClippingMediaSource}). *
    • {@link Renderer}s that render individual components of the media. The library * provides default implementations for common media types ({@link MediaCodecVideoRenderer}, * {@link MediaCodecAudioRenderer}, {@link TextRenderer} and {@link MetadataRenderer}). A Renderer - * consumes media of its corresponding type from the MediaSource being played. Renderers are - * injected when the player is created.
    • + * consumes media from the MediaSource being played. Renderers are injected when the player is + * created. *
    • A {@link TrackSelector} that selects tracks provided by the MediaSource to be * consumed by each of the available Renderers. The library provides a default implementation * ({@link DefaultTrackSelector}) suitable for most use cases. A TrackSelector is injected when @@ -66,14 +70,14 @@ import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; *

      An ExoPlayer can be built using the default components provided by the library, but may also * be built using custom implementations if non-standard behaviors are required. For example a * custom LoadControl could be injected to change the player's buffering strategy, or a custom - * Renderer could be injected to use a video codec not supported natively by Android. + * Renderer could be injected to add support for a video codec not supported natively by Android. * *

      The concept of injecting components that implement pieces of player functionality is present * throughout the library. The default component implementations listed above delegate work to * further injected components. This allows many sub-components to be individually replaced with * custom implementations. For example the default MediaSource implementations require one or more * {@link DataSource} factories to be injected via their constructors. By providing a custom factory - * it's possible to load data from a non-standard source or through a different network stack. + * it's possible to load data from a non-standard source, or through a different network stack. * *

      Threading model

      *

      The figure below shows ExoPlayer's threading model.

      @@ -99,7 +103,7 @@ import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; * thread via a second message queue. The application thread consumes messages from the queue, * updating the application visible state and calling corresponding listener methods.
    • *
    • Injected player components may use additional background threads. For example a MediaSource - * may use a background thread to load data. These are implementation specific.
    • + * may use background threads to load data. These are implementation specific. *
    */ public interface ExoPlayer extends Player { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Player.java b/library/core/src/main/java/com/google/android/exoplayer2/Player.java index 8ca6c20d7a..d2480c5b3a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Player.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Player.java @@ -24,7 +24,23 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; /** - * An interface for media players. + * A media player interface defining traditional high-level functionality, such as the ability to + * play, pause, seek and query properties of the currently playing media. + *

    + * Some important properties of media players that implement this interface are: + *

      + *
    • They can provide a {@link Timeline} representing the structure of the media being played, + * which can be obtained by calling {@link #getCurrentTimeline()}.
    • + *
    • They can provide a {@link TrackGroupArray} defining the currently available tracks, + * which can be obtained by calling {@link #getCurrentTrackGroups()}.
    • + *
    • They contain a number of renderers, each of which is able to render tracks of a single + * type (e.g. audio, video or text). The number of renderers and their respective track types + * can be obtained by calling {@link #getRendererCount()} and {@link #getRendererType(int)}. + *
    • + *
    • They can provide a {@link TrackSelectionArray} defining which of the currently available + * tracks are selected to be rendered by each renderer. This can be obtained by calling + * {@link #getCurrentTrackSelections()}}.
    • + *
    */ public interface Player { @@ -50,8 +66,8 @@ public interface Player { * Called when the available or selected tracks change. * * @param trackGroups The available tracks. Never null, but may be of length zero. - * @param trackSelections The track selections for each {@link Renderer}. Never null and always - * of length {@link #getRendererCount()}, but may contain null elements. + * @param trackSelections The track selections for each renderer. Never null and always of + * length {@link #getRendererCount()}, but may contain null elements. */ void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections); @@ -112,18 +128,17 @@ public interface Player { } /** - * The player does not have a source to play, so it is neither buffering nor ready to play. + * The player does not have any media to play. */ int STATE_IDLE = 1; /** - * The player not able to immediately play from the current position. The cause is - * {@link Renderer} specific, but this state typically occurs when more data needs to be - * loaded to be ready to play, or more data needs to be buffered for playback to resume. + * The player is not able to immediately play from its current position. This state typically + * occurs when more data needs to be loaded. */ int STATE_BUFFERING = 2; /** - * The player is able to immediately play from the current position. The player will be playing if - * {@link #getPlayWhenReady()} returns true, and paused otherwise. + * The player is able to immediately play from its current position. The player will be playing if + * {@link #getPlayWhenReady()} is true, and paused otherwise. */ int STATE_READY = 3; /** From 3eb85446a2ea4823ed08249ab626bbf34082397e Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 21 Jul 2017 07:52:07 -0700 Subject: [PATCH 0107/2472] Fully document DefaultTrackSelector ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=162740498 --- .../trackselection/DefaultTrackSelector.java | 387 ++++++++++++------ 1 file changed, 256 insertions(+), 131 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index 2407a2cca9..fe2b920933 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -33,35 +33,115 @@ import java.util.List; import java.util.concurrent.atomic.AtomicReference; /** - * A {@link MappingTrackSelector} suitable for most use cases. + * A default {@link TrackSelector} suitable for most use cases. + * + *

    Constraint based track selection

    + * Whilst this selector supports setting specific track overrides, the recommended way of + * changing which tracks are selected is by setting {@link Parameters} that constrain the track + * selection process. For example an instance can specify a preferred language for + * the audio track, and impose constraints on the maximum video resolution that should be selected + * for adaptive playbacks. Modifying the parameters is simple: + *
    + * {@code
    + * Parameters currentParameters = trackSelector.getParameters();
    + * // Generate new parameters to prefer German audio and impose a maximum video size constraint.
    + * Parameters newParameters = currentParameters
    + *     .withPreferredAudioLanguage("de")
    + *     .withMaxVideoSize(1024, 768);
    + * // Set the new parameters on the selector.
    + * trackSelector.setParameters(newParameters);}
    + * 
    + * There are several benefits to using constraint based track selection instead of specific track + * overrides: + *
      + *
    • You can specify constraints before knowing what tracks the media provides. This can + * simplify track selection code (e.g. you don't have to listen for changes in the available + * tracks before configuring the selector).
    • + *
    • Constraints can be applied consistently across all periods in a complex piece of media, + * even if those periods contain different tracks. In contrast, a specific track override is only + * applied to periods whose tracks match those for which the override was set.
    • + *
    + * + *

    Track overrides, disabling renderers and tunneling

    + * This selector extends {@link MappingTrackSelector}, and so inherits its support for setting + * specific track overrides, disabling renderers and configuring tunneled media playback. See + * {@link MappingTrackSelector} for details. + * + *

    Extending this class

    + * This class is designed to be extensible by developers who wish to customize its behavior but do + * not wish to implement their own {@link MappingTrackSelector} or {@link TrackSelector} from + * scratch. */ public class DefaultTrackSelector extends MappingTrackSelector { /** - * Holder for available configurations for the {@link DefaultTrackSelector}. + * Constraint parameters for {@link DefaultTrackSelector}. */ public static final class Parameters { - // Audio. + // Audio + /** + * The preferred language for audio, as well as for forced text tracks as defined by RFC 5646. + * {@code null} selects the default track, or the first track if there's no default. + */ public final String preferredAudioLanguage; - // Text. + // Text + /** + * The preferred language for text tracks as defined by RFC 5646. {@code null} selects the + * default track if there is one, or no track otherwise. + */ public final String preferredTextLanguage; - // Video. - public final boolean allowMixedMimeAdaptiveness; - public final boolean allowNonSeamlessAdaptiveness; + // Video + /** + * Maximum allowed video width. + */ public final int maxVideoWidth; + /** + * Maximum allowed video height. + */ public final int maxVideoHeight; + /** + * Maximum video bitrate. + */ public final int maxVideoBitrate; + /** + * Whether to exceed video constraints when no selection can be made otherwise. + */ public final boolean exceedVideoConstraintsIfNecessary; - public final boolean exceedRendererCapabilitiesIfNecessary; + /** + * Viewport width in pixels. Constrains video tracks selections for adaptive playbacks so that + * only tracks suitable for the viewport are selected. + */ public final int viewportWidth; + /** + * Viewport height in pixels. Constrains video tracks selections for adaptive playbacks so that + * only tracks suitable for the viewport are selected. + */ public final int viewportHeight; - public final boolean orientationMayChange; + /** + * Whether the viewport orientation may change during playback. Constrains video tracks + * selections for adaptive playbacks so that only tracks suitable for the viewport are selected. + */ + public final boolean viewportOrientationMayChange; + + // General + /** + * Whether to allow adaptive selections containing mixed mime types. + */ + public final boolean allowMixedMimeAdaptiveness; + /** + * Whether to allow adaptive selections where adaptation may not be completely seamless. + */ + public final boolean allowNonSeamlessAdaptiveness; + /** + * Whether to exceed renderer capabilities when no selection can be made otherwise. + */ + public final boolean exceedRendererCapabilitiesIfNecessary; /** - * Constructor with default selection parameters: + * Default parameters. The default values are: *
      *
    • No preferred audio language is set.
    • *
    • No preferred text language is set.
    • @@ -71,7 +151,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { *
    • No max video bitrate.
    • *
    • Video constraints are exceeded if no supported selection can be made otherwise.
    • *
    • Renderer capabilities are exceeded if no supported selection can be made.
    • - *
    • No viewport width/height constraints are set.
    • + *
    • No viewport constraints are set.
    • *
    */ public Parameters() { @@ -80,29 +160,24 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * @param preferredAudioLanguage The preferred language for audio, as well as for forced text - * tracks as defined by RFC 5646. {@code null} to select the default track, or first track - * if there's no default. - * @param preferredTextLanguage The preferred language for text tracks as defined by RFC 5646. - * {@code null} to select the default track, or first track if there's no default. - * @param allowMixedMimeAdaptiveness Whether to allow selections to contain mixed mime types. - * @param allowNonSeamlessAdaptiveness Whether non-seamless adaptation is allowed. - * @param maxVideoWidth Maximum allowed video width. - * @param maxVideoHeight Maximum allowed video height. - * @param maxVideoBitrate Maximum allowed video bitrate. - * @param exceedVideoConstraintsIfNecessary Whether to exceed video constraints when no - * selection can be made otherwise. - * @param exceedRendererCapabilitiesIfNecessary Whether to exceed renderer capabilities when no - * selection can be made otherwise. - * @param viewportWidth Viewport width in pixels. - * @param viewportHeight Viewport height in pixels. - * @param orientationMayChange Whether orientation may change during playback. + * @param preferredAudioLanguage See {@link #preferredAudioLanguage} + * @param preferredTextLanguage See {@link #preferredTextLanguage} + * @param allowMixedMimeAdaptiveness See {@link #allowMixedMimeAdaptiveness} + * @param allowNonSeamlessAdaptiveness See {@link #allowNonSeamlessAdaptiveness} + * @param maxVideoWidth See {@link #maxVideoWidth} + * @param maxVideoHeight See {@link #maxVideoHeight} + * @param maxVideoBitrate See {@link #maxVideoBitrate} + * @param exceedVideoConstraintsIfNecessary See {@link #exceedVideoConstraintsIfNecessary} + * @param exceedRendererCapabilitiesIfNecessary See {@link #preferredTextLanguage} + * @param viewportWidth See {@link #viewportWidth} + * @param viewportHeight See {@link #viewportHeight} + * @param viewportOrientationMayChange See {@link #viewportOrientationMayChange} */ public Parameters(String preferredAudioLanguage, String preferredTextLanguage, boolean allowMixedMimeAdaptiveness, boolean allowNonSeamlessAdaptiveness, int maxVideoWidth, int maxVideoHeight, int maxVideoBitrate, boolean exceedVideoConstraintsIfNecessary, boolean exceedRendererCapabilitiesIfNecessary, - int viewportWidth, int viewportHeight, boolean orientationMayChange) { + int viewportWidth, int viewportHeight, boolean viewportOrientationMayChange) { this.preferredAudioLanguage = preferredAudioLanguage; this.preferredTextLanguage = preferredTextLanguage; this.allowMixedMimeAdaptiveness = allowMixedMimeAdaptiveness; @@ -114,17 +189,15 @@ public class DefaultTrackSelector extends MappingTrackSelector { this.exceedRendererCapabilitiesIfNecessary = exceedRendererCapabilitiesIfNecessary; this.viewportWidth = viewportWidth; this.viewportHeight = viewportHeight; - this.orientationMayChange = orientationMayChange; + this.viewportOrientationMayChange = viewportOrientationMayChange; } /** - * Returns a {@link Parameters} instance with the provided preferred language for audio and - * forced text tracks. + * Returns an instance with the provided preferred language for audio and forced text tracks. * * @param preferredAudioLanguage The preferred language as defined by RFC 5646. {@code null} to * select the default track, or first track if there's no default. - * @return A {@link Parameters} instance with the provided preferred language for audio and - * forced text tracks. + * @return An instance with the provided preferred language for audio and forced text tracks. */ public Parameters withPreferredAudioLanguage(String preferredAudioLanguage) { preferredAudioLanguage = Util.normalizeLanguageCode(preferredAudioLanguage); @@ -134,15 +207,15 @@ public class DefaultTrackSelector extends MappingTrackSelector { return new Parameters(preferredAudioLanguage, preferredTextLanguage, allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, orientationMayChange); + viewportWidth, viewportHeight, viewportOrientationMayChange); } /** - * Returns a {@link Parameters} instance with the provided preferred language for text tracks. + * Returns an instance with the provided preferred language for text tracks. * * @param preferredTextLanguage The preferred language as defined by RFC 5646. {@code null} to * select the default track, or no track if there's no default. - * @return A {@link Parameters} instance with the provided preferred language for text tracks. + * @return An instance with the provided preferred language for text tracks. */ public Parameters withPreferredTextLanguage(String preferredTextLanguage) { preferredTextLanguage = Util.normalizeLanguageCode(preferredTextLanguage); @@ -152,14 +225,14 @@ public class DefaultTrackSelector extends MappingTrackSelector { return new Parameters(preferredAudioLanguage, preferredTextLanguage, allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, orientationMayChange); + viewportWidth, viewportHeight, viewportOrientationMayChange); } /** - * Returns a {@link Parameters} instance with the provided mixed mime adaptiveness allowance. + * Returns an instance with the provided mixed mime adaptiveness allowance. * * @param allowMixedMimeAdaptiveness Whether to allow selections to contain mixed mime types. - * @return A {@link Parameters} instance with the provided mixed mime adaptiveness allowance. + * @return An instance with the provided mixed mime adaptiveness allowance. */ public Parameters withAllowMixedMimeAdaptiveness(boolean allowMixedMimeAdaptiveness) { if (allowMixedMimeAdaptiveness == this.allowMixedMimeAdaptiveness) { @@ -168,14 +241,14 @@ public class DefaultTrackSelector extends MappingTrackSelector { return new Parameters(preferredAudioLanguage, preferredTextLanguage, allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, orientationMayChange); + viewportWidth, viewportHeight, viewportOrientationMayChange); } /** - * Returns a {@link Parameters} instance with the provided seamless adaptiveness allowance. + * Returns an instance with the provided seamless adaptiveness allowance. * * @param allowNonSeamlessAdaptiveness Whether non-seamless adaptation is allowed. - * @return A {@link Parameters} instance with the provided seamless adaptiveness allowance. + * @return An instance with the provided seamless adaptiveness allowance. */ public Parameters withAllowNonSeamlessAdaptiveness(boolean allowNonSeamlessAdaptiveness) { if (allowNonSeamlessAdaptiveness == this.allowNonSeamlessAdaptiveness) { @@ -184,15 +257,15 @@ public class DefaultTrackSelector extends MappingTrackSelector { return new Parameters(preferredAudioLanguage, preferredTextLanguage, allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, orientationMayChange); + viewportWidth, viewportHeight, viewportOrientationMayChange); } /** - * Returns a {@link Parameters} instance with the provided max video size. + * Returns an instance with the provided max video size. * * @param maxVideoWidth The max video width. * @param maxVideoHeight The max video width. - * @return A {@link Parameters} instance with the provided max video size. + * @return An instance with the provided max video size. */ public Parameters withMaxVideoSize(int maxVideoWidth, int maxVideoHeight) { if (maxVideoWidth == this.maxVideoWidth && maxVideoHeight == this.maxVideoHeight) { @@ -201,14 +274,14 @@ public class DefaultTrackSelector extends MappingTrackSelector { return new Parameters(preferredAudioLanguage, preferredTextLanguage, allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, orientationMayChange); + viewportWidth, viewportHeight, viewportOrientationMayChange); } /** - * Returns a {@link Parameters} instance with the provided max video bitrate. + * Returns an instance with the provided max video bitrate. * * @param maxVideoBitrate The max video bitrate. - * @return A {@link Parameters} instance with the provided max video bitrate. + * @return An instance with the provided max video bitrate. */ public Parameters withMaxVideoBitrate(int maxVideoBitrate) { if (maxVideoBitrate == this.maxVideoBitrate) { @@ -217,13 +290,13 @@ public class DefaultTrackSelector extends MappingTrackSelector { return new Parameters(preferredAudioLanguage, preferredTextLanguage, allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, orientationMayChange); + viewportWidth, viewportHeight, viewportOrientationMayChange); } /** * Equivalent to {@code withMaxVideoSize(1279, 719)}. * - * @return A {@link Parameters} instance with maximum standard definition as maximum video size. + * @return An instance with maximum standard definition as maximum video size. */ public Parameters withMaxVideoSizeSd() { return withMaxVideoSize(1279, 719); @@ -232,20 +305,18 @@ public class DefaultTrackSelector extends MappingTrackSelector { /** * Equivalent to {@code withMaxVideoSize(Integer.MAX_VALUE, Integer.MAX_VALUE)}. * - * @return A {@link Parameters} instance without video size constraints. + * @return An instance without video size constraints. */ public Parameters withoutVideoSizeConstraints() { return withMaxVideoSize(Integer.MAX_VALUE, Integer.MAX_VALUE); } /** - * Returns a {@link Parameters} instance with the provided - * {@code exceedVideoConstraintsIfNecessary} value. + * Returns an instance with the provided {@code exceedVideoConstraintsIfNecessary} value. * * @param exceedVideoConstraintsIfNecessary Whether to exceed video constraints when no * selection can be made otherwise. - * @return A {@link Parameters} instance with the provided - * {@code exceedVideoConstraintsIfNecessary} value. + * @return An instance with the provided {@code exceedVideoConstraintsIfNecessary} value. */ public Parameters withExceedVideoConstraintsIfNecessary( boolean exceedVideoConstraintsIfNecessary) { @@ -255,17 +326,15 @@ public class DefaultTrackSelector extends MappingTrackSelector { return new Parameters(preferredAudioLanguage, preferredTextLanguage, allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, orientationMayChange); + viewportWidth, viewportHeight, viewportOrientationMayChange); } /** - * Returns a {@link Parameters} instance with the provided - * {@code exceedRendererCapabilitiesIfNecessary} value. + * Returns an instance with the provided {@code exceedRendererCapabilitiesIfNecessary} value. * * @param exceedRendererCapabilitiesIfNecessary Whether to exceed renderer capabilities when no * selection can be made otherwise. - * @return A {@link Parameters} instance with the provided - * {@code exceedRendererCapabilitiesIfNecessary} value. + * @return An instance with the provided {@code exceedRendererCapabilitiesIfNecessary} value. */ public Parameters withExceedRendererCapabilitiesIfNecessary( boolean exceedRendererCapabilitiesIfNecessary) { @@ -275,48 +344,47 @@ public class DefaultTrackSelector extends MappingTrackSelector { return new Parameters(preferredAudioLanguage, preferredTextLanguage, allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, orientationMayChange); + viewportWidth, viewportHeight, viewportOrientationMayChange); } /** - * Returns a {@link Parameters} instance with the provided viewport size. + * Returns an instance with the provided viewport size. * * @param viewportWidth Viewport width in pixels. * @param viewportHeight Viewport height in pixels. - * @param orientationMayChange Whether orientation may change during playback. - * @return A {@link Parameters} instance with the provided viewport size. + * @param viewportOrientationMayChange Whether orientation may change during playback. + * @return An instance with the provided viewport size. */ public Parameters withViewportSize(int viewportWidth, int viewportHeight, - boolean orientationMayChange) { + boolean viewportOrientationMayChange) { if (viewportWidth == this.viewportWidth && viewportHeight == this.viewportHeight - && orientationMayChange == this.orientationMayChange) { + && viewportOrientationMayChange == this.viewportOrientationMayChange) { return this; } return new Parameters(preferredAudioLanguage, preferredTextLanguage, allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, orientationMayChange); + viewportWidth, viewportHeight, viewportOrientationMayChange); } /** - * Returns a {@link Parameters} instance where the viewport size is obtained from the provided - * {@link Context}. + * Returns an instance where the viewport size is obtained from the provided {@link Context}. * * @param context The context to obtain the viewport size from. - * @param orientationMayChange Whether orientation may change during playback. - * @return A {@link Parameters} instance where the viewport size is obtained from the provided - * {@link Context}. + * @param viewportOrientationMayChange Whether orientation may change during playback. + * @return An instance where the viewport size is obtained from the provided {@link Context}. */ - public Parameters withViewportSizeFromContext(Context context, boolean orientationMayChange) { + public Parameters withViewportSizeFromContext(Context context, + boolean viewportOrientationMayChange) { // Assume the viewport is fullscreen. Point viewportSize = Util.getPhysicalDisplaySize(context); - return withViewportSize(viewportSize.x, viewportSize.y, orientationMayChange); + return withViewportSize(viewportSize.x, viewportSize.y, viewportOrientationMayChange); } /** * Equivalent to {@code withViewportSize(Integer.MAX_VALUE, Integer.MAX_VALUE, true)}. * - * @return A {@link Parameters} instance without viewport size constraints. + * @return An instance without viewport size constraints. */ public Parameters withoutViewportSizeConstraints() { return withViewportSize(Integer.MAX_VALUE, Integer.MAX_VALUE, true); @@ -336,7 +404,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { && maxVideoWidth == other.maxVideoWidth && maxVideoHeight == other.maxVideoHeight && exceedVideoConstraintsIfNecessary == other.exceedVideoConstraintsIfNecessary && exceedRendererCapabilitiesIfNecessary == other.exceedRendererCapabilitiesIfNecessary - && orientationMayChange == other.orientationMayChange + && viewportOrientationMayChange == other.viewportOrientationMayChange && viewportWidth == other.viewportWidth && viewportHeight == other.viewportHeight && maxVideoBitrate == other.maxVideoBitrate && TextUtils.equals(preferredAudioLanguage, other.preferredAudioLanguage) @@ -354,7 +422,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { result = 31 * result + maxVideoBitrate; result = 31 * result + (exceedVideoConstraintsIfNecessary ? 1 : 0); result = 31 * result + (exceedRendererCapabilitiesIfNecessary ? 1 : 0); - result = 31 * result + (orientationMayChange ? 1 : 0); + result = 31 * result + (viewportOrientationMayChange ? 1 : 0); result = 31 * result + viewportWidth; result = 31 * result + viewportHeight; return result; @@ -441,12 +509,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (C.TRACK_TYPE_VIDEO == rendererCapabilities[i].getTrackType()) { if (!selectedVideoTracks) { rendererTrackSelections[i] = selectVideoTrack(rendererCapabilities[i], - rendererTrackGroupArrays[i], rendererFormatSupports[i], params.maxVideoWidth, - params.maxVideoHeight, params.maxVideoBitrate, params.allowNonSeamlessAdaptiveness, - params.allowMixedMimeAdaptiveness, params.viewportWidth, params.viewportHeight, - params.orientationMayChange, adaptiveTrackSelectionFactory, - params.exceedVideoConstraintsIfNecessary, - params.exceedRendererCapabilitiesIfNecessary); + rendererTrackGroupArrays[i], rendererFormatSupports[i], params, + adaptiveTrackSelectionFactory); selectedVideoTracks = rendererTrackSelections[i] != null; } seenVideoRendererWithMappedTracks |= rendererTrackGroupArrays[i].length > 0; @@ -463,8 +527,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { case C.TRACK_TYPE_AUDIO: if (!selectedAudioTracks) { rendererTrackSelections[i] = selectAudioTrack(rendererTrackGroupArrays[i], - rendererFormatSupports[i], params.preferredAudioLanguage, - params.exceedRendererCapabilitiesIfNecessary, params.allowMixedMimeAdaptiveness, + rendererFormatSupports[i], params, seenVideoRendererWithMappedTracks ? null : adaptiveTrackSelectionFactory); selectedAudioTracks = rendererTrackSelections[i] != null; } @@ -472,15 +535,13 @@ public class DefaultTrackSelector extends MappingTrackSelector { case C.TRACK_TYPE_TEXT: if (!selectedTextTracks) { rendererTrackSelections[i] = selectTextTrack(rendererTrackGroupArrays[i], - rendererFormatSupports[i], params.preferredTextLanguage, - params.preferredAudioLanguage, params.exceedRendererCapabilitiesIfNecessary); + rendererFormatSupports[i], params); selectedTextTracks = rendererTrackSelections[i] != null; } break; default: rendererTrackSelections[i] = selectOtherTrack(rendererCapabilities[i].getTrackType(), - rendererTrackGroupArrays[i], rendererFormatSupports[i], - params.exceedRendererCapabilitiesIfNecessary); + rendererTrackGroupArrays[i], rendererFormatSupports[i], params); break; } } @@ -489,42 +550,48 @@ public class DefaultTrackSelector extends MappingTrackSelector { // Video track selection implementation. + /** + * Called by {@link #selectTracks(RendererCapabilities[], TrackGroupArray[], int[][][])} to + * create a {@link TrackSelection} for a video renderer. + * + * @param rendererCapabilities The {@link RendererCapabilities} for the renderer. + * @param groups The {@link TrackGroupArray} mapped to the renderer. + * @param formatSupport The result of {@link RendererCapabilities#supportsFormat} for each mapped + * track, indexed by track group index and track index (in that order). + * @param params The selector's current constraint parameters. + * @param adaptiveTrackSelectionFactory A factory for generating adaptive track selections, or + * null if a fixed track selection is required. + * @return The {@link TrackSelection} for the renderer, or null if no selection was made. + * @throws ExoPlaybackException If an error occurs while selecting the tracks. + */ protected TrackSelection selectVideoTrack(RendererCapabilities rendererCapabilities, - TrackGroupArray groups, int[][] formatSupport, int maxVideoWidth, int maxVideoHeight, - int maxVideoBitrate, boolean allowNonSeamlessAdaptiveness, boolean allowMixedMimeAdaptiveness, - int viewportWidth, int viewportHeight, boolean orientationMayChange, - TrackSelection.Factory adaptiveTrackSelectionFactory, boolean exceedConstraintsIfNecessary, - boolean exceedRendererCapabilitiesIfNecessary) throws ExoPlaybackException { + TrackGroupArray groups, int[][] formatSupport, Parameters params, + TrackSelection.Factory adaptiveTrackSelectionFactory) throws ExoPlaybackException { TrackSelection selection = null; if (adaptiveTrackSelectionFactory != null) { selection = selectAdaptiveVideoTrack(rendererCapabilities, groups, formatSupport, - maxVideoWidth, maxVideoHeight, maxVideoBitrate, allowNonSeamlessAdaptiveness, - allowMixedMimeAdaptiveness, viewportWidth, viewportHeight, - orientationMayChange, adaptiveTrackSelectionFactory); + params, adaptiveTrackSelectionFactory); } if (selection == null) { - selection = selectFixedVideoTrack(groups, formatSupport, maxVideoWidth, maxVideoHeight, - maxVideoBitrate, viewportWidth, viewportHeight, orientationMayChange, - exceedConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary); + selection = selectFixedVideoTrack(groups, formatSupport, params); } return selection; } private static TrackSelection selectAdaptiveVideoTrack(RendererCapabilities rendererCapabilities, - TrackGroupArray groups, int[][] formatSupport, int maxVideoWidth, int maxVideoHeight, - int maxVideoBitrate, boolean allowNonSeamlessAdaptiveness, boolean allowMixedMimeAdaptiveness, - int viewportWidth, int viewportHeight, boolean orientationMayChange, + TrackGroupArray groups, int[][] formatSupport, Parameters params, TrackSelection.Factory adaptiveTrackSelectionFactory) throws ExoPlaybackException { - int requiredAdaptiveSupport = allowNonSeamlessAdaptiveness + int requiredAdaptiveSupport = params.allowNonSeamlessAdaptiveness ? (RendererCapabilities.ADAPTIVE_NOT_SEAMLESS | RendererCapabilities.ADAPTIVE_SEAMLESS) : RendererCapabilities.ADAPTIVE_SEAMLESS; - boolean allowMixedMimeTypes = allowMixedMimeAdaptiveness + boolean allowMixedMimeTypes = params.allowMixedMimeAdaptiveness && (rendererCapabilities.supportsMixedMimeTypeAdaptation() & requiredAdaptiveSupport) != 0; for (int i = 0; i < groups.length; i++) { TrackGroup group = groups.get(i); int[] adaptiveTracks = getAdaptiveVideoTracksForGroup(group, formatSupport[i], - allowMixedMimeTypes, requiredAdaptiveSupport, maxVideoWidth, maxVideoHeight, - maxVideoBitrate, viewportWidth, viewportHeight, orientationMayChange); + allowMixedMimeTypes, requiredAdaptiveSupport, params.maxVideoWidth, params.maxVideoHeight, + params.maxVideoBitrate, params.viewportWidth, params.viewportHeight, + params.viewportOrientationMayChange); if (adaptiveTracks.length > 0) { return adaptiveTrackSelectionFactory.createTrackSelection(group, adaptiveTracks); } @@ -535,13 +602,13 @@ public class DefaultTrackSelector extends MappingTrackSelector { private static int[] getAdaptiveVideoTracksForGroup(TrackGroup group, int[] formatSupport, boolean allowMixedMimeTypes, int requiredAdaptiveSupport, int maxVideoWidth, int maxVideoHeight, int maxVideoBitrate, int viewportWidth, int viewportHeight, - boolean orientationMayChange) { + boolean viewportOrientationMayChange) { if (group.length < 2) { return NO_TRACKS; } List selectedTrackIndices = getViewportFilteredTrackIndices(group, viewportWidth, - viewportHeight, orientationMayChange); + viewportHeight, viewportOrientationMayChange); if (selectedTrackIndices.size() < 2) { return NO_TRACKS; } @@ -612,9 +679,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { } private static TrackSelection selectFixedVideoTrack(TrackGroupArray groups, - int[][] formatSupport, int maxVideoWidth, int maxVideoHeight, int maxVideoBitrate, - int viewportWidth, int viewportHeight, boolean orientationMayChange, - boolean exceedConstraintsIfNecessary, boolean exceedRendererCapabilitiesIfNecessary) { + int[][] formatSupport, Parameters params) { TrackGroup selectedGroup = null; int selectedTrackIndex = 0; int selectedTrackScore = 0; @@ -623,16 +688,17 @@ public class DefaultTrackSelector extends MappingTrackSelector { for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) { TrackGroup trackGroup = groups.get(groupIndex); List selectedTrackIndices = getViewportFilteredTrackIndices(trackGroup, - viewportWidth, viewportHeight, orientationMayChange); + params.viewportWidth, params.viewportHeight, params.viewportOrientationMayChange); int[] trackFormatSupport = formatSupport[groupIndex]; for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { - if (isSupported(trackFormatSupport[trackIndex], exceedRendererCapabilitiesIfNecessary)) { + if (isSupported(trackFormatSupport[trackIndex], + params.exceedRendererCapabilitiesIfNecessary)) { Format format = trackGroup.getFormat(trackIndex); boolean isWithinConstraints = selectedTrackIndices.contains(trackIndex) - && (format.width == Format.NO_VALUE || format.width <= maxVideoWidth) - && (format.height == Format.NO_VALUE || format.height <= maxVideoHeight) - && (format.bitrate == Format.NO_VALUE || format.bitrate <= maxVideoBitrate); - if (!isWithinConstraints && !exceedConstraintsIfNecessary) { + && (format.width == Format.NO_VALUE || format.width <= params.maxVideoWidth) + && (format.height == Format.NO_VALUE || format.height <= params.maxVideoHeight) + && (format.bitrate == Format.NO_VALUE || format.bitrate <= params.maxVideoBitrate); + if (!isWithinConstraints && !params.exceedVideoConstraintsIfNecessary) { // Track should not be selected. continue; } @@ -687,9 +753,21 @@ public class DefaultTrackSelector extends MappingTrackSelector { // Audio track selection implementation. + /** + * Called by {@link #selectTracks(RendererCapabilities[], TrackGroupArray[], int[][][])} to + * create a {@link TrackSelection} for an audio renderer. + * + * @param groups The {@link TrackGroupArray} mapped to the renderer. + * @param formatSupport The result of {@link RendererCapabilities#supportsFormat} for each mapped + * track, indexed by track group index and track index (in that order). + * @param params The selector's current constraint parameters. + * @param adaptiveTrackSelectionFactory A factory for generating adaptive track selections, or + * null if a fixed track selection is required. + * @return The {@link TrackSelection} for the renderer, or null if no selection was made. + * @throws ExoPlaybackException If an error occurs while selecting the tracks. + */ protected TrackSelection selectAudioTrack(TrackGroupArray groups, int[][] formatSupport, - String preferredAudioLanguage, boolean exceedRendererCapabilitiesIfNecessary, - boolean allowMixedMimeAdaptiveness, TrackSelection.Factory adaptiveTrackSelectionFactory) { + Parameters params, TrackSelection.Factory adaptiveTrackSelectionFactory) { int selectedGroupIndex = C.INDEX_UNSET; int selectedTrackIndex = C.INDEX_UNSET; int selectedTrackScore = 0; @@ -697,10 +775,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { TrackGroup trackGroup = groups.get(groupIndex); int[] trackFormatSupport = formatSupport[groupIndex]; for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { - if (isSupported(trackFormatSupport[trackIndex], exceedRendererCapabilitiesIfNecessary)) { + if (isSupported(trackFormatSupport[trackIndex], + params.exceedRendererCapabilitiesIfNecessary)) { Format format = trackGroup.getFormat(trackIndex); int trackScore = getAudioTrackScore(trackFormatSupport[trackIndex], - preferredAudioLanguage, format); + params.preferredAudioLanguage, format); if (trackScore > selectedTrackScore) { selectedGroupIndex = groupIndex; selectedTrackIndex = trackIndex; @@ -718,7 +797,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { 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); + formatSupport[selectedGroupIndex], params.allowMixedMimeAdaptiveness); if (adaptiveTracks.length > 0) { return adaptiveTrackSelectionFactory.createTrackSelection(selectedGroup, adaptiveTracks); @@ -802,9 +881,19 @@ public class DefaultTrackSelector extends MappingTrackSelector { // Text track selection implementation. + /** + * Called by {@link #selectTracks(RendererCapabilities[], TrackGroupArray[], int[][][])} to + * create a {@link TrackSelection} for a text renderer. + * + * @param groups The {@link TrackGroupArray} mapped to the renderer. + * @param formatSupport The result of {@link RendererCapabilities#supportsFormat} for each mapped + * track, indexed by track group index and track index (in that order). + * @param params The selector's current constraint parameters. + * @return The {@link TrackSelection} for the renderer, or null if no selection was made. + * @throws ExoPlaybackException If an error occurs while selecting the tracks. + */ protected TrackSelection selectTextTrack(TrackGroupArray groups, int[][] formatSupport, - String preferredTextLanguage, String preferredAudioLanguage, - boolean exceedRendererCapabilitiesIfNecessary) { + Parameters params) { TrackGroup selectedGroup = null; int selectedTrackIndex = 0; int selectedTrackScore = 0; @@ -812,12 +901,13 @@ public class DefaultTrackSelector extends MappingTrackSelector { TrackGroup trackGroup = groups.get(groupIndex); int[] trackFormatSupport = formatSupport[groupIndex]; for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { - if (isSupported(trackFormatSupport[trackIndex], exceedRendererCapabilitiesIfNecessary)) { + if (isSupported(trackFormatSupport[trackIndex], + params.exceedRendererCapabilitiesIfNecessary)) { Format format = trackGroup.getFormat(trackIndex); boolean isDefault = (format.selectionFlags & C.SELECTION_FLAG_DEFAULT) != 0; boolean isForced = (format.selectionFlags & C.SELECTION_FLAG_FORCED) != 0; int trackScore; - if (formatHasLanguage(format, preferredTextLanguage)) { + if (formatHasLanguage(format, params.preferredTextLanguage)) { if (isDefault) { trackScore = 6; } else if (!isForced) { @@ -831,7 +921,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { } else if (isDefault) { trackScore = 3; } else if (isForced) { - if (formatHasLanguage(format, preferredAudioLanguage)) { + if (formatHasLanguage(format, params.preferredAudioLanguage)) { trackScore = 2; } else { trackScore = 1; @@ -857,8 +947,20 @@ public class DefaultTrackSelector extends MappingTrackSelector { // General track selection methods. + /** + * Called by {@link #selectTracks(RendererCapabilities[], TrackGroupArray[], int[][][])} to + * create a {@link TrackSelection} for a renderer whose type is neither video, audio or text. + * + * @param trackType The type of the renderer. + * @param groups The {@link TrackGroupArray} mapped to the renderer. + * @param formatSupport The result of {@link RendererCapabilities#supportsFormat} for each mapped + * track, indexed by track group index and track index (in that order). + * @param params The selector's current constraint parameters. + * @return The {@link TrackSelection} for the renderer, or null if no selection was made. + * @throws ExoPlaybackException If an error occurs while selecting the tracks. + */ protected TrackSelection selectOtherTrack(int trackType, TrackGroupArray groups, - int[][] formatSupport, boolean exceedRendererCapabilitiesIfNecessary) { + int[][] formatSupport, Parameters params) { TrackGroup selectedGroup = null; int selectedTrackIndex = 0; int selectedTrackScore = 0; @@ -866,7 +968,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { TrackGroup trackGroup = groups.get(groupIndex); int[] trackFormatSupport = formatSupport[groupIndex]; for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) { - if (isSupported(trackFormatSupport[trackIndex], exceedRendererCapabilitiesIfNecessary)) { + if (isSupported(trackFormatSupport[trackIndex], + params.exceedRendererCapabilitiesIfNecessary)) { Format format = trackGroup.getFormat(trackIndex); boolean isDefault = (format.selectionFlags & C.SELECTION_FLAG_DEFAULT) != 0; int trackScore = isDefault ? 2 : 1; @@ -885,12 +988,34 @@ public class DefaultTrackSelector extends MappingTrackSelector { : new FixedTrackSelection(selectedGroup, selectedTrackIndex); } + /** + * Applies the {@link RendererCapabilities#FORMAT_SUPPORT_MASK} to a value obtained from + * {@link RendererCapabilities#supportsFormat(Format)}, returning true if the result is + * {@link RendererCapabilities#FORMAT_HANDLED} or if {@code allowExceedsCapabilities} is set + * and the result is {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}. + * + * @param formatSupport A value obtained from {@link RendererCapabilities#supportsFormat(Format)}. + * @param allowExceedsCapabilities Whether to return true if the format support component of the + * value is {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}. + * @return True if the format support component is {@link RendererCapabilities#FORMAT_HANDLED}, or + * if {@code allowExceedsCapabilities} is set and the format support component is + * {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}. + */ protected static boolean isSupported(int formatSupport, boolean allowExceedsCapabilities) { int maskedSupport = formatSupport & RendererCapabilities.FORMAT_SUPPORT_MASK; return maskedSupport == RendererCapabilities.FORMAT_HANDLED || (allowExceedsCapabilities && maskedSupport == RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES); } + /** + * Returns whether a {@link Format} specifies a particular language, or {@code false} if + * {@code language} is null. + * + * @param format The {@link Format}. + * @param language The language. + * @return Whether the format specifies the language, or {@code false} if {@code language} is + * null. + */ protected static boolean formatHasLanguage(Format format, String language) { return language != null && TextUtils.equals(language, Util.normalizeLanguageCode(format.language)); From cdfe57833dc91cc8f4e611d04627a72dd1b8e439 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 21 Jul 2017 08:21:19 -0700 Subject: [PATCH 0108/2472] Update IMA extension README ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=162742982 --- extensions/ima/README.md | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/extensions/ima/README.md b/extensions/ima/README.md index 8d3bb99005..b5afcec94a 100644 --- a/extensions/ima/README.md +++ b/extensions/ima/README.md @@ -28,25 +28,30 @@ locally. Instructions for doing this can be found in ExoPlayer's ## Using the extension ## -Pass a single-window content `MediaSource` to `ImaAdsMediaSource`'s constructor, -along with a `ViewGroup` that is on top of the player and the ad tag URI to -show. The IMA documentation includes some [sample ad tags][] for testing. Then -pass the `ImaAdsMediaSource` to `ExoPlayer.prepare`. +To play ads alongside a single-window content `MediaSource`, prepare the player +with an `ImaAdsMediaSource` constructed using an `ImaAdsLoader`, the content +`MediaSource` and an overlay `ViewGroup` on top of the player. Pass an ad tag +URI from your ad campaign when creating the `ImaAdsLoader`. The IMA +documentation includes some [sample ad tags][] for testing. + +Resuming the player after entering the background requires some special handling +when playing ads. The player and its media source are released on entering the +background, and are recreated when the player returns to the foreground. When +playing ads it is necessary to persist ad playback state while in the background +by keeping a reference to the `ImaAdsLoader`. Reuse it when resuming playback of +the same content/ads by passing it in when constructing the new +`ImaAdsMediaSource`. It is also important to persist the player position when +entering the background by storing the value of `player.getContentPosition()`. +On returning to the foreground, seek to that position before preparing the new +player instance. Finally, it is important to call `ImaAdsLoader.release()` when +playback of the content/ads has finished and will not be resumed. You can try the IMA extension in the ExoPlayer demo app. To do this you must select and build one of the `withExtensions` build variants of the demo app in Android Studio. You can find IMA test content in the "IMA sample ad tags" -section of the app. +section of the app. The demo app's `PlayerActivity` also shows how to persist +the `ImaAdsLoader` instance and the player position when backgrounded during ad +playback. [top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md [sample ad tags]: https://developers.google.com/interactive-media-ads/docs/sdks/android/tags - -## Known issues ## - -This is a preview version with some known issues: - -* Tapping the 'More info' button on an ad in the demo app will pause the - activity, which destroys the ImaAdsMediaSource. Played ad breaks will be - shown to the user again if the demo app returns to the foreground. -* Ad loading timeouts are currently propagated as player errors, rather than - being silently handled by resuming content. From 4436e94ba85212472083128ba57a2087f19feb4b Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 21 Jul 2017 08:58:34 -0700 Subject: [PATCH 0109/2472] Hardcode libopus output frequency to 48000Hz Issue: #3080 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=162746202 --- .../exoplayer2/ext/opus/LibopusAudioRenderer.java | 13 ++++++++++++- .../android/exoplayer2/ext/opus/OpusDecoder.java | 14 ++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java index 564a41fc77..93fe033a31 100644 --- a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java +++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.ext.opus; import android.os.Handler; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.audio.AudioProcessor; import com.google.android.exoplayer2.audio.AudioRendererEventListener; @@ -32,6 +33,8 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer { private static final int NUM_BUFFERS = 16; private static final int INITIAL_INPUT_BUFFER_SIZE = 960 * 6; + private OpusDecoder decoder; + public LibopusAudioRenderer() { this(null, null); } @@ -69,8 +72,16 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer { @Override protected OpusDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto) throws OpusDecoderException { - return new OpusDecoder(NUM_BUFFERS, NUM_BUFFERS, INITIAL_INPUT_BUFFER_SIZE, + decoder = new OpusDecoder(NUM_BUFFERS, NUM_BUFFERS, INITIAL_INPUT_BUFFER_SIZE, format.initializationData, mediaCrypto); + return decoder; + } + + @Override + protected Format getOutputFormat() { + return Format.createAudioSampleFormat(null, MimeTypes.AUDIO_RAW, null, Format.NO_VALUE, + Format.NO_VALUE, decoder.getChannelCount(), decoder.getSampleRate(), C.ENCODING_PCM_16BIT, + null, null, 0, null); } } 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 95c38c34bb..b4a4622346 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 @@ -197,6 +197,20 @@ import java.util.List; opusClose(nativeDecoderContext); } + /** + * Returns the channel count of output audio. + */ + public int getChannelCount() { + return channelCount; + } + + /** + * Returns the sample rate of output audio. + */ + public int getSampleRate() { + return SAMPLE_RATE; + } + private static int nsToSamples(long ns) { return (int) (ns * SAMPLE_RATE / 1000000000); } From 13732fe618c4e2c1da2ec758c8b8ee4c4dc5ff99 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 21 Jul 2017 09:29:46 -0700 Subject: [PATCH 0110/2472] Release notes + version bump for 2.5.0-beta1 There's no way to represent a beta in our integer versioning scheme. I propose we just set it the same for all betas + the stable release. The versioning for the demo app isn't that important, so I've just put it directly to 2.5.0 as well. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=162749130 --- RELEASENOTES.md | 52 +++++++++++++++++++ constants.gradle | 2 +- demo/src/main/AndroidManifest.xml | 4 +- .../exoplayer2/ExoPlayerLibraryInfo.java | 6 +-- 4 files changed, 58 insertions(+), 6 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index ff1bd42fde..24da37808b 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,57 @@ # Release notes # +### r2.5.0-beta1 ### + +* IMA extension: Wraps the Google Interactive Media Ads (IMA) SDK to provide an + easy and seamless way of incorporating display ads into ExoPlayer playbacks. + You can read more about the IMA extension *A link to a blog post about this + extension will be added here prior to the stable 2.5.0 release.* +* MediaSession extension: Provides an easy to to connect ExoPlayer with + MediaSessionCompat in the Android Support Library. *A link to a blog post + about this extension will be added here prior to the stable 2.5.0 release.* +* RTMP extension: An extension for playing streams over RTMP. +* Build: Made it easier for application developers to depend on a local checkout + of ExoPlayer. You can learn how to do this + [here](https://medium.com/google-exoplayer/howto-2-depend-on-a-local-checkout-of-exoplayer-bcd7f8531720). +* Core playback improvements: + * Eliminated re-buffering when changing audio and text track selections during + playback of progressive streams + ([#2926](https://github.com/google/ExoPlayer/issues/2926)). + * New DynamicConcatenatingMediaSource class to support playback of dynamic + playlists. *A link to a blog post about DynamicConcatenatingMediaSource will + be added here prior to the stable 2.5.0 release.* + * New ExoPlayer.setRepeatMode method for dynamic toggling of repeat mode + during playback. Use of setRepeatMode should be preferred to + LoopingMediaSource for most looping use cases. You can read more about + setRepeatMode + [here](https://medium.com/google-exoplayer/repeat-modes-in-exoplayer-19dd85f036d3). + * Eliminated jank when switching video playback from one Surface to another on + API level 23+ for unencrypted content, and on devices that support the + EGL_EXT_protected_content OpenGL extension for protected content + ([#677](https://github.com/google/ExoPlayer/issues/677)). + * Enabled ExoPlayer instantiation on background threads without Loopers. + Events from such players are delivered on the application's main thread. +* HLS improvements: + * Optimized adaptive switches for playlists that specify the + EXT-X-INDEPENDENT-SEGMENTS tag. + * Optimized in-buffer seeking + ([#551](https://github.com/google/ExoPlayer/issues/551)). + * Eliminated re-buffering when changing audio and text track selections during + playback, provided the new selection does not require switching to different + renditions ([#2718](https://github.com/google/ExoPlayer/issues/2718)). + * Exposed all media playlist tags in ExoPlayer's MediaPlaylist object. +* DASH: Support for seamless switching across streams in different AdaptationSet + elements ([#2431](https://github.com/google/ExoPlayer/issues/2431)). +* DRM: Support for additional crypto schemes (cbc1, cbcs and cens) on + API level 24+ ([#1989](https://github.com/google/ExoPlayer/issues/1989)). +* Captions: Initial support for SSA/ASS subtitles + ([#889](https://github.com/google/ExoPlayer/issues/889)). +* AndroidTV: Fixed issue where tunneled video playback would not start on some + devices ([#2985](https://github.com/google/ExoPlayer/issues/2985)). +* Cronet extension: Support for a user-defined fallback if Cronet library is not + present. +* Misc bugfixes. + ### r2.4.4 ### * HLS/MPEG-TS: Some initial optimizations of MPEG-TS extractor performance diff --git a/constants.gradle b/constants.gradle index df36a01d55..73b80f6a83 100644 --- a/constants.gradle +++ b/constants.gradle @@ -24,7 +24,7 @@ project.ext { supportLibraryVersion = '25.4.0' dexmakerVersion = '1.2' mockitoVersion = '1.9.5' - releaseVersion = 'r2.4.4' + releaseVersion = 'r2.5.0-beta1' modulePrefix = ':' if (gradle.ext.has('exoplayerModulePrefix')) { modulePrefix += gradle.ext.exoplayerModulePrefix diff --git a/demo/src/main/AndroidManifest.xml b/demo/src/main/AndroidManifest.xml index afcddccac9..a39023353a 100644 --- a/demo/src/main/AndroidManifest.xml +++ b/demo/src/main/AndroidManifest.xml @@ -16,8 +16,8 @@ + android:versionCode="2500" + android:versionName="2.5.0"> diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java index ff55804e98..2abdfe5aee 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java @@ -31,13 +31,13 @@ public final class ExoPlayerLibraryInfo { * The version of the library expressed as a string, for example "1.2.3". */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. - public static final String VERSION = "2.4.4"; + public static final String VERSION = "2.5.0-beta1"; /** * The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final String VERSION_SLASHY = "ExoPlayerLib/2.4.4"; + public static final String VERSION_SLASHY = "ExoPlayerLib/2.5.0-beta1"; /** * The version of the library expressed as an integer, for example 1002003. @@ -47,7 +47,7 @@ public final class ExoPlayerLibraryInfo { * integer version 123045006 (123-045-006). */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final int VERSION_INT = 2004004; + public static final int VERSION_INT = 2005000; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} From be85da3a69b306e5d8ca9dc0faad806d51433aad Mon Sep 17 00:00:00 2001 From: Danny Brain Date: Wed, 26 Jul 2017 11:30:02 +1000 Subject: [PATCH 0111/2472] Allow PlaybackControlView to be overridden in SimpleExoPlayerView --- .../android/exoplayer2/ui/SimpleExoPlayerView.java | 14 +++++++++++++- library/ui/src/main/res/values/ids.xml | 1 + 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index a4083c940f..1c39b558bb 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -168,6 +168,15 @@ import java.util.List; *
  • Type: {@link View}
  • * * + *
  • {@code exo_controller} - An already inflated instance of + * {@link PlaybackControlView}. Allows you to use your own {@link PlaybackControlView} instead + * of default. Note: attrs such as rewind_increment will not be passed through to this + * instance and should be set at creation. {@code exo_controller_placeholder} will be ignored + * if this is set. + *
      + *
    • Type: {@link View}
    • + *
    + *
  • *
  • {@code exo_overlay} - A {@link FrameLayout} positioned on top of the player which * the app can access via {@link #getOverlayFrameLayout()}, provided for convenience. *
      @@ -315,8 +324,11 @@ public final class SimpleExoPlayerView extends FrameLayout { } // Playback control view. + PlaybackControlView customController = (PlaybackControlView) findViewById(R.id.exo_controller); View controllerPlaceholder = findViewById(R.id.exo_controller_placeholder); - if (controllerPlaceholder != null) { + if (customController != null) { + this.controller = customController; + } else if (controllerPlaceholder != null) { // Note: rewindMs and fastForwardMs are passed via attrs, so we don't need to make explicit // calls to set them. this.controller = new PlaybackControlView(context, attrs); diff --git a/library/ui/src/main/res/values/ids.xml b/library/ui/src/main/res/values/ids.xml index 815487a54e..b16b1729da 100644 --- a/library/ui/src/main/res/values/ids.xml +++ b/library/ui/src/main/res/values/ids.xml @@ -20,6 +20,7 @@ + From 908362bdf352883497a58ff4c520f69497463070 Mon Sep 17 00:00:00 2001 From: Michael Goffioul Date: Thu, 27 Jul 2017 15:44:28 -0400 Subject: [PATCH 0112/2472] Fix H262 segmentation. Prepend sequence headers to the next frame, instead of appending them to the previous frame. Tested decoders like FFMPEG and Google's Android/MPEG2 expects to read the sequence headers before the first frame they apply to. When sequence headers are appended to the previous frame, these are ignored and this leads to incorrect decoding. --- .../exoplayer2/extractor/ts/H262Reader.java | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/library/core/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 index 7266f847c4..92c8e8d800 100644 --- a/library/core/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 @@ -51,7 +51,7 @@ public final class H262Reader implements ElementaryStreamReader { // State that should be reset on seek. private final boolean[] prefixFlags; private final CsdBuffer csdBuffer; - private boolean foundFirstFrameInGroup; + private boolean foundPicture; private long totalBytesWritten; // Per packet state that gets reset at the start of each packet. @@ -60,8 +60,8 @@ public final class H262Reader implements ElementaryStreamReader { // Per sample state that gets reset at the start of each frame. private boolean isKeyframe; - private long framePosition; - private long frameTimeUs; + private long samplePosition; + private long sampleTimeUs; public H262Reader() { prefixFlags = new boolean[4]; @@ -73,7 +73,8 @@ public final class H262Reader implements ElementaryStreamReader { NalUnitUtil.clearPrefixFlags(prefixFlags); csdBuffer.reset(); pesPtsUsAvailable = false; - foundFirstFrameInGroup = false; + foundPicture = false; + samplePosition = C.POSITION_UNSET; totalBytesWritten = 0; } @@ -136,25 +137,28 @@ public final class H262Reader implements ElementaryStreamReader { } } - if (hasOutputFormat && (startCodeValue == START_GROUP || startCodeValue == START_PICTURE)) { + if (hasOutputFormat && (startCodeValue == START_PICTURE || startCodeValue == START_SEQUENCE_HEADER)) { int bytesWrittenPastStartCode = limit - startCodeOffset; - if (foundFirstFrameInGroup) { + boolean resetSample = (samplePosition == C.POSITION_UNSET); + if (foundPicture) { @C.BufferFlags int flags = isKeyframe ? C.BUFFER_FLAG_KEY_FRAME : 0; - int size = (int) (totalBytesWritten - framePosition) - bytesWrittenPastStartCode; - output.sampleMetadata(frameTimeUs, flags, size, bytesWrittenPastStartCode, null); + int size = (int) (totalBytesWritten - samplePosition) - bytesWrittenPastStartCode; + output.sampleMetadata(sampleTimeUs, flags, size, bytesWrittenPastStartCode, null); isKeyframe = false; + resetSample = true; } - if (startCodeValue == START_GROUP) { - foundFirstFrameInGroup = false; - isKeyframe = true; - } else /* startCodeValue == START_PICTURE */ { - frameTimeUs = pesPtsUsAvailable ? pesTimeUs : (frameTimeUs + frameDurationUs); - framePosition = totalBytesWritten - bytesWrittenPastStartCode; + foundPicture = (startCodeValue == START_PICTURE); + if (resetSample) { + samplePosition = totalBytesWritten - bytesWrittenPastStartCode; + sampleTimeUs = (pesPtsUsAvailable ? pesTimeUs : sampleTimeUs + frameDurationUs); pesPtsUsAvailable = false; - foundFirstFrameInGroup = true; } } + if (hasOutputFormat && startCodeValue == START_GROUP) { + isKeyframe = true; + } + offset = startCodeOffset; searchOffset = offset + 3; } From df562bc8d1d6d233e64dbf7e15df216df94aca7d Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Sat, 22 Jul 2017 02:17:02 -0700 Subject: [PATCH 0113/2472] Fix PlaybackControlView's repeat mode button update when player is null ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=162824522 --- .../google/android/exoplayer2/ui/PlaybackControlView.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java index 7c4afa772a..a99c2dfde2 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java @@ -660,6 +660,11 @@ public class PlaybackControlView extends FrameLayout { repeatToggleButton.setVisibility(View.GONE); return; } + if (player == null) { + setButtonEnabled(false, repeatToggleButton); + return; + } + setButtonEnabled(true, repeatToggleButton); switch (player.getRepeatMode()) { case Player.REPEAT_MODE_OFF: repeatToggleButton.setImageDrawable(repeatOffButtonDrawable); From 1bc01c09eefe4898de70b69d158ce700e45a65a6 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 24 Jul 2017 03:46:14 -0700 Subject: [PATCH 0114/2472] Add fake adaptive media period. This class extends the existing FakeMediaPeriod by using a ChunkSampleStream with chunks from a FakeChunkSource instead of a FakeSampleStream. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=162916656 --- .../testutil/FakeAdaptiveMediaPeriod.java | 118 ++++++++++++++++++ .../exoplayer2/testutil/FakeMediaPeriod.java | 16 +-- 2 files changed, 126 insertions(+), 8 deletions(-) create mode 100644 testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java new file mode 100644 index 0000000000..c8757e69cd --- /dev/null +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java @@ -0,0 +1,118 @@ +/* + * 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.testutil; + +import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; +import com.google.android.exoplayer2.source.CompositeSequenceableLoader; +import com.google.android.exoplayer2.source.MediaPeriod; +import com.google.android.exoplayer2.source.SampleStream; +import com.google.android.exoplayer2.source.SequenceableLoader; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.source.chunk.ChunkSampleStream; +import com.google.android.exoplayer2.trackselection.TrackSelection; +import com.google.android.exoplayer2.upstream.Allocator; +import com.google.android.exoplayer2.util.MimeTypes; +import java.util.ArrayList; +import java.util.List; + +/** + * Fake {@link MediaPeriod} that provides tracks from the given {@link TrackGroupArray}. Selecting a + * track will give the player a {@link ChunkSampleStream}. + */ +public class FakeAdaptiveMediaPeriod extends FakeMediaPeriod + implements SequenceableLoader.Callback> { + + private final EventDispatcher eventDispatcher; + private final Allocator allocator; + private final FakeChunkSource.Factory chunkSourceFactory; + private final long durationUs; + + private Callback callback; + private ChunkSampleStream[] sampleStreams; + private SequenceableLoader sequenceableLoader; + + public FakeAdaptiveMediaPeriod(TrackGroupArray trackGroupArray, EventDispatcher eventDispatcher, + Allocator allocator, FakeChunkSource.Factory chunkSourceFactory, long durationUs) { + super(trackGroupArray); + this.eventDispatcher = eventDispatcher; + this.allocator = allocator; + this.chunkSourceFactory = chunkSourceFactory; + this.durationUs = durationUs; + } + + @Override + public void prepare(Callback callback, long positionUs) { + super.prepare(callback, positionUs); + this.callback = callback; + } + + @Override + @SuppressWarnings("unchecked") + public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, + SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { + long returnPositionUs = super.selectTracks(selections, mayRetainStreamFlags, streams, + streamResetFlags, positionUs); + List> validStreams = new ArrayList<>(); + for (SampleStream stream : streams) { + if (stream != null) { + validStreams.add((ChunkSampleStream) stream); + } + } + this.sampleStreams = validStreams.toArray(new ChunkSampleStream[validStreams.size()]); + this.sequenceableLoader = new CompositeSequenceableLoader(sampleStreams); + return returnPositionUs; + } + + @Override + public long getBufferedPositionUs() { + super.getBufferedPositionUs(); + return sequenceableLoader.getBufferedPositionUs(); + } + + @Override + public long seekToUs(long positionUs) { + for (ChunkSampleStream sampleStream : sampleStreams) { + sampleStream.seekToUs(positionUs); + } + return super.seekToUs(positionUs); + } + + @Override + public long getNextLoadPositionUs() { + super.getNextLoadPositionUs(); + return sequenceableLoader.getNextLoadPositionUs(); + } + + @Override + public boolean continueLoading(long positionUs) { + super.continueLoading(positionUs); + return sequenceableLoader.continueLoading(positionUs); + } + + @Override + protected SampleStream createSampleStream(TrackSelection trackSelection) { + FakeChunkSource chunkSource = chunkSourceFactory.createChunkSource(trackSelection, durationUs); + return new ChunkSampleStream<>( + MimeTypes.getTrackType(trackSelection.getSelectedFormat().sampleMimeType), null, + chunkSource, this, allocator, 0, 3, eventDispatcher); + } + + @Override + public void onContinueLoadingRequested(ChunkSampleStream source) { + callback.onContinueLoadingRequested(this); + } + +} diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java index d8e501a298..3863cf7987 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java @@ -16,7 +16,6 @@ package com.google.android.exoplayer2.testutil; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.source.TrackGroup; @@ -26,10 +25,10 @@ import java.io.IOException; import junit.framework.Assert; /** - * Fake {@link MediaPeriod} that provides one track with a given {@link Format}. Selecting that - * track will give the player a {@link FakeSampleStream}. + * Fake {@link MediaPeriod} that provides tracks from the given {@link TrackGroupArray}. Selecting + * tracks will give the player {@link FakeSampleStream}s. */ -public final class FakeMediaPeriod implements MediaPeriod { +public class FakeMediaPeriod implements MediaPeriod { private final TrackGroupArray trackGroupArray; @@ -46,7 +45,6 @@ public final class FakeMediaPeriod implements MediaPeriod { @Override public void prepare(Callback callback, long positionUs) { Assert.assertFalse(preparedPeriod); - Assert.assertEquals(0, positionUs); preparedPeriod = true; callback.onPrepared(this); } @@ -71,8 +69,6 @@ public final class FakeMediaPeriod implements MediaPeriod { if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) { streams[i] = null; } - } - for (int i = 0; i < rendererCount; i++) { if (streams[i] == null && selections[i] != null) { TrackSelection selection = selections[i]; Assert.assertTrue(1 <= selection.length()); @@ -81,7 +77,7 @@ public final class FakeMediaPeriod implements MediaPeriod { int indexInTrackGroup = selection.getIndexInTrackGroup(selection.getSelectedIndex()); Assert.assertTrue(0 <= indexInTrackGroup); Assert.assertTrue(indexInTrackGroup < trackGroup.length); - streams[i] = new FakeSampleStream(selection.getSelectedFormat()); + streams[i] = createSampleStream(selection); streamResetFlags[i] = true; } } @@ -123,4 +119,8 @@ public final class FakeMediaPeriod implements MediaPeriod { return false; } + protected SampleStream createSampleStream(TrackSelection selection) { + return new FakeSampleStream(selection.getSelectedFormat()); + } + } From 0411add91e383bead238403c584fbb01edebc25b Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 24 Jul 2017 04:15:22 -0700 Subject: [PATCH 0115/2472] Add fake adaptive media source. This class extends the existing FakeMediaSource by creating a FakeAdaptiveMediaPeriod instead of a FakeMediaPeriod. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=162918487 --- .../testutil/FakeAdaptiveMediaSource.java | 52 +++++++++++++++++++ .../exoplayer2/testutil/FakeMediaSource.java | 9 +++- 2 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaSource.java diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaSource.java new file mode 100644 index 0000000000..59bcaf3e7c --- /dev/null +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaSource.java @@ -0,0 +1,52 @@ +/* + * 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.testutil; + +import android.os.Handler; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.Timeline.Period; +import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener; +import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.upstream.Allocator; + +/** + * Fake {@link MediaSource} that provides a given timeline. Creating the period returns a + * {@link FakeAdaptiveMediaPeriod} from the given {@link TrackGroupArray}. + */ +public class FakeAdaptiveMediaSource extends FakeMediaSource { + + private final EventDispatcher eventDispatcher; + private final FakeChunkSource.Factory chunkSourceFactory; + + public FakeAdaptiveMediaSource(Timeline timeline, Object manifest, + TrackGroupArray trackGroupArray, Handler eventHandler, + AdaptiveMediaSourceEventListener eventListener, FakeChunkSource.Factory chunkSourceFactory) { + super(timeline, manifest, trackGroupArray); + this.eventDispatcher = new EventDispatcher(eventHandler, eventListener); + this.chunkSourceFactory = chunkSourceFactory; + } + + @Override + protected FakeMediaPeriod createFakeMediaPeriod(MediaPeriodId id, TrackGroupArray trackGroupArray, + Allocator allocator) { + Period period = timeline.getPeriod(id.periodIndex, new Period()); + return new FakeAdaptiveMediaPeriod(trackGroupArray, eventDispatcher, allocator, + chunkSourceFactory, period.durationUs); + } + +} diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java index a2c1e9879e..9e7b498269 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java @@ -34,7 +34,7 @@ import junit.framework.Assert; */ public class FakeMediaSource implements MediaSource { - private final Timeline timeline; + protected final Timeline timeline; private final Object manifest; private final TrackGroupArray trackGroupArray; private final ArrayList activeMediaPeriods; @@ -82,7 +82,7 @@ public class FakeMediaSource implements MediaSource { Assertions.checkIndex(id.periodIndex, 0, timeline.getPeriodCount()); Assert.assertTrue(preparedSource); Assert.assertFalse(releasedSource); - FakeMediaPeriod mediaPeriod = new FakeMediaPeriod(trackGroupArray); + FakeMediaPeriod mediaPeriod = createFakeMediaPeriod(id, trackGroupArray, allocator); activeMediaPeriods.add(mediaPeriod); return mediaPeriod; } @@ -104,6 +104,11 @@ public class FakeMediaSource implements MediaSource { releasedSource = true; } + protected FakeMediaPeriod createFakeMediaPeriod(MediaPeriodId id, TrackGroupArray trackGroupArray, + Allocator allocator) { + return new FakeMediaPeriod(trackGroupArray); + } + private static TrackGroupArray buildTrackGroupArray(Format... formats) { TrackGroup[] trackGroups = new TrackGroup[formats.length]; for (int i = 0; i < formats.length; i++) { From 3991a6198bf9d51ea06e4189f4aad3dfae3a9de4 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 24 Jul 2017 04:16:47 -0700 Subject: [PATCH 0116/2472] Extend ActionSchedule with new actions. The new actions are: prepare source, set repeat mode, wait for timeline change, wait for position discontinuity, execute Runnable. Moreover, this change removes the restriction of using a SimpleExoPlayer to allow ActionSchedule to be used in other scenarios. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=162918554 --- .../android/exoplayer2/testutil/Action.java | 248 +++++++++++++++++- .../exoplayer2/testutil/ActionSchedule.java | 73 +++++- 2 files changed, 307 insertions(+), 14 deletions(-) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java index b1c6f081cf..bbb694d6d6 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java @@ -15,11 +15,20 @@ */ package com.google.android.exoplayer2.testutil; +import android.os.Handler; import android.util.Log; import android.view.Surface; +import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.testutil.ActionSchedule.ActionNode; import com.google.android.exoplayer2.trackselection.MappingTrackSelector; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; /** * Base class for actions to perform during playback tests. @@ -39,21 +48,41 @@ public abstract class Action { } /** - * Executes the action. + * Executes the action and schedules the next. * * @param player The player to which the action should be applied. * @param trackSelector The track selector to which the action should be applied. * @param surface The surface to use when applying actions. + * @param handler The handler to use to pass to the next action. + * @param nextAction The next action to schedule immediately after this action finished. */ - public final void doAction(SimpleExoPlayer player, MappingTrackSelector trackSelector, - Surface surface) { + public final void doActionAndScheduleNext(SimpleExoPlayer player, + MappingTrackSelector trackSelector, Surface surface, Handler handler, ActionNode nextAction) { Log.i(tag, description); - doActionImpl(player, trackSelector, surface); + doActionAndScheduleNextImpl(player, trackSelector, surface, handler, nextAction); } /** - * Called by {@link #doAction(SimpleExoPlayer, MappingTrackSelector, Surface)} do perform the - * action. + * Called by {@link #doActionAndScheduleNext(SimpleExoPlayer, MappingTrackSelector, Surface, + * Handler, ActionNode)} to perform the action and to schedule the next action node. + * + * @param player The player to which the action should be applied. + * @param trackSelector The track selector to which the action should be applied. + * @param surface The surface to use when applying actions. + * @param handler The handler to use to pass to the next action. + * @param nextAction The next action to schedule immediately after this action finished. + */ + protected void doActionAndScheduleNextImpl(SimpleExoPlayer player, + MappingTrackSelector trackSelector, Surface surface, Handler handler, ActionNode nextAction) { + doActionImpl(player, trackSelector, surface); + if (nextAction != null) { + nextAction.schedule(player, trackSelector, surface, handler); + } + } + + /** + * Called by {@link #doActionAndScheduleNextImpl(SimpleExoPlayer, MappingTrackSelector, Surface, + * Handler, ActionNode)} to perform the action. * * @param player The player to which the action should be applied. * @param trackSelector The track selector to which the action should be applied. @@ -63,7 +92,7 @@ public abstract class Action { Surface surface); /** - * Calls {@link ExoPlayer#seekTo(long)}. + * Calls {@link Player#seekTo(long)}. */ public static final class Seek extends Action { @@ -87,7 +116,7 @@ public abstract class Action { } /** - * Calls {@link ExoPlayer#stop()}. + * Calls {@link Player#stop()}. */ public static final class Stop extends Action { @@ -107,7 +136,7 @@ public abstract class Action { } /** - * Calls {@link ExoPlayer#setPlayWhenReady(boolean)}. + * Calls {@link Player#setPlayWhenReady(boolean)}. */ public static final class SetPlayWhenReady extends Action { @@ -197,5 +226,206 @@ public abstract class Action { } + /** + * Calls {@link ExoPlayer#prepare(MediaSource)}. + */ + public static final class PrepareSource extends Action { + + private final MediaSource mediaSource; + private final boolean resetPosition; + private final boolean resetState; + + /** + * @param tag A tag to use for logging. + */ + public PrepareSource(String tag, MediaSource mediaSource) { + this(tag, mediaSource, true, true); + } + + /** + * @param tag A tag to use for logging. + */ + public PrepareSource(String tag, MediaSource mediaSource, boolean resetPosition, + boolean resetState) { + super(tag, "PrepareSource"); + this.mediaSource = mediaSource; + this.resetPosition = resetPosition; + this.resetState = resetState; + } + + @Override + protected void doActionImpl(SimpleExoPlayer player, MappingTrackSelector trackSelector, + Surface surface) { + player.prepare(mediaSource, resetPosition, resetState); + } + + } + + /** + * Calls {@link Player#setRepeatMode(int)}. + */ + public static final class SetRepeatMode extends Action { + + private final @Player.RepeatMode int repeatMode; + + /** + * @param tag A tag to use for logging. + */ + public SetRepeatMode(String tag, @Player.RepeatMode int repeatMode) { + super(tag, "SetRepeatMode:" + repeatMode); + this.repeatMode = repeatMode; + } + + @Override + protected void doActionImpl(SimpleExoPlayer player, MappingTrackSelector trackSelector, + Surface surface) { + player.setRepeatMode(repeatMode); + } + + } + + /** + * Waits for {@link Player.EventListener#onTimelineChanged(Timeline, Object)}. + */ + public static final class WaitForTimelineChanged extends Action { + + private final Timeline expectedTimeline; + + /** + * @param tag A tag to use for logging. + */ + public WaitForTimelineChanged(String tag, Timeline expectedTimeline) { + super(tag, "WaitForTimelineChanged"); + this.expectedTimeline = expectedTimeline; + } + + @Override + protected void doActionAndScheduleNextImpl(final SimpleExoPlayer player, + final MappingTrackSelector trackSelector, final Surface surface, final Handler handler, + final ActionNode nextAction) { + PlayerListener listener = new PlayerListener() { + @Override + public void onTimelineChanged(Timeline timeline, Object manifest) { + if (timeline.equals(expectedTimeline)) { + player.removeListener(this); + nextAction.schedule(player, trackSelector, surface, handler); + } + } + }; + player.addListener(listener); + if (player.getCurrentTimeline().equals(expectedTimeline)) { + player.removeListener(listener); + nextAction.schedule(player, trackSelector, surface, handler); + } + } + + @Override + protected void doActionImpl(SimpleExoPlayer player, MappingTrackSelector trackSelector, + Surface surface) { + // Not triggered. + } + + } + + /** + * Waits for {@link Player.EventListener#onPositionDiscontinuity()}. + */ + public static final class WaitForPositionDiscontinuity extends Action { + + /** + * @param tag A tag to use for logging. + */ + public WaitForPositionDiscontinuity(String tag) { + super(tag, "WaitForPositionDiscontinuity"); + } + + @Override + protected void doActionAndScheduleNextImpl(final SimpleExoPlayer player, + final MappingTrackSelector trackSelector, final Surface surface, final Handler handler, + final ActionNode nextAction) { + player.addListener(new PlayerListener() { + @Override + public void onPositionDiscontinuity() { + player.removeListener(this); + nextAction.schedule(player, trackSelector, surface, handler); + } + }); + } + + @Override + protected void doActionImpl(SimpleExoPlayer player, MappingTrackSelector trackSelector, + Surface surface) { + // Not triggered. + } + + } + + /** + * Calls {@link Runnable#run()}. + */ + public static final class ExecuteRunnable extends Action { + + private final Runnable runnable; + + /** + * @param tag A tag to use for logging. + */ + public ExecuteRunnable(String tag, Runnable runnable) { + super(tag, "ExecuteRunnable"); + this.runnable = runnable; + } + + @Override + protected void doActionImpl(SimpleExoPlayer player, MappingTrackSelector trackSelector, + Surface surface) { + runnable.run(); + } + + } + + /** Listener implementation used for overriding. Does nothing. */ + private static class PlayerListener implements Player.EventListener { + + @Override + public void onTimelineChanged(Timeline timeline, Object manifest) { + + } + + @Override + public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { + + } + + @Override + public void onLoadingChanged(boolean isLoading) { + + } + + @Override + public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + + } + + @Override + public void onRepeatModeChanged(@ExoPlayer.RepeatMode int repeatMode) { + + } + + @Override + public void onPlayerError(ExoPlaybackException error) { + + } + + @Override + public void onPositionDiscontinuity() { + + } + + @Override + public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { + + } + + } } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java index 66f7ebca95..4392dd9d3f 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java @@ -18,13 +18,22 @@ package com.google.android.exoplayer2.testutil; import android.os.Handler; import android.view.Surface; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.testutil.Action.ClearVideoSurface; +import com.google.android.exoplayer2.testutil.Action.ExecuteRunnable; +import com.google.android.exoplayer2.testutil.Action.PrepareSource; import com.google.android.exoplayer2.testutil.Action.Seek; import com.google.android.exoplayer2.testutil.Action.SetPlayWhenReady; import com.google.android.exoplayer2.testutil.Action.SetRendererDisabled; +import com.google.android.exoplayer2.testutil.Action.SetRepeatMode; import com.google.android.exoplayer2.testutil.Action.SetVideoSurface; import com.google.android.exoplayer2.testutil.Action.Stop; +import com.google.android.exoplayer2.testutil.Action.WaitForPositionDiscontinuity; +import com.google.android.exoplayer2.testutil.Action.WaitForTimelineChanged; import com.google.android.exoplayer2.trackselection.MappingTrackSelector; /** @@ -179,6 +188,63 @@ public final class ActionSchedule { return apply(new SetVideoSurface(tag)); } + /** + * Schedules a new source preparation action to be executed. + * + * @return The builder, for convenience. + */ + public Builder prepareSource(MediaSource mediaSource) { + return apply(new PrepareSource(tag, mediaSource)); + } + + /** + * Schedules a new source preparation action to be executed. + * @see ExoPlayer#prepare(MediaSource, boolean, boolean). + * + * @return The builder, for convenience. + */ + public Builder prepareSource(MediaSource mediaSource, boolean resetPosition, + boolean resetState) { + return apply(new PrepareSource(tag, mediaSource, resetPosition, resetState)); + } + + /** + * Schedules a repeat mode setting action to be executed. + * + * @return The builder, for convenience. + */ + public Builder setRepeatMode(@Player.RepeatMode int repeatMode) { + return apply(new SetRepeatMode(tag, repeatMode)); + } + + /** + * Schedules a delay until the timeline changed to a specified expected timeline. + * + * @param expectedTimeline The expected timeline to wait for. + * @return The builder, for convenience. + */ + public Builder waitForTimelineChanged(Timeline expectedTimeline) { + return apply(new WaitForTimelineChanged(tag, expectedTimeline)); + } + + /** + * Schedules a delay until the next position discontinuity. + * + * @return The builder, for convenience. + */ + public Builder waitForPositionDiscontinuity() { + return apply(new WaitForPositionDiscontinuity(tag)); + } + + /** + * Schedules a {@link Runnable} to be executed. + * + * @return The builder, for convenience. + */ + public Builder executeRunnable(Runnable runnable) { + return apply(new ExecuteRunnable(tag, runnable)); + } + public ActionSchedule build() { return new ActionSchedule(rootNode); } @@ -195,7 +261,7 @@ public final class ActionSchedule { /** * Wraps an {@link Action}, allowing a delay and a next {@link Action} to be specified. */ - private static final class ActionNode implements Runnable { + /* package */ static final class ActionNode implements Runnable { private final Action action; private final long delayMs; @@ -257,10 +323,7 @@ public final class ActionSchedule { @Override public void run() { - action.doAction(player, trackSelector, surface); - if (next != null) { - next.schedule(player, trackSelector, surface, mainHandler); - } + action.doActionAndScheduleNext(player, trackSelector, surface, mainHandler, next); if (repeatIntervalMs != C.TIME_UNSET) { mainHandler.postDelayed(this, repeatIntervalMs); } From 3f7b4dc75fca331e13880a9a9daf04b697e5afba Mon Sep 17 00:00:00 2001 From: eguven Date: Tue, 25 Jul 2017 03:41:23 -0700 Subject: [PATCH 0117/2472] Add HlsDownloader ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=163052346 --- library/hls/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/library/hls/build.gradle b/library/hls/build.gradle index 77680569f0..c870c3d162 100644 --- a/library/hls/build.gradle +++ b/library/hls/build.gradle @@ -33,6 +33,7 @@ android { dependencies { compile project(modulePrefix + 'library-core') compile 'com.android.support:support-annotations:' + supportLibraryVersion + androidTestCompile project(':testutils') androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion androidTestCompile 'org.mockito:mockito-core:' + mockitoVersion From 2dc1870dd43003733e218a9d9d25a751c987d722 Mon Sep 17 00:00:00 2001 From: eguven Date: Tue, 25 Jul 2017 13:45:54 -0700 Subject: [PATCH 0118/2472] Simple Java style fix Put space between ] and {. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=163116816 --- .../ogg/DefaultOggSeekerUtilMethodsTest.java | 8 ++++---- .../exoplayer2/util/ParsableByteArrayTest.java | 8 ++++---- .../exoplayer2/text/cea/Cea708Decoder.java | 16 ++++++++-------- .../exoplayer2/source/dash/DashUtilTest.java | 2 +- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/library/core/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 index d52deb108f..1acc208c29 100644 --- a/library/core/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 @@ -34,7 +34,7 @@ public class DefaultOggSeekerUtilMethodsTest extends TestCase { FakeExtractorInput extractorInput = TestData.createInput( TestUtil.joinByteArrays( TestUtil.buildTestData(4000, random), - new byte[]{'O', 'g', 'g', 'S'}, + new byte[] {'O', 'g', 'g', 'S'}, TestUtil.buildTestData(4000, random) ), false); skipToNextPage(extractorInput); @@ -45,7 +45,7 @@ public class DefaultOggSeekerUtilMethodsTest extends TestCase { FakeExtractorInput extractorInput = TestData.createInput( TestUtil.joinByteArrays( TestUtil.buildTestData(2046, random), - new byte[]{'O', 'g', 'g', 'S'}, + new byte[] {'O', 'g', 'g', 'S'}, TestUtil.buildTestData(4000, random) ), false); skipToNextPage(extractorInput); @@ -55,7 +55,7 @@ public class DefaultOggSeekerUtilMethodsTest extends TestCase { public void testSkipToNextPageInputShorterThanPeekLength() throws Exception { FakeExtractorInput extractorInput = TestData.createInput( TestUtil.joinByteArrays( - new byte[]{'x', 'O', 'g', 'g', 'S'} + new byte[] {'x', 'O', 'g', 'g', 'S'} ), false); skipToNextPage(extractorInput); assertEquals(1, extractorInput.getPosition()); @@ -63,7 +63,7 @@ public class DefaultOggSeekerUtilMethodsTest extends TestCase { public void testSkipToNextPageNoMatch() throws Exception { FakeExtractorInput extractorInput = TestData.createInput( - new byte[]{'g', 'g', 'S', 'O', 'g', 'g'}, false); + new byte[] {'g', 'g', 'S', 'O', 'g', 'g'}, false); try { skipToNextPage(extractorInput); fail(); diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ParsableByteArrayTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ParsableByteArrayTest.java index 49719b95f7..324d668c7a 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ParsableByteArrayTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ParsableByteArrayTest.java @@ -279,7 +279,7 @@ public class ParsableByteArrayTest extends TestCase { } public void testReadLittleEndianLong() { - ParsableByteArray byteArray = new ParsableByteArray(new byte[]{ + ParsableByteArray byteArray = new ParsableByteArray(new byte[] { 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, (byte) 0xFF }); @@ -296,7 +296,7 @@ public class ParsableByteArrayTest extends TestCase { } public void testReadLittleEndianInt() { - ParsableByteArray byteArray = new ParsableByteArray(new byte[]{ + ParsableByteArray byteArray = new ParsableByteArray(new byte[] { 0x01, 0x00, 0x00, (byte) 0xFF }); assertEquals(0xFF000001, byteArray.readLittleEndianInt()); @@ -311,7 +311,7 @@ public class ParsableByteArrayTest extends TestCase { } public void testReadLittleEndianUnsignedShort() { - ParsableByteArray byteArray = new ParsableByteArray(new byte[]{ + ParsableByteArray byteArray = new ParsableByteArray(new byte[] { 0x01, (byte) 0xFF, 0x02, (byte) 0xFF }); assertEquals(0xFF01, byteArray.readLittleEndianUnsignedShort()); @@ -321,7 +321,7 @@ public class ParsableByteArrayTest extends TestCase { } public void testReadLittleEndianShort() { - ParsableByteArray byteArray = new ParsableByteArray(new byte[]{ + ParsableByteArray byteArray = new ParsableByteArray(new byte[] { 0x01, (byte) 0xFF, 0x02, (byte) 0xFF }); assertEquals((short) 0xFF01, byteArray.readLittleEndianShort()); diff --git a/library/core/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 index 8fd70f7a67..030f0cdbb0 100644 --- a/library/core/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 @@ -811,43 +811,43 @@ public final class Cea708Decoder extends CeaDecoder { private static final int PEN_OFFSET_NORMAL = 1; // The window style properties are specified in the CEA-708 specification. - private static final int[] WINDOW_STYLE_JUSTIFICATION = new int[]{ + private static final int[] WINDOW_STYLE_JUSTIFICATION = new int[] { JUSTIFICATION_LEFT, JUSTIFICATION_LEFT, JUSTIFICATION_LEFT, JUSTIFICATION_LEFT, JUSTIFICATION_LEFT, JUSTIFICATION_CENTER, JUSTIFICATION_LEFT }; - private static final int[] WINDOW_STYLE_PRINT_DIRECTION = new int[]{ + private static final int[] WINDOW_STYLE_PRINT_DIRECTION = new int[] { DIRECTION_LEFT_TO_RIGHT, DIRECTION_LEFT_TO_RIGHT, DIRECTION_LEFT_TO_RIGHT, DIRECTION_LEFT_TO_RIGHT, DIRECTION_LEFT_TO_RIGHT, DIRECTION_LEFT_TO_RIGHT, DIRECTION_TOP_TO_BOTTOM }; - private static final int[] WINDOW_STYLE_SCROLL_DIRECTION = new int[]{ + private static final int[] WINDOW_STYLE_SCROLL_DIRECTION = new int[] { DIRECTION_BOTTOM_TO_TOP, DIRECTION_BOTTOM_TO_TOP, DIRECTION_BOTTOM_TO_TOP, DIRECTION_BOTTOM_TO_TOP, DIRECTION_BOTTOM_TO_TOP, DIRECTION_BOTTOM_TO_TOP, DIRECTION_RIGHT_TO_LEFT }; - private static final boolean[] WINDOW_STYLE_WORD_WRAP = new boolean[]{ + private static final boolean[] WINDOW_STYLE_WORD_WRAP = new boolean[] { false, false, false, true, true, true, false }; - private static final int[] WINDOW_STYLE_FILL = new int[]{ + private static final int[] WINDOW_STYLE_FILL = new int[] { COLOR_SOLID_BLACK, COLOR_TRANSPARENT, COLOR_SOLID_BLACK, COLOR_SOLID_BLACK, COLOR_TRANSPARENT, COLOR_SOLID_BLACK, COLOR_SOLID_BLACK }; // The pen style properties are specified in the CEA-708 specification. - private static final int[] PEN_STYLE_FONT_STYLE = new int[]{ + private static final int[] PEN_STYLE_FONT_STYLE = new int[] { PEN_FONT_STYLE_DEFAULT, PEN_FONT_STYLE_MONOSPACED_WITH_SERIFS, PEN_FONT_STYLE_PROPORTIONALLY_SPACED_WITH_SERIFS, PEN_FONT_STYLE_MONOSPACED_WITHOUT_SERIFS, PEN_FONT_STYLE_PROPORTIONALLY_SPACED_WITHOUT_SERIFS, PEN_FONT_STYLE_MONOSPACED_WITHOUT_SERIFS, PEN_FONT_STYLE_PROPORTIONALLY_SPACED_WITHOUT_SERIFS }; - private static final int[] PEN_STYLE_EDGE_TYPE = new int[]{ + private static final int[] PEN_STYLE_EDGE_TYPE = new int[] { BORDER_AND_EDGE_TYPE_NONE, BORDER_AND_EDGE_TYPE_NONE, BORDER_AND_EDGE_TYPE_NONE, BORDER_AND_EDGE_TYPE_NONE, BORDER_AND_EDGE_TYPE_NONE, BORDER_AND_EDGE_TYPE_UNIFORM, BORDER_AND_EDGE_TYPE_UNIFORM }; - private static final int[] PEN_STYLE_BACKGROUND = new int[]{ + private static final int[] PEN_STYLE_BACKGROUND = new int[] { COLOR_SOLID_BLACK, COLOR_SOLID_BLACK, COLOR_SOLID_BLACK, COLOR_SOLID_BLACK, COLOR_SOLID_BLACK, COLOR_TRANSPARENT, COLOR_TRANSPARENT}; 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 index bac1c272e8..7a1c78b2e8 100644 --- 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 @@ -76,7 +76,7 @@ public final class DashUtilTest extends TestCase { private static DrmInitData newDrmInitData() { return new DrmInitData(new SchemeData(C.WIDEVINE_UUID, null, "mimeType", - new byte[]{1, 4, 7, 0, 3, 6})); + new byte[] {1, 4, 7, 0, 3, 6})); } } From cdb71a80aae7cc1bb605b74cf83623b2786e4990 Mon Sep 17 00:00:00 2001 From: eguven Date: Wed, 26 Jul 2017 03:03:48 -0700 Subject: [PATCH 0119/2472] Remove empty line after copyright text ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=163186057 --- .../exoplayer2/ext/cronet/ByteArrayUploadDataProviderTest.java | 1 - .../android/exoplayer2/ext/cronet/CronetDataSourceTest.java | 1 - .../exoplayer2/ext/mediasession/RepeatModeActionProvider.java | 1 - .../google/android/exoplayer2/drm/OfflineLicenseHelperTest.java | 1 - .../java/com/google/android/exoplayer2/util/AtomicFileTest.java | 1 - .../com/google/android/exoplayer2/drm/OfflineLicenseHelper.java | 1 - .../main/java/com/google/android/exoplayer2/util/AtomicFile.java | 1 - 7 files changed, 7 deletions(-) diff --git a/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/ByteArrayUploadDataProviderTest.java b/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/ByteArrayUploadDataProviderTest.java index 4282244a7a..203fd5e21c 100644 --- a/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/ByteArrayUploadDataProviderTest.java +++ b/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/ByteArrayUploadDataProviderTest.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.google.android.exoplayer2.ext.cronet; import static org.junit.Assert.assertArrayEquals; diff --git a/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java b/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java index 06a356487e..c7050dbd0c 100644 --- a/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java +++ b/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.google.android.exoplayer2.ext.cronet; import static org.junit.Assert.assertArrayEquals; diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java index 1f33245059..36d95914f7 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java @@ -14,7 +14,6 @@ package com.google.android.exoplayer2.ext.mediasession; * See the License for the specific language governing permissions and * limitations under the License. */ - import android.content.Context; import android.os.Bundle; import android.support.v4.media.session.PlaybackStateCompat; diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java index 9f5b067b5e..43c867f435 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.google.android.exoplayer2.drm; import static org.mockito.Matchers.any; diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/util/AtomicFileTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/util/AtomicFileTest.java index 6c5d7c76f7..b4f1d50293 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/util/AtomicFileTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/util/AtomicFileTest.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.google.android.exoplayer2.util; import android.test.InstrumentationTestCase; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java index 040ca50c76..2eb3463b3d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.google.android.exoplayer2.drm; import android.media.MediaDrm; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/AtomicFile.java b/library/core/src/main/java/com/google/android/exoplayer2/util/AtomicFile.java index f2e30d981b..e85c07fba9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/AtomicFile.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/AtomicFile.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.google.android.exoplayer2.util; import android.support.annotation.NonNull; From b631755b634b14ca13b25742869fc9d9f13f1d9d Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 26 Jul 2017 03:08:52 -0700 Subject: [PATCH 0120/2472] Clean up test runner for ExoPlayer tests. Currently the ExoPlayerWrapper is used to run tests using an ExoPlayer implementation. Some properties of the test are set in the constructor, some are set by overloading the class, others are hard-coded in the ExoPlayerWrapper class, and a mechanism similar to the existing ActionSchedule is missing. This change does the following: 1. Renames ExoPlayerWrapper to ExoPlayerTestRunner as it better reflects its purpose. 2. Adds an internal Builder to easily set-up the test in a coherent way. This allows to only set necessary test components while using defaults for the rest. 3. Integrate ActionSchedule. 4. Apply the new structure to the existing tests currently using ExoPlayerWrapper. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=163186578 --- .../android/exoplayer2/ExoPlayerTest.java | 202 ++++------ .../testutil/ExoPlayerTestRunner.java | 379 ++++++++++++++++++ .../exoplayer2/testutil/ExoPlayerWrapper.java | 192 --------- 3 files changed, 467 insertions(+), 306 deletions(-) create mode 100644 testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java delete mode 100644 testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerWrapper.java diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index bf4ea6e972..bc72ebc060 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -15,20 +15,18 @@ */ package com.google.android.exoplayer2; -import android.util.Pair; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.testutil.ExoPlayerWrapper; +import com.google.android.exoplayer2.testutil.ActionSchedule; +import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner; +import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner.Builder; import com.google.android.exoplayer2.testutil.FakeMediaClockRenderer; import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeRenderer; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; -import com.google.android.exoplayer2.util.MimeTypes; -import java.util.LinkedList; import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; import junit.framework.TestCase; /** @@ -43,67 +41,59 @@ public final class ExoPlayerTest extends TestCase { */ private static final int TIMEOUT_MS = 10000; - private static final Format TEST_VIDEO_FORMAT = Format.createVideoSampleFormat(null, - MimeTypes.VIDEO_H264, null, Format.NO_VALUE, Format.NO_VALUE, 1280, 720, Format.NO_VALUE, - null, null); - private static final Format TEST_AUDIO_FORMAT = Format.createAudioSampleFormat(null, - MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, Format.NO_VALUE, 2, 44100, null, null, 0, null); - /** * Tests playback of a source that exposes an empty timeline. Playback is expected to end without * error. */ public void testPlayEmptyTimeline() throws Exception { - ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper(); Timeline timeline = Timeline.EMPTY; - MediaSource mediaSource = new FakeMediaSource(timeline, null); FakeRenderer renderer = new FakeRenderer(); - playerWrapper.setup(mediaSource, renderer); - playerWrapper.blockUntilEnded(TIMEOUT_MS); - assertEquals(0, playerWrapper.positionDiscontinuityCount); + ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() + .setTimeline(timeline).setRenderers(renderer) + .build().start().blockUntilEnded(TIMEOUT_MS); + testRunner.assertPositionDiscontinuityCount(0); + testRunner.assertTimelinesEqual(timeline); assertEquals(0, renderer.formatReadCount); assertEquals(0, renderer.bufferReadCount); assertFalse(renderer.isEnded); - playerWrapper.assertSourceInfosEquals(Pair.create(timeline, null)); } /** * Tests playback of a source that exposes a single period. */ public void testPlaySinglePeriodTimeline() throws Exception { - ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper(); Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(false, false, 0)); Object manifest = new Object(); - MediaSource mediaSource = new FakeMediaSource(timeline, manifest, TEST_VIDEO_FORMAT); - FakeRenderer renderer = new FakeRenderer(TEST_VIDEO_FORMAT); - playerWrapper.setup(mediaSource, renderer); - playerWrapper.blockUntilEnded(TIMEOUT_MS); - assertEquals(0, playerWrapper.positionDiscontinuityCount); + FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); + ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() + .setTimeline(timeline).setManifest(manifest).setRenderers(renderer) + .build().start().blockUntilEnded(TIMEOUT_MS); + testRunner.assertPositionDiscontinuityCount(0); + testRunner.assertTimelinesEqual(timeline); + testRunner.assertManifestsEqual(manifest); + testRunner.assertTrackGroupsEqual(new TrackGroupArray(new TrackGroup(Builder.VIDEO_FORMAT))); assertEquals(1, renderer.formatReadCount); assertEquals(1, renderer.bufferReadCount); assertTrue(renderer.isEnded); - assertEquals(new TrackGroupArray(new TrackGroup(TEST_VIDEO_FORMAT)), playerWrapper.trackGroups); - playerWrapper.assertSourceInfosEquals(Pair.create(timeline, manifest)); } /** * Tests playback of a source that exposes three periods. */ public void testPlayMultiPeriodTimeline() throws Exception { - ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper(); Timeline timeline = new FakeTimeline( new TimelineWindowDefinition(false, false, 0), new TimelineWindowDefinition(false, false, 0), new TimelineWindowDefinition(false, false, 0)); - MediaSource mediaSource = new FakeMediaSource(timeline, null, TEST_VIDEO_FORMAT); - FakeRenderer renderer = new FakeRenderer(TEST_VIDEO_FORMAT); - playerWrapper.setup(mediaSource, renderer); - playerWrapper.blockUntilEnded(TIMEOUT_MS); - assertEquals(2, playerWrapper.positionDiscontinuityCount); + FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); + ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() + .setTimeline(timeline).setRenderers(renderer) + .build().start().blockUntilEnded(TIMEOUT_MS); + testRunner.assertPositionDiscontinuityCount(2); + testRunner.assertTimelinesEqual(timeline); assertEquals(3, renderer.formatReadCount); assertEquals(1, renderer.bufferReadCount); assertTrue(renderer.isEnded); - playerWrapper.assertSourceInfosEquals(Pair.create(timeline, null)); } /** @@ -111,16 +101,12 @@ public final class ExoPlayerTest extends TestCase { * source. */ public void testReadAheadToEndDoesNotResetRenderer() throws Exception { - final ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper(); Timeline timeline = new FakeTimeline( new TimelineWindowDefinition(false, false, 10), new TimelineWindowDefinition(false, false, 10), new TimelineWindowDefinition(false, false, 10)); - MediaSource mediaSource = new FakeMediaSource(timeline, null, TEST_VIDEO_FORMAT, - TEST_AUDIO_FORMAT); - - FakeRenderer videoRenderer = new FakeRenderer(TEST_VIDEO_FORMAT); - FakeMediaClockRenderer audioRenderer = new FakeMediaClockRenderer(TEST_AUDIO_FORMAT) { + final FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT); + FakeMediaClockRenderer audioRenderer = new FakeMediaClockRenderer(Builder.AUDIO_FORMAT) { @Override public long getPositionUs() { @@ -143,35 +129,30 @@ public final class ExoPlayerTest extends TestCase { @Override public boolean isEnded() { - // Allow playback to end once the final period is playing. - return playerWrapper.positionDiscontinuityCount == 2; + return videoRenderer.isEnded(); } }; - playerWrapper.setup(mediaSource, videoRenderer, audioRenderer); - playerWrapper.blockUntilEnded(TIMEOUT_MS); - assertEquals(2, playerWrapper.positionDiscontinuityCount); + ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() + .setTimeline(timeline).setRenderers(videoRenderer, audioRenderer) + .setSupportedFormats(Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT) + .build().start().blockUntilEnded(TIMEOUT_MS); + testRunner.assertPositionDiscontinuityCount(2); + testRunner.assertTimelinesEqual(timeline); assertEquals(1, audioRenderer.positionResetCount); assertTrue(videoRenderer.isEnded); assertTrue(audioRenderer.isEnded); - playerWrapper.assertSourceInfosEquals(Pair.create(timeline, null)); } public void testRepreparationGivesFreshSourceInfo() throws Exception { - ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper(); 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 + FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); 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. + MediaSource firstSource = new FakeMediaSource(timeline, firstSourceManifest, + Builder.VIDEO_FORMAT); final CountDownLatch queuedSourceInfoCountDownLatch = new CountDownLatch(1); final CountDownLatch completePreparationCountDownLatch = new CountDownLatch(1); - playerWrapper.prepare(new FakeMediaSource(timeline, new Object(), TEST_VIDEO_FORMAT) { + MediaSource secondSource = new FakeMediaSource(timeline, new Object(), Builder.VIDEO_FORMAT) { @Override public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { super.prepareSource(player, isTopLevelSource, listener); @@ -185,29 +166,49 @@ public final class ExoPlayerTest extends TestCase { 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); + MediaSource thirdSource = new FakeMediaSource(timeline, thirdSourceManifest, + Builder.VIDEO_FORMAT); + // Prepare the player with a source with the first manifest and a non-empty timeline. Prepare + // the player again with a source and a new manifest, which will never be exposed. Allow the + // test thread to prepare the player with a third source, and block the playback thread until + // the test thread's call to prepare() has returned. + ActionSchedule actionSchedule = new ActionSchedule.Builder("testRepreparation") + .waitForTimelineChanged(timeline) + .prepareSource(secondSource) + .executeRunnable(new Runnable() { + @Override + public void run() { + try { + queuedSourceInfoCountDownLatch.await(); + } catch (InterruptedException e) { + // Ignore. + } + } + }) + .prepareSource(thirdSource) + .executeRunnable(new Runnable() { + @Override + public void run() { + completePreparationCountDownLatch.countDown(); + } + }) + .build(); + ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() + .setMediaSource(firstSource).setRenderers(renderer).setActionSchedule(actionSchedule) + .build().start().blockUntilEnded(TIMEOUT_MS); + testRunner.assertPositionDiscontinuityCount(0); // 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)); + testRunner.assertTimelinesEqual(timeline, Timeline.EMPTY, timeline); + testRunner.assertManifestsEqual(firstSourceManifest, null, thirdSourceManifest); + testRunner.assertTrackGroupsEqual(new TrackGroupArray(new TrackGroup(Builder.VIDEO_FORMAT))); + assertEquals(1, renderer.formatReadCount); + assertEquals(1, renderer.bufferReadCount); + assertTrue(renderer.isEnded); } public void testRepeatModeChanges() throws Exception { @@ -215,49 +216,22 @@ public final class ExoPlayerTest extends TestCase { new TimelineWindowDefinition(true, false, 100000), new TimelineWindowDefinition(true, false, 100000), new TimelineWindowDefinition(true, false, 100000)); - final int[] actionSchedule = { // 0 -> 1 - Player.REPEAT_MODE_ONE, // 1 -> 1 - Player.REPEAT_MODE_OFF, // 1 -> 2 - Player.REPEAT_MODE_ONE, // 2 -> 2 - Player.REPEAT_MODE_ALL, // 2 -> 0 - Player.REPEAT_MODE_ONE, // 0 -> 0 - -1, // 0 -> 0 - Player.REPEAT_MODE_OFF, // 0 -> 1 - -1, // 1 -> 2 - -1 // 2 -> ended - }; - int[] expectedWindowIndices = {1, 1, 2, 2, 0, 0, 0, 1, 2}; - final LinkedList windowIndices = new LinkedList<>(); - final CountDownLatch actionCounter = new CountDownLatch(actionSchedule.length); - ExoPlayerWrapper playerWrapper = new ExoPlayerWrapper() { - @Override - @SuppressWarnings("ResourceType") - public void onPositionDiscontinuity() { - super.onPositionDiscontinuity(); - int actionIndex = actionSchedule.length - (int) actionCounter.getCount(); - if (actionSchedule[actionIndex] != -1) { - player.setRepeatMode(actionSchedule[actionIndex]); - } - windowIndices.add(player.getCurrentWindowIndex()); - actionCounter.countDown(); - } - }; - MediaSource mediaSource = new FakeMediaSource(timeline, null, TEST_VIDEO_FORMAT); - FakeRenderer renderer = new FakeRenderer(TEST_VIDEO_FORMAT); - playerWrapper.setup(mediaSource, renderer); - boolean finished = actionCounter.await(TIMEOUT_MS, TimeUnit.MILLISECONDS); - playerWrapper.release(); - assertTrue("Test playback timed out waiting for action schedule to end.", finished); - if (playerWrapper.exception != null) { - throw playerWrapper.exception; - } - assertEquals(expectedWindowIndices.length, windowIndices.size()); - for (int i = 0; i < expectedWindowIndices.length; i++) { - assertEquals(expectedWindowIndices[i], windowIndices.get(i).intValue()); - } - assertEquals(9, playerWrapper.positionDiscontinuityCount); + FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); + ActionSchedule actionSchedule = new ActionSchedule.Builder("testRepeatMode") // 0 -> 1 + .waitForPositionDiscontinuity().setRepeatMode(Player.REPEAT_MODE_ONE) // 1 -> 1 + .waitForPositionDiscontinuity().setRepeatMode(Player.REPEAT_MODE_OFF) // 1 -> 2 + .waitForPositionDiscontinuity().setRepeatMode(Player.REPEAT_MODE_ONE) // 2 -> 2 + .waitForPositionDiscontinuity().setRepeatMode(Player.REPEAT_MODE_ALL) // 2 -> 0 + .waitForPositionDiscontinuity().setRepeatMode(Player.REPEAT_MODE_ONE) // 0 -> 0 + .waitForPositionDiscontinuity() // 0 -> 0 + .waitForPositionDiscontinuity().setRepeatMode(Player.REPEAT_MODE_OFF) // 0 -> end + .build(); + ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() + .setTimeline(timeline).setRenderers(renderer).setActionSchedule(actionSchedule) + .build().start().blockUntilEnded(TIMEOUT_MS); + testRunner.assertPlayedPeriodIndices(0, 1, 1, 2, 2, 0, 0, 0, 1, 2); + testRunner.assertTimelinesEqual(timeline); assertTrue(renderer.isEnded); - playerWrapper.assertSourceInfosEquals(Pair.create(timeline, null)); } } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java new file mode 100644 index 0000000000..2bfef0b4ab --- /dev/null +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java @@ -0,0 +1,379 @@ +/* + * 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.testutil; + +import android.os.Handler; +import android.os.HandlerThread; +import com.google.android.exoplayer2.DefaultLoadControl; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.ExoPlayerFactory; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.LoadControl; +import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Renderer; +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; +import com.google.android.exoplayer2.metadata.MetadataRenderer; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner.Builder.PlayerFactory; +import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; +import com.google.android.exoplayer2.text.TextRenderer.Output; +import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; +import com.google.android.exoplayer2.trackselection.MappingTrackSelector; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.video.VideoRendererEventListener; +import java.util.LinkedList; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import junit.framework.Assert; + +/** + * Helper class to run an ExoPlayer test. + */ +public final class ExoPlayerTestRunner implements Player.EventListener { + + /** + * Builder to set-up a {@link ExoPlayerTestRunner}. Default fake implementations will be used for + * unset test properties. + */ + public static final class Builder { + + /** + * Factory to create an {@link SimpleExoPlayer} instance. The player will be created on its own + * {@link HandlerThread}. + */ + public interface PlayerFactory { + + SimpleExoPlayer createExoPlayer(RenderersFactory renderersFactory, + MappingTrackSelector trackSelector, LoadControl loadControl); + + } + + public static final Format VIDEO_FORMAT = Format.createVideoSampleFormat(null, + MimeTypes.VIDEO_H264, null, Format.NO_VALUE, Format.NO_VALUE, 1280, 720, Format.NO_VALUE, + null, null); + public static final Format AUDIO_FORMAT = Format.createAudioSampleFormat(null, + MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, Format.NO_VALUE, 2, 44100, null, null, 0, null); + + private PlayerFactory playerFactory; + private Timeline timeline; + private Object manifest; + private MediaSource mediaSource; + private MappingTrackSelector trackSelector; + private LoadControl loadControl; + private Format[] supportedFormats; + private Renderer[] renderers; + private RenderersFactory renderersFactory; + private ActionSchedule actionSchedule; + private Player.EventListener eventListener; + + public Builder setTimeline(Timeline timeline) { + Assert.assertNull(mediaSource); + this.timeline = timeline; + return this; + } + + public Builder setManifest(Object manifest) { + Assert.assertNull(mediaSource); + this.manifest = manifest; + return this; + } + + /** Replaces {@link #setTimeline(Timeline)} and {@link #setManifest(Object)}. */ + public Builder setMediaSource(MediaSource mediaSource) { + Assert.assertNull(timeline); + Assert.assertNull(manifest); + this.mediaSource = mediaSource; + return this; + } + + public Builder setTrackSelector(MappingTrackSelector trackSelector) { + this.trackSelector = trackSelector; + return this; + } + + public Builder setLoadControl(LoadControl loadControl) { + this.loadControl = loadControl; + return this; + } + + public Builder setSupportedFormats(Format... supportedFormats) { + this.supportedFormats = supportedFormats; + return this; + } + + public Builder setRenderers(Renderer... renderers) { + Assert.assertNull(renderersFactory); + this.renderers = renderers; + return this; + } + + /** Replaces {@link #setRenderers(Renderer...)}. */ + public Builder setRenderersFactory(RenderersFactory renderersFactory) { + Assert.assertNull(renderers); + this.renderersFactory = renderersFactory; + return this; + } + + public Builder setExoPlayer(PlayerFactory playerFactory) { + this.playerFactory = playerFactory; + return this; + } + + public Builder setActionSchedule(ActionSchedule actionSchedule) { + this.actionSchedule = actionSchedule; + return this; + } + + public Builder setEventListener(Player.EventListener eventListener) { + this.eventListener = eventListener; + return this; + } + + public ExoPlayerTestRunner build() { + if (supportedFormats == null) { + supportedFormats = new Format[] { VIDEO_FORMAT }; + } + if (trackSelector == null) { + trackSelector = new DefaultTrackSelector(); + } + if (renderersFactory == null) { + if (renderers == null) { + renderers = new Renderer[] { new FakeRenderer(supportedFormats) }; + } + renderersFactory = new RenderersFactory() { + @Override + public Renderer[] createRenderers(Handler eventHandler, + VideoRendererEventListener videoRendererEventListener, + AudioRendererEventListener audioRendererEventListener, Output textRendererOutput, + MetadataRenderer.Output metadataRendererOutput) { + return renderers; + } + }; + } + if (loadControl == null) { + loadControl = new DefaultLoadControl(); + } + if (playerFactory == null) { + playerFactory = new PlayerFactory() { + @Override + public SimpleExoPlayer createExoPlayer(RenderersFactory renderersFactory, + MappingTrackSelector trackSelector, LoadControl loadControl) { + return ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector, loadControl); + } + }; + } + if (mediaSource == null) { + if (timeline == null) { + timeline = new FakeTimeline(new TimelineWindowDefinition(false, false, 0)); + } + mediaSource = new FakeMediaSource(timeline, manifest, supportedFormats); + } + return new ExoPlayerTestRunner(playerFactory, mediaSource, renderersFactory, trackSelector, + loadControl, actionSchedule, eventListener); + } + } + + private final PlayerFactory playerFactory; + private final MediaSource mediaSource; + private final RenderersFactory renderersFactory; + private final MappingTrackSelector trackSelector; + private final LoadControl loadControl; + private final ActionSchedule actionSchedule; + private final Player.EventListener eventListener; + + private final HandlerThread playerThread; + private final Handler handler; + private final CountDownLatch endedCountDownLatch; + private final LinkedList timelines; + private final LinkedList manifests; + private final LinkedList periodIndices; + + private SimpleExoPlayer player; + private Exception exception; + private TrackGroupArray trackGroups; + private int positionDiscontinuityCount; + + private ExoPlayerTestRunner(PlayerFactory playerFactory, MediaSource mediaSource, + RenderersFactory renderersFactory, MappingTrackSelector trackSelector, + LoadControl loadControl, ActionSchedule actionSchedule, Player.EventListener eventListener) { + this.playerFactory = playerFactory; + this.mediaSource = mediaSource; + this.renderersFactory = renderersFactory; + this.trackSelector = trackSelector; + this.loadControl = loadControl; + this.actionSchedule = actionSchedule; + this.eventListener = eventListener; + this.timelines = new LinkedList<>(); + this.manifests = new LinkedList<>(); + this.periodIndices = new LinkedList<>(); + this.endedCountDownLatch = new CountDownLatch(1); + this.playerThread = new HandlerThread("ExoPlayerTest thread"); + playerThread.start(); + this.handler = new Handler(playerThread.getLooper()); + } + + // Called on the test thread to run the test. + + public ExoPlayerTestRunner start() { + handler.post(new Runnable() { + @Override + public void run() { + try { + player = playerFactory.createExoPlayer(renderersFactory, trackSelector, loadControl); + player.addListener(ExoPlayerTestRunner.this); + if (eventListener != null) { + player.addListener(eventListener); + } + player.setPlayWhenReady(true); + if (actionSchedule != null) { + actionSchedule.start(player, trackSelector, null, handler); + } + player.prepare(mediaSource); + } catch (Exception e) { + handleException(e); + } + } + }); + return this; + } + + public ExoPlayerTestRunner blockUntilEnded(long timeoutMs) throws Exception { + if (!endedCountDownLatch.await(timeoutMs, TimeUnit.MILLISECONDS)) { + exception = new TimeoutException("Test playback timed out waiting for playback to end."); + } + release(); + // Throw any pending exception (from playback, timing out or releasing). + if (exception != null) { + throw exception; + } + return this; + } + + // Assertions called on the test thread after test finished. + + public void assertTimelinesEqual(Timeline... timelines) { + Assert.assertEquals(timelines.length, this.timelines.size()); + for (Timeline timeline : timelines) { + Assert.assertEquals(timeline, this.timelines.remove()); + } + } + + public void assertManifestsEqual(Object... manifests) { + Assert.assertEquals(manifests.length, this.manifests.size()); + for (Object manifest : manifests) { + Assert.assertEquals(manifest, this.manifests.remove()); + } + } + + public void assertTrackGroupsEqual(TrackGroupArray trackGroupArray) { + Assert.assertEquals(trackGroupArray, this.trackGroups); + } + + public void assertPositionDiscontinuityCount(int expectedCount) { + Assert.assertEquals(expectedCount, positionDiscontinuityCount); + } + + public void assertPlayedPeriodIndices(int... periodIndices) { + Assert.assertEquals(periodIndices.length, this.periodIndices.size()); + for (int periodIndex : periodIndices) { + Assert.assertEquals(periodIndex, (int) this.periodIndices.remove()); + } + } + + // Private implementation details. + + private void release() throws InterruptedException { + handler.post(new Runnable() { + @Override + public void run() { + try { + if (player != null) { + player.release(); + } + } catch (Exception e) { + handleException(e); + } finally { + playerThread.quit(); + } + } + }); + playerThread.join(); + } + + private void handleException(Exception exception) { + if (this.exception == null) { + this.exception = exception; + } + endedCountDownLatch.countDown(); + } + + // Player.EventListener + + @Override + public void onTimelineChanged(Timeline timeline, Object manifest) { + timelines.add(timeline); + manifests.add(manifest); + } + + @Override + public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { + this.trackGroups = trackGroups; + } + + @Override + public void onLoadingChanged(boolean isLoading) { + // Do nothing. + } + + @Override + public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + if (periodIndices.isEmpty() && playbackState == Player.STATE_READY) { + periodIndices.add(player.getCurrentPeriodIndex()); + } + if (playbackState == Player.STATE_ENDED) { + endedCountDownLatch.countDown(); + } + } + + @Override + public void onRepeatModeChanged(@Player.RepeatMode int repeatMode) { + // Do nothing. + } + + @Override + public void onPlayerError(ExoPlaybackException error) { + handleException(exception); + } + + @Override + public void onPositionDiscontinuity() { + positionDiscontinuityCount++; + periodIndices.add(player.getCurrentPeriodIndex()); + } + + @Override + public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { + // Do nothing. + } + +} diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerWrapper.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerWrapper.java deleted file mode 100644 index ab247283e6..0000000000 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerWrapper.java +++ /dev/null @@ -1,192 +0,0 @@ -/* - * 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.testutil; - -import android.os.Handler; -import android.os.HandlerThread; -import android.util.Pair; -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.Player; -import com.google.android.exoplayer2.Renderer; -import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; -import com.google.android.exoplayer2.trackselection.TrackSelectionArray; -import java.util.LinkedList; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import junit.framework.Assert; - -/** - * Wraps a player with its own handler thread. - */ -public class ExoPlayerWrapper implements Player.EventListener { - - private final CountDownLatch sourceInfoCountDownLatch; - private final CountDownLatch endedCountDownLatch; - private final HandlerThread playerThread; - private final Handler handler; - private final LinkedList> sourceInfos; - - public ExoPlayer player; - public TrackGroupArray trackGroups; - public Exception exception; - - // Written only on the main thread. - public volatile int positionDiscontinuityCount; - - public ExoPlayerWrapper() { - 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 waiting for playback to end."); - } - release(); - // Throw any pending exception (from playback, timing out or releasing). - if (exception != null) { - throw exception; - } - } - - 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 - public void run() { - try { - player = ExoPlayerFactory.newInstance(renderers, new DefaultTrackSelector()); - player.addListener(ExoPlayerWrapper.this); - player.setPlayWhenReady(true); - player.prepare(mediaSource); - } catch (Exception e) { - handleError(e); - } - } - }); - } - - 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 - public void run() { - try { - if (player != null) { - player.release(); - } - } catch (Exception e) { - handleError(e); - } finally { - playerThread.quit(); - } - } - }); - playerThread.join(); - } - - private void handleError(Exception exception) { - if (this.exception == null) { - this.exception = exception; - } - endedCountDownLatch.countDown(); - } - - @SafeVarargs - public final void assertSourceInfosEquals(Pair... sourceInfos) { - Assert.assertEquals(sourceInfos.length, this.sourceInfos.size()); - for (Pair sourceInfo : sourceInfos) { - Assert.assertEquals(sourceInfo, this.sourceInfos.remove()); - } - } - - // Player.EventListener implementation. - - @Override - public void onLoadingChanged(boolean isLoading) { - // Do nothing. - } - - @Override - public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { - if (playbackState == Player.STATE_ENDED) { - endedCountDownLatch.countDown(); - } - } - - @Override - public void onRepeatModeChanged(int repeatMode) { - // Do nothing. - } - - @Override - public void onTimelineChanged(Timeline timeline, Object manifest) { - sourceInfos.add(Pair.create(timeline, manifest)); - sourceInfoCountDownLatch.countDown(); - } - - @Override - public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { - this.trackGroups = trackGroups; - } - - @Override - public void onPlayerError(ExoPlaybackException exception) { - handleError(exception); - } - - @SuppressWarnings("NonAtomicVolatileUpdate") - @Override - public void onPositionDiscontinuity() { - positionDiscontinuityCount++; - } - - @Override - public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { - // Do nothing. - } - -} From 9936bc670293514811681cf9e1e3d89780f29ada Mon Sep 17 00:00:00 2001 From: eguven Date: Wed, 26 Jul 2017 04:47:56 -0700 Subject: [PATCH 0121/2472] Replace iterable foreach loops with indexed for loops ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=163193118 --- .../exoplayer2/ext/ima/ImaAdsLoader.java | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index 6e2206d6ae..bd3f6df826 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -436,13 +436,13 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, } if (!imaPlayingAd) { imaPlayingAd = true; - for (VideoAdPlayerCallback callback : adCallbacks) { - callback.onPlay(); + for (int i = 0; i < adCallbacks.size(); i++) { + adCallbacks.get(i).onPlay(); } } else if (imaPausedInAd) { imaPausedInAd = false; - for (VideoAdPlayerCallback callback : adCallbacks) { - callback.onResume(); + for (int i = 0; i < adCallbacks.size(); i++) { + adCallbacks.get(i).onResume(); } } } @@ -473,8 +473,8 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, return; } imaPausedInAd = true; - for (VideoAdPlayerCallback callback : adCallbacks) { - callback.onPause(); + for (int i = 0; i < adCallbacks.size(); i++) { + adCallbacks.get(i).onPause(); } } @@ -519,8 +519,8 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, } else if (imaPlayingAd && playbackState == Player.STATE_ENDED) { // IMA is waiting for the ad playback to finish so invoke the callback now. // Either CONTENT_RESUME_REQUESTED will be passed next, or playAd will be called again. - for (VideoAdPlayerCallback callback : adCallbacks) { - callback.onEnded(); + for (int i = 0; i < adCallbacks.size(); i++) { + adCallbacks.get(i).onEnded(); } } } @@ -533,8 +533,8 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, @Override public void onPlayerError(ExoPlaybackException error) { if (playingAd) { - for (VideoAdPlayerCallback callback : adCallbacks) { - callback.onError(); + for (int i = 0; i < adCallbacks.size(); i++) { + adCallbacks.get(i).onError(); } } } @@ -584,8 +584,8 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, if (adFinished) { // IMA is waiting for the ad playback to finish so invoke the callback now. // Either CONTENT_RESUME_REQUESTED will be passed next, or playAd will be called again. - for (VideoAdPlayerCallback callback : adCallbacks) { - callback.onEnded(); + for (int i = 0; i < adCallbacks.size(); i++) { + adCallbacks.get(i).onEnded(); } } if (!wasPlayingAd && playingAd) { From bcc69c2da7afc8a8463b9ec11fab78af2305564a Mon Sep 17 00:00:00 2001 From: eguven Date: Wed, 26 Jul 2017 08:58:02 -0700 Subject: [PATCH 0122/2472] Add HlsMasterPlaylist.copy method Creates a copy of this playlist which includes only the variants identified by the given variantUrls. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=163212562 --- .../hls/playlist/HlsMasterPlaylist.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/library/hls/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 index b38763f7e8..0b237e75e7 100644 --- a/library/hls/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,6 +17,7 @@ package com.google.android.exoplayer2.source.hls.playlist; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.util.MimeTypes; +import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -108,6 +109,20 @@ public final class HlsMasterPlaylist extends HlsPlaylist { ? Collections.unmodifiableList(muxedCaptionFormats) : null; } + /** + * Returns a copy of this playlist which includes only the renditions identified by the given + * urls. + * + * @param renditionUrls List of rendition urls. + * @return A copy of this playlist which includes only the renditions identified by the given + * urls. + */ + public HlsMasterPlaylist copy(List renditionUrls) { + return new HlsMasterPlaylist(baseUri, tags, copyRenditionList(variants, renditionUrls), + copyRenditionList(audios, renditionUrls), copyRenditionList(subtitles, renditionUrls), + muxedAudioFormat, muxedCaptionFormats); + } + /** * Creates a playlist with a single variant. * @@ -121,4 +136,15 @@ public final class HlsMasterPlaylist extends HlsPlaylist { emptyList, null, null); } + private static List copyRenditionList(List variants, List variantUrls) { + List copyVariants = new ArrayList<>(); + for (int i = 0; i < variants.size(); i++) { + HlsUrl variant = variants.get(i); + if (variantUrls.contains(variant.url)) { + copyVariants.add(variant); + } + } + return copyVariants; + } + } From 5278a63768e51222af9163331142978373c6d94d Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 27 Jul 2017 06:20:24 -0700 Subject: [PATCH 0123/2472] Change copyRenditionsList parameters names Also instantiate the resulting list with a predicted size to minimize list resizing. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=163332285 --- .../source/hls/playlist/HlsMasterPlaylist.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/library/hls/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 index 0b237e75e7..04192def9d 100644 --- a/library/hls/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 @@ -118,8 +118,8 @@ public final class HlsMasterPlaylist extends HlsPlaylist { * urls. */ public HlsMasterPlaylist copy(List renditionUrls) { - return new HlsMasterPlaylist(baseUri, tags, copyRenditionList(variants, renditionUrls), - copyRenditionList(audios, renditionUrls), copyRenditionList(subtitles, renditionUrls), + return new HlsMasterPlaylist(baseUri, tags, copyRenditionsList(variants, renditionUrls), + copyRenditionsList(audios, renditionUrls), copyRenditionsList(subtitles, renditionUrls), muxedAudioFormat, muxedCaptionFormats); } @@ -136,15 +136,15 @@ public final class HlsMasterPlaylist extends HlsPlaylist { emptyList, null, null); } - private static List copyRenditionList(List variants, List variantUrls) { - List copyVariants = new ArrayList<>(); - for (int i = 0; i < variants.size(); i++) { - HlsUrl variant = variants.get(i); - if (variantUrls.contains(variant.url)) { - copyVariants.add(variant); + private static List copyRenditionsList(List renditions, List urls) { + List copiedRenditions = new ArrayList<>(urls.size()); + for (int i = 0; i < renditions.size(); i++) { + HlsUrl rendition = renditions.get(i); + if (urls.contains(rendition.url)) { + copiedRenditions.add(rendition); } } - return copyVariants; + return copiedRenditions; } } From 84d19c464b04e0dc54a133ed5f56fdfb04ed8266 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 27 Jul 2017 07:20:41 -0700 Subject: [PATCH 0124/2472] Add support for AAC-LATM in transport streams ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=163337073 --- .../exoplayer2/util/ParsableBitArrayTest.java | 78 +++-- .../ts/DefaultTsPayloadReaderFactory.java | 5 +- .../exoplayer2/extractor/ts/LatmReader.java | 306 ++++++++++++++++++ .../exoplayer2/extractor/ts/TsExtractor.java | 3 +- .../util/CodecSpecificDataUtil.java | 75 ++++- .../exoplayer2/util/ParsableBitArray.java | 37 +++ 6 files changed, 478 insertions(+), 26 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ParsableBitArrayTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ParsableBitArrayTest.java index cfb9cd78be..fc8318d826 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ParsableBitArrayTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ParsableBitArrayTest.java @@ -16,7 +16,6 @@ package com.google.android.exoplayer2.util; import android.test.MoreAsserts; - import junit.framework.TestCase; /** @@ -27,8 +26,14 @@ public final class ParsableBitArrayTest extends TestCase { private static final byte[] TEST_DATA = new byte[] {0x3C, (byte) 0xD2, (byte) 0x5F, (byte) 0x01, (byte) 0xFF, (byte) 0x14, (byte) 0x60, (byte) 0x99}; + private ParsableBitArray testArray; + + @Override + public void setUp() { + testArray = new ParsableBitArray(TEST_DATA); + } + public void testReadAllBytes() { - ParsableBitArray testArray = new ParsableBitArray(TEST_DATA); byte[] bytesRead = new byte[TEST_DATA.length]; testArray.readBytes(bytesRead, 0, TEST_DATA.length); MoreAsserts.assertEquals(TEST_DATA, bytesRead); @@ -37,13 +42,12 @@ public final class ParsableBitArrayTest extends TestCase { } public void testReadBit() { - ParsableBitArray testArray = new ParsableBitArray(TEST_DATA); - assertReadBitsToEnd(0, testArray); + assertReadBitsToEnd(0); } public void testReadBits() { - ParsableBitArray testArray = new ParsableBitArray(TEST_DATA); assertEquals(getTestDataBits(0, 5), testArray.readBits(5)); + assertEquals(getTestDataBits(5, 0), testArray.readBits(0)); assertEquals(getTestDataBits(5, 3), testArray.readBits(3)); assertEquals(getTestDataBits(8, 16), testArray.readBits(16)); assertEquals(getTestDataBits(24, 3), testArray.readBits(3)); @@ -52,67 +56,97 @@ public final class ParsableBitArrayTest extends TestCase { assertEquals(getTestDataBits(50, 14), testArray.readBits(14)); } + public void testReadBitsToByteArray() { + byte[] result = new byte[TEST_DATA.length]; + // Test read within byte boundaries. + testArray.readBits(result, 0, 6); + assertEquals(TEST_DATA[0] & 0xFC, result[0]); + // Test read across byte boundaries. + testArray.readBits(result, 0, 8); + assertEquals(((TEST_DATA[0] & 0x03) << 6) | ((TEST_DATA[1] & 0xFC) >> 2), result[0]); + // Test reading across multiple bytes. + testArray.readBits(result, 1, 50); + for (int i = 1; i < 7; i++) { + assertEquals((byte) (((TEST_DATA[i] & 0x03) << 6) | ((TEST_DATA[i + 1] & 0xFC) >> 2)), + result[i]); + } + assertEquals((byte) (TEST_DATA[7] & 0x03) << 6, result[7]); + assertEquals(0, testArray.bitsLeft()); + // Test read last buffer byte across input data bytes. + testArray.setPosition(31); + result[3] = 0; + testArray.readBits(result, 3, 3); + assertEquals((byte) 0xE0, result[3]); + // Test read bits in the middle of a input data byte. + result[0] = 0; + assertEquals(34, testArray.getPosition()); + testArray.readBits(result, 0, 3); + assertEquals((byte) 0xE0, result[0]); + // Test read 0 bits. + testArray.setPosition(32); + result[1] = 0; + testArray.readBits(result, 1, 0); + assertEquals(0, result[1]); + // Test least significant bits are unmodified. + result[1] = (byte) 0xFF; + testArray.setPosition(16); + testArray.readBits(result, 0, 9); + assertEquals(0x5F, result[0]); + assertEquals(0x7F, result[1]); + } + public void testRead32BitsByteAligned() { - ParsableBitArray testArray = new ParsableBitArray(TEST_DATA); assertEquals(getTestDataBits(0, 32), testArray.readBits(32)); assertEquals(getTestDataBits(32, 32), testArray.readBits(32)); } public void testRead32BitsNonByteAligned() { - ParsableBitArray testArray = new ParsableBitArray(TEST_DATA); assertEquals(getTestDataBits(0, 5), testArray.readBits(5)); assertEquals(getTestDataBits(5, 32), testArray.readBits(32)); } public void testSkipBytes() { - ParsableBitArray testArray = new ParsableBitArray(TEST_DATA); testArray.skipBytes(2); - assertReadBitsToEnd(16, testArray); + assertReadBitsToEnd(16); } public void testSkipBitsByteAligned() { - ParsableBitArray testArray = new ParsableBitArray(TEST_DATA); testArray.skipBits(16); - assertReadBitsToEnd(16, testArray); + assertReadBitsToEnd(16); } public void testSkipBitsNonByteAligned() { - ParsableBitArray testArray = new ParsableBitArray(TEST_DATA); testArray.skipBits(5); - assertReadBitsToEnd(5, testArray); + assertReadBitsToEnd(5); } public void testSetPositionByteAligned() { - ParsableBitArray testArray = new ParsableBitArray(TEST_DATA); testArray.setPosition(16); - assertReadBitsToEnd(16, testArray); + assertReadBitsToEnd(16); } public void testSetPositionNonByteAligned() { - ParsableBitArray testArray = new ParsableBitArray(TEST_DATA); testArray.setPosition(5); - assertReadBitsToEnd(5, testArray); + assertReadBitsToEnd(5); } public void testByteAlignFromNonByteAligned() { - ParsableBitArray testArray = new ParsableBitArray(TEST_DATA); testArray.setPosition(11); testArray.byteAlign(); assertEquals(2, testArray.getBytePosition()); assertEquals(16, testArray.getPosition()); - assertReadBitsToEnd(16, testArray); + assertReadBitsToEnd(16); } public void testByteAlignFromByteAligned() { - ParsableBitArray testArray = new ParsableBitArray(TEST_DATA); testArray.setPosition(16); testArray.byteAlign(); // Should be a no-op. assertEquals(2, testArray.getBytePosition()); assertEquals(16, testArray.getPosition()); - assertReadBitsToEnd(16, testArray); + assertReadBitsToEnd(16); } - private static void assertReadBitsToEnd(int expectedStartPosition, ParsableBitArray testArray) { + private void assertReadBitsToEnd(int expectedStartPosition) { int position = testArray.getPosition(); assertEquals(expectedStartPosition, position); for (int i = position; i < TEST_DATA.length * 8; i++) { diff --git a/library/core/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 index 40cfd7f8d9..bd013f96a3 100644 --- a/library/core/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 @@ -94,9 +94,12 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact case TsExtractor.TS_STREAM_TYPE_MPA: case TsExtractor.TS_STREAM_TYPE_MPA_LSF: return new PesReader(new MpegAudioReader(esInfo.language)); - case TsExtractor.TS_STREAM_TYPE_AAC: + case TsExtractor.TS_STREAM_TYPE_AAC_ADTS: return isSet(FLAG_IGNORE_AAC_STREAM) ? null : new PesReader(new AdtsReader(false, esInfo.language)); + case TsExtractor.TS_STREAM_TYPE_AAC_LATM: + return isSet(FLAG_IGNORE_AAC_STREAM) + ? null : new PesReader(new LatmReader(esInfo.language)); case TsExtractor.TS_STREAM_TYPE_AC3: case TsExtractor.TS_STREAM_TYPE_E_AC3: return new PesReader(new Ac3Reader(esInfo.language)); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java new file mode 100644 index 0000000000..425dc43ea7 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java @@ -0,0 +1,306 @@ +/* + * 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 android.support.annotation.Nullable; +import android.util.Pair; +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.TrackIdGenerator; +import com.google.android.exoplayer2.util.CodecSpecificDataUtil; +import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.ParsableBitArray; +import com.google.android.exoplayer2.util.ParsableByteArray; +import java.util.Collections; + +/** + * Parses and extracts samples from an AAC/LATM elementary stream. + */ +public final class LatmReader implements ElementaryStreamReader { + + private static final int STATE_FINDING_SYNC_1 = 0; + private static final int STATE_FINDING_SYNC_2 = 1; + private static final int STATE_READING_HEADER = 2; + private static final int STATE_READING_SAMPLE = 3; + + private static final int INITIAL_BUFFER_SIZE = 1024; + private static final int SYNC_BYTE_FIRST = 0x56; + private static final int SYNC_BYTE_SECOND = 0xE0; + + private final String language; + private final ParsableByteArray sampleDataBuffer; + private final ParsableBitArray sampleBitArray; + + // Track output info. + private TrackOutput output; + private Format format; + private String formatId; + + // Parser state info. + private int state; + private int bytesRead; + private int sampleSize; + private int secondHeaderByte; + private long timeUs; + + // Container data. + private boolean streamMuxRead; + private int audioMuxVersion; + private int audioMuxVersionA; + private int numSubframes; + private int frameLengthType; + private boolean otherDataPresent; + private long otherDataLenBits; + private int sampleRateHz; + private long sampleDurationUs; + private int channelCount; + + /** + * @param language Track language. + */ + public LatmReader(@Nullable String language) { + this.language = language; + sampleDataBuffer = new ParsableByteArray(INITIAL_BUFFER_SIZE); + sampleBitArray = new ParsableBitArray(sampleDataBuffer.data); + } + + @Override + public void seek() { + state = STATE_FINDING_SYNC_1; + streamMuxRead = false; + } + + @Override + public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { + idGenerator.generateNewId(); + output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_AUDIO); + formatId = idGenerator.getFormatId(); + } + + @Override + public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { + timeUs = pesTimeUs; + } + + @Override + public void consume(ParsableByteArray data) { + int bytesToRead; + while (data.bytesLeft() > 0) { + switch (state) { + case STATE_FINDING_SYNC_1: + if (data.readUnsignedByte() == SYNC_BYTE_FIRST) { + state = STATE_FINDING_SYNC_2; + } + break; + case STATE_FINDING_SYNC_2: + int secondByte = data.readUnsignedByte(); + if ((secondByte & SYNC_BYTE_SECOND) == SYNC_BYTE_SECOND) { + secondHeaderByte = secondByte; + state = STATE_READING_HEADER; + } else if (secondByte != SYNC_BYTE_FIRST) { + state = STATE_FINDING_SYNC_1; + } + break; + case STATE_READING_HEADER: + sampleSize = ((secondHeaderByte & ~SYNC_BYTE_SECOND) << 8) | data.readUnsignedByte(); + if (sampleSize > sampleDataBuffer.data.length) { + resetBufferForSize(sampleSize); + } + bytesRead = 0; + state = STATE_READING_SAMPLE; + break; + case STATE_READING_SAMPLE: + bytesToRead = Math.min(data.bytesLeft(), sampleSize - bytesRead); + data.readBytes(sampleBitArray.data, bytesRead, bytesToRead); + bytesRead += bytesToRead; + if (bytesRead == sampleSize) { + sampleBitArray.setPosition(0); + parseAudioMuxElement(sampleBitArray); + state = STATE_FINDING_SYNC_1; + } + break; + } + } + } + + @Override + public void packetFinished() { + // Do nothing. + } + + /** + * Parses an AudioMuxElement as defined in 14496-3:2009, Section 1.7.3.1, Table 1.41. + * + * @param data A {@link ParsableBitArray} containing the AudioMuxElement's bytes. + */ + private void parseAudioMuxElement(ParsableBitArray data) { + boolean useSameStreamMux = data.readBit(); + if (!useSameStreamMux) { + streamMuxRead = true; + parseStreamMuxConfig(data); + } else if (!streamMuxRead) { + return; // Parsing cannot continue without StreamMuxConfig information. + } + + if (audioMuxVersionA == 0) { + if (numSubframes != 0) { + throw new UnsupportedOperationException(); + } + int muxSlotLengthBytes = parsePayloadLengthInfo(data); + parsePayloadMux(data, muxSlotLengthBytes); + if (otherDataPresent) { + data.skipBits((int) otherDataLenBits); + } + } else { + throw new UnsupportedOperationException(); // Not defined by ISO/IEC 14496-3:2009. + } + } + + /** + * Parses a StreamMuxConfig as defined in ISO/IEC 14496-3:2009 Section 1.7.3.1, Table 1.42. + */ + private void parseStreamMuxConfig(ParsableBitArray data) { + audioMuxVersion = data.readBits(1); + audioMuxVersionA = audioMuxVersion == 1 ? data.readBits(1) : 0; + if (audioMuxVersionA == 0) { + if (audioMuxVersion == 1) { + latmGetValue(data); // Skip taraBufferFullness. + } + if (!data.readBit()) { + throw new UnsupportedOperationException(); + } + numSubframes = data.readBits(6); + int numProgram = data.readBits(4); + int numLayer = data.readBits(3); + if (numProgram != 0 || numLayer != 0) { + throw new UnsupportedOperationException(); + } + if (audioMuxVersion == 0) { + int startPosition = data.getPosition(); + int readBits = parseAudioSpecificConfig(data); + data.setPosition(startPosition); + byte[] initData = new byte[(readBits + 7) / 8]; + data.readBits(initData, 0, readBits); + Format format = Format.createAudioSampleFormat(formatId, MimeTypes.AUDIO_AAC, null, + Format.NO_VALUE, Format.NO_VALUE, channelCount, sampleRateHz, + Collections.singletonList(initData), null, 0, language); + if (!format.equals(this.format)) { + this.format = format; + sampleDurationUs = (C.MICROS_PER_SECOND * 1024) / format.sampleRate; + output.format(format); + } + } else { + int ascLen = (int) latmGetValue(data); + int bitsRead = parseAudioSpecificConfig(data); + data.skipBits(ascLen - bitsRead); // fillBits. + } + parseFrameLength(data); + otherDataPresent = data.readBit(); + otherDataLenBits = 0; + if (otherDataPresent) { + if (audioMuxVersion == 1) { + otherDataLenBits = latmGetValue(data); + } else { + boolean otherDataLenEsc; + do { + otherDataLenEsc = data.readBit(); + otherDataLenBits = (otherDataLenBits << 8) + data.readBits(8); + } while (otherDataLenEsc); + } + } + boolean crcCheckPresent = data.readBit(); + if (crcCheckPresent) { + data.skipBits(8); // crcCheckSum. + } + } else { + throw new UnsupportedOperationException(); // This is not defined by ISO/IEC 14496-3:2009. + } + } + + private void parseFrameLength(ParsableBitArray data) { + frameLengthType = data.readBits(3); + switch (frameLengthType) { + case 0: + data.skipBits(8); // latmBufferFullness. + break; + case 1: + data.skipBits(9); // frameLength. + break; + case 3: + case 4: + case 5: + data.skipBits(6); // CELPframeLengthTableIndex. + break; + case 6: + case 7: + data.skipBits(1); // HVXCframeLengthTableIndex. + break; + } + } + + private int parseAudioSpecificConfig(ParsableBitArray data) { + int bitsLeft = data.bitsLeft(); + Pair config = CodecSpecificDataUtil.parseAacAudioSpecificConfig(data); + sampleRateHz = config.first; + channelCount = config.second; + return bitsLeft - data.bitsLeft(); + } + + private int parsePayloadLengthInfo(ParsableBitArray data) { + int muxSlotLengthBytes = 0; + // Assuming single program and single layer. + if (frameLengthType == 0) { + int tmp; + do { + tmp = data.readBits(8); + muxSlotLengthBytes += tmp; + } while (tmp == 255); + return muxSlotLengthBytes; + } else { + throw new UnsupportedOperationException(); + } + } + + private void parsePayloadMux(ParsableBitArray data, int muxLengthBytes) { + // The start of sample data in + int bitPosition = data.getPosition(); + if ((bitPosition & 0x07) == 0) { + // Sample data is byte-aligned. We can output it directly. + sampleDataBuffer.setPosition(bitPosition >> 3); + } else { + // Sample data is not byte-aligned and we need align it ourselves before outputting. + // Byte alignment is needed because LATM framing is not supported by MediaCodec. + data.readBits(sampleDataBuffer.data, 0, muxLengthBytes * 8); + sampleDataBuffer.setPosition(0); + } + output.sampleData(sampleDataBuffer, muxLengthBytes); + output.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, muxLengthBytes, 0, null); + timeUs += sampleDurationUs; + } + + private void resetBufferForSize(int newSize) { + sampleDataBuffer.reset(newSize); + sampleBitArray.reset(sampleDataBuffer.data); + } + + private static long latmGetValue(ParsableBitArray data) { + int bytesForValue = data.readBits(2); + return data.readBits((bytesForValue + 1) * 8); + } + +} diff --git a/library/core/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 index 2929b8a076..90506ab2f6 100644 --- a/library/core/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 @@ -84,7 +84,8 @@ public final class TsExtractor implements Extractor { public static final int TS_STREAM_TYPE_MPA = 0x03; public static final int TS_STREAM_TYPE_MPA_LSF = 0x04; - public static final int TS_STREAM_TYPE_AAC = 0x0F; + public static final int TS_STREAM_TYPE_AAC_ADTS = 0x0F; + public static final int TS_STREAM_TYPE_AAC_LATM = 0x11; public static final int TS_STREAM_TYPE_AC3 = 0x81; public static final int TS_STREAM_TYPE_DTS = 0x8A; public static final int TS_STREAM_TYPE_HDMV_DTS = 0x82; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/CodecSpecificDataUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/util/CodecSpecificDataUtil.java index 0093c3b826..468e8dd666 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/CodecSpecificDataUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/CodecSpecificDataUtil.java @@ -83,11 +83,21 @@ public final class CodecSpecificDataUtil { /** * Parses an AudioSpecificConfig, as defined in ISO 14496-3 1.6.2.1 * - * @param audioSpecificConfig The AudioSpecificConfig to parse. + * @param audioSpecificConfig A byte array containing the AudioSpecificConfig to parse. * @return A pair consisting of the sample rate in Hz and the channel count. */ public static Pair parseAacAudioSpecificConfig(byte[] audioSpecificConfig) { - ParsableBitArray bitArray = new ParsableBitArray(audioSpecificConfig); + return parseAacAudioSpecificConfig(new ParsableBitArray(audioSpecificConfig)); + } + + /** + * Parses an AudioSpecificConfig, as defined in ISO 14496-3 1.6.2.1 + * + * @param bitArray A {@link ParsableBitArray} containing the AudioSpecificConfig to parse. The + * position is advanced to the end of the AudioSpecificConfig. + * @return A pair consisting of the sample rate in Hz and the channel count. + */ + public static Pair parseAacAudioSpecificConfig(ParsableBitArray bitArray) { int audioObjectType = getAacAudioObjectType(bitArray); int sampleRate = getAacSamplingFrequency(bitArray); int channelConfiguration = bitArray.readBits(4); @@ -104,6 +114,39 @@ public final class CodecSpecificDataUtil { channelConfiguration = bitArray.readBits(4); } } + + switch (audioObjectType) { + case 1: + case 2: + case 3: + case 4: + case 6: + case 7: + case 17: + case 19: + case 20: + case 21: + case 22: + case 23: + parseGaSpecificConfig(bitArray, audioObjectType, channelConfiguration); + break; + default: + throw new UnsupportedOperationException(); + } + switch (audioObjectType) { + case 17: + case 19: + case 20: + case 21: + case 22: + case 23: + int epConfig = bitArray.readBits(2); + if (epConfig == 2 || epConfig == 3) { + throw new UnsupportedOperationException(); + } + break; + } + // For supported containers, bits_to_decode() is always 0. int channelCount = AUDIO_SPECIFIC_CONFIG_CHANNEL_COUNT_TABLE[channelConfiguration]; Assertions.checkArgument(channelCount != AUDIO_SPECIFIC_CONFIG_CHANNEL_CONFIGURATION_INVALID); return Pair.create(sampleRate, channelCount); @@ -269,4 +312,32 @@ public final class CodecSpecificDataUtil { return samplingFrequency; } + private static void parseGaSpecificConfig(ParsableBitArray bitArray, int audioObjectType, + int channelConfiguration) { + bitArray.skipBits(1); // frameLengthFlag. + boolean dependsOnCoreDecoder = bitArray.readBit(); + if (dependsOnCoreDecoder) { + bitArray.skipBits(14); // coreCoderDelay. + } + boolean extensionFlag = bitArray.readBit(); + if (channelConfiguration == 0) { + throw new UnsupportedOperationException(); // TODO: Implement programConfigElement(); + } + if (audioObjectType == 6 || audioObjectType == 20) { + bitArray.skipBits(3); // layerNr. + } + if (extensionFlag) { + if (audioObjectType == 22) { + bitArray.skipBits(16); // numOfSubFrame (5), layer_length(11). + } + if (audioObjectType == 17 || audioObjectType == 19 || audioObjectType == 20 + || audioObjectType == 23) { + // aacSectionDataResilienceFlag, aacScalefactorDataResilienceFlag, + // aacSpectralDataResilienceFlag. + bitArray.skipBits(3); + } + bitArray.skipBits(1); // extensionFlag3. + } + } + } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java b/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java index 0456bcb879..e0238cb14d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java @@ -155,6 +155,9 @@ public final class ParsableBitArray { * @return An integer whose bottom n bits hold the read data. */ public int readBits(int numBits) { + if (numBits == 0) { + return 0; + } int returnValue = 0; bitOffset += numBits; while (bitOffset > 8) { @@ -171,6 +174,40 @@ public final class ParsableBitArray { return returnValue; } + /** + * Reads {@code numBits} bits into {@code buffer}. + * + * @param buffer The array into which the read data should be written. The trailing + * {@code numBits % 8} bits are written into the most significant bits of the last modified + * {@code buffer} byte. The remaining ones are unmodified. + * @param offset The offset in {@code buffer} at which the read data should be written. + * @param numBits The number of bits to read. + */ + public void readBits(byte[] buffer, int offset, int numBits) { + // Whole bytes. + int to = offset + (numBits >> 3) /* numBits / 8 */; + for (int i = offset; i < to; i++) { + buffer[i] = (byte) (data[byteOffset++] << bitOffset); + buffer[i] |= (data[byteOffset] & 0xFF) >> (8 - bitOffset); + } + // Trailing bits. + int bitsLeft = numBits & 7 /* numBits % 8 */; + buffer[to] &= 0xFF >> bitsLeft; // Set to 0 the bits that are going to be overwritten. + if (bitOffset + bitsLeft > 8) { + // We read the rest of data[byteOffset] and increase byteOffset. + buffer[to] |= (byte) ((data[byteOffset++] & 0xFF) << bitOffset); + bitOffset -= 8; + } + bitOffset += bitsLeft; + int lastDataByteTrailingBits = (data[byteOffset] & 0xFF) >> (8 - bitOffset); + buffer[to] |= (byte) (lastDataByteTrailingBits << (8 - bitsLeft)); + if (bitOffset == 8) { + bitOffset = 0; + byteOffset++; + } + assertValidOffset(); + } + /** * Aligns the position to the next byte boundary. Does nothing if the position is already aligned. */ From ad5ca0812d532645a60407881244df6ecad0e957 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 27 Jul 2017 08:15:15 -0700 Subject: [PATCH 0125/2472] Add unit test for FakeDataSource. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=163341994 --- .../com/google/android/exoplayer2/testutil/FakeDataSet.java | 4 ++-- .../google/android/exoplayer2/testutil/FakeDataSource.java | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSet.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSet.java index 2580205361..fd85b02d78 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSet.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSet.java @@ -162,8 +162,8 @@ public class FakeDataSet { } /** - * Appends data of the specified length. No actual data is available and this data should not - * be read. + * Appends a data segment of the specified length. No actual data is available and the + * {@link FakeDataSource} will perform no copy operations when this data is read. */ public FakeData appendReadData(int length) { Assertions.checkState(length > 0); 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 6180a8aa77..aacd265e45 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 @@ -166,6 +166,7 @@ public class FakeDataSource implements DataSource { // Do not allow crossing of the segment boundary. readLength = Math.min(readLength, current.length - current.bytesRead); // Perform the read and return. + Assertions.checkArgument(buffer.length - offset >= readLength); if (current.data != null) { System.arraycopy(current.data, current.bytesRead, buffer, offset, readLength); } From c3d7ccc3cb00bde4f1c2ae40d2e5a12ef8e494a8 Mon Sep 17 00:00:00 2001 From: eguven Date: Thu, 27 Jul 2017 08:30:25 -0700 Subject: [PATCH 0126/2472] Replace iterable foreach loops with indexed for loops ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=163343347 --- .../source/DynamicConcatenatingMediaSource.java | 8 ++++---- .../exoplayer2/upstream/cache/CachedContentIndex.java | 8 ++++---- .../android/exoplayer2/upstream/cache/SimpleCache.java | 7 +++---- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java index ad2e154f6d..79f7d8dd48 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java @@ -186,8 +186,8 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl @Override public void maybeThrowSourceInfoRefreshError() throws IOException { - for (MediaSourceHolder mediaSourceHolder : mediaSourceHolders) { - mediaSourceHolder.mediaSource.maybeThrowSourceInfoRefreshError(); + for (int i = 0; i < mediaSourceHolders.size(); i++) { + mediaSourceHolders.get(i).mediaSource.maybeThrowSourceInfoRefreshError(); } } @@ -221,8 +221,8 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl @Override public void releaseSource() { - for (MediaSourceHolder mediaSourceHolder : mediaSourceHolders) { - mediaSourceHolder.mediaSource.releaseSource(); + for (int i = 0; i < mediaSourceHolders.size(); i++) { + mediaSourceHolders.get(i).mediaSource.releaseSource(); } } diff --git a/library/core/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 index 58cc70d68d..10bc298579 100644 --- a/library/core/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 @@ -34,9 +34,9 @@ import java.io.OutputStream; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; -import java.util.LinkedList; import java.util.Random; import java.util.Set; import javax.crypto.Cipher; @@ -176,14 +176,14 @@ import javax.crypto.spec.SecretKeySpec; /** Removes empty {@link CachedContent} instances from index. */ public void removeEmpty() { - LinkedList cachedContentToBeRemoved = new LinkedList<>(); + ArrayList cachedContentToBeRemoved = new ArrayList<>(); for (CachedContent cachedContent : keyToContent.values()) { if (cachedContent.isEmpty()) { cachedContentToBeRemoved.add(cachedContent.key); } } - for (String key : cachedContentToBeRemoved) { - removeEmpty(key); + for (int i = 0; i < cachedContentToBeRemoved.size(); i++) { + removeEmpty(cachedContentToBeRemoved.get(i)); } } diff --git a/library/core/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 index 2da6ba759b..62bd2783b8 100644 --- a/library/core/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 @@ -22,7 +22,6 @@ import java.io.File; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; -import java.util.LinkedList; import java.util.NavigableSet; import java.util.Set; import java.util.TreeSet; @@ -308,7 +307,7 @@ public final class SimpleCache implements Cache { * no longer exist. */ private void removeStaleSpansAndCachedContents() throws CacheException { - LinkedList spansToBeRemoved = new LinkedList<>(); + ArrayList spansToBeRemoved = new ArrayList<>(); for (CachedContent cachedContent : index.getAll()) { for (CacheSpan span : cachedContent.getSpans()) { if (!span.file.exists()) { @@ -316,9 +315,9 @@ public final class SimpleCache implements Cache { } } } - for (CacheSpan span : spansToBeRemoved) { + for (int i = 0; i < spansToBeRemoved.size(); i++) { // Remove span but not CachedContent to prevent multiple index.store() calls. - removeSpan(span, false); + removeSpan(spansToBeRemoved.get(i), false); } index.removeEmpty(); index.store(); From 5bd5af8c60b1996fd819d0250f7dce8e1d3f46e3 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 27 Jul 2017 08:34:46 -0700 Subject: [PATCH 0127/2472] Replace canStop with blockUntilEnded in HostedActiviy. The canStop method gets called every second in a seperate thread until the test can be stopped. Replacing it with blockUntilEnded forwards the responsibility to stop to the test itself which has better control over the stop condition. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=163343824 --- .../exoplayer2/testutil/ExoHostedTest.java | 15 ++-- .../exoplayer2/testutil/HostActivity.java | 69 +++++-------------- 2 files changed, 28 insertions(+), 56 deletions(-) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java index b61b484e32..4aba23d691 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.testutil; +import android.os.ConditionVariable; import android.os.Handler; import android.os.SystemClock; import android.util.Log; @@ -72,6 +73,7 @@ public abstract class ExoHostedTest implements HostedTest, Player.EventListener, private final long expectedPlayingTimeMs; private final DecoderCounters videoDecoderCounters; private final DecoderCounters audioDecoderCounters; + private final ConditionVariable playerFinished; private ActionSchedule pendingSchedule; private Handler actionHandler; @@ -81,7 +83,7 @@ public abstract class ExoHostedTest implements HostedTest, Player.EventListener, private ExoPlaybackException playerError; private Player.EventListener playerEventListener; private boolean playerWasPrepared; - private boolean playerFinished; + private boolean playing; private long totalPlayingTimeMs; private long lastPlayingStartTimeMs; @@ -114,8 +116,9 @@ public abstract class ExoHostedTest implements HostedTest, Player.EventListener, this.tag = tag; this.expectedPlayingTimeMs = expectedPlayingTimeMs; this.failOnPlayerError = failOnPlayerError; - videoDecoderCounters = new DecoderCounters(); - audioDecoderCounters = new DecoderCounters(); + this.playerFinished = new ConditionVariable(); + this.videoDecoderCounters = new DecoderCounters(); + this.audioDecoderCounters = new DecoderCounters(); } /** @@ -169,8 +172,8 @@ public abstract class ExoHostedTest implements HostedTest, Player.EventListener, } @Override - public final boolean canStop() { - return playerFinished; + public final boolean blockUntilEnded(long timeoutMs) { + return playerFinished.block(timeoutMs); } @Override @@ -219,7 +222,7 @@ public abstract class ExoHostedTest implements HostedTest, Player.EventListener, playerWasPrepared |= playbackState != Player.STATE_IDLE; if (playbackState == Player.STATE_ENDED || (playbackState == Player.STATE_IDLE && playerWasPrepared)) { - playerFinished = true; + playerFinished.open(); } boolean playing = playWhenReady && playbackState == Player.STATE_READY; if (!this.playing && playing) { diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/HostActivity.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/HostActivity.java index 831344aa8b..e1f7eae379 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/HostActivity.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/HostActivity.java @@ -24,7 +24,6 @@ import android.net.wifi.WifiManager; import android.net.wifi.WifiManager.WifiLock; import android.os.Bundle; import android.os.ConditionVariable; -import android.os.Handler; import android.os.PowerManager; import android.os.PowerManager.WakeLock; import android.util.Log; @@ -57,17 +56,19 @@ public final class HostActivity extends Activity implements SurfaceHolder.Callba void onStart(HostActivity host, Surface surface); /** - * Called on the main thread to check whether the test is ready to be stopped. + * Called on the main thread to block until the test has stopped or {@link #onStop()} is called. * - * @return Whether the test is ready to be stopped. + * @param timeoutMs The maximum time to block in milliseconds. + * @return Whether the test has stopped successful. */ - boolean canStop(); + boolean blockUntilEnded(long timeoutMs); /** * Called on the main thread when the test is stopped. *

      - * The test will be stopped if {@link #canStop()} returns true, if the {@link HostActivity} has - * been paused, or if the {@link HostActivity}'s {@link Surface} has been destroyed. + * The test will be stopped when {@link #blockUntilEnded(long)} returns, if the + * {@link HostActivity} has been paused, or if the {@link HostActivity}'s {@link Surface} has + * been destroyed. */ void onStop(); @@ -85,13 +86,10 @@ public final class HostActivity extends Activity implements SurfaceHolder.Callba private WakeLock wakeLock; private WifiLock wifiLock; private SurfaceView surfaceView; - private Handler mainHandler; - private CheckCanStopRunnable checkCanStopRunnable; private HostedTest hostedTest; - private ConditionVariable hostedTestStoppedCondition; private boolean hostedTestStarted; - private boolean hostedTestFinished; + private boolean forcedFinished; /** * Executes a {@link HostedTest} inside the host. @@ -100,7 +98,7 @@ public final class HostActivity extends Activity implements SurfaceHolder.Callba * @param timeoutMs The number of milliseconds to wait for the test to finish. If the timeout * is exceeded then the test will fail. */ - public void runTest(final HostedTest hostedTest, long timeoutMs) { + public void runTest(HostedTest hostedTest, long timeoutMs) { runTest(hostedTest, timeoutMs, true); } @@ -111,27 +109,31 @@ public final class HostActivity extends Activity implements SurfaceHolder.Callba * @param timeoutMs The number of milliseconds to wait for the test to finish. * @param failOnTimeout Whether the test fails when the timeout is exceeded. */ - public void runTest(final HostedTest hostedTest, long timeoutMs, boolean failOnTimeout) { + public void runTest(HostedTest hostedTest, long timeoutMs, boolean failOnTimeout) { Assertions.checkArgument(timeoutMs > 0); Assertions.checkState(Thread.currentThread() != getMainLooper().getThread()); Assertions.checkState(this.hostedTest == null); this.hostedTest = Assertions.checkNotNull(hostedTest); - hostedTestStoppedCondition = new ConditionVariable(); hostedTestStarted = false; - hostedTestFinished = false; + forcedFinished = false; + final ConditionVariable testStarted = new ConditionVariable(); runOnUiThread(new Runnable() { @Override public void run() { maybeStartHostedTest(); + testStarted.open(); } }); + testStarted.block(); - if (hostedTestStoppedCondition.block(timeoutMs)) { - if (hostedTestFinished) { + if (hostedTest.blockUntilEnded(timeoutMs)) { + hostedTest.onStop(); + if (!forcedFinished) { Log.d(TAG, "Test finished. Checking pass conditions."); hostedTest.onFinished(); + this.hostedTest = null; Log.d(TAG, "Pass conditions checked."); } else { String message = "Test released before it finished. Activity may have been paused whilst " @@ -146,7 +148,6 @@ public final class HostActivity extends Activity implements SurfaceHolder.Callba fail(message); } maybeStopHostedTest(); - hostedTestStoppedCondition.block(); } } @@ -160,8 +161,6 @@ public final class HostActivity extends Activity implements SurfaceHolder.Callba surfaceView = (SurfaceView) findViewById( getResources().getIdentifier("surface_view", "id", getPackageName())); surfaceView.getHolder().addCallback(this); - mainHandler = new Handler(); - checkCanStopRunnable = new CheckCanStopRunnable(); } @Override @@ -225,24 +224,14 @@ public final class HostActivity extends Activity implements SurfaceHolder.Callba hostedTestStarted = true; Log.d(TAG, "Starting test."); hostedTest.onStart(this, surface); - checkCanStopRunnable.startChecking(); } } private void maybeStopHostedTest() { if (hostedTest != null && hostedTestStarted) { + forcedFinished = true; hostedTest.onStop(); hostedTest = null; - mainHandler.removeCallbacks(checkCanStopRunnable); - // We post opening of the stopped condition so that any events posted to the main thread as a - // result of hostedTest.onStop() are guaranteed to be handled before hostedTest.onFinished() - // is called from runTest. - mainHandler.post(new Runnable() { - @Override - public void run() { - hostedTestStoppedCondition.open(); - } - }); } } @@ -251,24 +240,4 @@ public final class HostActivity extends Activity implements SurfaceHolder.Callba return Util.SDK_INT < 12 ? WifiManager.WIFI_MODE_FULL : WifiManager.WIFI_MODE_FULL_HIGH_PERF; } - private final class CheckCanStopRunnable implements Runnable { - - private static final long CHECK_INTERVAL_MS = 1000; - - private void startChecking() { - mainHandler.post(this); - } - - @Override - public void run() { - if (hostedTest.canStop()) { - hostedTestFinished = true; - maybeStopHostedTest(); - } else { - mainHandler.postDelayed(this, CHECK_INTERVAL_MS); - } - } - - } - } From bb044563245b3cf41772cf73ee5c75d84633f00b Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Fri, 28 Jul 2017 04:39:57 -0700 Subject: [PATCH 0128/2472] Fix IndexOutOfBounds when reading a multiple of 8 number of bits ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=163455269 --- .../android/exoplayer2/util/ParsableBitArrayTest.java | 6 +++++- .../google/android/exoplayer2/util/ParsableBitArray.java | 3 +++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ParsableBitArrayTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ParsableBitArrayTest.java index fc8318d826..d7b2b36740 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ParsableBitArrayTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ParsableBitArrayTest.java @@ -87,9 +87,13 @@ public final class ParsableBitArrayTest extends TestCase { result[1] = 0; testArray.readBits(result, 1, 0); assertEquals(0, result[1]); + // Test reading a number of bits divisible by 8. + testArray.setPosition(0); + testArray.readBits(result, 0, 16); + assertEquals(TEST_DATA[0], result[0]); + assertEquals(TEST_DATA[1], result[1]); // Test least significant bits are unmodified. result[1] = (byte) 0xFF; - testArray.setPosition(16); testArray.readBits(result, 0, 9); assertEquals(0x5F, result[0]); assertEquals(0x7F, result[1]); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java b/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java index e0238cb14d..fdee7fb5e6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java @@ -192,6 +192,9 @@ public final class ParsableBitArray { } // Trailing bits. int bitsLeft = numBits & 7 /* numBits % 8 */; + if (bitsLeft == 0) { + return; + } buffer[to] &= 0xFF >> bitsLeft; // Set to 0 the bits that are going to be overwritten. if (bitOffset + bitsLeft > 8) { // We read the rest of data[byteOffset] and increase byteOffset. From 1f0c85bd73c3ba7bfe8ce913fe22f2a69bb9112a Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 28 Jul 2017 04:45:52 -0700 Subject: [PATCH 0129/2472] Allow apps to handle ad clicked/tapped events Issue: #3106 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=163455563 --- .../exoplayer2/ext/ima/ImaAdsLoader.java | 22 ++++++++++- .../exoplayer2/ext/ima/ImaAdsMediaSource.java | 38 +++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index bd3f6df826..d801ba182d 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -61,7 +61,7 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, /** * Listener for ad loader events. All methods are called on the main thread. */ - public interface EventListener { + /* package */ interface EventListener { /** * Called when the ad playback state has been updated. @@ -77,6 +77,16 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, */ void onLoadError(IOException error); + /** + * Called when the user clicks through an ad (for example, following a 'learn more' link). + */ + void onAdClicked(); + + /** + * Called when the user taps a non-clickthrough part of an ad. + */ + void onAdTapped(); + } static { @@ -337,6 +347,16 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, imaPausedContent = true; pauseContentInternal(); break; + case TAPPED: + if (eventListener != null) { + eventListener.onAdTapped(); + } + break; + case CLICKED: + if (eventListener != null) { + eventListener.onAdClicked(); + } + break; case CONTENT_RESUME_REQUESTED: imaPausedContent = false; resumeContentInternal(); diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java index 9c2eb4b404..0bf5773d2c 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java @@ -53,6 +53,16 @@ public final class ImaAdsMediaSource implements MediaSource { */ void onAdLoadError(IOException error); + /** + * Called when the user clicks through an ad (for example, following a 'learn more' link). + */ + void onAdClicked(); + + /** + * Called when the user taps a non-clickthrough part of an ad. + */ + void onAdTapped(); + } private static final String TAG = "ImaAdsMediaSource"; @@ -305,6 +315,34 @@ public final class ImaAdsMediaSource implements MediaSource { }); } + @Override + public void onAdClicked() { + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + if (!released) { + eventListener.onAdClicked(); + } + } + }); + } + } + + @Override + public void onAdTapped() { + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + if (!released) { + eventListener.onAdTapped(); + } + } + }); + } + } + } } From cdeea279739e4a5e9e418bee76051493ce6b8f5b Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 31 Jul 2017 03:30:09 -0700 Subject: [PATCH 0130/2472] Handle release() while initializing the ads manager Also don't detach any attached player in release() to prevent a possible NullPointerException if ImaAdsLoader.release() runs first, then the MediaSource is released and detaches the player. This is safe because if the loader was attached it's guaranteed to be detached. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=163673750 --- .../exoplayer2/ext/ima/ImaAdsLoader.java | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index d801ba182d..a1db09b3d8 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -187,6 +187,10 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, * Whether {@link #getContentProgress()} has sent {@link #pendingContentPositionMs} to IMA. */ private boolean sentPendingContentPositionMs; + /** + * Whether {@link #release()} has been called. + */ + private boolean released; /** * Creates a new IMA ads loader. @@ -252,7 +256,7 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, if (imaPausedContent) { adsManager.resume(); } - } else if (adTagUri != null) { + } else { requestAds(); } } @@ -278,12 +282,10 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, * Releases the loader. Must be called when the instance is no longer needed. */ public void release() { + released = true; if (adsManager != null) { adsManager.destroy(); adsManager = null; - if (player != null) { - detachPlayer(); - } } } @@ -291,7 +293,12 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, @Override public void onAdsManagerLoaded(AdsManagerLoadedEvent adsManagerLoadedEvent) { - adsManager = adsManagerLoadedEvent.getAdsManager(); + AdsManager adsManager = adsManagerLoadedEvent.getAdsManager(); + if (released) { + adsManager.destroy(); + return; + } + this.adsManager = adsManager; adsManager.addAdErrorListener(this); adsManager.addAdEventListener(this); if (ENABLE_PRELOADING) { From c2d7e05429afeead1cac0d441f748b18c7319c0b Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 31 Jul 2017 05:49:58 -0700 Subject: [PATCH 0131/2472] Propagate skipped input buffers through to CodecCounters ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=163683081 --- .../exoplayer2/source/SampleQueueTest.java | 24 +++++++++---------- .../android/exoplayer2/BaseRenderer.java | 5 ++-- .../exoplayer2/decoder/DecoderCounters.java | 7 ++++++ .../mediacodec/MediaCodecRenderer.java | 2 +- .../source/ClippingMediaPeriod.java | 4 ++-- .../exoplayer2/source/EmptySampleStream.java | 4 ++-- .../source/ExtractorMediaPeriod.java | 16 +++++++------ .../source/SampleMetadataQueue.java | 23 ++++++++++-------- .../exoplayer2/source/SampleQueue.java | 16 +++++++++---- .../exoplayer2/source/SampleStream.java | 3 ++- .../source/SingleSampleMediaPeriod.java | 6 +++-- .../source/chunk/ChunkSampleStream.java | 22 +++++++++++------ .../source/hls/HlsSampleStream.java | 4 ++-- .../source/hls/HlsSampleStreamWrapper.java | 13 ++++++---- .../exoplayer2/ui/DebugTextViewHelper.java | 3 ++- .../exoplayer2/testutil/FakeSampleStream.java | 4 ++-- 16 files changed, 95 insertions(+), 61 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/SampleQueueTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/SampleQueueTest.java index 76ea0e34cf..77e61e39a9 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/SampleQueueTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/SampleQueueTest.java @@ -258,45 +258,45 @@ public class SampleQueueTest extends TestCase { public void testAdvanceToBeforeBuffer() { writeTestData(); - boolean result = sampleQueue.advanceTo(TEST_SAMPLE_TIMESTAMPS[0] - 1, true, false); + int skipCount = sampleQueue.advanceTo(TEST_SAMPLE_TIMESTAMPS[0] - 1, true, false); // Should fail and have no effect. - assertFalse(result); + assertEquals(SampleQueue.ADVANCE_FAILED, skipCount); assertReadTestData(); assertNoSamplesToRead(TEST_FORMAT_2); } public void testAdvanceToStartOfBuffer() { writeTestData(); - boolean result = sampleQueue.advanceTo(TEST_SAMPLE_TIMESTAMPS[0], true, false); + int skipCount = sampleQueue.advanceTo(TEST_SAMPLE_TIMESTAMPS[0], true, false); // Should succeed but have no effect (we're already at the first frame). - assertTrue(result); + assertEquals(0, skipCount); assertReadTestData(); assertNoSamplesToRead(TEST_FORMAT_2); } public void testAdvanceToEndOfBuffer() { writeTestData(); - boolean result = sampleQueue.advanceTo(LAST_SAMPLE_TIMESTAMP, true, false); - // Should succeed and skip to 2nd keyframe. - assertTrue(result); + int skipCount = sampleQueue.advanceTo(LAST_SAMPLE_TIMESTAMP, true, false); + // Should succeed and skip to 2nd keyframe (the 4th frame). + assertEquals(4, skipCount); assertReadTestData(null, TEST_DATA_SECOND_KEYFRAME_INDEX); assertNoSamplesToRead(TEST_FORMAT_2); } public void testAdvanceToAfterBuffer() { writeTestData(); - boolean result = sampleQueue.advanceTo(LAST_SAMPLE_TIMESTAMP + 1, true, false); + int skipCount = sampleQueue.advanceTo(LAST_SAMPLE_TIMESTAMP + 1, true, false); // Should fail and have no effect. - assertFalse(result); + assertEquals(SampleQueue.ADVANCE_FAILED, skipCount); assertReadTestData(); assertNoSamplesToRead(TEST_FORMAT_2); } public void testAdvanceToAfterBufferAllowed() { writeTestData(); - boolean result = sampleQueue.advanceTo(LAST_SAMPLE_TIMESTAMP + 1, true, true); - // Should succeed and skip to 2nd keyframe. - assertTrue(result); + int skipCount = sampleQueue.advanceTo(LAST_SAMPLE_TIMESTAMP + 1, true, true); + // Should succeed and skip to 2nd keyframe (the 4th frame). + assertEquals(4, skipCount); assertReadTestData(null, TEST_DATA_SECOND_KEYFRAME_INDEX); assertNoSamplesToRead(TEST_FORMAT_2); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java index a88a1dd615..7f14837965 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java @@ -296,9 +296,10 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { * {@code positionUs} is beyond it. * * @param positionUs The position in microseconds. + * @return The number of samples that were skipped. */ - protected void skipSource(long positionUs) { - stream.skipData(positionUs - streamOffsetUs); + protected int skipSource(long positionUs) { + return stream.skipData(positionUs - streamOffsetUs); } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/decoder/DecoderCounters.java b/library/core/src/main/java/com/google/android/exoplayer2/decoder/DecoderCounters.java index 3c2d6d96e9..7a532110d3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/decoder/DecoderCounters.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/decoder/DecoderCounters.java @@ -36,6 +36,12 @@ public final class DecoderCounters { * The number of queued input buffers. */ public int inputBufferCount; + /** + * The number of skipped input buffers. + *

      + * A skipped input buffer is an input buffer that was deliberately not sent to the decoder. + */ + public int skippedInputBufferCount; /** * The number of rendered output buffers. */ @@ -79,6 +85,7 @@ public final class DecoderCounters { decoderInitCount += other.decoderInitCount; decoderReleaseCount += other.decoderReleaseCount; inputBufferCount += other.inputBufferCount; + skippedInputBufferCount += other.skippedInputBufferCount; renderedOutputBufferCount += other.renderedOutputBufferCount; skippedOutputBufferCount += other.skippedOutputBufferCount; droppedOutputBufferCount += other.droppedOutputBufferCount; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java index 01229c1104..7c0549de25 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java @@ -530,7 +530,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { while (feedInputBuffer()) {} TraceUtil.endSection(); } else { - skipSource(positionUs); + decoderCounters.skippedInputBufferCount += 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 diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java index 12f58d9a21..a8c33b4625 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java @@ -286,8 +286,8 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb } @Override - public void skipData(long positionUs) { - stream.skipData(startUs + positionUs); + public int skipData(long positionUs) { + return stream.skipData(startUs + positionUs); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/EmptySampleStream.java b/library/core/src/main/java/com/google/android/exoplayer2/source/EmptySampleStream.java index 7aab22d8a0..299b816cc8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/EmptySampleStream.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/EmptySampleStream.java @@ -43,8 +43,8 @@ public final class EmptySampleStream implements SampleStream { } @Override - public void skipData(long positionUs) { - // Do nothing. + public int skipData(long positionUs) { + return 0; } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java index e7273f834b..511f7f4a8a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -238,7 +238,7 @@ import java.util.Arrays; // sample queue, or if we haven't read anything from the queue since the previous seek // (this case is common for sparse tracks such as metadata tracks). In all other cases a // seek is required. - seekRequired = !sampleQueue.advanceTo(positionUs, true, true) + seekRequired = sampleQueue.advanceTo(positionUs, true, true) == SampleQueue.ADVANCE_FAILED && sampleQueue.getReadIndex() != 0; } } @@ -371,12 +371,13 @@ import java.util.Arrays; lastSeekPositionUs); } - /* package */ void skipData(int track, long positionUs) { + /* package */ int skipData(int track, long positionUs) { SampleQueue sampleQueue = sampleQueues[track]; if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) { - sampleQueue.advanceToEnd(); + return sampleQueue.advanceToEnd(); } else { - sampleQueue.advanceTo(positionUs, true, true); + int skipCount = sampleQueue.advanceTo(positionUs, true, true); + return skipCount == SampleQueue.ADVANCE_FAILED ? 0 : skipCount; } } @@ -558,7 +559,8 @@ import java.util.Arrays; for (int i = 0; i < trackCount; i++) { SampleQueue sampleQueue = sampleQueues[i]; sampleQueue.rewind(); - boolean seekInsideQueue = sampleQueue.advanceTo(positionUs, true, false); + boolean seekInsideQueue = sampleQueue.advanceTo(positionUs, true, false) + != SampleQueue.ADVANCE_FAILED; // If we have AV tracks then an in-buffer seek is successful if the seek into every AV queue // is successful. We ignore whether seeks within non-AV queues are successful in this case, as // they may be sparse or poorly interleaved. If we only have non-AV tracks then a seek is @@ -632,8 +634,8 @@ import java.util.Arrays; } @Override - public void skipData(long positionUs) { - ExtractorMediaPeriod.this.skipData(track, positionUs); + public int skipData(long positionUs) { + return ExtractorMediaPeriod.this.skipData(track, positionUs); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java index 03b2e3b715..d70c59b195 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java @@ -253,32 +253,35 @@ import com.google.android.exoplayer2.util.Util; * @param allowTimeBeyondBuffer Whether the operation can succeed if {@code timeUs} is beyond the * end of the queue, by advancing the read position to the last sample (or keyframe) in the * queue. - * @return Whether the operation was a success. A successful advance is one in which the read - * position was unchanged or advanced, and is now at a sample meeting the specified criteria. + * @return The number of samples that were skipped if the operation was successful, which may be + * equal to 0, or {@link SampleQueue#ADVANCE_FAILED} if the operation was not successful. A + * successful advance is one in which the read position was unchanged or advanced, and is now + * at a sample meeting the specified criteria. */ - public synchronized boolean advanceTo(long timeUs, boolean toKeyframe, + public synchronized int advanceTo(long timeUs, boolean toKeyframe, boolean allowTimeBeyondBuffer) { int relativeReadIndex = getRelativeIndex(readPosition); if (!hasNextSample() || timeUs < timesUs[relativeReadIndex] || (timeUs > largestQueuedTimestampUs && !allowTimeBeyondBuffer)) { - return false; + return SampleQueue.ADVANCE_FAILED; } int offset = findSampleBefore(relativeReadIndex, length - readPosition, timeUs, toKeyframe); if (offset == -1) { - return false; + return SampleQueue.ADVANCE_FAILED; } readPosition += offset; - return true; + return offset; } /** * Advances the read position to the end of the queue. + * + * @return The number of samples that were skipped. */ - public synchronized void advanceToEnd() { - if (!hasNextSample()) { - return; - } + public synchronized int advanceToEnd() { + int skipCount = length - readPosition; readPosition = length; + return skipCount; } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java index c7bae8f8b4..b83cf7df5b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java @@ -49,6 +49,8 @@ public final class SampleQueue implements TrackOutput { } + public static final int ADVANCE_FAILED = -1; + private static final int INITIAL_SCRATCH_SIZE = 32; private final Allocator allocator; @@ -255,9 +257,11 @@ public final class SampleQueue implements TrackOutput { /** * Advances the read position to the end of the queue. + * + * @return The number of samples that were skipped. */ - public void advanceToEnd() { - metadataQueue.advanceToEnd(); + public int advanceToEnd() { + return metadataQueue.advanceToEnd(); } /** @@ -268,10 +272,12 @@ public final class SampleQueue implements TrackOutput { * time, rather than to any sample before or at that time. * @param allowTimeBeyondBuffer Whether the operation can succeed if {@code timeUs} is beyond the * end of the queue, by advancing the read position to the last sample (or keyframe). - * @return Whether the operation was a success. A successful advance is one in which the read - * position was unchanged or advanced, and is now at a sample meeting the specified criteria. + * @return The number of samples that were skipped if the operation was successful, which may be + * equal to 0, or {@link #ADVANCE_FAILED} if the operation was not successful. A successful + * advance is one in which the read position was unchanged or advanced, and is now at a sample + * meeting the specified criteria. */ - public boolean advanceTo(long timeUs, boolean toKeyframe, boolean allowTimeBeyondBuffer) { + public int advanceTo(long timeUs, boolean toKeyframe, boolean allowTimeBeyondBuffer) { return metadataQueue.advanceTo(timeUs, toKeyframe, allowTimeBeyondBuffer); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleStream.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleStream.java index dc58c29c22..06efc980e2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleStream.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleStream.java @@ -70,7 +70,8 @@ public interface SampleStream { * {@code positionUs} is beyond it. * * @param positionUs The specified time. + * @return The number of samples that were skipped. */ - void skipData(long positionUs); + int skipData(long positionUs); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java index 3435c01eeb..b19f398d86 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java @@ -235,10 +235,12 @@ import java.util.Arrays; } @Override - public void skipData(long positionUs) { - if (positionUs > 0) { + public int skipData(long positionUs) { + if (positionUs > 0 && streamState != STREAM_STATE_END_OF_STREAM) { streamState = STREAM_STATE_END_OF_STREAM; + return 1; } + return 0; } } diff --git a/library/core/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 index 0fc3d5881e..f2609a0ffd 100644 --- a/library/core/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 @@ -160,6 +160,7 @@ public class ChunkSampleStream implements SampleStream, S * @return An estimate of the absolute position in microseconds up to which data is buffered, or * {@link C#TIME_END_OF_SOURCE} if the track is fully buffered. */ + @Override public long getBufferedPositionUs() { if (loadingFinished) { return C.TIME_END_OF_SOURCE; @@ -185,8 +186,8 @@ public class ChunkSampleStream implements SampleStream, S public void seekToUs(long positionUs) { lastSeekPositionUs = positionUs; // If we're not pending a reset, see if we can seek within the primary sample queue. - boolean seekInsideBuffer = !isPendingReset() && primarySampleQueue.advanceTo(positionUs, true, - positionUs < getNextLoadPositionUs()); + boolean seekInsideBuffer = !isPendingReset() && (primarySampleQueue.advanceTo(positionUs, true, + positionUs < getNextLoadPositionUs()) != SampleQueue.ADVANCE_FAILED); if (seekInsideBuffer) { // We succeeded. Discard samples and corresponding chunks prior to the seek position. discardDownstreamMediaChunks(primarySampleQueue.getReadIndex()); @@ -266,13 +267,19 @@ public class ChunkSampleStream implements SampleStream, S } @Override - public void skipData(long positionUs) { + public int skipData(long positionUs) { + int skipCount; if (loadingFinished && positionUs > primarySampleQueue.getLargestQueuedTimestampUs()) { primarySampleQueue.advanceToEnd(); + skipCount = primarySampleQueue.advanceToEnd(); } else { - primarySampleQueue.advanceTo(positionUs, true, true); + skipCount = primarySampleQueue.advanceTo(positionUs, true, true); + if (skipCount == SampleQueue.ADVANCE_FAILED) { + skipCount = 0; + } } primarySampleQueue.discardToRead(); + return skipCount; } // Loader.Callback implementation. @@ -470,11 +477,12 @@ public class ChunkSampleStream implements SampleStream, S } @Override - public void skipData(long positionUs) { + public int skipData(long positionUs) { if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) { - sampleQueue.advanceToEnd(); + return sampleQueue.advanceToEnd(); } else { - sampleQueue.advanceTo(positionUs, true, true); + int skipCount = sampleQueue.advanceTo(positionUs, true, true); + return skipCount == SampleQueue.ADVANCE_FAILED ? 0 : skipCount; } } diff --git a/library/hls/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 index 450644f60f..e423a682f3 100644 --- a/library/hls/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 skipData(long positionUs) { - sampleStreamWrapper.skipData(group, positionUs); + public int skipData(long positionUs) { + return sampleStreamWrapper.skipData(group, positionUs); } } diff --git a/library/hls/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 index 0b6d1863bd..00a3cd4a85 100644 --- a/library/hls/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 @@ -229,7 +229,7 @@ import java.util.LinkedList; // sample queue, or if we haven't read anything from the queue since the previous seek // (this case is common for sparse tracks such as metadata tracks). In all other cases a // seek is required. - seekRequired = !sampleQueue.advanceTo(positionUs, true, true) + seekRequired = sampleQueue.advanceTo(positionUs, true, true) == SampleQueue.ADVANCE_FAILED && sampleQueue.getReadIndex() != 0; } } @@ -320,6 +320,7 @@ import java.util.LinkedList; return true; } + @Override public long getBufferedPositionUs() { if (loadingFinished) { return C.TIME_END_OF_SOURCE; @@ -402,12 +403,13 @@ import java.util.LinkedList; lastSeekPositionUs); } - /* package */ void skipData(int trackGroupIndex, long positionUs) { + /* package */ int skipData(int trackGroupIndex, long positionUs) { SampleQueue sampleQueue = sampleQueues[trackGroupIndex]; if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) { - sampleQueue.advanceToEnd(); + return sampleQueue.advanceToEnd(); } else { - sampleQueue.advanceTo(positionUs, true, true); + int skipCount = sampleQueue.advanceTo(positionUs, true, true); + return skipCount == SampleQueue.ADVANCE_FAILED ? 0 : skipCount; } } @@ -760,7 +762,8 @@ import java.util.LinkedList; for (int i = 0; i < trackCount; i++) { SampleQueue sampleQueue = sampleQueues[i]; sampleQueue.rewind(); - boolean seekInsideQueue = sampleQueue.advanceTo(positionUs, true, false); + boolean seekInsideQueue = sampleQueue.advanceTo(positionUs, true, false) + != SampleQueue.ADVANCE_FAILED; // If we have AV tracks then an in-queue seek is successful if the seek into every AV queue // is successful. We ignore whether seeks within non-AV queues are successful in this case, as // they may be sparse or poorly interleaved. If we only have non-AV tracks then a seek is diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java index 2b8705bb74..8c4bad1862 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java @@ -183,8 +183,9 @@ public final class DebugTextViewHelper implements Runnable, Player.EventListener return ""; } counters.ensureUpdated(); - return " rb:" + counters.renderedOutputBufferCount + return " sib:" + counters.skippedInputBufferCount + " sb:" + counters.skippedOutputBufferCount + + " rb:" + counters.renderedOutputBufferCount + " db:" + counters.droppedOutputBufferCount + " mcdb:" + counters.maxConsecutiveDroppedOutputBufferCount; } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSampleStream.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSampleStream.java index 4e1e32980f..699b850f73 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSampleStream.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSampleStream.java @@ -60,8 +60,8 @@ public final class FakeSampleStream implements SampleStream { } @Override - public void skipData(long positionUs) { - // Do nothing. + public int skipData(long positionUs) { + return 0; } } From 3ec0ccabbc96ecd2164a6cd646493b92e72f7825 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 31 Jul 2017 07:00:10 -0700 Subject: [PATCH 0132/2472] Check for null entries in ConcatenatingMediaSource We do this in the dynamic case, and I think we've seen a few GitHub issues where developers do this and don't understand what they've done wrong (because the failure comes later). ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=163688557 --- .../android/exoplayer2/source/ConcatenatingMediaSource.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java index de42df9a14..2c998e8a06 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java @@ -57,6 +57,9 @@ public final class ConcatenatingMediaSource implements MediaSource { * {@link MediaSource} instance to be present more than once in the array. */ public ConcatenatingMediaSource(boolean isRepeatOneAtomic, MediaSource... mediaSources) { + for (MediaSource mediaSource : mediaSources) { + Assertions.checkNotNull(mediaSource); + } this.mediaSources = mediaSources; this.isRepeatOneAtomic = isRepeatOneAtomic; timelines = new Timeline[mediaSources.length]; From ecd9cff0588f739a815f79b0f7f7433282547166 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 31 Jul 2017 07:53:08 -0700 Subject: [PATCH 0133/2472] Robustness fix for malformed ID3 metadata Issue: #3116 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=163693235 --- .../exoplayer2/metadata/id3/Id3Decoder.java | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/library/core/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 index df3353fb18..0e4256be5c 100644 --- a/library/core/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 @@ -483,13 +483,8 @@ public final class Id3Decoder implements MetadataDecoder { int ownerEndIndex = indexOfZeroByte(data, 0); String owner = new String(data, 0, ownerEndIndex, "ISO-8859-1"); - byte[] privateData; int privateDataStartIndex = ownerEndIndex + 1; - if (privateDataStartIndex < data.length) { - privateData = Arrays.copyOfRange(data, privateDataStartIndex, data.length); - } else { - privateData = new byte[0]; - } + byte[] privateData = copyOfRangeIfValid(data, privateDataStartIndex, data.length); return new PrivFrame(owner, privateData); } @@ -516,7 +511,7 @@ public final class Id3Decoder implements MetadataDecoder { descriptionEndIndex - descriptionStartIndex, charset); int objectDataStartIndex = descriptionEndIndex + delimiterLength(encoding); - byte[] objectData = Arrays.copyOfRange(data, objectDataStartIndex, data.length); + byte[] objectData = copyOfRangeIfValid(data, objectDataStartIndex, data.length); return new GeobFrame(mimeType, filename, description, objectData); } @@ -553,7 +548,7 @@ public final class Id3Decoder implements MetadataDecoder { descriptionEndIndex - descriptionStartIndex, charset); int pictureDataStartIndex = descriptionEndIndex + delimiterLength(encoding); - byte[] pictureData = Arrays.copyOfRange(data, pictureDataStartIndex, data.length); + byte[] pictureData = copyOfRangeIfValid(data, pictureDataStartIndex, data.length); return new ApicFrame(mimeType, description, pictureType, pictureData); } @@ -749,6 +744,22 @@ public final class Id3Decoder implements MetadataDecoder { ? 1 : 2; } + /** + * Copies the specified range of an array, or returns a zero length array if the range is invalid. + * + * @param data The array from which to copy. + * @param from The start of the range to copy (inclusive). + * @param to The end of the range to copy (exclusive). + * @return The copied data, or a zero length array if the range is invalid. + */ + private static byte[] copyOfRangeIfValid(byte[] data, int from, int to) { + if (to <= from) { + // Invalid or zero length range. + return new byte[0]; + } + return Arrays.copyOfRange(data, from, data.length); + } + private static final class Id3Header { private final int majorVersion; From 516762946d52e09ece780046715716cafdb40c8a Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 31 Jul 2017 08:08:29 -0700 Subject: [PATCH 0134/2472] Fix typo in Id3Decoder ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=163694825 --- .../com/google/android/exoplayer2/metadata/id3/Id3Decoder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/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 index 0e4256be5c..6b2e5c3675 100644 --- a/library/core/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 @@ -757,7 +757,7 @@ public final class Id3Decoder implements MetadataDecoder { // Invalid or zero length range. return new byte[0]; } - return Arrays.copyOfRange(data, from, data.length); + return Arrays.copyOfRange(data, from, to); } private static final class Id3Header { From 957158b7ffe1a07d267bf31cb6a2cb52d672e8bb Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 31 Jul 2017 09:50:08 -0700 Subject: [PATCH 0135/2472] Fix 2.5.0 lint errors - Publish IMA extension - Force IMA to use the correct version of the support library - Add missing sr translations for repeat mode strings ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=163705883 --- extensions/ima/build.gradle | 17 +++++++++++++++++ .../exoplayer2/mediacodec/MediaCodecInfo.java | 2 +- library/ui/src/main/res/values-sr/strings.xml | 3 +++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/extensions/ima/build.gradle b/extensions/ima/build.gradle index 7732751296..fc7dc088e8 100644 --- a/extensions/ima/build.gradle +++ b/extensions/ima/build.gradle @@ -14,6 +14,12 @@ android { dependencies { compile project(modulePrefix + 'library-core') + // This dependency is necessary to force the supportLibraryVersion of + // com.android.support:support-v4 to be used. Else an older version (25.2.0) is included via: + // com.google.android.gms:play-services-ads:11.0.2 + // |-- com.google.android.gms:play-services-ads-lite:[11.0.2] -> 11.0.2 + // |-- com.google.android.gms:play-services-basement:[11.0.2] -> 11.0.2 + // |-- com.android.support:support-v4:25.2.0 compile 'com.android.support:support-annotations:' + supportLibraryVersion compile 'com.google.ads.interactivemedia.v3:interactivemedia:3.7.4' compile 'com.google.android.gms:play-services-ads:11.0.2' @@ -23,3 +29,14 @@ dependencies { androidTestCompile 'org.mockito:mockito-core:' + mockitoVersion androidTestCompile 'com.android.support.test:runner:' + testSupportLibraryVersion } + +ext { + javadocTitle = 'IMA extension' +} +apply from: '../../javadoc_library.gradle' + +ext { + releaseArtifact = 'extension-ima' + releaseDescription = 'Interactive Media Ads extension for ExoPlayer.' +} +apply from: '../../publish.gradle' diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java index 17ef2c4456..2e5b04f4a9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java @@ -64,7 +64,7 @@ public final class MediaCodecInfo { /** * Whether the decoder is secure. * - * @see CodecCapabilities#isFeatureRequired(String) + * @see CodecCapabilities#isFeatureSupported(String) * @see CodecCapabilities#FEATURE_SecurePlayback */ public final boolean secure; diff --git a/library/ui/src/main/res/values-sr/strings.xml b/library/ui/src/main/res/values-sr/strings.xml index 175ad4fe7f..0d54de5f6a 100644 --- a/library/ui/src/main/res/values-sr/strings.xml +++ b/library/ui/src/main/res/values-sr/strings.xml @@ -22,4 +22,7 @@ "Заустави" "Премотај уназад" "Премотај унапред" + "Понови све" + "Понављање је искључено" + "Понови једну" From 8e7a52483a38543d16a7cfd1d5a8c0717f5533e5 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Sat, 22 Jul 2017 02:17:02 -0700 Subject: [PATCH 0136/2472] Fix PlaybackControlView's repeat mode button update when player is null ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=162824522 --- .../google/android/exoplayer2/ui/PlaybackControlView.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java index 7c4afa772a..a99c2dfde2 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java @@ -660,6 +660,11 @@ public class PlaybackControlView extends FrameLayout { repeatToggleButton.setVisibility(View.GONE); return; } + if (player == null) { + setButtonEnabled(false, repeatToggleButton); + return; + } + setButtonEnabled(true, repeatToggleButton); switch (player.getRepeatMode()) { case Player.REPEAT_MODE_OFF: repeatToggleButton.setImageDrawable(repeatOffButtonDrawable); From ad5c8af01957975bb8eb61dc7f3edf60e2e4aa39 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 28 Jul 2017 04:45:52 -0700 Subject: [PATCH 0137/2472] Allow apps to handle ad clicked/tapped events Issue: #3106 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=163455563 --- .../exoplayer2/ext/ima/ImaAdsLoader.java | 22 ++++++++++- .../exoplayer2/ext/ima/ImaAdsMediaSource.java | 38 +++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index 6e2206d6ae..fc8000b397 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -61,7 +61,7 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, /** * Listener for ad loader events. All methods are called on the main thread. */ - public interface EventListener { + /* package */ interface EventListener { /** * Called when the ad playback state has been updated. @@ -77,6 +77,16 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, */ void onLoadError(IOException error); + /** + * Called when the user clicks through an ad (for example, following a 'learn more' link). + */ + void onAdClicked(); + + /** + * Called when the user taps a non-clickthrough part of an ad. + */ + void onAdTapped(); + } static { @@ -337,6 +347,16 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, imaPausedContent = true; pauseContentInternal(); break; + case TAPPED: + if (eventListener != null) { + eventListener.onAdTapped(); + } + break; + case CLICKED: + if (eventListener != null) { + eventListener.onAdClicked(); + } + break; case CONTENT_RESUME_REQUESTED: imaPausedContent = false; resumeContentInternal(); diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java index 9c2eb4b404..0bf5773d2c 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java @@ -53,6 +53,16 @@ public final class ImaAdsMediaSource implements MediaSource { */ void onAdLoadError(IOException error); + /** + * Called when the user clicks through an ad (for example, following a 'learn more' link). + */ + void onAdClicked(); + + /** + * Called when the user taps a non-clickthrough part of an ad. + */ + void onAdTapped(); + } private static final String TAG = "ImaAdsMediaSource"; @@ -305,6 +315,34 @@ public final class ImaAdsMediaSource implements MediaSource { }); } + @Override + public void onAdClicked() { + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + if (!released) { + eventListener.onAdClicked(); + } + } + }); + } + } + + @Override + public void onAdTapped() { + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + if (!released) { + eventListener.onAdTapped(); + } + } + }); + } + } + } } From aeb2f620f11717aa0f1a413a5c7d5cd57ff48056 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 31 Jul 2017 03:30:09 -0700 Subject: [PATCH 0138/2472] Handle release() while initializing the ads manager Also don't detach any attached player in release() to prevent a possible NullPointerException if ImaAdsLoader.release() runs first, then the MediaSource is released and detaches the player. This is safe because if the loader was attached it's guaranteed to be detached. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=163673750 --- .../exoplayer2/ext/ima/ImaAdsLoader.java | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index fc8000b397..6541dad0ac 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -187,6 +187,10 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, * Whether {@link #getContentProgress()} has sent {@link #pendingContentPositionMs} to IMA. */ private boolean sentPendingContentPositionMs; + /** + * Whether {@link #release()} has been called. + */ + private boolean released; /** * Creates a new IMA ads loader. @@ -252,7 +256,7 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, if (imaPausedContent) { adsManager.resume(); } - } else if (adTagUri != null) { + } else { requestAds(); } } @@ -278,12 +282,10 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, * Releases the loader. Must be called when the instance is no longer needed. */ public void release() { + released = true; if (adsManager != null) { adsManager.destroy(); adsManager = null; - if (player != null) { - detachPlayer(); - } } } @@ -291,7 +293,12 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, @Override public void onAdsManagerLoaded(AdsManagerLoadedEvent adsManagerLoadedEvent) { - adsManager = adsManagerLoadedEvent.getAdsManager(); + AdsManager adsManager = adsManagerLoadedEvent.getAdsManager(); + if (released) { + adsManager.destroy(); + return; + } + this.adsManager = adsManager; adsManager.addAdErrorListener(this); adsManager.addAdEventListener(this); if (ENABLE_PRELOADING) { From ecbcf09804c80f7a66d03c94f51f44be88b11803 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 31 Jul 2017 07:00:10 -0700 Subject: [PATCH 0139/2472] Check for null entries in ConcatenatingMediaSource We do this in the dynamic case, and I think we've seen a few GitHub issues where developers do this and don't understand what they've done wrong (because the failure comes later). ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=163688557 --- .../android/exoplayer2/source/ConcatenatingMediaSource.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java index de42df9a14..2c998e8a06 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java @@ -57,6 +57,9 @@ public final class ConcatenatingMediaSource implements MediaSource { * {@link MediaSource} instance to be present more than once in the array. */ public ConcatenatingMediaSource(boolean isRepeatOneAtomic, MediaSource... mediaSources) { + for (MediaSource mediaSource : mediaSources) { + Assertions.checkNotNull(mediaSource); + } this.mediaSources = mediaSources; this.isRepeatOneAtomic = isRepeatOneAtomic; timelines = new Timeline[mediaSources.length]; From 2b5bd800e3b3f2e85731f8bd7a7565bdac77610e Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 31 Jul 2017 07:53:08 -0700 Subject: [PATCH 0140/2472] Robustness fix for malformed ID3 metadata Issue: #3116 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=163693235 --- .../exoplayer2/metadata/id3/Id3Decoder.java | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/library/core/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 index df3353fb18..0e4256be5c 100644 --- a/library/core/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 @@ -483,13 +483,8 @@ public final class Id3Decoder implements MetadataDecoder { int ownerEndIndex = indexOfZeroByte(data, 0); String owner = new String(data, 0, ownerEndIndex, "ISO-8859-1"); - byte[] privateData; int privateDataStartIndex = ownerEndIndex + 1; - if (privateDataStartIndex < data.length) { - privateData = Arrays.copyOfRange(data, privateDataStartIndex, data.length); - } else { - privateData = new byte[0]; - } + byte[] privateData = copyOfRangeIfValid(data, privateDataStartIndex, data.length); return new PrivFrame(owner, privateData); } @@ -516,7 +511,7 @@ public final class Id3Decoder implements MetadataDecoder { descriptionEndIndex - descriptionStartIndex, charset); int objectDataStartIndex = descriptionEndIndex + delimiterLength(encoding); - byte[] objectData = Arrays.copyOfRange(data, objectDataStartIndex, data.length); + byte[] objectData = copyOfRangeIfValid(data, objectDataStartIndex, data.length); return new GeobFrame(mimeType, filename, description, objectData); } @@ -553,7 +548,7 @@ public final class Id3Decoder implements MetadataDecoder { descriptionEndIndex - descriptionStartIndex, charset); int pictureDataStartIndex = descriptionEndIndex + delimiterLength(encoding); - byte[] pictureData = Arrays.copyOfRange(data, pictureDataStartIndex, data.length); + byte[] pictureData = copyOfRangeIfValid(data, pictureDataStartIndex, data.length); return new ApicFrame(mimeType, description, pictureType, pictureData); } @@ -749,6 +744,22 @@ public final class Id3Decoder implements MetadataDecoder { ? 1 : 2; } + /** + * Copies the specified range of an array, or returns a zero length array if the range is invalid. + * + * @param data The array from which to copy. + * @param from The start of the range to copy (inclusive). + * @param to The end of the range to copy (exclusive). + * @return The copied data, or a zero length array if the range is invalid. + */ + private static byte[] copyOfRangeIfValid(byte[] data, int from, int to) { + if (to <= from) { + // Invalid or zero length range. + return new byte[0]; + } + return Arrays.copyOfRange(data, from, data.length); + } + private static final class Id3Header { private final int majorVersion; From bf4a460e4c8ae9c772f99fad69ebecd42849496b Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 31 Jul 2017 08:08:29 -0700 Subject: [PATCH 0141/2472] Fix typo in Id3Decoder ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=163694825 --- .../com/google/android/exoplayer2/metadata/id3/Id3Decoder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/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 index 0e4256be5c..6b2e5c3675 100644 --- a/library/core/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 @@ -757,7 +757,7 @@ public final class Id3Decoder implements MetadataDecoder { // Invalid or zero length range. return new byte[0]; } - return Arrays.copyOfRange(data, from, data.length); + return Arrays.copyOfRange(data, from, to); } private static final class Id3Header { From 85445536c31809d78351023ec26d3da18eea7f58 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 31 Jul 2017 09:50:08 -0700 Subject: [PATCH 0142/2472] Fix 2.5.0 lint errors - Publish IMA extension - Force IMA to use the correct version of the support library - Add missing sr translations for repeat mode strings ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=163705883 --- extensions/ima/build.gradle | 17 +++++++++++++++++ .../exoplayer2/mediacodec/MediaCodecInfo.java | 2 +- library/ui/src/main/res/values-sr/strings.xml | 3 +++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/extensions/ima/build.gradle b/extensions/ima/build.gradle index 7732751296..fc7dc088e8 100644 --- a/extensions/ima/build.gradle +++ b/extensions/ima/build.gradle @@ -14,6 +14,12 @@ android { dependencies { compile project(modulePrefix + 'library-core') + // This dependency is necessary to force the supportLibraryVersion of + // com.android.support:support-v4 to be used. Else an older version (25.2.0) is included via: + // com.google.android.gms:play-services-ads:11.0.2 + // |-- com.google.android.gms:play-services-ads-lite:[11.0.2] -> 11.0.2 + // |-- com.google.android.gms:play-services-basement:[11.0.2] -> 11.0.2 + // |-- com.android.support:support-v4:25.2.0 compile 'com.android.support:support-annotations:' + supportLibraryVersion compile 'com.google.ads.interactivemedia.v3:interactivemedia:3.7.4' compile 'com.google.android.gms:play-services-ads:11.0.2' @@ -23,3 +29,14 @@ dependencies { androidTestCompile 'org.mockito:mockito-core:' + mockitoVersion androidTestCompile 'com.android.support.test:runner:' + testSupportLibraryVersion } + +ext { + javadocTitle = 'IMA extension' +} +apply from: '../../javadoc_library.gradle' + +ext { + releaseArtifact = 'extension-ima' + releaseDescription = 'Interactive Media Ads extension for ExoPlayer.' +} +apply from: '../../publish.gradle' diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java index 17ef2c4456..2e5b04f4a9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java @@ -64,7 +64,7 @@ public final class MediaCodecInfo { /** * Whether the decoder is secure. * - * @see CodecCapabilities#isFeatureRequired(String) + * @see CodecCapabilities#isFeatureSupported(String) * @see CodecCapabilities#FEATURE_SecurePlayback */ public final boolean secure; diff --git a/library/ui/src/main/res/values-sr/strings.xml b/library/ui/src/main/res/values-sr/strings.xml index 175ad4fe7f..0d54de5f6a 100644 --- a/library/ui/src/main/res/values-sr/strings.xml +++ b/library/ui/src/main/res/values-sr/strings.xml @@ -22,4 +22,7 @@ "Заустави" "Премотај уназад" "Премотај унапред" + "Понови све" + "Понављање је искључено" + "Понови једну" From 9c2528a70f6fa56db6c9b10b794c5bf945731b1d Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 31 Jul 2017 21:28:22 +0100 Subject: [PATCH 0143/2472] Correctly handle reading 0 bits --- .../com/google/android/exoplayer2/util/ParsableBitArray.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java b/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java index 0456bcb879..199ceff892 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java @@ -155,6 +155,9 @@ public final class ParsableBitArray { * @return An integer whose bottom n bits hold the read data. */ public int readBits(int numBits) { + if (numBits == 0) { + return 0; + } int returnValue = 0; bitOffset += numBits; while (bitOffset > 8) { From 1151104e2f9c5e10da869dfb59860ca4a890e371 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 1 Aug 2017 02:18:48 -0700 Subject: [PATCH 0144/2472] Use TrackGroup instead of TrackSelection in FakeAdaptiveDataSet. This was a bug as the data set should contain sample data for all available tracks. Also added a unit test. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=163806579 --- .../testutil/FakeAdaptiveDataSet.java | 21 +++++++++---------- .../exoplayer2/testutil/FakeChunkSource.java | 3 ++- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveDataSet.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveDataSet.java index f4476ddf93..add0c5d22f 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveDataSet.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveDataSet.java @@ -17,11 +17,11 @@ package com.google.android.exoplayer2.testutil; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.trackselection.TrackSelection; +import com.google.android.exoplayer2.source.TrackGroup; /** * Fake data set emulating the data of an adaptive media source. - * It provides chunk data for all {@link Format}s in the given {@link TrackSelection}. + * It provides chunk data for all {@link Format}s in the given {@link TrackGroup}. */ public final class FakeAdaptiveDataSet extends FakeDataSet { @@ -36,8 +36,8 @@ public final class FakeAdaptiveDataSet extends FakeDataSet { this.chunkDurationUs = chunkDurationUs; } - public FakeAdaptiveDataSet createDataSet(TrackSelection trackSelection, long mediaDurationUs) { - return new FakeAdaptiveDataSet(trackSelection, mediaDurationUs, chunkDurationUs); + public FakeAdaptiveDataSet createDataSet(TrackGroup trackGroup, long mediaDurationUs) { + return new FakeAdaptiveDataSet(trackGroup, mediaDurationUs, chunkDurationUs); } } @@ -46,15 +46,14 @@ public final class FakeAdaptiveDataSet extends FakeDataSet { private final long chunkDurationUs; private final long lastChunkDurationUs; - public FakeAdaptiveDataSet(TrackSelection trackSelection, long mediaDurationUs, - long chunkDurationUs) { + public FakeAdaptiveDataSet(TrackGroup trackGroup, long mediaDurationUs, long chunkDurationUs) { this.chunkDurationUs = chunkDurationUs; - int selectionCount = trackSelection.length(); + int trackCount = trackGroup.length; long lastChunkDurationUs = mediaDurationUs % chunkDurationUs; int fullChunks = (int) (mediaDurationUs / chunkDurationUs); - for (int i = 0; i < selectionCount; i++) { + for (int i = 0; i < trackCount; i++) { String uri = getUri(i); - Format format = trackSelection.getFormat(i); + Format format = trackGroup.getFormat(i); int chunkLength = (int) (format.bitrate * chunkDurationUs / (8 * C.MICROS_PER_SECOND)); FakeData newData = this.newData(uri); for (int j = 0; j < fullChunks; j++) { @@ -74,8 +73,8 @@ public final class FakeAdaptiveDataSet extends FakeDataSet { return chunkCount; } - public String getUri(int trackSelectionIndex) { - return "fake://adaptive.media/" + Integer.toString(trackSelectionIndex); + public String getUri(int trackIndex) { + return "fake://adaptive.media/" + trackIndex; } public long getChunkDuration(int chunkIndex) { diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeChunkSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeChunkSource.java index 0c970caa15..b8f25bfbce 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeChunkSource.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeChunkSource.java @@ -50,7 +50,8 @@ public final class FakeChunkSource implements ChunkSource { } public FakeChunkSource createChunkSource(TrackSelection trackSelection, long durationUs) { - FakeAdaptiveDataSet dataSet = dataSetFactory.createDataSet(trackSelection, durationUs); + FakeAdaptiveDataSet dataSet = + dataSetFactory.createDataSet(trackSelection.getTrackGroup(), durationUs); dataSourceFactory.setFakeDataSet(dataSet); DataSource dataSource = dataSourceFactory.createDataSource(); return new FakeChunkSource(trackSelection, dataSource, dataSet); From 55620348d4974ba8dfd0834e7ea968c41d6bab44 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 1 Aug 2017 04:44:02 -0700 Subject: [PATCH 0145/2472] Fix sequence extension position calculation ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=163814942 --- .../com/google/android/exoplayer2/extractor/ts/H262Reader.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/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 index 92c8e8d800..add8079105 100644 --- a/library/core/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 @@ -257,7 +257,7 @@ public final class H262Reader implements ElementaryStreamReader { public boolean onStartCode(int startCodeValue, int bytesAlreadyPassed) { if (isFilling) { if (sequenceExtensionPosition == 0 && startCodeValue == START_EXTENSION) { - sequenceExtensionPosition = length; + sequenceExtensionPosition = length - bytesAlreadyPassed; } else { length -= bytesAlreadyPassed; isFilling = false; From 13513b9c3eb881bc87028245ea569c409b9fae26 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 1 Aug 2017 07:50:12 -0700 Subject: [PATCH 0146/2472] Clean up MediaSessionExt documentation ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=163828712 --- .../DefaultPlaybackController.java | 42 +++--- .../mediasession/MediaSessionConnector.java | 126 ++++++++---------- .../RepeatModeActionProvider.java | 19 +-- .../mediasession/TimelineQueueNavigator.java | 25 ++-- .../exoplayer2/ui/PlaybackControlView.java | 16 ++- 5 files changed, 112 insertions(+), 116 deletions(-) diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/DefaultPlaybackController.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/DefaultPlaybackController.java index 231c1f1ea5..c3586b29e6 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/DefaultPlaybackController.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/DefaultPlaybackController.java @@ -21,40 +21,50 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Player; /** - * A default implementation of the {@link MediaSessionConnector.PlaybackController}. You can safely - * override any method for instance to intercept calls for a given action. + * A default implementation of {@link MediaSessionConnector.PlaybackController}. + *

      + * Methods can be safely overridden by subclasses to intercept calls for given actions. */ public class DefaultPlaybackController implements MediaSessionConnector.PlaybackController { + /** + * The default fast forward increment, in milliseconds. + */ + public static final int DEFAULT_FAST_FORWARD_MS = 15000; + /** + * The default rewind increment, in milliseconds. + */ + public static final int DEFAULT_REWIND_MS = 5000; + private static final long BASE_ACTIONS = PlaybackStateCompat.ACTION_PLAY_PAUSE | PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PAUSE | PlaybackStateCompat.ACTION_STOP; - protected final long fastForwardIncrementMs; protected final long rewindIncrementMs; + protected final long fastForwardIncrementMs; /** - * Creates a new {@link DefaultPlaybackController}. This is equivalent to calling - * {@code DefaultPlaybackController(15000L, 5000L)}. + * Creates a new instance. + *

      + * Equivalent to {@code DefaultPlaybackController( + * DefaultPlaybackController.DEFAULT_REWIND_MS, + * DefaultPlaybackController.DEFAULT_FAST_FORWARD_MS)}. */ public DefaultPlaybackController() { - this(15000L, 5000L); + this(DEFAULT_REWIND_MS, DEFAULT_FAST_FORWARD_MS); } /** - * Creates a new {@link DefaultPlaybackController} and sets the fast forward and rewind increments - * in milliseconds. + * Creates a new instance with the given fast forward and rewind increments. * - * @param fastForwardIncrementMs A positive value will cause the - * {@link PlaybackStateCompat#ACTION_FAST_FORWARD} playback action to be added. A zero or a - * negative value will cause it to be removed. - * @param rewindIncrementMs A positive value will cause the - * {@link PlaybackStateCompat#ACTION_REWIND} playback action to be added. A zero or a - * negative value will cause it to be removed. + * @param rewindIncrementMs The rewind increment in milliseconds. A zero or negative value will + * cause the rewind action to be disabled. + * @param fastForwardIncrementMs The fast forward increment in milliseconds. A zero or negative + * value will cause the fast forward action to be removed. */ - public DefaultPlaybackController(long fastForwardIncrementMs, long rewindIncrementMs) { - this.fastForwardIncrementMs = fastForwardIncrementMs; + public DefaultPlaybackController(long rewindIncrementMs, long fastForwardIncrementMs) { this.rewindIncrementMs = rewindIncrementMs; + this.fastForwardIncrementMs = fastForwardIncrementMs; } @Override diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java index a300acfffa..0e839b8083 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java @@ -44,30 +44,27 @@ import java.util.List; import java.util.Map; /** - * Mediates between a {@link MediaSessionCompat} and an {@link Player} instance set with - * {@link #setPlayer(Player, PlaybackPreparer, CustomActionProvider...)}. + * Connects a {@link MediaSessionCompat} to a {@link Player}. *

      - * The {@code MediaSessionConnector} listens for media actions sent by a media controller and - * realizes these actions by calling appropriate ExoPlayer methods. Further, the state of ExoPlayer - * will be synced automatically with the {@link PlaybackStateCompat} of the media session to - * broadcast state transitions to clients. You can optionally extend this behaviour by providing - * various collaborators. - *

      - * Media actions to initiate media playback ({@code PlaybackStateCompat#ACTION_PREPARE_*} and - * {@code PlaybackStateCompat#ACTION_PLAY_*} need to be handled by a {@link PlaybackPreparer} which - * build a {@link com.google.android.exoplayer2.source.MediaSource} to prepare ExoPlayer. Deploy - * your preparer by calling {@link #setPlaybackPreparer(PlaybackPreparer)}. - *

      - * To support a media session queue and navigation within this queue, you can set a - * {@link QueueNavigator} to maintain the queue yourself and implement queue navigation commands - * (like 'skip to next') sent by controllers. It's recommended to use the - * {@link TimelineQueueNavigator} to allow users navigating the windows of the ExoPlayer timeline. - *

      - * If you want to allow media controllers to manipulate the queue, implement a {@link QueueEditor} - * and deploy it with {@link #setQueueEditor(QueueEditor)}. - *

      - * Set an {@link ErrorMessageProvider} to provide an error code and a human readable error message - * to be broadcast to controllers. + * The connector listens for actions sent by the media session's controller and implements these + * actions by calling appropriate ExoPlayer methods. The playback state of the media session is + * automatically synced with the player. The connector can also be optionally extended by providing + * various collaborators: + *

        + *
      • Actions to initiate media playback ({@code PlaybackStateCompat#ACTION_PREPARE_*} and + * {@code PlaybackStateCompat#ACTION_PLAY_*}) can be handled by a {@link PlaybackPreparer} passed + * when calling {@link #setPlayer(Player, PlaybackPreparer, CustomActionProvider...)}. Custom + * actions can be handled by passing one or more {@link CustomActionProvider}s in a similar way. + *
      • + *
      • To enable a media queue and navigation within it, you can set a {@link QueueNavigator} by + * calling {@link #setQueueNavigator(QueueNavigator)}. Use of {@link TimelineQueueNavigator} is + * recommended for most use cases.
      • + *
      • To enable editing of the media queue, you can set a {@link QueueEditor} by calling + * {@link #setQueueEditor(QueueEditor)}.
      • + *
      • An {@link ErrorMessageProvider} for providing human readable error messages and + * corresponding error codes can be set by calling + * {@link #setErrorMessageProvider(ErrorMessageProvider)}.
      • + *
      */ public final class MediaSessionConnector { @@ -78,12 +75,7 @@ public final class MediaSessionConnector { public static final String EXTRAS_PITCH = "EXO_PITCH"; /** - * Interface to which media controller commands regarding preparing playback for a given media - * clip are delegated to. - *

      - * Normally preparing playback includes preparing the player with a - * {@link com.google.android.exoplayer2.source.MediaSource} and setting up the media session queue - * with a corresponding list of queue items. + * Interface to which playback preparation actions are delegated. */ public interface PlaybackPreparer { @@ -131,7 +123,7 @@ public final class MediaSessionConnector { } /** - * Controller to handle playback actions. + * Interface to which playback actions are delegated. */ public interface PlaybackController { @@ -178,8 +170,8 @@ public final class MediaSessionConnector { } /** - * Navigator to handle queue navigation actions and maintain the media session queue with - * {#link MediaSessionCompat#setQueue(List)} to provide the active queue item to the connector. + * Handles queue navigation actions, and updates the media session queue by calling + * {@code MediaSessionCompat.setQueue()}. */ public interface QueueNavigator { @@ -211,7 +203,7 @@ public final class MediaSessionConnector { */ void onCurrentWindowIndexChanged(Player player); /** - * Gets the id of the currently active queue item or + * Gets the id of the currently active queue item, or * {@link MediaSessionCompat.QueueItem#UNKNOWN_ID} if the active item is unknown. *

      * To let the connector publish metadata for the active queue item, the queue item with the @@ -241,7 +233,7 @@ public final class MediaSessionConnector { } /** - * Editor to manipulate the queue. + * Handles media session queue edits. */ public interface QueueEditor { @@ -302,12 +294,12 @@ public final class MediaSessionConnector { } /** - * Provides an user readable error code and a message for {@link ExoPlaybackException}s. + * Converts an exception into an error code and a user readable error message. */ public interface ErrorMessageProvider { /** - * Returns a pair of an error code and a user readable error message for a given - * {@link ExoPlaybackException}. + * Returns a pair consisting of an error code and a user readable error message for a given + * exception. */ Pair getErrorMessage(ExoPlaybackException playbackException); } @@ -316,6 +308,7 @@ public final class MediaSessionConnector { * The wrapped {@link MediaSessionCompat}. */ public final MediaSessionCompat mediaSession; + private final MediaControllerCompat mediaController; private final Handler handler; private final boolean doMaintainMetadata; @@ -334,11 +327,10 @@ public final class MediaSessionConnector { private ExoPlaybackException playbackException; /** - * Creates a {@code MediaSessionConnector}. This is equivalent to calling - * {@code #MediaSessionConnector(mediaSession, new DefaultPlaybackController)}. + * Creates an instance. Must be called on the same thread that is used to construct the player + * instances passed to {@link #setPlayer(Player, PlaybackPreparer, CustomActionProvider...)}. *

      - * Constructing the {@link MediaSessionConnector} needs to be done on the same thread as - * constructing the player instance. + * Equivalent to {@code MediaSessionConnector(mediaSession, new DefaultPlaybackController())}. * * @param mediaSession The {@link MediaSessionCompat} to connect to. */ @@ -347,14 +339,13 @@ public final class MediaSessionConnector { } /** - * Creates a {@code MediaSessionConnector}. This is equivalent to calling - * {@code #MediaSessionConnector(mediaSession, playbackController, true)}. + * Creates an instance. Must be called on the same thread that is used to construct the player + * instances passed to {@link #setPlayer(Player, PlaybackPreparer, CustomActionProvider...)}. *

      - * Constructing the {@link MediaSessionConnector} needs to be done on the same thread as - * constructing the player instance. + * Equivalent to {@code MediaSessionConnector(mediaSession, playbackController, true)}. * * @param mediaSession The {@link MediaSessionCompat} to connect to. - * @param playbackController The {@link PlaybackController}. + * @param playbackController A {@link PlaybackController} for handling playback actions. */ public MediaSessionConnector(MediaSessionCompat mediaSession, PlaybackController playbackController) { @@ -362,19 +353,14 @@ public final class MediaSessionConnector { } /** - * Creates a {@code MediaSessionConnector} with {@link CustomActionProvider}s. - *

      - * If you choose to pass {@code false} for {@code doMaintainMetadata} you need to maintain the - * metadata of the media session yourself (provide at least the duration to allow clients to show - * a progress bar). - *

      - * Constructing the {@link MediaSessionConnector} needs to be done on the same thread as - * constructing the player instance. + * Creates an instance. Must be called on the same thread that is used to construct the player + * instances passed to {@link #setPlayer(Player, PlaybackPreparer, CustomActionProvider...)}. * * @param mediaSession The {@link MediaSessionCompat} to connect to. - * @param playbackController The {@link PlaybackController}. - * @param doMaintainMetadata Sets whether the connector should maintain the metadata of the - * session. + * @param playbackController A {@link PlaybackController} for handling playback actions. + * @param doMaintainMetadata Whether the connector should maintain the metadata of the session. If + * {@code false}, you need to maintain the metadata of the media session yourself (provide at + * least the duration to allow clients to show a progress bar). */ public MediaSessionConnector(MediaSessionCompat mediaSession, PlaybackController playbackController, boolean doMaintainMetadata) { @@ -392,17 +378,14 @@ public final class MediaSessionConnector { } /** - * Sets the player to which media commands sent by a media controller are delegated. - *

      - * The media session callback is set if the {@code player} is not {@code null} and the callback is - * removed if the {@code player} is {@code null}. + * Sets the player to be connected to the media session. *

      * The order in which any {@link CustomActionProvider}s are passed determines the order of the * actions published with the playback state of the session. * * @param player The player to be connected to the {@code MediaSession}. - * @param playbackPreparer The playback preparer for the player. - * @param customActionProviders Optional {@link CustomActionProvider}s to publish and handle + * @param playbackPreparer An optional {@link PlaybackPreparer} for preparing the player. + * @param customActionProviders An optional {@link CustomActionProvider}s to publish and handle * custom actions. */ public void setPlayer(Player player, PlaybackPreparer playbackPreparer, @@ -411,7 +394,7 @@ public final class MediaSessionConnector { this.player.removeListener(exoPlayerEventListener); mediaSession.setCallback(null); } - setPlaybackPreparer(playbackPreparer); + this.playbackPreparer = playbackPreparer; this.player = player; this.customActionProviders = (player != null && customActionProviders != null) ? customActionProviders : new CustomActionProvider[0]; @@ -424,9 +407,9 @@ public final class MediaSessionConnector { } /** - * Sets the optional {@link ErrorMessageProvider}. + * Sets the {@link ErrorMessageProvider}. * - * @param errorMessageProvider The {@link ErrorMessageProvider}. + * @param errorMessageProvider The error message provider. */ public void setErrorMessageProvider(ErrorMessageProvider errorMessageProvider) { this.errorMessageProvider = errorMessageProvider; @@ -437,25 +420,21 @@ public final class MediaSessionConnector { * {@code ACTION_SKIP_TO_PREVIOUS}, {@code ACTION_SKIP_TO_QUEUE_ITEM} and * {@code ACTION_SET_SHUFFLE_MODE_ENABLED}. * - * @param queueNavigator The navigator to handle queue navigation. + * @param queueNavigator The queue navigator. */ public void setQueueNavigator(QueueNavigator queueNavigator) { this.queueNavigator = queueNavigator; } /** - * Sets the queue editor to handle commands to manipulate the queue sent by a media controller. + * Sets the {@link QueueEditor} to handle queue edits sent by the media controller. * - * @param queueEditor The editor to handle queue manipulation actions. + * @param queueEditor The queue editor. */ public void setQueueEditor(QueueEditor queueEditor) { this.queueEditor = queueEditor; } - private void setPlaybackPreparer(PlaybackPreparer playbackPreparer) { - this.playbackPreparer = playbackPreparer; - } - private void updateMediaSessionPlaybackState() { PlaybackStateCompat.Builder builder = new PlaybackStateCompat.Builder(); if (player == null) { @@ -603,6 +582,7 @@ public final class MediaSessionConnector { } private class ExoPlayerEventListener implements Player.EventListener { + @Override public void onTimelineChanged(Timeline timeline, Object manifest) { if (queueNavigator != null) { diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java index 36d95914f7..db0190de0f 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java @@ -25,10 +25,13 @@ import com.google.android.exoplayer2.util.RepeatModeUtil; */ public final class RepeatModeActionProvider implements MediaSessionConnector.CustomActionProvider { + /** + * The default repeat toggle modes. + */ + public static final @RepeatModeUtil.RepeatToggleModes int DEFAULT_REPEAT_TOGGLE_MODES = + RepeatModeUtil.REPEAT_TOGGLE_MODE_ONE | RepeatModeUtil.REPEAT_TOGGLE_MODE_ALL; + private static final String ACTION_REPEAT_MODE = "ACTION_EXO_REPEAT_MODE"; - @RepeatModeUtil.RepeatToggleModes - private static final int DEFAULT_REPEAT_MODES = RepeatModeUtil.REPEAT_TOGGLE_MODE_ONE - | RepeatModeUtil.REPEAT_TOGGLE_MODE_ALL; private final Player player; @RepeatModeUtil.RepeatToggleModes @@ -38,20 +41,20 @@ public final class RepeatModeActionProvider implements MediaSessionConnector.Cus private final CharSequence repeatOffDescription; /** - * Creates a new {@link RepeatModeActionProvider}. + * Creates a new instance. *

      - * This is equivalent to calling the two argument constructor with - * {@code RepeatModeUtil#REPEAT_TOGGLE_MODE_ONE | RepeatModeUtil#REPEAT_TOGGLE_MODE_ALL}. + * Equivalent to {@code RepeatModeActionProvider(context, player, + * RepeatModeActionProvider.DEFAULT_REPEAT_TOGGLE_MODES)}. * * @param context The context. * @param player The player on which to toggle the repeat mode. */ public RepeatModeActionProvider(Context context, Player player) { - this(context, player, DEFAULT_REPEAT_MODES); + this(context, player, DEFAULT_REPEAT_TOGGLE_MODES); } /** - * Creates a new {@link RepeatModeActionProvider} for the given repeat toggle modes. + * Creates a new instance enabling the given repeat toggle modes. * * @param context The context. * @param player The player on which to toggle the repeat mode. diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java index 60aa5a5ba0..521b4cd6e3 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java @@ -29,9 +29,8 @@ import java.util.Collections; import java.util.List; /** - * An abstract implementation of the {@link MediaSessionConnector.QueueNavigator} that's based on an - * {@link Player}'s current {@link Timeline} and maps the timeline of the player to the media - * session queue. + * An abstract implementation of the {@link MediaSessionConnector.QueueNavigator} that maps the + * windows of a {@link Player}'s {@link Timeline} to the media session queue. */ public abstract class TimelineQueueNavigator implements MediaSessionConnector.QueueNavigator { @@ -44,10 +43,9 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu private long activeQueueItemId; /** - * Creates a new timeline queue navigator for a given {@link MediaSessionCompat}. + * Creates an instance for a given {@link MediaSessionCompat}. *

      - * This is equivalent to calling - * {@code #TimelineQueueNavigator(mediaSession, DEFAULT_MAX_QUEUE_SIZE)}. + * Equivalent to {@code TimelineQueueNavigator(mediaSession, DEFAULT_MAX_QUEUE_SIZE)}. * * @param mediaSession The {@link MediaSessionCompat}. */ @@ -56,12 +54,11 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu } /** - * Creates a new timeline queue navigator for a given {@link MediaSessionCompat} and a maximum - * queue size of {@code maxQueueSize}. + * Creates an instance for a given {@link MediaSessionCompat} and maximum queue size. *

      - * If the actual queue size is larger than {@code maxQueueSize} a floating window of - * {@code maxQueueSize} is applied and moved back and forth when the user is navigating within the - * queue. + * If the number of windows in the {@link Player}'s {@link Timeline} exceeds {@code maxQueueSize}, + * the media session queue will correspond to {@code maxQueueSize} windows centered on the one + * currently being played. * * @param mediaSession The {@link MediaSessionCompat}. * @param maxQueueSize The maximum queue size. @@ -80,12 +77,6 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu */ public abstract MediaDescriptionCompat getMediaDescription(int windowIndex); - /** - * Supports the following media actions: {@code PlaybackStateCompat.ACTION_SKIP_TO_NEXT | - * PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS | PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM}. - * - * @return The bit mask of the supported media actions. - */ @Override public long getSupportedQueueNavigatorActions(Player player) { if (player == null || player.getCurrentTimeline().getWindowCount() < 2) { diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java index a99c2dfde2..6ddbfed973 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java @@ -249,11 +249,23 @@ public class PlaybackControlView extends FrameLayout { }; + /** + * The default fast forward increment, in milliseconds. + */ public static final int DEFAULT_FAST_FORWARD_MS = 15000; + /** + * The default rewind increment, in milliseconds. + */ public static final int DEFAULT_REWIND_MS = 5000; + /** + * The default show timeout, in milliseconds. + */ public static final int DEFAULT_SHOW_TIMEOUT_MS = 5000; - public static final @RepeatModeUtil.RepeatToggleModes int DEFAULT_REPEAT_TOGGLE_MODES - = RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE; + /** + * The default repeat toggle modes. + */ + public static final @RepeatModeUtil.RepeatToggleModes int DEFAULT_REPEAT_TOGGLE_MODES = + RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE; /** * The maximum number of windows that can be shown in a multi-window time bar. From c01c8cd2a6acee76532d9b32465a4b4743281c9b Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 1 Aug 2017 08:08:58 -0700 Subject: [PATCH 0147/2472] Bump version to 2.5.0-beta2 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=163830353 --- RELEASENOTES.md | 8 +++++--- constants.gradle | 2 +- .../google/android/exoplayer2/ExoPlayerLibraryInfo.java | 4 ++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 24da37808b..379b84b4e7 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,11 +1,11 @@ # Release notes # -### r2.5.0-beta1 ### +### r2.5.0 (beta) ### * IMA extension: Wraps the Google Interactive Media Ads (IMA) SDK to provide an easy and seamless way of incorporating display ads into ExoPlayer playbacks. - You can read more about the IMA extension *A link to a blog post about this - extension will be added here prior to the stable 2.5.0 release.* + You can read more about the IMA extension + [here](https://medium.com/google-exoplayer/playing-ads-with-exoplayer-and-ima-868dfd767ea). * MediaSession extension: Provides an easy to to connect ExoPlayer with MediaSessionCompat in the Android Support Library. *A link to a blog post about this extension will be added here prior to the stable 2.5.0 release.* @@ -48,6 +48,8 @@ ([#889](https://github.com/google/ExoPlayer/issues/889)). * AndroidTV: Fixed issue where tunneled video playback would not start on some devices ([#2985](https://github.com/google/ExoPlayer/issues/2985)). +* MPEG-TS: Fixed segmentation issue when parsing H262 + ([#2891](https://github.com/google/ExoPlayer/issues/2891)). * Cronet extension: Support for a user-defined fallback if Cronet library is not present. * Misc bugfixes. diff --git a/constants.gradle b/constants.gradle index 73b80f6a83..0db74945c4 100644 --- a/constants.gradle +++ b/constants.gradle @@ -24,7 +24,7 @@ project.ext { supportLibraryVersion = '25.4.0' dexmakerVersion = '1.2' mockitoVersion = '1.9.5' - releaseVersion = 'r2.5.0-beta1' + releaseVersion = 'r2.5.0-beta2' modulePrefix = ':' if (gradle.ext.has('exoplayerModulePrefix')) { modulePrefix += gradle.ext.exoplayerModulePrefix diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java index 2abdfe5aee..c04a777e14 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java @@ -31,13 +31,13 @@ public final class ExoPlayerLibraryInfo { * The version of the library expressed as a string, for example "1.2.3". */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. - public static final String VERSION = "2.5.0-beta1"; + public static final String VERSION = "2.5.0-beta2"; /** * The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final String VERSION_SLASHY = "ExoPlayerLib/2.5.0-beta1"; + public static final String VERSION_SLASHY = "ExoPlayerLib/2.5.0-beta2"; /** * The version of the library expressed as an integer, for example 1002003. From 1a95a35434ec335d0c25d16cb80200673fb5a69d Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 1 Aug 2017 18:20:54 +0100 Subject: [PATCH 0148/2472] Minor cleanup --- .../src/androidTest/assets/ts/sample.ps.0.dump | 2 +- .../src/androidTest/assets/ts/sample.ts.0.dump | 4 ++-- .../exoplayer2/extractor/ts/H262Reader.java | 7 ++++--- .../exoplayer2/ui/SimpleExoPlayerView.java | 16 ++++++++-------- 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/library/core/src/androidTest/assets/ts/sample.ps.0.dump b/library/core/src/androidTest/assets/ts/sample.ps.0.dump index 3b44fb6fb9..98f3c6a85a 100644 --- a/library/core/src/androidTest/assets/ts/sample.ps.0.dump +++ b/library/core/src/androidTest/assets/ts/sample.ps.0.dump @@ -69,7 +69,7 @@ track 224: sample 0: time = 40000 flags = 1 - data = length 20616, hash CA38A5B5 + data = length 20646, hash 576390B sample 1: time = 80000 flags = 0 diff --git a/library/core/src/androidTest/assets/ts/sample.ts.0.dump b/library/core/src/androidTest/assets/ts/sample.ts.0.dump index 26c6665aaa..83f1337816 100644 --- a/library/core/src/androidTest/assets/ts/sample.ts.0.dump +++ b/library/core/src/androidTest/assets/ts/sample.ts.0.dump @@ -28,9 +28,9 @@ track 256: data = length 22, hash CE183139 sample count = 2 sample 0: - time = 33366 + time = 0 flags = 1 - data = length 20669, hash 26DABA0F + data = length 20711, hash 34341E8 sample 1: time = 66733 flags = 0 diff --git a/library/core/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 index add8079105..a00bace56c 100644 --- a/library/core/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 @@ -137,9 +137,10 @@ public final class H262Reader implements ElementaryStreamReader { } } - if (hasOutputFormat && (startCodeValue == START_PICTURE || startCodeValue == START_SEQUENCE_HEADER)) { + if (hasOutputFormat + && (startCodeValue == START_PICTURE || startCodeValue == START_SEQUENCE_HEADER)) { int bytesWrittenPastStartCode = limit - startCodeOffset; - boolean resetSample = (samplePosition == C.POSITION_UNSET); + boolean resetSample = samplePosition == C.POSITION_UNSET; if (foundPicture) { @C.BufferFlags int flags = isKeyframe ? C.BUFFER_FLAG_KEY_FRAME : 0; int size = (int) (totalBytesWritten - samplePosition) - bytesWrittenPastStartCode; @@ -147,7 +148,7 @@ public final class H262Reader implements ElementaryStreamReader { isKeyframe = false; resetSample = true; } - foundPicture = (startCodeValue == START_PICTURE); + foundPicture = startCodeValue == START_PICTURE; if (resetSample) { samplePosition = totalBytesWritten - bytesWrittenPastStartCode; sampleTimeUs = (pesPtsUsAvailable ? pesTimeUs : sampleTimeUs + frameDurationUs); diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index 1c39b558bb..2bba9071fd 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -126,7 +126,8 @@ import java.util.List; *

    • Default: {@code R.id.exo_playback_control_view}
    • * *
    • All attributes that can be set on a {@link PlaybackControlView} can also be set on a - * SimpleExoPlayerView, and will be propagated to the inflated {@link PlaybackControlView}. + * SimpleExoPlayerView, and will be propagated to the inflated {@link PlaybackControlView} + * unless the layout is overridden to specify a custom {@code exo_controller} (see below). *
    • * * @@ -163,18 +164,17 @@ import java.util.List; * * *
    • {@code exo_controller_placeholder} - A placeholder that's replaced with the inflated - * {@link PlaybackControlView}. + * {@link PlaybackControlView}. Ignored if an {@code exo_controller} view exists. *
        *
      • Type: {@link View}
      • *
      *
    • - *
    • {@code exo_controller} - An already inflated instance of - * {@link PlaybackControlView}. Allows you to use your own {@link PlaybackControlView} instead - * of default. Note: attrs such as rewind_increment will not be passed through to this - * instance and should be set at creation. {@code exo_controller_placeholder} will be ignored - * if this is set. + *
    • {@code exo_controller} - An already inflated {@link PlaybackControlView}. Allows use + * of a custom extension of {@link PlaybackControlView}. Note that attributes such as + * {@code rewind_increment} will not be automatically propagated through to this instance. If + * a view exists with this id, any {@code exo_controller_placeholder} view will be ignored. *
        - *
      • Type: {@link View}
      • + *
      • Type: {@link PlaybackControlView}
      • *
      *
    • *
    • {@code exo_overlay} - A {@link FrameLayout} positioned on top of the player which From 962a71314a670fb7e489a12444430f19623dce10 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 2 Aug 2017 01:25:41 -0700 Subject: [PATCH 0149/2472] Fix handling of H.262 CSD The start code for H.262 codec-specific data may be across a packet boundary. Before this change the offset passed to CsdBuffer.onData may have been before the start point of the data in the newData buffer. After this change, start codes are added directly to the CSD buffer when it's filling and any start code bytes added by onData (at the end of a packet) are discarded. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=163943584 --- .../exoplayer2/extractor/ts/H262Reader.java | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/library/core/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 index a00bace56c..160a9c5a71 100644 --- a/library/core/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 @@ -103,9 +103,8 @@ public final class H262Reader implements ElementaryStreamReader { totalBytesWritten += data.bytesLeft(); output.sampleData(data, data.bytesLeft()); - int searchOffset = offset; while (true) { - int startCodeOffset = NalUnitUtil.findNalUnit(dataArray, searchOffset, limit, prefixFlags); + int startCodeOffset = NalUnitUtil.findNalUnit(dataArray, offset, limit, prefixFlags); if (startCodeOffset == limit) { // We've scanned to the end of the data without finding another start code. @@ -126,7 +125,7 @@ public final class H262Reader implements ElementaryStreamReader { csdBuffer.onData(dataArray, offset, startCodeOffset); } // This is the number of bytes belonging to the next start code that have already been - // passed to csdDataTargetBuffer. + // passed to csdBuffer. int bytesAlreadyPassed = lengthToStartCode < 0 ? -lengthToStartCode : 0; if (csdBuffer.onStartCode(startCodeValue, bytesAlreadyPassed)) { // The csd data is complete, so we can decode and output the media format. @@ -160,8 +159,7 @@ public final class H262Reader implements ElementaryStreamReader { isKeyframe = true; } - offset = startCodeOffset; - searchOffset = offset + 3; + offset = startCodeOffset + 3; } } @@ -226,6 +224,8 @@ public final class H262Reader implements ElementaryStreamReader { private static final class CsdBuffer { + private static final byte[] START_CODE = new byte[] {0, 0, 1}; + private boolean isFilling; public int length; @@ -249,24 +249,25 @@ public final class H262Reader implements ElementaryStreamReader { * Called when a start code is encountered in the stream. * * @param startCodeValue The start code value. - * @param bytesAlreadyPassed The number of bytes of the start code that have already been - * passed to {@link #onData(byte[], int, int)}, or 0. + * @param bytesAlreadyPassed The number of bytes of the start code that have been passed to + * {@link #onData(byte[], int, int)}, or 0. * @return Whether the csd data is now complete. If true is returned, neither - * this method or {@link #onData(byte[], int, int)} should be called again without an + * this method nor {@link #onData(byte[], int, int)} should be called again without an * interleaving call to {@link #reset()}. */ public boolean onStartCode(int startCodeValue, int bytesAlreadyPassed) { if (isFilling) { + length -= bytesAlreadyPassed; if (sequenceExtensionPosition == 0 && startCodeValue == START_EXTENSION) { - sequenceExtensionPosition = length - bytesAlreadyPassed; + sequenceExtensionPosition = length; } else { - length -= bytesAlreadyPassed; isFilling = false; return true; } } else if (startCodeValue == START_SEQUENCE_HEADER) { isFilling = true; } + onData(START_CODE, 0, START_CODE.length); return false; } From 8e08d982aba3edf87c74f6462a99cadc793fa487 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 2 Aug 2017 03:07:56 -0700 Subject: [PATCH 0150/2472] Further fix H262 segmentation Issue: #2891 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=163951910 --- .../androidTest/assets/ts/sample.ts.0.dump | 2 +- .../exoplayer2/extractor/ts/H262Reader.java | 46 ++++++++----------- 2 files changed, 21 insertions(+), 27 deletions(-) diff --git a/library/core/src/androidTest/assets/ts/sample.ts.0.dump b/library/core/src/androidTest/assets/ts/sample.ts.0.dump index 83f1337816..91e48b1722 100644 --- a/library/core/src/androidTest/assets/ts/sample.ts.0.dump +++ b/library/core/src/androidTest/assets/ts/sample.ts.0.dump @@ -28,7 +28,7 @@ track 256: data = length 22, hash CE183139 sample count = 2 sample 0: - time = 0 + time = 33366 flags = 1 data = length 20711, hash 34341E8 sample 1: diff --git a/library/core/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 index 160a9c5a71..a3502a3242 100644 --- a/library/core/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 @@ -51,17 +51,17 @@ public final class H262Reader implements ElementaryStreamReader { // State that should be reset on seek. private final boolean[] prefixFlags; private final CsdBuffer csdBuffer; - private boolean foundPicture; private long totalBytesWritten; + private boolean startedFirstSample; // Per packet state that gets reset at the start of each packet. private long pesTimeUs; - private boolean pesPtsUsAvailable; - // Per sample state that gets reset at the start of each frame. - private boolean isKeyframe; + // Per sample state that gets reset at the start of each sample. private long samplePosition; private long sampleTimeUs; + private boolean sampleIsKeyframe; + private boolean sampleHasPicture; public H262Reader() { prefixFlags = new boolean[4]; @@ -72,10 +72,8 @@ public final class H262Reader implements ElementaryStreamReader { public void seek() { NalUnitUtil.clearPrefixFlags(prefixFlags); csdBuffer.reset(); - pesPtsUsAvailable = false; - foundPicture = false; - samplePosition = C.POSITION_UNSET; totalBytesWritten = 0; + startedFirstSample = false; } @Override @@ -87,10 +85,7 @@ public final class H262Reader implements ElementaryStreamReader { @Override public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { - pesPtsUsAvailable = pesTimeUs != C.TIME_UNSET; - if (pesPtsUsAvailable) { - this.pesTimeUs = pesTimeUs; - } + this.pesTimeUs = pesTimeUs; } @Override @@ -136,27 +131,26 @@ public final class H262Reader implements ElementaryStreamReader { } } - if (hasOutputFormat - && (startCodeValue == START_PICTURE || startCodeValue == START_SEQUENCE_HEADER)) { + if (startCodeValue == START_PICTURE || startCodeValue == START_SEQUENCE_HEADER) { int bytesWrittenPastStartCode = limit - startCodeOffset; - boolean resetSample = samplePosition == C.POSITION_UNSET; - if (foundPicture) { - @C.BufferFlags int flags = isKeyframe ? C.BUFFER_FLAG_KEY_FRAME : 0; + if (startedFirstSample && sampleHasPicture && hasOutputFormat) { + // Output the sample. + @C.BufferFlags int flags = sampleIsKeyframe ? C.BUFFER_FLAG_KEY_FRAME : 0; int size = (int) (totalBytesWritten - samplePosition) - bytesWrittenPastStartCode; output.sampleMetadata(sampleTimeUs, flags, size, bytesWrittenPastStartCode, null); - isKeyframe = false; - resetSample = true; } - foundPicture = startCodeValue == START_PICTURE; - if (resetSample) { + if (!startedFirstSample || sampleHasPicture) { + // Start the next sample. samplePosition = totalBytesWritten - bytesWrittenPastStartCode; - sampleTimeUs = (pesPtsUsAvailable ? pesTimeUs : sampleTimeUs + frameDurationUs); - pesPtsUsAvailable = false; + sampleTimeUs = pesTimeUs != C.TIME_UNSET ? pesTimeUs + : (startedFirstSample ? (sampleTimeUs + frameDurationUs) : 0); + sampleIsKeyframe = false; + pesTimeUs = C.TIME_UNSET; + startedFirstSample = true; } - } - - if (hasOutputFormat && startCodeValue == START_GROUP) { - isKeyframe = true; + sampleHasPicture = startCodeValue == START_PICTURE; + } else if (startCodeValue == START_GROUP) { + sampleIsKeyframe = true; } offset = startCodeOffset + 3; From 4a6a5b527fd411340a3a8300b29d7c222685f0f5 Mon Sep 17 00:00:00 2001 From: Danny Brain Date: Wed, 26 Jul 2017 11:30:02 +1000 Subject: [PATCH 0151/2472] Allow PlaybackControlView to be overridden in SimpleExoPlayerView --- .../android/exoplayer2/ui/SimpleExoPlayerView.java | 14 +++++++++++++- library/ui/src/main/res/values/ids.xml | 1 + 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index a4083c940f..1c39b558bb 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -168,6 +168,15 @@ import java.util.List; *
    • Type: {@link View}
    • * * + *
    • {@code exo_controller} - An already inflated instance of + * {@link PlaybackControlView}. Allows you to use your own {@link PlaybackControlView} instead + * of default. Note: attrs such as rewind_increment will not be passed through to this + * instance and should be set at creation. {@code exo_controller_placeholder} will be ignored + * if this is set. + *
        + *
      • Type: {@link View}
      • + *
      + *
    • *
    • {@code exo_overlay} - A {@link FrameLayout} positioned on top of the player which * the app can access via {@link #getOverlayFrameLayout()}, provided for convenience. *
        @@ -315,8 +324,11 @@ public final class SimpleExoPlayerView extends FrameLayout { } // Playback control view. + PlaybackControlView customController = (PlaybackControlView) findViewById(R.id.exo_controller); View controllerPlaceholder = findViewById(R.id.exo_controller_placeholder); - if (controllerPlaceholder != null) { + if (customController != null) { + this.controller = customController; + } else if (controllerPlaceholder != null) { // Note: rewindMs and fastForwardMs are passed via attrs, so we don't need to make explicit // calls to set them. this.controller = new PlaybackControlView(context, attrs); diff --git a/library/ui/src/main/res/values/ids.xml b/library/ui/src/main/res/values/ids.xml index 815487a54e..b16b1729da 100644 --- a/library/ui/src/main/res/values/ids.xml +++ b/library/ui/src/main/res/values/ids.xml @@ -20,6 +20,7 @@ + From 893c7f34ba333d290bdbffcf16c186b0b6dcbbe8 Mon Sep 17 00:00:00 2001 From: Michael Goffioul Date: Thu, 27 Jul 2017 15:44:28 -0400 Subject: [PATCH 0152/2472] Fix H262 segmentation. Prepend sequence headers to the next frame, instead of appending them to the previous frame. Tested decoders like FFMPEG and Google's Android/MPEG2 expects to read the sequence headers before the first frame they apply to. When sequence headers are appended to the previous frame, these are ignored and this leads to incorrect decoding. --- .../exoplayer2/extractor/ts/H262Reader.java | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/library/core/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 index 7266f847c4..92c8e8d800 100644 --- a/library/core/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 @@ -51,7 +51,7 @@ public final class H262Reader implements ElementaryStreamReader { // State that should be reset on seek. private final boolean[] prefixFlags; private final CsdBuffer csdBuffer; - private boolean foundFirstFrameInGroup; + private boolean foundPicture; private long totalBytesWritten; // Per packet state that gets reset at the start of each packet. @@ -60,8 +60,8 @@ public final class H262Reader implements ElementaryStreamReader { // Per sample state that gets reset at the start of each frame. private boolean isKeyframe; - private long framePosition; - private long frameTimeUs; + private long samplePosition; + private long sampleTimeUs; public H262Reader() { prefixFlags = new boolean[4]; @@ -73,7 +73,8 @@ public final class H262Reader implements ElementaryStreamReader { NalUnitUtil.clearPrefixFlags(prefixFlags); csdBuffer.reset(); pesPtsUsAvailable = false; - foundFirstFrameInGroup = false; + foundPicture = false; + samplePosition = C.POSITION_UNSET; totalBytesWritten = 0; } @@ -136,25 +137,28 @@ public final class H262Reader implements ElementaryStreamReader { } } - if (hasOutputFormat && (startCodeValue == START_GROUP || startCodeValue == START_PICTURE)) { + if (hasOutputFormat && (startCodeValue == START_PICTURE || startCodeValue == START_SEQUENCE_HEADER)) { int bytesWrittenPastStartCode = limit - startCodeOffset; - if (foundFirstFrameInGroup) { + boolean resetSample = (samplePosition == C.POSITION_UNSET); + if (foundPicture) { @C.BufferFlags int flags = isKeyframe ? C.BUFFER_FLAG_KEY_FRAME : 0; - int size = (int) (totalBytesWritten - framePosition) - bytesWrittenPastStartCode; - output.sampleMetadata(frameTimeUs, flags, size, bytesWrittenPastStartCode, null); + int size = (int) (totalBytesWritten - samplePosition) - bytesWrittenPastStartCode; + output.sampleMetadata(sampleTimeUs, flags, size, bytesWrittenPastStartCode, null); isKeyframe = false; + resetSample = true; } - if (startCodeValue == START_GROUP) { - foundFirstFrameInGroup = false; - isKeyframe = true; - } else /* startCodeValue == START_PICTURE */ { - frameTimeUs = pesPtsUsAvailable ? pesTimeUs : (frameTimeUs + frameDurationUs); - framePosition = totalBytesWritten - bytesWrittenPastStartCode; + foundPicture = (startCodeValue == START_PICTURE); + if (resetSample) { + samplePosition = totalBytesWritten - bytesWrittenPastStartCode; + sampleTimeUs = (pesPtsUsAvailable ? pesTimeUs : sampleTimeUs + frameDurationUs); pesPtsUsAvailable = false; - foundFirstFrameInGroup = true; } } + if (hasOutputFormat && startCodeValue == START_GROUP) { + isKeyframe = true; + } + offset = startCodeOffset; searchOffset = offset + 3; } From 395249a950ec73b3475ff84239f4d14dffa048bc Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 1 Aug 2017 04:44:02 -0700 Subject: [PATCH 0153/2472] Fix sequence extension position calculation ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=163814942 --- .../com/google/android/exoplayer2/extractor/ts/H262Reader.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/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 index 92c8e8d800..add8079105 100644 --- a/library/core/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 @@ -257,7 +257,7 @@ public final class H262Reader implements ElementaryStreamReader { public boolean onStartCode(int startCodeValue, int bytesAlreadyPassed) { if (isFilling) { if (sequenceExtensionPosition == 0 && startCodeValue == START_EXTENSION) { - sequenceExtensionPosition = length; + sequenceExtensionPosition = length - bytesAlreadyPassed; } else { length -= bytesAlreadyPassed; isFilling = false; From b2da61f70bfb9b1fea13db63fd2ce6dac92dc3ad Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 1 Aug 2017 07:50:12 -0700 Subject: [PATCH 0154/2472] Clean up MediaSessionExt documentation ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=163828712 --- .../DefaultPlaybackController.java | 42 +++--- .../mediasession/MediaSessionConnector.java | 126 ++++++++---------- .../RepeatModeActionProvider.java | 19 +-- .../mediasession/TimelineQueueNavigator.java | 25 ++-- .../exoplayer2/ui/PlaybackControlView.java | 16 ++- 5 files changed, 112 insertions(+), 116 deletions(-) diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/DefaultPlaybackController.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/DefaultPlaybackController.java index 231c1f1ea5..c3586b29e6 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/DefaultPlaybackController.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/DefaultPlaybackController.java @@ -21,40 +21,50 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Player; /** - * A default implementation of the {@link MediaSessionConnector.PlaybackController}. You can safely - * override any method for instance to intercept calls for a given action. + * A default implementation of {@link MediaSessionConnector.PlaybackController}. + *

        + * Methods can be safely overridden by subclasses to intercept calls for given actions. */ public class DefaultPlaybackController implements MediaSessionConnector.PlaybackController { + /** + * The default fast forward increment, in milliseconds. + */ + public static final int DEFAULT_FAST_FORWARD_MS = 15000; + /** + * The default rewind increment, in milliseconds. + */ + public static final int DEFAULT_REWIND_MS = 5000; + private static final long BASE_ACTIONS = PlaybackStateCompat.ACTION_PLAY_PAUSE | PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PAUSE | PlaybackStateCompat.ACTION_STOP; - protected final long fastForwardIncrementMs; protected final long rewindIncrementMs; + protected final long fastForwardIncrementMs; /** - * Creates a new {@link DefaultPlaybackController}. This is equivalent to calling - * {@code DefaultPlaybackController(15000L, 5000L)}. + * Creates a new instance. + *

        + * Equivalent to {@code DefaultPlaybackController( + * DefaultPlaybackController.DEFAULT_REWIND_MS, + * DefaultPlaybackController.DEFAULT_FAST_FORWARD_MS)}. */ public DefaultPlaybackController() { - this(15000L, 5000L); + this(DEFAULT_REWIND_MS, DEFAULT_FAST_FORWARD_MS); } /** - * Creates a new {@link DefaultPlaybackController} and sets the fast forward and rewind increments - * in milliseconds. + * Creates a new instance with the given fast forward and rewind increments. * - * @param fastForwardIncrementMs A positive value will cause the - * {@link PlaybackStateCompat#ACTION_FAST_FORWARD} playback action to be added. A zero or a - * negative value will cause it to be removed. - * @param rewindIncrementMs A positive value will cause the - * {@link PlaybackStateCompat#ACTION_REWIND} playback action to be added. A zero or a - * negative value will cause it to be removed. + * @param rewindIncrementMs The rewind increment in milliseconds. A zero or negative value will + * cause the rewind action to be disabled. + * @param fastForwardIncrementMs The fast forward increment in milliseconds. A zero or negative + * value will cause the fast forward action to be removed. */ - public DefaultPlaybackController(long fastForwardIncrementMs, long rewindIncrementMs) { - this.fastForwardIncrementMs = fastForwardIncrementMs; + public DefaultPlaybackController(long rewindIncrementMs, long fastForwardIncrementMs) { this.rewindIncrementMs = rewindIncrementMs; + this.fastForwardIncrementMs = fastForwardIncrementMs; } @Override diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java index a300acfffa..0e839b8083 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java @@ -44,30 +44,27 @@ import java.util.List; import java.util.Map; /** - * Mediates between a {@link MediaSessionCompat} and an {@link Player} instance set with - * {@link #setPlayer(Player, PlaybackPreparer, CustomActionProvider...)}. + * Connects a {@link MediaSessionCompat} to a {@link Player}. *

        - * The {@code MediaSessionConnector} listens for media actions sent by a media controller and - * realizes these actions by calling appropriate ExoPlayer methods. Further, the state of ExoPlayer - * will be synced automatically with the {@link PlaybackStateCompat} of the media session to - * broadcast state transitions to clients. You can optionally extend this behaviour by providing - * various collaborators. - *

        - * Media actions to initiate media playback ({@code PlaybackStateCompat#ACTION_PREPARE_*} and - * {@code PlaybackStateCompat#ACTION_PLAY_*} need to be handled by a {@link PlaybackPreparer} which - * build a {@link com.google.android.exoplayer2.source.MediaSource} to prepare ExoPlayer. Deploy - * your preparer by calling {@link #setPlaybackPreparer(PlaybackPreparer)}. - *

        - * To support a media session queue and navigation within this queue, you can set a - * {@link QueueNavigator} to maintain the queue yourself and implement queue navigation commands - * (like 'skip to next') sent by controllers. It's recommended to use the - * {@link TimelineQueueNavigator} to allow users navigating the windows of the ExoPlayer timeline. - *

        - * If you want to allow media controllers to manipulate the queue, implement a {@link QueueEditor} - * and deploy it with {@link #setQueueEditor(QueueEditor)}. - *

        - * Set an {@link ErrorMessageProvider} to provide an error code and a human readable error message - * to be broadcast to controllers. + * The connector listens for actions sent by the media session's controller and implements these + * actions by calling appropriate ExoPlayer methods. The playback state of the media session is + * automatically synced with the player. The connector can also be optionally extended by providing + * various collaborators: + *

          + *
        • Actions to initiate media playback ({@code PlaybackStateCompat#ACTION_PREPARE_*} and + * {@code PlaybackStateCompat#ACTION_PLAY_*}) can be handled by a {@link PlaybackPreparer} passed + * when calling {@link #setPlayer(Player, PlaybackPreparer, CustomActionProvider...)}. Custom + * actions can be handled by passing one or more {@link CustomActionProvider}s in a similar way. + *
        • + *
        • To enable a media queue and navigation within it, you can set a {@link QueueNavigator} by + * calling {@link #setQueueNavigator(QueueNavigator)}. Use of {@link TimelineQueueNavigator} is + * recommended for most use cases.
        • + *
        • To enable editing of the media queue, you can set a {@link QueueEditor} by calling + * {@link #setQueueEditor(QueueEditor)}.
        • + *
        • An {@link ErrorMessageProvider} for providing human readable error messages and + * corresponding error codes can be set by calling + * {@link #setErrorMessageProvider(ErrorMessageProvider)}.
        • + *
        */ public final class MediaSessionConnector { @@ -78,12 +75,7 @@ public final class MediaSessionConnector { public static final String EXTRAS_PITCH = "EXO_PITCH"; /** - * Interface to which media controller commands regarding preparing playback for a given media - * clip are delegated to. - *

        - * Normally preparing playback includes preparing the player with a - * {@link com.google.android.exoplayer2.source.MediaSource} and setting up the media session queue - * with a corresponding list of queue items. + * Interface to which playback preparation actions are delegated. */ public interface PlaybackPreparer { @@ -131,7 +123,7 @@ public final class MediaSessionConnector { } /** - * Controller to handle playback actions. + * Interface to which playback actions are delegated. */ public interface PlaybackController { @@ -178,8 +170,8 @@ public final class MediaSessionConnector { } /** - * Navigator to handle queue navigation actions and maintain the media session queue with - * {#link MediaSessionCompat#setQueue(List)} to provide the active queue item to the connector. + * Handles queue navigation actions, and updates the media session queue by calling + * {@code MediaSessionCompat.setQueue()}. */ public interface QueueNavigator { @@ -211,7 +203,7 @@ public final class MediaSessionConnector { */ void onCurrentWindowIndexChanged(Player player); /** - * Gets the id of the currently active queue item or + * Gets the id of the currently active queue item, or * {@link MediaSessionCompat.QueueItem#UNKNOWN_ID} if the active item is unknown. *

        * To let the connector publish metadata for the active queue item, the queue item with the @@ -241,7 +233,7 @@ public final class MediaSessionConnector { } /** - * Editor to manipulate the queue. + * Handles media session queue edits. */ public interface QueueEditor { @@ -302,12 +294,12 @@ public final class MediaSessionConnector { } /** - * Provides an user readable error code and a message for {@link ExoPlaybackException}s. + * Converts an exception into an error code and a user readable error message. */ public interface ErrorMessageProvider { /** - * Returns a pair of an error code and a user readable error message for a given - * {@link ExoPlaybackException}. + * Returns a pair consisting of an error code and a user readable error message for a given + * exception. */ Pair getErrorMessage(ExoPlaybackException playbackException); } @@ -316,6 +308,7 @@ public final class MediaSessionConnector { * The wrapped {@link MediaSessionCompat}. */ public final MediaSessionCompat mediaSession; + private final MediaControllerCompat mediaController; private final Handler handler; private final boolean doMaintainMetadata; @@ -334,11 +327,10 @@ public final class MediaSessionConnector { private ExoPlaybackException playbackException; /** - * Creates a {@code MediaSessionConnector}. This is equivalent to calling - * {@code #MediaSessionConnector(mediaSession, new DefaultPlaybackController)}. + * Creates an instance. Must be called on the same thread that is used to construct the player + * instances passed to {@link #setPlayer(Player, PlaybackPreparer, CustomActionProvider...)}. *

        - * Constructing the {@link MediaSessionConnector} needs to be done on the same thread as - * constructing the player instance. + * Equivalent to {@code MediaSessionConnector(mediaSession, new DefaultPlaybackController())}. * * @param mediaSession The {@link MediaSessionCompat} to connect to. */ @@ -347,14 +339,13 @@ public final class MediaSessionConnector { } /** - * Creates a {@code MediaSessionConnector}. This is equivalent to calling - * {@code #MediaSessionConnector(mediaSession, playbackController, true)}. + * Creates an instance. Must be called on the same thread that is used to construct the player + * instances passed to {@link #setPlayer(Player, PlaybackPreparer, CustomActionProvider...)}. *

        - * Constructing the {@link MediaSessionConnector} needs to be done on the same thread as - * constructing the player instance. + * Equivalent to {@code MediaSessionConnector(mediaSession, playbackController, true)}. * * @param mediaSession The {@link MediaSessionCompat} to connect to. - * @param playbackController The {@link PlaybackController}. + * @param playbackController A {@link PlaybackController} for handling playback actions. */ public MediaSessionConnector(MediaSessionCompat mediaSession, PlaybackController playbackController) { @@ -362,19 +353,14 @@ public final class MediaSessionConnector { } /** - * Creates a {@code MediaSessionConnector} with {@link CustomActionProvider}s. - *

        - * If you choose to pass {@code false} for {@code doMaintainMetadata} you need to maintain the - * metadata of the media session yourself (provide at least the duration to allow clients to show - * a progress bar). - *

        - * Constructing the {@link MediaSessionConnector} needs to be done on the same thread as - * constructing the player instance. + * Creates an instance. Must be called on the same thread that is used to construct the player + * instances passed to {@link #setPlayer(Player, PlaybackPreparer, CustomActionProvider...)}. * * @param mediaSession The {@link MediaSessionCompat} to connect to. - * @param playbackController The {@link PlaybackController}. - * @param doMaintainMetadata Sets whether the connector should maintain the metadata of the - * session. + * @param playbackController A {@link PlaybackController} for handling playback actions. + * @param doMaintainMetadata Whether the connector should maintain the metadata of the session. If + * {@code false}, you need to maintain the metadata of the media session yourself (provide at + * least the duration to allow clients to show a progress bar). */ public MediaSessionConnector(MediaSessionCompat mediaSession, PlaybackController playbackController, boolean doMaintainMetadata) { @@ -392,17 +378,14 @@ public final class MediaSessionConnector { } /** - * Sets the player to which media commands sent by a media controller are delegated. - *

        - * The media session callback is set if the {@code player} is not {@code null} and the callback is - * removed if the {@code player} is {@code null}. + * Sets the player to be connected to the media session. *

        * The order in which any {@link CustomActionProvider}s are passed determines the order of the * actions published with the playback state of the session. * * @param player The player to be connected to the {@code MediaSession}. - * @param playbackPreparer The playback preparer for the player. - * @param customActionProviders Optional {@link CustomActionProvider}s to publish and handle + * @param playbackPreparer An optional {@link PlaybackPreparer} for preparing the player. + * @param customActionProviders An optional {@link CustomActionProvider}s to publish and handle * custom actions. */ public void setPlayer(Player player, PlaybackPreparer playbackPreparer, @@ -411,7 +394,7 @@ public final class MediaSessionConnector { this.player.removeListener(exoPlayerEventListener); mediaSession.setCallback(null); } - setPlaybackPreparer(playbackPreparer); + this.playbackPreparer = playbackPreparer; this.player = player; this.customActionProviders = (player != null && customActionProviders != null) ? customActionProviders : new CustomActionProvider[0]; @@ -424,9 +407,9 @@ public final class MediaSessionConnector { } /** - * Sets the optional {@link ErrorMessageProvider}. + * Sets the {@link ErrorMessageProvider}. * - * @param errorMessageProvider The {@link ErrorMessageProvider}. + * @param errorMessageProvider The error message provider. */ public void setErrorMessageProvider(ErrorMessageProvider errorMessageProvider) { this.errorMessageProvider = errorMessageProvider; @@ -437,25 +420,21 @@ public final class MediaSessionConnector { * {@code ACTION_SKIP_TO_PREVIOUS}, {@code ACTION_SKIP_TO_QUEUE_ITEM} and * {@code ACTION_SET_SHUFFLE_MODE_ENABLED}. * - * @param queueNavigator The navigator to handle queue navigation. + * @param queueNavigator The queue navigator. */ public void setQueueNavigator(QueueNavigator queueNavigator) { this.queueNavigator = queueNavigator; } /** - * Sets the queue editor to handle commands to manipulate the queue sent by a media controller. + * Sets the {@link QueueEditor} to handle queue edits sent by the media controller. * - * @param queueEditor The editor to handle queue manipulation actions. + * @param queueEditor The queue editor. */ public void setQueueEditor(QueueEditor queueEditor) { this.queueEditor = queueEditor; } - private void setPlaybackPreparer(PlaybackPreparer playbackPreparer) { - this.playbackPreparer = playbackPreparer; - } - private void updateMediaSessionPlaybackState() { PlaybackStateCompat.Builder builder = new PlaybackStateCompat.Builder(); if (player == null) { @@ -603,6 +582,7 @@ public final class MediaSessionConnector { } private class ExoPlayerEventListener implements Player.EventListener { + @Override public void onTimelineChanged(Timeline timeline, Object manifest) { if (queueNavigator != null) { diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java index 1f33245059..abefe533ce 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java @@ -26,10 +26,13 @@ import com.google.android.exoplayer2.util.RepeatModeUtil; */ public final class RepeatModeActionProvider implements MediaSessionConnector.CustomActionProvider { + /** + * The default repeat toggle modes. + */ + public static final @RepeatModeUtil.RepeatToggleModes int DEFAULT_REPEAT_TOGGLE_MODES = + RepeatModeUtil.REPEAT_TOGGLE_MODE_ONE | RepeatModeUtil.REPEAT_TOGGLE_MODE_ALL; + private static final String ACTION_REPEAT_MODE = "ACTION_EXO_REPEAT_MODE"; - @RepeatModeUtil.RepeatToggleModes - private static final int DEFAULT_REPEAT_MODES = RepeatModeUtil.REPEAT_TOGGLE_MODE_ONE - | RepeatModeUtil.REPEAT_TOGGLE_MODE_ALL; private final Player player; @RepeatModeUtil.RepeatToggleModes @@ -39,20 +42,20 @@ public final class RepeatModeActionProvider implements MediaSessionConnector.Cus private final CharSequence repeatOffDescription; /** - * Creates a new {@link RepeatModeActionProvider}. + * Creates a new instance. *

        - * This is equivalent to calling the two argument constructor with - * {@code RepeatModeUtil#REPEAT_TOGGLE_MODE_ONE | RepeatModeUtil#REPEAT_TOGGLE_MODE_ALL}. + * Equivalent to {@code RepeatModeActionProvider(context, player, + * RepeatModeActionProvider.DEFAULT_REPEAT_TOGGLE_MODES)}. * * @param context The context. * @param player The player on which to toggle the repeat mode. */ public RepeatModeActionProvider(Context context, Player player) { - this(context, player, DEFAULT_REPEAT_MODES); + this(context, player, DEFAULT_REPEAT_TOGGLE_MODES); } /** - * Creates a new {@link RepeatModeActionProvider} for the given repeat toggle modes. + * Creates a new instance enabling the given repeat toggle modes. * * @param context The context. * @param player The player on which to toggle the repeat mode. diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java index 60aa5a5ba0..521b4cd6e3 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java @@ -29,9 +29,8 @@ import java.util.Collections; import java.util.List; /** - * An abstract implementation of the {@link MediaSessionConnector.QueueNavigator} that's based on an - * {@link Player}'s current {@link Timeline} and maps the timeline of the player to the media - * session queue. + * An abstract implementation of the {@link MediaSessionConnector.QueueNavigator} that maps the + * windows of a {@link Player}'s {@link Timeline} to the media session queue. */ public abstract class TimelineQueueNavigator implements MediaSessionConnector.QueueNavigator { @@ -44,10 +43,9 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu private long activeQueueItemId; /** - * Creates a new timeline queue navigator for a given {@link MediaSessionCompat}. + * Creates an instance for a given {@link MediaSessionCompat}. *

        - * This is equivalent to calling - * {@code #TimelineQueueNavigator(mediaSession, DEFAULT_MAX_QUEUE_SIZE)}. + * Equivalent to {@code TimelineQueueNavigator(mediaSession, DEFAULT_MAX_QUEUE_SIZE)}. * * @param mediaSession The {@link MediaSessionCompat}. */ @@ -56,12 +54,11 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu } /** - * Creates a new timeline queue navigator for a given {@link MediaSessionCompat} and a maximum - * queue size of {@code maxQueueSize}. + * Creates an instance for a given {@link MediaSessionCompat} and maximum queue size. *

        - * If the actual queue size is larger than {@code maxQueueSize} a floating window of - * {@code maxQueueSize} is applied and moved back and forth when the user is navigating within the - * queue. + * If the number of windows in the {@link Player}'s {@link Timeline} exceeds {@code maxQueueSize}, + * the media session queue will correspond to {@code maxQueueSize} windows centered on the one + * currently being played. * * @param mediaSession The {@link MediaSessionCompat}. * @param maxQueueSize The maximum queue size. @@ -80,12 +77,6 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu */ public abstract MediaDescriptionCompat getMediaDescription(int windowIndex); - /** - * Supports the following media actions: {@code PlaybackStateCompat.ACTION_SKIP_TO_NEXT | - * PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS | PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM}. - * - * @return The bit mask of the supported media actions. - */ @Override public long getSupportedQueueNavigatorActions(Player player) { if (player == null || player.getCurrentTimeline().getWindowCount() < 2) { diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java index a99c2dfde2..6ddbfed973 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java @@ -249,11 +249,23 @@ public class PlaybackControlView extends FrameLayout { }; + /** + * The default fast forward increment, in milliseconds. + */ public static final int DEFAULT_FAST_FORWARD_MS = 15000; + /** + * The default rewind increment, in milliseconds. + */ public static final int DEFAULT_REWIND_MS = 5000; + /** + * The default show timeout, in milliseconds. + */ public static final int DEFAULT_SHOW_TIMEOUT_MS = 5000; - public static final @RepeatModeUtil.RepeatToggleModes int DEFAULT_REPEAT_TOGGLE_MODES - = RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE; + /** + * The default repeat toggle modes. + */ + public static final @RepeatModeUtil.RepeatToggleModes int DEFAULT_REPEAT_TOGGLE_MODES = + RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE; /** * The maximum number of windows that can be shown in a multi-window time bar. From 18bb04d6d9fcca6fc3a077a75e97c32fd5305ddf Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 1 Aug 2017 08:08:58 -0700 Subject: [PATCH 0155/2472] Bump version to 2.5.0-beta2 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=163830353 --- RELEASENOTES.md | 8 +++++--- constants.gradle | 2 +- .../google/android/exoplayer2/ExoPlayerLibraryInfo.java | 4 ++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 24da37808b..379b84b4e7 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,11 +1,11 @@ # Release notes # -### r2.5.0-beta1 ### +### r2.5.0 (beta) ### * IMA extension: Wraps the Google Interactive Media Ads (IMA) SDK to provide an easy and seamless way of incorporating display ads into ExoPlayer playbacks. - You can read more about the IMA extension *A link to a blog post about this - extension will be added here prior to the stable 2.5.0 release.* + You can read more about the IMA extension + [here](https://medium.com/google-exoplayer/playing-ads-with-exoplayer-and-ima-868dfd767ea). * MediaSession extension: Provides an easy to to connect ExoPlayer with MediaSessionCompat in the Android Support Library. *A link to a blog post about this extension will be added here prior to the stable 2.5.0 release.* @@ -48,6 +48,8 @@ ([#889](https://github.com/google/ExoPlayer/issues/889)). * AndroidTV: Fixed issue where tunneled video playback would not start on some devices ([#2985](https://github.com/google/ExoPlayer/issues/2985)). +* MPEG-TS: Fixed segmentation issue when parsing H262 + ([#2891](https://github.com/google/ExoPlayer/issues/2891)). * Cronet extension: Support for a user-defined fallback if Cronet library is not present. * Misc bugfixes. diff --git a/constants.gradle b/constants.gradle index 73b80f6a83..0db74945c4 100644 --- a/constants.gradle +++ b/constants.gradle @@ -24,7 +24,7 @@ project.ext { supportLibraryVersion = '25.4.0' dexmakerVersion = '1.2' mockitoVersion = '1.9.5' - releaseVersion = 'r2.5.0-beta1' + releaseVersion = 'r2.5.0-beta2' modulePrefix = ':' if (gradle.ext.has('exoplayerModulePrefix')) { modulePrefix += gradle.ext.exoplayerModulePrefix diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java index 2abdfe5aee..c04a777e14 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java @@ -31,13 +31,13 @@ public final class ExoPlayerLibraryInfo { * The version of the library expressed as a string, for example "1.2.3". */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. - public static final String VERSION = "2.5.0-beta1"; + public static final String VERSION = "2.5.0-beta2"; /** * The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final String VERSION_SLASHY = "ExoPlayerLib/2.5.0-beta1"; + public static final String VERSION_SLASHY = "ExoPlayerLib/2.5.0-beta2"; /** * The version of the library expressed as an integer, for example 1002003. From 0717782fdc4e0dedf5b253bb294d10640beae262 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 1 Aug 2017 18:20:54 +0100 Subject: [PATCH 0156/2472] Minor cleanup --- .../src/androidTest/assets/ts/sample.ps.0.dump | 2 +- .../src/androidTest/assets/ts/sample.ts.0.dump | 4 ++-- .../exoplayer2/extractor/ts/H262Reader.java | 7 ++++--- .../exoplayer2/ui/SimpleExoPlayerView.java | 16 ++++++++-------- 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/library/core/src/androidTest/assets/ts/sample.ps.0.dump b/library/core/src/androidTest/assets/ts/sample.ps.0.dump index 3b44fb6fb9..98f3c6a85a 100644 --- a/library/core/src/androidTest/assets/ts/sample.ps.0.dump +++ b/library/core/src/androidTest/assets/ts/sample.ps.0.dump @@ -69,7 +69,7 @@ track 224: sample 0: time = 40000 flags = 1 - data = length 20616, hash CA38A5B5 + data = length 20646, hash 576390B sample 1: time = 80000 flags = 0 diff --git a/library/core/src/androidTest/assets/ts/sample.ts.0.dump b/library/core/src/androidTest/assets/ts/sample.ts.0.dump index 26c6665aaa..83f1337816 100644 --- a/library/core/src/androidTest/assets/ts/sample.ts.0.dump +++ b/library/core/src/androidTest/assets/ts/sample.ts.0.dump @@ -28,9 +28,9 @@ track 256: data = length 22, hash CE183139 sample count = 2 sample 0: - time = 33366 + time = 0 flags = 1 - data = length 20669, hash 26DABA0F + data = length 20711, hash 34341E8 sample 1: time = 66733 flags = 0 diff --git a/library/core/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 index add8079105..a00bace56c 100644 --- a/library/core/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 @@ -137,9 +137,10 @@ public final class H262Reader implements ElementaryStreamReader { } } - if (hasOutputFormat && (startCodeValue == START_PICTURE || startCodeValue == START_SEQUENCE_HEADER)) { + if (hasOutputFormat + && (startCodeValue == START_PICTURE || startCodeValue == START_SEQUENCE_HEADER)) { int bytesWrittenPastStartCode = limit - startCodeOffset; - boolean resetSample = (samplePosition == C.POSITION_UNSET); + boolean resetSample = samplePosition == C.POSITION_UNSET; if (foundPicture) { @C.BufferFlags int flags = isKeyframe ? C.BUFFER_FLAG_KEY_FRAME : 0; int size = (int) (totalBytesWritten - samplePosition) - bytesWrittenPastStartCode; @@ -147,7 +148,7 @@ public final class H262Reader implements ElementaryStreamReader { isKeyframe = false; resetSample = true; } - foundPicture = (startCodeValue == START_PICTURE); + foundPicture = startCodeValue == START_PICTURE; if (resetSample) { samplePosition = totalBytesWritten - bytesWrittenPastStartCode; sampleTimeUs = (pesPtsUsAvailable ? pesTimeUs : sampleTimeUs + frameDurationUs); diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index 1c39b558bb..2bba9071fd 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -126,7 +126,8 @@ import java.util.List; *

      • Default: {@code R.id.exo_playback_control_view}
      • *
      *
    • All attributes that can be set on a {@link PlaybackControlView} can also be set on a - * SimpleExoPlayerView, and will be propagated to the inflated {@link PlaybackControlView}. + * SimpleExoPlayerView, and will be propagated to the inflated {@link PlaybackControlView} + * unless the layout is overridden to specify a custom {@code exo_controller} (see below). *
    • * * @@ -163,18 +164,17 @@ import java.util.List; * * *
    • {@code exo_controller_placeholder} - A placeholder that's replaced with the inflated - * {@link PlaybackControlView}. + * {@link PlaybackControlView}. Ignored if an {@code exo_controller} view exists. *
        *
      • Type: {@link View}
      • *
      *
    • - *
    • {@code exo_controller} - An already inflated instance of - * {@link PlaybackControlView}. Allows you to use your own {@link PlaybackControlView} instead - * of default. Note: attrs such as rewind_increment will not be passed through to this - * instance and should be set at creation. {@code exo_controller_placeholder} will be ignored - * if this is set. + *
    • {@code exo_controller} - An already inflated {@link PlaybackControlView}. Allows use + * of a custom extension of {@link PlaybackControlView}. Note that attributes such as + * {@code rewind_increment} will not be automatically propagated through to this instance. If + * a view exists with this id, any {@code exo_controller_placeholder} view will be ignored. *
        - *
      • Type: {@link View}
      • + *
      • Type: {@link PlaybackControlView}
      • *
      *
    • *
    • {@code exo_overlay} - A {@link FrameLayout} positioned on top of the player which From e604daaa0954381f1e6917d66fb410627a967935 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 2 Aug 2017 01:25:41 -0700 Subject: [PATCH 0157/2472] Fix handling of H.262 CSD The start code for H.262 codec-specific data may be across a packet boundary. Before this change the offset passed to CsdBuffer.onData may have been before the start point of the data in the newData buffer. After this change, start codes are added directly to the CSD buffer when it's filling and any start code bytes added by onData (at the end of a packet) are discarded. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=163943584 --- .../exoplayer2/extractor/ts/H262Reader.java | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/library/core/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 index a00bace56c..160a9c5a71 100644 --- a/library/core/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 @@ -103,9 +103,8 @@ public final class H262Reader implements ElementaryStreamReader { totalBytesWritten += data.bytesLeft(); output.sampleData(data, data.bytesLeft()); - int searchOffset = offset; while (true) { - int startCodeOffset = NalUnitUtil.findNalUnit(dataArray, searchOffset, limit, prefixFlags); + int startCodeOffset = NalUnitUtil.findNalUnit(dataArray, offset, limit, prefixFlags); if (startCodeOffset == limit) { // We've scanned to the end of the data without finding another start code. @@ -126,7 +125,7 @@ public final class H262Reader implements ElementaryStreamReader { csdBuffer.onData(dataArray, offset, startCodeOffset); } // This is the number of bytes belonging to the next start code that have already been - // passed to csdDataTargetBuffer. + // passed to csdBuffer. int bytesAlreadyPassed = lengthToStartCode < 0 ? -lengthToStartCode : 0; if (csdBuffer.onStartCode(startCodeValue, bytesAlreadyPassed)) { // The csd data is complete, so we can decode and output the media format. @@ -160,8 +159,7 @@ public final class H262Reader implements ElementaryStreamReader { isKeyframe = true; } - offset = startCodeOffset; - searchOffset = offset + 3; + offset = startCodeOffset + 3; } } @@ -226,6 +224,8 @@ public final class H262Reader implements ElementaryStreamReader { private static final class CsdBuffer { + private static final byte[] START_CODE = new byte[] {0, 0, 1}; + private boolean isFilling; public int length; @@ -249,24 +249,25 @@ public final class H262Reader implements ElementaryStreamReader { * Called when a start code is encountered in the stream. * * @param startCodeValue The start code value. - * @param bytesAlreadyPassed The number of bytes of the start code that have already been - * passed to {@link #onData(byte[], int, int)}, or 0. + * @param bytesAlreadyPassed The number of bytes of the start code that have been passed to + * {@link #onData(byte[], int, int)}, or 0. * @return Whether the csd data is now complete. If true is returned, neither - * this method or {@link #onData(byte[], int, int)} should be called again without an + * this method nor {@link #onData(byte[], int, int)} should be called again without an * interleaving call to {@link #reset()}. */ public boolean onStartCode(int startCodeValue, int bytesAlreadyPassed) { if (isFilling) { + length -= bytesAlreadyPassed; if (sequenceExtensionPosition == 0 && startCodeValue == START_EXTENSION) { - sequenceExtensionPosition = length - bytesAlreadyPassed; + sequenceExtensionPosition = length; } else { - length -= bytesAlreadyPassed; isFilling = false; return true; } } else if (startCodeValue == START_SEQUENCE_HEADER) { isFilling = true; } + onData(START_CODE, 0, START_CODE.length); return false; } From cad25e5a4dba6e2b3d113c80ab5e25b5c8026e1d Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 2 Aug 2017 03:07:56 -0700 Subject: [PATCH 0158/2472] Further fix H262 segmentation Issue: #2891 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=163951910 --- .../androidTest/assets/ts/sample.ts.0.dump | 2 +- .../exoplayer2/extractor/ts/H262Reader.java | 46 ++++++++----------- 2 files changed, 21 insertions(+), 27 deletions(-) diff --git a/library/core/src/androidTest/assets/ts/sample.ts.0.dump b/library/core/src/androidTest/assets/ts/sample.ts.0.dump index 83f1337816..91e48b1722 100644 --- a/library/core/src/androidTest/assets/ts/sample.ts.0.dump +++ b/library/core/src/androidTest/assets/ts/sample.ts.0.dump @@ -28,7 +28,7 @@ track 256: data = length 22, hash CE183139 sample count = 2 sample 0: - time = 0 + time = 33366 flags = 1 data = length 20711, hash 34341E8 sample 1: diff --git a/library/core/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 index 160a9c5a71..a3502a3242 100644 --- a/library/core/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 @@ -51,17 +51,17 @@ public final class H262Reader implements ElementaryStreamReader { // State that should be reset on seek. private final boolean[] prefixFlags; private final CsdBuffer csdBuffer; - private boolean foundPicture; private long totalBytesWritten; + private boolean startedFirstSample; // Per packet state that gets reset at the start of each packet. private long pesTimeUs; - private boolean pesPtsUsAvailable; - // Per sample state that gets reset at the start of each frame. - private boolean isKeyframe; + // Per sample state that gets reset at the start of each sample. private long samplePosition; private long sampleTimeUs; + private boolean sampleIsKeyframe; + private boolean sampleHasPicture; public H262Reader() { prefixFlags = new boolean[4]; @@ -72,10 +72,8 @@ public final class H262Reader implements ElementaryStreamReader { public void seek() { NalUnitUtil.clearPrefixFlags(prefixFlags); csdBuffer.reset(); - pesPtsUsAvailable = false; - foundPicture = false; - samplePosition = C.POSITION_UNSET; totalBytesWritten = 0; + startedFirstSample = false; } @Override @@ -87,10 +85,7 @@ public final class H262Reader implements ElementaryStreamReader { @Override public void packetStarted(long pesTimeUs, boolean dataAlignmentIndicator) { - pesPtsUsAvailable = pesTimeUs != C.TIME_UNSET; - if (pesPtsUsAvailable) { - this.pesTimeUs = pesTimeUs; - } + this.pesTimeUs = pesTimeUs; } @Override @@ -136,27 +131,26 @@ public final class H262Reader implements ElementaryStreamReader { } } - if (hasOutputFormat - && (startCodeValue == START_PICTURE || startCodeValue == START_SEQUENCE_HEADER)) { + if (startCodeValue == START_PICTURE || startCodeValue == START_SEQUENCE_HEADER) { int bytesWrittenPastStartCode = limit - startCodeOffset; - boolean resetSample = samplePosition == C.POSITION_UNSET; - if (foundPicture) { - @C.BufferFlags int flags = isKeyframe ? C.BUFFER_FLAG_KEY_FRAME : 0; + if (startedFirstSample && sampleHasPicture && hasOutputFormat) { + // Output the sample. + @C.BufferFlags int flags = sampleIsKeyframe ? C.BUFFER_FLAG_KEY_FRAME : 0; int size = (int) (totalBytesWritten - samplePosition) - bytesWrittenPastStartCode; output.sampleMetadata(sampleTimeUs, flags, size, bytesWrittenPastStartCode, null); - isKeyframe = false; - resetSample = true; } - foundPicture = startCodeValue == START_PICTURE; - if (resetSample) { + if (!startedFirstSample || sampleHasPicture) { + // Start the next sample. samplePosition = totalBytesWritten - bytesWrittenPastStartCode; - sampleTimeUs = (pesPtsUsAvailable ? pesTimeUs : sampleTimeUs + frameDurationUs); - pesPtsUsAvailable = false; + sampleTimeUs = pesTimeUs != C.TIME_UNSET ? pesTimeUs + : (startedFirstSample ? (sampleTimeUs + frameDurationUs) : 0); + sampleIsKeyframe = false; + pesTimeUs = C.TIME_UNSET; + startedFirstSample = true; } - } - - if (hasOutputFormat && startCodeValue == START_GROUP) { - isKeyframe = true; + sampleHasPicture = startCodeValue == START_PICTURE; + } else if (startCodeValue == START_GROUP) { + sampleIsKeyframe = true; } offset = startCodeOffset + 3; From 55fe5d21a21ca257f631f8613e3ea6b26105333a Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 2 Aug 2017 05:57:38 -0700 Subject: [PATCH 0159/2472] Create the demos dir Now the demo app is under demos/main. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=163967883 --- {demo => demos/main}/README.md | 0 {demo => demos/main}/build.gradle | 0 {demo => demos/main}/src/main/AndroidManifest.xml | 0 .../main}/src/main/assets/media.exolist.json | 0 .../android/exoplayer2/demo/DemoApplication.java | 0 .../google/android/exoplayer2/demo/DemoUtil.java | 0 .../google/android/exoplayer2/demo/EventLogger.java | 0 .../android/exoplayer2/demo/PlayerActivity.java | 0 .../exoplayer2/demo/SampleChooserActivity.java | 0 .../exoplayer2/demo/TrackSelectionHelper.java | 0 .../main}/src/main/res/drawable-xhdpi/ic_banner.png | Bin .../main}/src/main/res/layout/list_divider.xml | 0 .../main}/src/main/res/layout/player_activity.xml | 0 .../src/main/res/layout/sample_chooser_activity.xml | 0 .../src/main/res/layout/track_selection_dialog.xml | 0 .../main}/src/main/res/mipmap-hdpi/ic_launcher.png | Bin .../main}/src/main/res/mipmap-mdpi/ic_launcher.png | Bin .../main}/src/main/res/mipmap-xhdpi/ic_launcher.png | Bin .../src/main/res/mipmap-xxhdpi/ic_launcher.png | Bin .../src/main/res/mipmap-xxxhdpi/ic_launcher.png | Bin .../main}/src/main/res/values/strings.xml | 0 {demo => demos/main}/src/main/res/values/styles.xml | 0 settings.gradle | 2 +- 23 files changed, 1 insertion(+), 1 deletion(-) rename {demo => demos/main}/README.md (100%) rename {demo => demos/main}/build.gradle (100%) rename {demo => demos/main}/src/main/AndroidManifest.xml (100%) rename {demo => demos/main}/src/main/assets/media.exolist.json (100%) rename {demo => demos/main}/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java (100%) rename {demo => demos/main}/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java (100%) rename {demo => demos/main}/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java (100%) rename {demo => demos/main}/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java (100%) rename {demo => demos/main}/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java (100%) rename {demo => demos/main}/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionHelper.java (100%) rename {demo => demos/main}/src/main/res/drawable-xhdpi/ic_banner.png (100%) rename {demo => demos/main}/src/main/res/layout/list_divider.xml (100%) rename {demo => demos/main}/src/main/res/layout/player_activity.xml (100%) rename {demo => demos/main}/src/main/res/layout/sample_chooser_activity.xml (100%) rename {demo => demos/main}/src/main/res/layout/track_selection_dialog.xml (100%) rename {demo => demos/main}/src/main/res/mipmap-hdpi/ic_launcher.png (100%) rename {demo => demos/main}/src/main/res/mipmap-mdpi/ic_launcher.png (100%) rename {demo => demos/main}/src/main/res/mipmap-xhdpi/ic_launcher.png (100%) rename {demo => demos/main}/src/main/res/mipmap-xxhdpi/ic_launcher.png (100%) rename {demo => demos/main}/src/main/res/mipmap-xxxhdpi/ic_launcher.png (100%) rename {demo => demos/main}/src/main/res/values/strings.xml (100%) rename {demo => demos/main}/src/main/res/values/styles.xml (100%) diff --git a/demo/README.md b/demos/main/README.md similarity index 100% rename from demo/README.md rename to demos/main/README.md diff --git a/demo/build.gradle b/demos/main/build.gradle similarity index 100% rename from demo/build.gradle rename to demos/main/build.gradle diff --git a/demo/src/main/AndroidManifest.xml b/demos/main/src/main/AndroidManifest.xml similarity index 100% rename from demo/src/main/AndroidManifest.xml rename to demos/main/src/main/AndroidManifest.xml diff --git a/demo/src/main/assets/media.exolist.json b/demos/main/src/main/assets/media.exolist.json similarity index 100% rename from demo/src/main/assets/media.exolist.json rename to demos/main/src/main/assets/media.exolist.json diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java similarity index 100% rename from demo/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java rename to demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java similarity index 100% rename from demo/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java rename to demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java similarity index 100% rename from demo/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java rename to demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java similarity index 100% rename from demo/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java rename to demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java similarity index 100% rename from demo/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java rename to demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java diff --git a/demo/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionHelper.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionHelper.java similarity index 100% rename from demo/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionHelper.java rename to demos/main/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionHelper.java diff --git a/demo/src/main/res/drawable-xhdpi/ic_banner.png b/demos/main/src/main/res/drawable-xhdpi/ic_banner.png similarity index 100% rename from demo/src/main/res/drawable-xhdpi/ic_banner.png rename to demos/main/src/main/res/drawable-xhdpi/ic_banner.png diff --git a/demo/src/main/res/layout/list_divider.xml b/demos/main/src/main/res/layout/list_divider.xml similarity index 100% rename from demo/src/main/res/layout/list_divider.xml rename to demos/main/src/main/res/layout/list_divider.xml diff --git a/demo/src/main/res/layout/player_activity.xml b/demos/main/src/main/res/layout/player_activity.xml similarity index 100% rename from demo/src/main/res/layout/player_activity.xml rename to demos/main/src/main/res/layout/player_activity.xml diff --git a/demo/src/main/res/layout/sample_chooser_activity.xml b/demos/main/src/main/res/layout/sample_chooser_activity.xml similarity index 100% rename from demo/src/main/res/layout/sample_chooser_activity.xml rename to demos/main/src/main/res/layout/sample_chooser_activity.xml diff --git a/demo/src/main/res/layout/track_selection_dialog.xml b/demos/main/src/main/res/layout/track_selection_dialog.xml similarity index 100% rename from demo/src/main/res/layout/track_selection_dialog.xml rename to demos/main/src/main/res/layout/track_selection_dialog.xml diff --git a/demo/src/main/res/mipmap-hdpi/ic_launcher.png b/demos/main/src/main/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from demo/src/main/res/mipmap-hdpi/ic_launcher.png rename to demos/main/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/demo/src/main/res/mipmap-mdpi/ic_launcher.png b/demos/main/src/main/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from demo/src/main/res/mipmap-mdpi/ic_launcher.png rename to demos/main/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/demo/src/main/res/mipmap-xhdpi/ic_launcher.png b/demos/main/src/main/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from demo/src/main/res/mipmap-xhdpi/ic_launcher.png rename to demos/main/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/demo/src/main/res/mipmap-xxhdpi/ic_launcher.png b/demos/main/src/main/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from demo/src/main/res/mipmap-xxhdpi/ic_launcher.png rename to demos/main/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/demo/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/demos/main/src/main/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from demo/src/main/res/mipmap-xxxhdpi/ic_launcher.png rename to demos/main/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/demo/src/main/res/values/strings.xml b/demos/main/src/main/res/values/strings.xml similarity index 100% rename from demo/src/main/res/values/strings.xml rename to demos/main/src/main/res/values/strings.xml diff --git a/demo/src/main/res/values/styles.xml b/demos/main/src/main/res/values/styles.xml similarity index 100% rename from demo/src/main/res/values/styles.xml rename to demos/main/src/main/res/values/styles.xml diff --git a/settings.gradle b/settings.gradle index fb31055f5e..766d46bbae 100644 --- a/settings.gradle +++ b/settings.gradle @@ -20,7 +20,7 @@ if (gradle.ext.has('exoplayerModulePrefix')) { include modulePrefix + 'demo' include modulePrefix + 'playbacktests' -project(modulePrefix + 'demo').projectDir = new File(rootDir, 'demo') +project(modulePrefix + 'demo').projectDir = new File(rootDir, 'demos/main') project(modulePrefix + 'playbacktests').projectDir = new File(rootDir, 'playbacktests') apply from: 'core_settings.gradle' From 22ea9ed6875384800bcd86772709ee68c2ebdf67 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 2 Aug 2017 05:58:45 -0700 Subject: [PATCH 0160/2472] Clean-up ExoHostedTest/HostActivity. This change clearly seperates the hosting of a test (HostActivity) from the internals of hosting an ExoPlayer lifecycle (ExoHostedTest). ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=163967960 --- .../exoplayer2/testutil/ExoHostedTest.java | 36 ++++++++---- .../exoplayer2/testutil/HostActivity.java | 56 ++++++++----------- 2 files changed, 49 insertions(+), 43 deletions(-) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java index 4aba23d691..77e197515b 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java @@ -73,7 +73,7 @@ public abstract class ExoHostedTest implements HostedTest, Player.EventListener, private final long expectedPlayingTimeMs; private final DecoderCounters videoDecoderCounters; private final DecoderCounters audioDecoderCounters; - private final ConditionVariable playerFinished; + private final ConditionVariable testFinished; private ActionSchedule pendingSchedule; private Handler actionHandler; @@ -116,7 +116,7 @@ public abstract class ExoHostedTest implements HostedTest, Player.EventListener, this.tag = tag; this.expectedPlayingTimeMs = expectedPlayingTimeMs; this.failOnPlayerError = failOnPlayerError; - this.playerFinished = new ConditionVariable(); + this.testFinished = new ConditionVariable(); this.videoDecoderCounters = new DecoderCounters(); this.audioDecoderCounters = new DecoderCounters(); } @@ -172,16 +172,13 @@ public abstract class ExoHostedTest implements HostedTest, Player.EventListener, } @Override - public final boolean blockUntilEnded(long timeoutMs) { - return playerFinished.block(timeoutMs); + public final boolean blockUntilStopped(long timeoutMs) { + return testFinished.block(timeoutMs); } @Override - public final void onStop() { - actionHandler.removeCallbacksAndMessages(null); - sourceDurationMs = player.getDuration(); - player.release(); - player = null; + public final boolean forceStop() { + return stopTest(); } @Override @@ -222,7 +219,7 @@ public abstract class ExoHostedTest implements HostedTest, Player.EventListener, playerWasPrepared |= playbackState != Player.STATE_IDLE; if (playbackState == Player.STATE_ENDED || (playbackState == Player.STATE_IDLE && playerWasPrepared)) { - playerFinished.open(); + stopTest(); } boolean playing = playWhenReady && playbackState == Player.STATE_READY; if (!this.playing && playing) { @@ -337,6 +334,25 @@ public abstract class ExoHostedTest implements HostedTest, Player.EventListener, // Internal logic + private boolean stopTest() { + if (player == null) { + return false; + } + actionHandler.removeCallbacksAndMessages(null); + sourceDurationMs = player.getDuration(); + player.release(); + player = null; + // We post opening of the finished condition so that any events posted to the main thread as a + // result of player.release() are guaranteed to be handled before the test returns. + actionHandler.post(new Runnable() { + @Override + public void run() { + testFinished.open(); + } + }); + return true; + } + protected DrmSessionManager buildDrmSessionManager(String userAgent) { // Do nothing. Interested subclasses may override. return null; diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/HostActivity.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/HostActivity.java index e1f7eae379..8e4b9001dd 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/HostActivity.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/HostActivity.java @@ -56,21 +56,20 @@ public final class HostActivity extends Activity implements SurfaceHolder.Callba void onStart(HostActivity host, Surface surface); /** - * Called on the main thread to block until the test has stopped or {@link #onStop()} is called. + * Called on the main thread to block until the test has stopped or {@link #forceStop()} is + * called. * * @param timeoutMs The maximum time to block in milliseconds. * @return Whether the test has stopped successful. */ - boolean blockUntilEnded(long timeoutMs); + boolean blockUntilStopped(long timeoutMs); /** - * Called on the main thread when the test is stopped. - *

      - * The test will be stopped when {@link #blockUntilEnded(long)} returns, if the - * {@link HostActivity} has been paused, or if the {@link HostActivity}'s {@link Surface} has - * been destroyed. + * Called on the main thread to force stop the test (if it is not stopped already). + * + * @return Whether the test was forced stopped. */ - void onStop(); + boolean forceStop(); /** * Called on the test thread after the test has finished and been stopped. @@ -89,7 +88,8 @@ public final class HostActivity extends Activity implements SurfaceHolder.Callba private HostedTest hostedTest; private boolean hostedTestStarted; - private boolean forcedFinished; + private ConditionVariable hostedTestStartedCondition; + private boolean forcedStopped; /** * Executes a {@link HostedTest} inside the host. @@ -109,34 +109,31 @@ public final class HostActivity extends Activity implements SurfaceHolder.Callba * @param timeoutMs The number of milliseconds to wait for the test to finish. * @param failOnTimeout Whether the test fails when the timeout is exceeded. */ - public void runTest(HostedTest hostedTest, long timeoutMs, boolean failOnTimeout) { + public void runTest(final HostedTest hostedTest, long timeoutMs, boolean failOnTimeout) { Assertions.checkArgument(timeoutMs > 0); Assertions.checkState(Thread.currentThread() != getMainLooper().getThread()); - Assertions.checkState(this.hostedTest == null); - this.hostedTest = Assertions.checkNotNull(hostedTest); + Assertions.checkNotNull(hostedTest); + hostedTestStartedCondition = new ConditionVariable(); + forcedStopped = false; hostedTestStarted = false; - forcedFinished = false; - final ConditionVariable testStarted = new ConditionVariable(); runOnUiThread(new Runnable() { @Override public void run() { + HostActivity.this.hostedTest = hostedTest; maybeStartHostedTest(); - testStarted.open(); } }); - testStarted.block(); + hostedTestStartedCondition.block(); - if (hostedTest.blockUntilEnded(timeoutMs)) { - hostedTest.onStop(); - if (!forcedFinished) { - Log.d(TAG, "Test finished. Checking pass conditions."); + if (hostedTest.blockUntilStopped(timeoutMs)) { + if (!forcedStopped) { + Log.d(TAG, "Checking test pass conditions."); hostedTest.onFinished(); - this.hostedTest = null; Log.d(TAG, "Pass conditions checked."); } else { - String message = "Test released before it finished. Activity may have been paused whilst " + String message = "Test force stopped. Activity may have been paused whilst " + "test was in progress."; Log.e(TAG, message); fail(message); @@ -147,8 +144,8 @@ public final class HostActivity extends Activity implements SurfaceHolder.Callba if (failOnTimeout) { fail(message); } - maybeStopHostedTest(); } + this.hostedTest = null; } // Activity lifecycle @@ -175,12 +172,6 @@ public final class HostActivity extends Activity implements SurfaceHolder.Callba super.onStart(); } - @Override - public void onResume() { - super.onResume(); - maybeStartHostedTest(); - } - @Override public void onPause() { super.onPause(); @@ -224,14 +215,13 @@ public final class HostActivity extends Activity implements SurfaceHolder.Callba hostedTestStarted = true; Log.d(TAG, "Starting test."); hostedTest.onStart(this, surface); + hostedTestStartedCondition.open(); } } private void maybeStopHostedTest() { - if (hostedTest != null && hostedTestStarted) { - forcedFinished = true; - hostedTest.onStop(); - hostedTest = null; + if (hostedTest != null && hostedTestStarted && !forcedStopped) { + forcedStopped = hostedTest.forceStop(); } } From 49b83c01ca2d005f54530adef360b5e9ae9bd235 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 2 Aug 2017 07:07:38 -0700 Subject: [PATCH 0161/2472] Throw ParserException if parsing unsupported media This is for consistency with what we do elsewhere; specifically in FragmentedMp4Extractor. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=163974960 --- .../extractor/ts/AdtsReaderTest.java | 7 +++--- .../extractor/flv/AudioTagPayloadReader.java | 3 ++- .../exoplayer2/extractor/mp4/AtomParsers.java | 2 +- .../exoplayer2/extractor/ts/AdtsReader.java | 5 ++-- .../extractor/ts/ElementaryStreamReader.java | 4 +++- .../exoplayer2/extractor/ts/LatmReader.java | 23 ++++++++++--------- .../exoplayer2/extractor/ts/PesReader.java | 4 +++- .../exoplayer2/extractor/ts/PsExtractor.java | 6 +++-- .../extractor/ts/TsPayloadReader.java | 4 +++- .../util/CodecSpecificDataUtil.java | 13 +++++++---- 10 files changed, 44 insertions(+), 27 deletions(-) diff --git a/library/core/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 index bcfa90a565..6a31250e15 100644 --- a/library/core/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 @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.extractor.ts; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; import com.google.android.exoplayer2.testutil.FakeExtractorOutput; import com.google.android.exoplayer2.testutil.FakeTrackOutput; @@ -154,20 +155,20 @@ public class AdtsReaderTest extends TestCase { } } - public void testAdtsDataOnly() throws Exception { + public void testAdtsDataOnly() throws ParserException { data.setPosition(ID3_DATA_1.length + ID3_DATA_2.length); feed(); assertSampleCounts(0, 1); adtsOutput.assertSample(0, ADTS_CONTENT, 0, C.BUFFER_FLAG_KEY_FRAME, null); } - private void feedLimited(int limit) { + private void feedLimited(int limit) throws ParserException { maybeStartPacket(); data.setLimit(limit); feed(); } - private void feed() { + private void feed() throws ParserException { maybeStartPacket(); adtsReader.consume(data); } diff --git a/library/core/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 index 2f21898007..ec5ad88aeb 100644 --- a/library/core/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 @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor.flv; import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.util.CodecSpecificDataUtil; import com.google.android.exoplayer2.util.MimeTypes; @@ -85,7 +86,7 @@ import java.util.Collections; } @Override - protected void parsePayload(ParsableByteArray data, long timeUs) { + protected void parsePayload(ParsableByteArray data, long timeUs) throws ParserException { if (audioFormat == AUDIO_FORMAT_MP3) { int sampleSize = data.bytesLeft(); output.sampleData(data, sampleSize); diff --git a/library/core/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 index f7e3e846e9..0a5e0e8a6d 100644 --- a/library/core/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 @@ -816,7 +816,7 @@ 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) { + StsdData out, int entryIndex) throws ParserException { parent.setPosition(position + Atom.HEADER_SIZE + StsdData.STSD_HEADER_SIZE); int quickTimeSoundDescriptionVersion = 0; diff --git a/library/core/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 index 7277df5bb8..96b964a4c4 100644 --- a/library/core/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 @@ -19,6 +19,7 @@ import android.util.Log; import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.DummyTrackOutput; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; @@ -128,7 +129,7 @@ public final class AdtsReader implements ElementaryStreamReader { } @Override - public void consume(ParsableByteArray data) { + public void consume(ParsableByteArray data) throws ParserException { while (data.bytesLeft() > 0) { switch (state) { case STATE_FINDING_SAMPLE: @@ -276,7 +277,7 @@ public final class AdtsReader implements ElementaryStreamReader { /** * Parses the sample header. */ - private void parseAdtsHeader() { + private void parseAdtsHeader() throws ParserException { adtsScratch.setPosition(0); if (!hasOutputFormat) { diff --git a/library/core/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 index 57bcf31fc5..fa7f78c8c0 100644 --- a/library/core/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 @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor.ts; +import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -50,8 +51,9 @@ public interface ElementaryStreamReader { * Consumes (possibly partial) data from the current packet. * * @param data The data to consume. + * @throws ParserException If the data could not be parsed. */ - void consume(ParsableByteArray data); + void consume(ParsableByteArray data) throws ParserException; /** * Called when a packet ends. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java index 425dc43ea7..d21943eae8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java @@ -19,6 +19,7 @@ import android.support.annotation.Nullable; import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; @@ -98,7 +99,7 @@ public final class LatmReader implements ElementaryStreamReader { } @Override - public void consume(ParsableByteArray data) { + public void consume(ParsableByteArray data) throws ParserException { int bytesToRead; while (data.bytesLeft() > 0) { switch (state) { @@ -148,7 +149,7 @@ public final class LatmReader implements ElementaryStreamReader { * * @param data A {@link ParsableBitArray} containing the AudioMuxElement's bytes. */ - private void parseAudioMuxElement(ParsableBitArray data) { + private void parseAudioMuxElement(ParsableBitArray data) throws ParserException { boolean useSameStreamMux = data.readBit(); if (!useSameStreamMux) { streamMuxRead = true; @@ -159,7 +160,7 @@ public final class LatmReader implements ElementaryStreamReader { if (audioMuxVersionA == 0) { if (numSubframes != 0) { - throw new UnsupportedOperationException(); + throw new ParserException(); } int muxSlotLengthBytes = parsePayloadLengthInfo(data); parsePayloadMux(data, muxSlotLengthBytes); @@ -167,14 +168,14 @@ public final class LatmReader implements ElementaryStreamReader { data.skipBits((int) otherDataLenBits); } } else { - throw new UnsupportedOperationException(); // Not defined by ISO/IEC 14496-3:2009. + throw new ParserException(); // Not defined by ISO/IEC 14496-3:2009. } } /** * Parses a StreamMuxConfig as defined in ISO/IEC 14496-3:2009 Section 1.7.3.1, Table 1.42. */ - private void parseStreamMuxConfig(ParsableBitArray data) { + private void parseStreamMuxConfig(ParsableBitArray data) throws ParserException { audioMuxVersion = data.readBits(1); audioMuxVersionA = audioMuxVersion == 1 ? data.readBits(1) : 0; if (audioMuxVersionA == 0) { @@ -182,13 +183,13 @@ public final class LatmReader implements ElementaryStreamReader { latmGetValue(data); // Skip taraBufferFullness. } if (!data.readBit()) { - throw new UnsupportedOperationException(); + throw new ParserException(); } numSubframes = data.readBits(6); int numProgram = data.readBits(4); int numLayer = data.readBits(3); if (numProgram != 0 || numLayer != 0) { - throw new UnsupportedOperationException(); + throw new ParserException(); } if (audioMuxVersion == 0) { int startPosition = data.getPosition(); @@ -228,7 +229,7 @@ public final class LatmReader implements ElementaryStreamReader { data.skipBits(8); // crcCheckSum. } } else { - throw new UnsupportedOperationException(); // This is not defined by ISO/IEC 14496-3:2009. + throw new ParserException(); // This is not defined by ISO/IEC 14496-3:2009. } } @@ -253,7 +254,7 @@ public final class LatmReader implements ElementaryStreamReader { } } - private int parseAudioSpecificConfig(ParsableBitArray data) { + private int parseAudioSpecificConfig(ParsableBitArray data) throws ParserException { int bitsLeft = data.bitsLeft(); Pair config = CodecSpecificDataUtil.parseAacAudioSpecificConfig(data); sampleRateHz = config.first; @@ -261,7 +262,7 @@ public final class LatmReader implements ElementaryStreamReader { return bitsLeft - data.bitsLeft(); } - private int parsePayloadLengthInfo(ParsableBitArray data) { + private int parsePayloadLengthInfo(ParsableBitArray data) throws ParserException { int muxSlotLengthBytes = 0; // Assuming single program and single layer. if (frameLengthType == 0) { @@ -272,7 +273,7 @@ public final class LatmReader implements ElementaryStreamReader { } while (tmp == 255); return muxSlotLengthBytes; } else { - throw new UnsupportedOperationException(); + throw new ParserException(); } } diff --git a/library/core/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 index 59696b9dea..4863df42eb 100644 --- a/library/core/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 @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.extractor.ts; import android.util.Log; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -77,7 +78,8 @@ public final class PesReader implements TsPayloadReader { } @Override - public final void consume(ParsableByteArray data, boolean payloadUnitStartIndicator) { + public final void consume(ParsableByteArray data, boolean payloadUnitStartIndicator) + throws ParserException { if (payloadUnitStartIndicator) { switch (state) { case STATE_FINDING_HEADER: diff --git a/library/core/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 index 883fb8f880..69c5745eaa 100644 --- a/library/core/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 @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.extractor.ts; import android.util.SparseArray; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorOutput; @@ -30,7 +31,7 @@ import com.google.android.exoplayer2.util.TimestampAdjuster; import java.io.IOException; /** - * Facilitates the extraction of data from the MPEG-2 TS container format. + * Facilitates the extraction of data from the MPEG-2 PS container format. */ public final class PsExtractor implements Extractor { @@ -275,8 +276,9 @@ public final class PsExtractor implements Extractor { * Consumes the payload of a PS packet. * * @param data The PES packet. The position will be set to the start of the payload. + * @throws ParserException If the payload could not be parsed. */ - public void consume(ParsableByteArray data) { + public void consume(ParsableByteArray data) throws ParserException { data.readBytes(pesScratch.data, 0, 3); pesScratch.setPosition(0); parseHeader(); diff --git a/library/core/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 index e7996c66c3..efa764b572 100644 --- a/library/core/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 @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.extractor.ts; import android.util.SparseArray; +import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -196,7 +197,8 @@ public interface TsPayloadReader { * * @param data The TS packet. The position will be set to the start of the payload. * @param payloadUnitStartIndicator Whether payloadUnitStartIndicator was set on the TS packet. + * @throws ParserException If the payload could not be parsed. */ - void consume(ParsableByteArray data, boolean payloadUnitStartIndicator); + void consume(ParsableByteArray data, boolean payloadUnitStartIndicator) throws ParserException; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/CodecSpecificDataUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/util/CodecSpecificDataUtil.java index 468e8dd666..c9884abe78 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/CodecSpecificDataUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/CodecSpecificDataUtil.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.util; import android.util.Pair; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ParserException; import java.util.ArrayList; import java.util.List; @@ -85,8 +86,10 @@ public final class CodecSpecificDataUtil { * * @param audioSpecificConfig A byte array containing the AudioSpecificConfig to parse. * @return A pair consisting of the sample rate in Hz and the channel count. + * @throws ParserException If the AudioSpecificConfig cannot be parsed as it's not supported. */ - public static Pair parseAacAudioSpecificConfig(byte[] audioSpecificConfig) { + public static Pair parseAacAudioSpecificConfig(byte[] audioSpecificConfig) + throws ParserException { return parseAacAudioSpecificConfig(new ParsableBitArray(audioSpecificConfig)); } @@ -96,8 +99,10 @@ public final class CodecSpecificDataUtil { * @param bitArray A {@link ParsableBitArray} containing the AudioSpecificConfig to parse. The * position is advanced to the end of the AudioSpecificConfig. * @return A pair consisting of the sample rate in Hz and the channel count. + * @throws ParserException If the AudioSpecificConfig cannot be parsed as it's not supported. */ - public static Pair parseAacAudioSpecificConfig(ParsableBitArray bitArray) { + public static Pair parseAacAudioSpecificConfig(ParsableBitArray bitArray) + throws ParserException { int audioObjectType = getAacAudioObjectType(bitArray); int sampleRate = getAacSamplingFrequency(bitArray); int channelConfiguration = bitArray.readBits(4); @@ -131,7 +136,7 @@ public final class CodecSpecificDataUtil { parseGaSpecificConfig(bitArray, audioObjectType, channelConfiguration); break; default: - throw new UnsupportedOperationException(); + throw new ParserException("Unsupported audio object type: " + audioObjectType); } switch (audioObjectType) { case 17: @@ -142,7 +147,7 @@ public final class CodecSpecificDataUtil { case 23: int epConfig = bitArray.readBits(2); if (epConfig == 2 || epConfig == 3) { - throw new UnsupportedOperationException(); + throw new ParserException("Unsupported epConfig: " + epConfig); } break; } From 412138b2eaf475edc4d7c997aad569ae87573aca Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 2 Aug 2017 09:26:04 -0700 Subject: [PATCH 0162/2472] Some extractor fixes - Fix Ogg extractor to work without sniffing. - Fix extractors to handle seek() before init(). - Add tests for both issues. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=163992343 --- .../ext/flac/FlacExtractorTest.java | 2 +- .../exoplayer2/ext/flac/FlacExtractor.java | 10 ++- .../extractor/flv/FlvExtractorTest.java | 2 +- .../extractor/mkv/MatroskaExtractorTest.java | 6 +- .../extractor/mp3/Mp3ExtractorTest.java | 4 +- .../mp4/FragmentedMp4ExtractorTest.java | 6 +- .../extractor/mp4/Mp4ExtractorTest.java | 2 +- .../extractor/ogg/OggExtractorTest.java | 9 +- .../extractor/rawcc/RawCcExtractorTest.java | 2 +- .../extractor/ts/Ac3ExtractorTest.java | 2 +- .../extractor/ts/AdtsExtractorTest.java | 2 +- .../extractor/ts/PsExtractorTest.java | 2 +- .../extractor/ts/TsExtractorTest.java | 2 +- .../extractor/wav/WavExtractorTest.java | 2 +- .../exoplayer2/extractor/Extractor.java | 3 +- .../extractor/ogg/OggExtractor.java | 66 ++++++++------ .../extractor/ogg/StreamReader.java | 9 +- .../exoplayer2/extractor/ts/Ac3Extractor.java | 4 +- .../extractor/ts/AdtsExtractor.java | 5 +- .../exoplayer2/testutil/ExtractorAsserts.java | 87 +++++++++++-------- 20 files changed, 132 insertions(+), 95 deletions(-) diff --git a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java index 5954985100..7b193997c3 100644 --- a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java +++ b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java @@ -26,7 +26,7 @@ import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; public class FlacExtractorTest extends InstrumentationTestCase { public void testSample() throws Exception { - ExtractorAsserts.assertOutput(new ExtractorFactory() { + ExtractorAsserts.assertBehavior(new ExtractorFactory() { @Override public Extractor create() { return new FlacExtractor(); diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java index d13194793e..7b71b5c743 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java @@ -159,13 +159,17 @@ public final class FlacExtractor implements Extractor { if (position == 0) { metadataParsed = false; } - decoderJni.reset(position); + if (decoderJni != null) { + decoderJni.reset(position); + } } @Override public void release() { - decoderJni.release(); - decoderJni = null; + if (decoderJni != null) { + decoderJni.release(); + decoderJni = null; + } } } diff --git a/library/core/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 index 4587c98317..fc8d181eac 100644 --- a/library/core/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 @@ -26,7 +26,7 @@ import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; public final class FlvExtractorTest extends InstrumentationTestCase { public void testSample() throws Exception { - ExtractorAsserts.assertOutput(new ExtractorFactory() { + ExtractorAsserts.assertBehavior(new ExtractorFactory() { @Override public Extractor create() { return new FlvExtractor(); diff --git a/library/core/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 index 57beec3ac6..624a5ccb7e 100644 --- a/library/core/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 @@ -26,7 +26,7 @@ import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; public final class MatroskaExtractorTest extends InstrumentationTestCase { public void testMkvSample() throws Exception { - ExtractorAsserts.assertOutput(new ExtractorFactory() { + ExtractorAsserts.assertBehavior(new ExtractorFactory() { @Override public Extractor create() { return new MatroskaExtractor(); @@ -35,7 +35,7 @@ public final class MatroskaExtractorTest extends InstrumentationTestCase { } public void testWebmSubsampleEncryption() throws Exception { - ExtractorAsserts.assertOutput(new ExtractorFactory() { + ExtractorAsserts.assertBehavior(new ExtractorFactory() { @Override public Extractor create() { return new MatroskaExtractor(); @@ -44,7 +44,7 @@ public final class MatroskaExtractorTest extends InstrumentationTestCase { } public void testWebmSubsampleEncryptionWithAltrefFrames() throws Exception { - ExtractorAsserts.assertOutput(new ExtractorFactory() { + ExtractorAsserts.assertBehavior(new ExtractorFactory() { @Override public Extractor create() { return new MatroskaExtractor(); diff --git a/library/core/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 index 3ad6a74bc9..0f98624d69 100644 --- a/library/core/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 @@ -26,7 +26,7 @@ import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; public final class Mp3ExtractorTest extends InstrumentationTestCase { public void testMp3Sample() throws Exception { - ExtractorAsserts.assertOutput(new ExtractorFactory() { + ExtractorAsserts.assertBehavior(new ExtractorFactory() { @Override public Extractor create() { return new Mp3Extractor(); @@ -35,7 +35,7 @@ public final class Mp3ExtractorTest extends InstrumentationTestCase { } public void testTrimmedMp3Sample() throws Exception { - ExtractorAsserts.assertOutput(new ExtractorFactory() { + ExtractorAsserts.assertBehavior(new ExtractorFactory() { @Override public Extractor create() { return new Mp3Extractor(); diff --git a/library/core/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 index d8da8760e4..76c13495c1 100644 --- a/library/core/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 @@ -27,13 +27,13 @@ import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; public final class FragmentedMp4ExtractorTest extends InstrumentationTestCase { public void testSample() throws Exception { - ExtractorAsserts - .assertOutput(getExtractorFactory(), "mp4/sample_fragmented.mp4", getInstrumentation()); + ExtractorAsserts.assertBehavior(getExtractorFactory(), "mp4/sample_fragmented.mp4", + getInstrumentation()); } public void testSampleWithSeiPayloadParsing() throws Exception { // Enabling the CEA-608 track enables SEI payload parsing. - ExtractorAsserts.assertOutput( + ExtractorAsserts.assertBehavior( getExtractorFactory(FragmentedMp4Extractor.FLAG_ENABLE_CEA608_TRACK), "mp4/sample_fragmented_sei.mp4", getInstrumentation()); } diff --git a/library/core/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 index a534d6dd24..5e327e5502 100644 --- a/library/core/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 @@ -28,7 +28,7 @@ import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; public final class Mp4ExtractorTest extends InstrumentationTestCase { public void testMp4Sample() throws Exception { - ExtractorAsserts.assertOutput(new ExtractorFactory() { + ExtractorAsserts.assertBehavior(new ExtractorFactory() { @Override public Extractor create() { return new Mp4Extractor(); diff --git a/library/core/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 index 26b7991869..3be23422cc 100644 --- a/library/core/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 @@ -36,20 +36,21 @@ public final class OggExtractorTest extends InstrumentationTestCase { }; public void testOpus() throws Exception { - ExtractorAsserts.assertOutput(OGG_EXTRACTOR_FACTORY, "ogg/bear.opus", getInstrumentation()); + ExtractorAsserts.assertBehavior(OGG_EXTRACTOR_FACTORY, "ogg/bear.opus", getInstrumentation()); } public void testFlac() throws Exception { - ExtractorAsserts.assertOutput(OGG_EXTRACTOR_FACTORY, "ogg/bear_flac.ogg", getInstrumentation()); + ExtractorAsserts.assertBehavior(OGG_EXTRACTOR_FACTORY, "ogg/bear_flac.ogg", + getInstrumentation()); } public void testFlacNoSeektable() throws Exception { - ExtractorAsserts.assertOutput(OGG_EXTRACTOR_FACTORY, "ogg/bear_flac_noseektable.ogg", + ExtractorAsserts.assertBehavior(OGG_EXTRACTOR_FACTORY, "ogg/bear_flac_noseektable.ogg", getInstrumentation()); } public void testVorbis() throws Exception { - ExtractorAsserts.assertOutput(OGG_EXTRACTOR_FACTORY, "ogg/bear_vorbis.ogg", + ExtractorAsserts.assertBehavior(OGG_EXTRACTOR_FACTORY, "ogg/bear_vorbis.ogg", getInstrumentation()); } diff --git a/library/core/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 index 5a9d60512c..18050f48a3 100644 --- a/library/core/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 @@ -30,7 +30,7 @@ import com.google.android.exoplayer2.util.MimeTypes; public final class RawCcExtractorTest extends InstrumentationTestCase { public void testRawCcSample() throws Exception { - ExtractorAsserts.assertOutput( + ExtractorAsserts.assertBehavior( new ExtractorFactory() { @Override public Extractor create() { diff --git a/library/core/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 index 1c18e44373..31633361db 100644 --- a/library/core/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 @@ -26,7 +26,7 @@ import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; public final class Ac3ExtractorTest extends InstrumentationTestCase { public void testSample() throws Exception { - ExtractorAsserts.assertOutput(new ExtractorFactory() { + ExtractorAsserts.assertBehavior(new ExtractorFactory() { @Override public Extractor create() { return new Ac3Extractor(); diff --git a/library/core/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 index bc05be6fa8..9eb65d2091 100644 --- a/library/core/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 @@ -26,7 +26,7 @@ import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; public final class AdtsExtractorTest extends InstrumentationTestCase { public void testSample() throws Exception { - ExtractorAsserts.assertOutput(new ExtractorFactory() { + ExtractorAsserts.assertBehavior(new ExtractorFactory() { @Override public Extractor create() { return new AdtsExtractor(); diff --git a/library/core/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 index e6937ccbc8..78ef05a769 100644 --- a/library/core/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 @@ -26,7 +26,7 @@ import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; public final class PsExtractorTest extends InstrumentationTestCase { public void testSample() throws Exception { - ExtractorAsserts.assertOutput(new ExtractorFactory() { + ExtractorAsserts.assertBehavior(new ExtractorFactory() { @Override public Extractor create() { return new PsExtractor(); diff --git a/library/core/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 index 09c9facab0..b6eddb5112 100644 --- a/library/core/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 @@ -45,7 +45,7 @@ public final class TsExtractorTest extends InstrumentationTestCase { private static final int TS_SYNC_BYTE = 0x47; // First byte of each TS packet. public void testSample() throws Exception { - ExtractorAsserts.assertOutput(new ExtractorFactory() { + ExtractorAsserts.assertBehavior(new ExtractorFactory() { @Override public Extractor create() { return new TsExtractor(); diff --git a/library/core/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 index 7c969fd386..36c05aa72e 100644 --- a/library/core/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 @@ -26,7 +26,7 @@ import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; public final class WavExtractorTest extends InstrumentationTestCase { public void testSample() throws Exception { - ExtractorAsserts.assertOutput(new ExtractorFactory() { + ExtractorAsserts.assertBehavior(new ExtractorFactory() { @Override public Extractor create() { return new WavExtractor(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/Extractor.java index de3dfd5266..7a2bc15da9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/Extractor.java @@ -63,7 +63,8 @@ public interface Extractor { void init(ExtractorOutput output); /** - * Extracts data read from a provided {@link ExtractorInput}. + * Extracts data read from a provided {@link ExtractorInput}. Must not be called before + * {@link #init(ExtractorOutput)}. *

      * A single call to this method will block until some progress has been made, but will not block * for longer than this. Hence each call will consume only a small amount of input data. diff --git a/library/core/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 index cc3c5de311..54e168c665 100644 --- a/library/core/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 @@ -45,30 +45,14 @@ public class OggExtractor implements Extractor { private static final int MAX_VERIFICATION_BYTES = 8; + private ExtractorOutput output; private StreamReader streamReader; + private boolean streamReaderInitialized; @Override public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { try { - OggPageHeader header = new OggPageHeader(); - if (!header.populate(input, true) || (header.type & 0x02) != 0x02) { - return false; - } - - int length = Math.min(header.bodySize, MAX_VERIFICATION_BYTES); - ParsableByteArray scratch = new ParsableByteArray(length); - input.peekFully(scratch.data, 0, length); - - if (FlacReader.verifyBitstreamType(resetPosition(scratch))) { - streamReader = new FlacReader(); - } else if (VorbisReader.verifyBitstreamType(resetPosition(scratch))) { - streamReader = new VorbisReader(); - } else if (OpusReader.verifyBitstreamType(resetPosition(scratch))) { - streamReader = new OpusReader(); - } else { - return false; - } - return true; + return sniffInternal(input); } catch (ParserException e) { return false; } @@ -76,15 +60,14 @@ public class OggExtractor implements Extractor { @Override public void init(ExtractorOutput output) { - TrackOutput trackOutput = output.track(0, C.TRACK_TYPE_AUDIO); - output.endTracks(); - // TODO: fix the case if sniff() isn't called - streamReader.init(output, trackOutput); + this.output = output; } @Override public void seek(long position, long timeUs) { - streamReader.seek(position, timeUs); + if (streamReader != null) { + streamReader.seek(position, timeUs); + } } @Override @@ -95,12 +78,41 @@ public class OggExtractor implements Extractor { @Override public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException, InterruptedException { + if (streamReader == null) { + if (!sniffInternal(input)) { + throw new ParserException("Failed to determine bitstream type"); + } + input.resetPeekPosition(); + } + if (!streamReaderInitialized) { + TrackOutput trackOutput = output.track(0, C.TRACK_TYPE_AUDIO); + output.endTracks(); + streamReader.init(output, trackOutput); + streamReaderInitialized = true; + } return streamReader.read(input, seekPosition); } - //@VisibleForTesting - /* package */ StreamReader getStreamReader() { - return streamReader; + private boolean sniffInternal(ExtractorInput input) throws IOException, InterruptedException { + OggPageHeader header = new OggPageHeader(); + if (!header.populate(input, true) || (header.type & 0x02) != 0x02) { + return false; + } + + int length = Math.min(header.bodySize, MAX_VERIFICATION_BYTES); + ParsableByteArray scratch = new ParsableByteArray(length); + input.peekFully(scratch.data, 0, length); + + if (FlacReader.verifyBitstreamType(resetPosition(scratch))) { + streamReader = new FlacReader(); + } else if (VorbisReader.verifyBitstreamType(resetPosition(scratch))) { + streamReader = new VorbisReader(); + } else if (OpusReader.verifyBitstreamType(resetPosition(scratch))) { + streamReader = new OpusReader(); + } else { + return false; + } + return true; } private static ParsableByteArray resetPosition(ParsableByteArray scratch) { diff --git a/library/core/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 index c203b0c6bd..d136468faa 100644 --- a/library/core/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 @@ -41,7 +41,8 @@ import java.io.IOException; OggSeeker oggSeeker; } - private OggPacket oggPacket; + private final OggPacket oggPacket; + private TrackOutput trackOutput; private ExtractorOutput extractorOutput; private OggSeeker oggSeeker; @@ -55,11 +56,13 @@ import java.io.IOException; private boolean seekMapSet; private boolean formatSet; + public StreamReader() { + oggPacket = new OggPacket(); + } + void init(ExtractorOutput output, TrackOutput trackOutput) { this.extractorOutput = output; this.trackOutput = trackOutput; - this.oggPacket = new OggPacket(); - reset(true); } diff --git a/library/core/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 index 9c9536beec..8bab6b7ed1 100644 --- a/library/core/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 @@ -56,9 +56,9 @@ public final class Ac3Extractor implements Extractor { private static final int ID3_TAG = Util.getIntegerCodeForString("ID3"); private final long firstSampleTimestampUs; + private final Ac3Reader reader; private final ParsableByteArray sampleData; - private Ac3Reader reader; private boolean startedPacket; public Ac3Extractor() { @@ -67,6 +67,7 @@ public final class Ac3Extractor implements Extractor { public Ac3Extractor(long firstSampleTimestampUs) { this.firstSampleTimestampUs = firstSampleTimestampUs; + reader = new Ac3Reader(); sampleData = new ParsableByteArray(MAX_SYNC_FRAME_SIZE); } @@ -117,7 +118,6 @@ public final class Ac3Extractor implements Extractor { @Override public void init(ExtractorOutput output) { - reader = new Ac3Reader(); // TODO: Add support for embedded ID3. reader.createTracks(output, new TrackIdGenerator(0, 1)); output.endTracks(); output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET)); diff --git a/library/core/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 index f7dadd51b2..a1851aa0ea 100644 --- a/library/core/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 @@ -55,10 +55,9 @@ public final class AdtsExtractor implements Extractor { private static final int MAX_SNIFF_BYTES = 8 * 1024; private final long firstSampleTimestampUs; + private final AdtsReader reader; private final ParsableByteArray packetBuffer; - // Accessed only by the loading thread. - private AdtsReader reader; private boolean startedPacket; public AdtsExtractor() { @@ -67,6 +66,7 @@ public final class AdtsExtractor implements Extractor { public AdtsExtractor(long firstSampleTimestampUs) { this.firstSampleTimestampUs = firstSampleTimestampUs; + reader = new AdtsReader(true); packetBuffer = new ParsableByteArray(MAX_PACKET_SIZE); } @@ -127,7 +127,6 @@ public final class AdtsExtractor implements Extractor { @Override public void init(ExtractorOutput output) { - reader = new AdtsReader(true); reader.createTracks(output, new TrackIdGenerator(0, 1)); output.endTracks(); output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET)); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExtractorAsserts.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExtractorAsserts.java index fb78b3a634..db63662c45 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExtractorAsserts.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExtractorAsserts.java @@ -18,6 +18,8 @@ package com.google.android.exoplayer2.testutil; import android.app.Instrumentation; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.Extractor; +import com.google.android.exoplayer2.extractor.ExtractorInput; +import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.testutil.FakeExtractorInput.SimulatedIOException; @@ -42,46 +44,57 @@ public final class ExtractorAsserts { private static final String UNKNOWN_LENGTH_EXTENSION = ".unklen" + DUMP_EXTENSION; /** - * Calls {@link #assertOutput(Extractor, String, byte[], Instrumentation, boolean, boolean, - * boolean)} with all possible combinations of "simulate" parameters. + * Asserts that an extractor behaves correctly given valid input data: + *

        + *
      • Calls {@link Extractor#seek(long, long)} and {@link Extractor#release()} without calling + * {@link Extractor#init(ExtractorOutput)} to check these calls do not fail.
      • + *
      • Calls {@link #assertOutput(Extractor, String, byte[], Instrumentation, boolean, boolean, + * boolean, boolean)} with all possible combinations of "simulate" parameters.
      • + *
      * * @param factory An {@link ExtractorFactory} which creates instances of the {@link Extractor} * class which is to be tested. - * @param sampleFile The path to the input sample. + * @param file The path to the input sample. * @param instrumentation To be used to load the sample file. * @throws IOException If reading from the input fails. * @throws InterruptedException If interrupted while reading from the input. - * @see #assertOutput(Extractor, String, byte[], Instrumentation, boolean, boolean, boolean) */ - public static void assertOutput(ExtractorFactory factory, String sampleFile, + public static void assertBehavior(ExtractorFactory factory, String file, Instrumentation instrumentation) throws IOException, InterruptedException { - byte[] fileData = TestUtil.getByteArray(instrumentation, sampleFile); - assertOutput(factory, sampleFile, fileData, instrumentation); + // Check behavior prior to initialization. + Extractor extractor = factory.create(); + extractor.seek(0, 0); + extractor.release(); + // Assert output. + byte[] fileData = TestUtil.getByteArray(instrumentation, file); + assertOutput(factory, file, fileData, instrumentation); } /** * Calls {@link #assertOutput(Extractor, String, byte[], Instrumentation, boolean, boolean, - * boolean)} with all possible combinations of "simulate" parameters. + * boolean, boolean)} with all possible combinations of "simulate" parameters with + * {@code sniffFirst} set to true, and makes one additional call with the "simulate" and + * {@code sniffFirst} parameters all set to false. * * @param factory An {@link ExtractorFactory} which creates instances of the {@link Extractor} * class which is to be tested. - * @param sampleFile The path to the input sample. - * @param fileData Content of the input file. + * @param file The path to the input sample. + * @param data Content of the input file. * @param instrumentation To be used to load the sample file. * @throws IOException If reading from the input fails. * @throws InterruptedException If interrupted while reading from the input. - * @see #assertOutput(Extractor, String, byte[], Instrumentation, boolean, boolean, boolean) */ - public static void assertOutput(ExtractorFactory factory, String sampleFile, byte[] fileData, + public static void assertOutput(ExtractorFactory factory, String file, byte[] data, Instrumentation instrumentation) throws IOException, InterruptedException { - assertOutput(factory.create(), sampleFile, fileData, instrumentation, false, false, false); - assertOutput(factory.create(), sampleFile, fileData, instrumentation, true, false, false); - assertOutput(factory.create(), sampleFile, fileData, instrumentation, false, true, false); - assertOutput(factory.create(), sampleFile, fileData, instrumentation, true, true, false); - assertOutput(factory.create(), sampleFile, fileData, instrumentation, false, false, true); - assertOutput(factory.create(), sampleFile, fileData, instrumentation, true, false, true); - assertOutput(factory.create(), sampleFile, fileData, instrumentation, false, true, true); - assertOutput(factory.create(), sampleFile, fileData, instrumentation, true, true, true); + assertOutput(factory.create(), file, data, instrumentation, true, false, false, false); + assertOutput(factory.create(), file, data, instrumentation, true, false, false, true); + assertOutput(factory.create(), file, data, instrumentation, true, false, true, false); + assertOutput(factory.create(), file, data, instrumentation, true, false, true, true); + assertOutput(factory.create(), file, data, instrumentation, true, true, false, false); + assertOutput(factory.create(), file, data, instrumentation, true, true, false, true); + assertOutput(factory.create(), file, data, instrumentation, true, true, true, false); + assertOutput(factory.create(), file, data, instrumentation, true, true, true, true); + assertOutput(factory.create(), file, data, instrumentation, false, false, false, false); } /** @@ -91,34 +104,38 @@ public final class ExtractorAsserts { * #UNKNOWN_LENGTH_EXTENSION}" exists, it's preferred. * * @param extractor The {@link Extractor} to be tested. - * @param sampleFile The path to the input sample. - * @param fileData Content of the input file. + * @param file The path to the input sample. + * @param data Content of the input file. * @param instrumentation To be used to load the sample file. - * @param simulateIOErrors If true simulates IOErrors. - * @param simulateUnknownLength If true simulates unknown input length. - * @param simulatePartialReads If true simulates partial reads. + * @param sniffFirst Whether to sniff the data by calling {@link Extractor#sniff(ExtractorInput)} + * prior to consuming it. + * @param simulateIOErrors Whether to simulate IO errors. + * @param simulateUnknownLength Whether to simulate unknown input length. + * @param simulatePartialReads Whether to simulate partial reads. * @return The {@link FakeExtractorOutput} used in the test. * @throws IOException If reading from the input fails. * @throws InterruptedException If interrupted while reading from the input. */ - public static FakeExtractorOutput assertOutput(Extractor extractor, String sampleFile, - byte[] fileData, Instrumentation instrumentation, boolean simulateIOErrors, + public static FakeExtractorOutput assertOutput(Extractor extractor, String file, byte[] data, + Instrumentation instrumentation, boolean sniffFirst, boolean simulateIOErrors, boolean simulateUnknownLength, boolean simulatePartialReads) throws IOException, InterruptedException { - FakeExtractorInput input = new FakeExtractorInput.Builder().setData(fileData) + FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data) .setSimulateIOErrors(simulateIOErrors) .setSimulateUnknownLength(simulateUnknownLength) .setSimulatePartialReads(simulatePartialReads).build(); - Assert.assertTrue(TestUtil.sniffTestData(extractor, input)); - input.resetPeekPosition(); - FakeExtractorOutput extractorOutput = consumeTestData(extractor, input, 0, true); + if (sniffFirst) { + Assert.assertTrue(TestUtil.sniffTestData(extractor, input)); + input.resetPeekPosition(); + } + FakeExtractorOutput extractorOutput = consumeTestData(extractor, input, 0, true); if (simulateUnknownLength - && assetExists(instrumentation, sampleFile + UNKNOWN_LENGTH_EXTENSION)) { - extractorOutput.assertOutput(instrumentation, sampleFile + UNKNOWN_LENGTH_EXTENSION); + && assetExists(instrumentation, file + UNKNOWN_LENGTH_EXTENSION)) { + extractorOutput.assertOutput(instrumentation, file + UNKNOWN_LENGTH_EXTENSION); } else { - extractorOutput.assertOutput(instrumentation, sampleFile + ".0" + DUMP_EXTENSION); + extractorOutput.assertOutput(instrumentation, file + ".0" + DUMP_EXTENSION); } SeekMap seekMap = extractorOutput.seekMap; @@ -133,7 +150,7 @@ public final class ExtractorAsserts { } consumeTestData(extractor, input, timeUs, extractorOutput, false); - extractorOutput.assertOutput(instrumentation, sampleFile + '.' + j + DUMP_EXTENSION); + extractorOutput.assertOutput(instrumentation, file + '.' + j + DUMP_EXTENSION); } } From 5ca84ebfd8bc933c676c3b1c5e8194ab21a95002 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 2 Aug 2017 09:26:58 -0700 Subject: [PATCH 0163/2472] Remove child data holder helper from AbstractConcatenatedTimeline. This helper class required a scratch instance to write on. Such a scratch instance may violate the immuatability of the timelines if used by multiple threads simultaneously. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=163992458 --- .../source/AbstractConcatenatedTimeline.java | 141 ++++++++---------- .../source/ConcatenatingMediaSource.java | 61 ++++---- .../DynamicConcatenatingMediaSource.java | 45 +++--- .../exoplayer2/source/LoopingMediaSource.java | 40 +++-- 4 files changed, 148 insertions(+), 139 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java b/library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java index 3bee3cc73f..42ac938677 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java @@ -25,68 +25,25 @@ import com.google.android.exoplayer2.Timeline; */ /* package */ abstract class AbstractConcatenatedTimeline extends Timeline { - /** - * Meta data of a child timeline. - */ - protected static final class ChildDataHolder { + private final int childCount; - /** - * Child timeline. - */ - public Timeline timeline; - - /** - * First period index belonging to the child timeline. - */ - public int firstPeriodIndexInChild; - - /** - * First window index belonging to the child timeline. - */ - public int firstWindowIndexInChild; - - /** - * UID of child timeline. - */ - public Object uid; - - /** - * Set child holder data. - * - * @param timeline Child timeline. - * @param firstPeriodIndexInChild First period index belonging to the child timeline. - * @param firstWindowIndexInChild First window index belonging to the child timeline. - * @param uid UID of child timeline. - */ - public void setData(Timeline timeline, int firstPeriodIndexInChild, int firstWindowIndexInChild, - Object uid) { - this.timeline = timeline; - this.firstPeriodIndexInChild = firstPeriodIndexInChild; - this.firstWindowIndexInChild = firstWindowIndexInChild; - this.uid = uid; - } - - } - - private final ChildDataHolder childDataHolder; - - public AbstractConcatenatedTimeline() { - childDataHolder = new ChildDataHolder(); + public AbstractConcatenatedTimeline(int childCount) { + this.childCount = childCount; } @Override public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) { - getChildDataByWindowIndex(windowIndex, childDataHolder); - int firstWindowIndexInChild = childDataHolder.firstWindowIndexInChild; - int nextWindowIndexInChild = childDataHolder.timeline.getNextWindowIndex( + int childIndex = getChildIndexByWindowIndex(windowIndex); + int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex); + int nextWindowIndexInChild = getTimelineByChildIndex(childIndex).getNextWindowIndex( windowIndex - firstWindowIndexInChild, repeatMode == Player.REPEAT_MODE_ALL ? Player.REPEAT_MODE_OFF : repeatMode); if (nextWindowIndexInChild != C.INDEX_UNSET) { return firstWindowIndexInChild + nextWindowIndexInChild; } else { - firstWindowIndexInChild += childDataHolder.timeline.getWindowCount(); - if (firstWindowIndexInChild < getWindowCount()) { - return firstWindowIndexInChild; + int nextChildIndex = childIndex + 1; + if (nextChildIndex < childCount) { + return getFirstWindowIndexByChildIndex(nextChildIndex); } else if (repeatMode == Player.REPEAT_MODE_ALL) { return 0; } else { @@ -97,9 +54,9 @@ import com.google.android.exoplayer2.Timeline; @Override public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) { - getChildDataByWindowIndex(windowIndex, childDataHolder); - int firstWindowIndexInChild = childDataHolder.firstWindowIndexInChild; - int previousWindowIndexInChild = childDataHolder.timeline.getPreviousWindowIndex( + int childIndex = getChildIndexByWindowIndex(windowIndex); + int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex); + int previousWindowIndexInChild = getTimelineByChildIndex(childIndex).getPreviousWindowIndex( windowIndex - firstWindowIndexInChild, repeatMode == Player.REPEAT_MODE_ALL ? Player.REPEAT_MODE_OFF : repeatMode); if (previousWindowIndexInChild != C.INDEX_UNSET) { @@ -118,11 +75,11 @@ import com.google.android.exoplayer2.Timeline; @Override public final Window getWindow(int windowIndex, Window window, boolean setIds, long defaultPositionProjectionUs) { - getChildDataByWindowIndex(windowIndex, childDataHolder); - int firstWindowIndexInChild = childDataHolder.firstWindowIndexInChild; - int firstPeriodIndexInChild = childDataHolder.firstPeriodIndexInChild; - childDataHolder.timeline.getWindow(windowIndex - firstWindowIndexInChild, window, setIds, - defaultPositionProjectionUs); + int childIndex = getChildIndexByWindowIndex(windowIndex); + int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex); + int firstPeriodIndexInChild = getFirstPeriodIndexByChildIndex(childIndex); + getTimelineByChildIndex(childIndex).getWindow(windowIndex - firstWindowIndexInChild, window, + setIds, defaultPositionProjectionUs); window.firstPeriodIndex += firstPeriodIndexInChild; window.lastPeriodIndex += firstPeriodIndexInChild; return window; @@ -130,13 +87,14 @@ import com.google.android.exoplayer2.Timeline; @Override public final Period getPeriod(int periodIndex, Period period, boolean setIds) { - getChildDataByPeriodIndex(periodIndex, childDataHolder); - int firstWindowIndexInChild = childDataHolder.firstWindowIndexInChild; - int firstPeriodIndexInChild = childDataHolder.firstPeriodIndexInChild; - childDataHolder.timeline.getPeriod(periodIndex - firstPeriodIndexInChild, period, setIds); + int childIndex = getChildIndexByPeriodIndex(periodIndex); + int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex); + int firstPeriodIndexInChild = getFirstPeriodIndexByChildIndex(childIndex); + getTimelineByChildIndex(childIndex).getPeriod(periodIndex - firstPeriodIndexInChild, period, + setIds); period.windowIndex += firstWindowIndexInChild; if (setIds) { - period.uid = Pair.create(childDataHolder.uid, period.uid); + period.uid = Pair.create(getChildUidByChildIndex(childIndex), period.uid); } return period; } @@ -149,37 +107,64 @@ import com.google.android.exoplayer2.Timeline; Pair childUidAndPeriodUid = (Pair) uid; Object childUid = childUidAndPeriodUid.first; Object periodUid = childUidAndPeriodUid.second; - if (!getChildDataByChildUid(childUid, childDataHolder)) { + int childIndex = getChildIndexByChildUid(childUid); + if (childIndex == C.INDEX_UNSET) { return C.INDEX_UNSET; } - int periodIndexInChild = childDataHolder.timeline.getIndexOfPeriod(periodUid); + int periodIndexInChild = getTimelineByChildIndex(childIndex).getIndexOfPeriod(periodUid); return periodIndexInChild == C.INDEX_UNSET ? C.INDEX_UNSET - : childDataHolder.firstPeriodIndexInChild + periodIndexInChild; + : getFirstPeriodIndexByChildIndex(childIndex) + periodIndexInChild; } /** - * Populates {@link ChildDataHolder} for the child timeline containing the given period index. + * Returns the index of the child timeline containing the given period index. * * @param periodIndex A valid period index within the bounds of the timeline. - * @param childData A data holder to be populated. */ - protected abstract void getChildDataByPeriodIndex(int periodIndex, ChildDataHolder childData); + protected abstract int getChildIndexByPeriodIndex(int periodIndex); /** - * Populates {@link ChildDataHolder} for the child timeline containing the given window index. + * Returns the index of the child timeline containing the given window index. * * @param windowIndex A valid window index within the bounds of the timeline. - * @param childData A data holder to be populated. */ - protected abstract void getChildDataByWindowIndex(int windowIndex, ChildDataHolder childData); + protected abstract int getChildIndexByWindowIndex(int windowIndex); /** - * Populates {@link ChildDataHolder} for the child timeline with the given UID. + * Returns the index of the child timeline with the given UID or {@link C#INDEX_UNSET} if not + * found. * * @param childUid A child UID. - * @param childData A data holder to be populated. - * @return Whether a child with the given UID was found. + * @return Index of child timeline or {@link C#INDEX_UNSET} if UID was not found. */ - protected abstract boolean getChildDataByChildUid(Object childUid, ChildDataHolder childData); + protected abstract int getChildIndexByChildUid(Object childUid); + + /** + * Returns the child timeline for the child with the given index. + * + * @param childIndex A valid child index within the bounds of the timeline. + */ + protected abstract Timeline getTimelineByChildIndex(int childIndex); + + /** + * Returns the first period index belonging to the child timeline with the given index. + * + * @param childIndex A valid child index within the bounds of the timeline. + */ + protected abstract int getFirstPeriodIndexByChildIndex(int childIndex); + + /** + * Returns the first window index belonging to the child timeline with the given index. + * + * @param childIndex A valid child index within the bounds of the timeline. + */ + protected abstract int getFirstWindowIndexByChildIndex(int childIndex); + + /** + * Returns the UID of the child timeline with the given index. + * + * @param childIndex A valid child index within the bounds of the timeline. + */ + protected abstract Object getChildUidByChildIndex(int childIndex); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java index 2c998e8a06..5d2bbcc33e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; @@ -97,7 +98,7 @@ public final class ConcatenatingMediaSource implements MediaSource { public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { int sourceIndex = timeline.getChildIndexByPeriodIndex(id.periodIndex); MediaPeriodId periodIdInSource = - new MediaPeriodId(id.periodIndex - timeline.getFirstPeriodIndexInChild(sourceIndex)); + new MediaPeriodId(id.periodIndex - timeline.getFirstPeriodIndexByChildIndex(sourceIndex)); MediaPeriod mediaPeriod = mediaSources[sourceIndex].createPeriod(periodIdInSource, allocator); sourceIndexByMediaPeriod.put(mediaPeriod, sourceIndex); return mediaPeriod; @@ -166,6 +167,7 @@ public final class ConcatenatingMediaSource implements MediaSource { private final boolean isRepeatOneAtomic; public ConcatenatedTimeline(Timeline[] timelines, boolean isRepeatOneAtomic) { + super(timelines.length); int[] sourcePeriodOffsets = new int[timelines.length]; int[] sourceWindowOffsets = new int[timelines.length]; long periodCount = 0; @@ -212,44 +214,43 @@ public final class ConcatenatingMediaSource implements MediaSource { } @Override - protected void getChildDataByPeriodIndex(int periodIndex, ChildDataHolder childData) { - int childIndex = getChildIndexByPeriodIndex(periodIndex); - getChildDataByChildIndex(childIndex, childData); - } - - @Override - protected void getChildDataByWindowIndex(int windowIndex, ChildDataHolder childData) { - int childIndex = Util.binarySearchFloor(sourceWindowOffsets, windowIndex, true, false) + 1; - getChildDataByChildIndex(childIndex, childData); - } - - @Override - protected boolean getChildDataByChildUid(Object childUid, ChildDataHolder childData) { - if (!(childUid instanceof Integer)) { - return false; - } - int childIndex = (Integer) childUid; - getChildDataByChildIndex(childIndex, childData); - return true; - } - - private void getChildDataByChildIndex(int childIndex, ChildDataHolder childData) { - childData.setData(timelines[childIndex], getFirstPeriodIndexInChild(childIndex), - getFirstWindowIndexInChild(childIndex), childIndex); - } - - private int getChildIndexByPeriodIndex(int periodIndex) { + protected int getChildIndexByPeriodIndex(int periodIndex) { return Util.binarySearchFloor(sourcePeriodOffsets, periodIndex, true, false) + 1; } - private int getFirstPeriodIndexInChild(int childIndex) { + @Override + protected int getChildIndexByWindowIndex(int windowIndex) { + return Util.binarySearchFloor(sourceWindowOffsets, windowIndex, true, false) + 1; + } + + @Override + protected int getChildIndexByChildUid(Object childUid) { + if (!(childUid instanceof Integer)) { + return C.INDEX_UNSET; + } + return (Integer) childUid; + } + + @Override + protected Timeline getTimelineByChildIndex(int childIndex) { + return timelines[childIndex]; + } + + @Override + protected int getFirstPeriodIndexByChildIndex(int childIndex) { return childIndex == 0 ? 0 : sourcePeriodOffsets[childIndex - 1]; } - private int getFirstWindowIndexInChild(int childIndex) { + @Override + protected int getFirstWindowIndexByChildIndex(int childIndex) { return childIndex == 0 ? 0 : sourceWindowOffsets[childIndex - 1]; } + @Override + protected Object getChildUidByChildIndex(int childIndex) { + return childIndex; + } + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java index 79f7d8dd48..c2d2e5f11e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java @@ -397,6 +397,7 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl public ConcatenatedTimeline(Collection mediaSourceHolders, int windowCount, int periodCount) { + super(mediaSourceHolders.size()); this.windowCount = windowCount; this.periodCount = periodCount; int childCount = mediaSourceHolders.size(); @@ -416,28 +417,42 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl } @Override - protected void getChildDataByPeriodIndex(int periodIndex, ChildDataHolder childDataHolder) { - int index = Util.binarySearchFloor(firstPeriodInChildIndices, periodIndex, true, false); - setChildData(index, childDataHolder); + protected int getChildIndexByPeriodIndex(int periodIndex) { + return Util.binarySearchFloor(firstPeriodInChildIndices, periodIndex, true, false); } @Override - protected void getChildDataByWindowIndex(int windowIndex, ChildDataHolder childDataHolder) { - int index = Util.binarySearchFloor(firstWindowInChildIndices, windowIndex, true, false); - setChildData(index, childDataHolder); + protected int getChildIndexByWindowIndex(int windowIndex) { + return Util.binarySearchFloor(firstWindowInChildIndices, windowIndex, true, false); } @Override - protected boolean getChildDataByChildUid(Object childUid, ChildDataHolder childDataHolder) { + protected int getChildIndexByChildUid(Object childUid) { if (!(childUid instanceof Integer)) { - return false; + return C.INDEX_UNSET; } int index = childIndexByUid.get((int) childUid, -1); - if (index == -1) { - return false; - } - setChildData(index, childDataHolder); - return true; + return index == -1 ? C.INDEX_UNSET : index; + } + + @Override + protected Timeline getTimelineByChildIndex(int childIndex) { + return timelines[childIndex]; + } + + @Override + protected int getFirstPeriodIndexByChildIndex(int childIndex) { + return firstPeriodInChildIndices[childIndex]; + } + + @Override + protected int getFirstWindowIndexByChildIndex(int childIndex) { + return firstWindowInChildIndices[childIndex]; + } + + @Override + protected Object getChildUidByChildIndex(int childIndex) { + return uids[childIndex]; } @Override @@ -450,10 +465,6 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl return periodCount; } - private void setChildData(int srcIndex, ChildDataHolder dest) { - dest.setData(timelines[srcIndex], firstPeriodInChildIndices[srcIndex], - firstWindowInChildIndices[srcIndex], uids[srcIndex]); - } } private static final class DeferredTimeline extends Timeline { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java index f0032e0ee0..a6e93a92b9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java @@ -101,6 +101,7 @@ public final class LoopingMediaSource implements MediaSource { private final int loopCount; public LoopingTimeline(Timeline childTimeline, int loopCount) { + super(loopCount); this.childTimeline = childTimeline; childPeriodCount = childTimeline.getPeriodCount(); childWindowCount = childTimeline.getWindowCount(); @@ -120,30 +121,41 @@ public final class LoopingMediaSource implements MediaSource { } @Override - protected void getChildDataByPeriodIndex(int periodIndex, ChildDataHolder childData) { - int childIndex = periodIndex / childPeriodCount; - getChildDataByChildIndex(childIndex, childData); + protected int getChildIndexByPeriodIndex(int periodIndex) { + return periodIndex / childPeriodCount; } @Override - protected void getChildDataByWindowIndex(int windowIndex, ChildDataHolder childData) { - int childIndex = windowIndex / childWindowCount; - getChildDataByChildIndex(childIndex, childData); + protected int getChildIndexByWindowIndex(int windowIndex) { + return windowIndex / childWindowCount; } @Override - protected boolean getChildDataByChildUid(Object childUid, ChildDataHolder childData) { + protected int getChildIndexByChildUid(Object childUid) { if (!(childUid instanceof Integer)) { - return false; + return C.INDEX_UNSET; } - int childIndex = (Integer) childUid; - getChildDataByChildIndex(childIndex, childData); - return true; + return (Integer) childUid; } - private void getChildDataByChildIndex(int childIndex, ChildDataHolder childData) { - childData.setData(childTimeline, childIndex * childPeriodCount, childIndex * childWindowCount, - childIndex); + @Override + protected Timeline getTimelineByChildIndex(int childIndex) { + return childTimeline; + } + + @Override + protected int getFirstPeriodIndexByChildIndex(int childIndex) { + return childIndex * childPeriodCount; + } + + @Override + protected int getFirstWindowIndexByChildIndex(int childIndex) { + return childIndex * childWindowCount; + } + + @Override + protected Object getChildUidByChildIndex(int childIndex) { + return childIndex; } } From 1f019ca80854885d2d42e1fa08acfed9527bb41c Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 2 Aug 2017 10:44:38 -0700 Subject: [PATCH 0164/2472] Fix build.gradle to use modulePrefix Issue: #3131 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164004981 --- library/hls/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/hls/build.gradle b/library/hls/build.gradle index c870c3d162..14a26b0e12 100644 --- a/library/hls/build.gradle +++ b/library/hls/build.gradle @@ -33,7 +33,7 @@ android { dependencies { compile project(modulePrefix + 'library-core') compile 'com.android.support:support-annotations:' + supportLibraryVersion - androidTestCompile project(':testutils') + androidTestCompile project(modulePrefix + 'testutils') androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion androidTestCompile 'org.mockito:mockito-core:' + mockitoVersion From a3df29a2462bed0f0c9a6c31d022de785baab9e9 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 2 Aug 2017 09:26:04 -0700 Subject: [PATCH 0165/2472] Some extractor fixes - Fix Ogg extractor to work without sniffing. - Fix extractors to handle seek() before init(). - Add tests for both issues. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=163992343 --- .../ext/flac/FlacExtractorTest.java | 2 +- .../exoplayer2/ext/flac/FlacExtractor.java | 10 ++- .../extractor/flv/FlvExtractorTest.java | 2 +- .../extractor/mkv/MatroskaExtractorTest.java | 6 +- .../extractor/mp3/Mp3ExtractorTest.java | 4 +- .../mp4/FragmentedMp4ExtractorTest.java | 6 +- .../extractor/mp4/Mp4ExtractorTest.java | 2 +- .../extractor/ogg/OggExtractorTest.java | 9 +- .../extractor/rawcc/RawCcExtractorTest.java | 2 +- .../extractor/ts/Ac3ExtractorTest.java | 2 +- .../extractor/ts/AdtsExtractorTest.java | 2 +- .../extractor/ts/PsExtractorTest.java | 2 +- .../extractor/ts/TsExtractorTest.java | 2 +- .../extractor/wav/WavExtractorTest.java | 2 +- .../exoplayer2/extractor/Extractor.java | 3 +- .../extractor/ogg/OggExtractor.java | 66 ++++++++------ .../extractor/ogg/StreamReader.java | 9 +- .../exoplayer2/extractor/ts/Ac3Extractor.java | 4 +- .../extractor/ts/AdtsExtractor.java | 5 +- .../exoplayer2/testutil/ExtractorAsserts.java | 87 +++++++++++-------- 20 files changed, 132 insertions(+), 95 deletions(-) diff --git a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java index 5954985100..7b193997c3 100644 --- a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java +++ b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java @@ -26,7 +26,7 @@ import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; public class FlacExtractorTest extends InstrumentationTestCase { public void testSample() throws Exception { - ExtractorAsserts.assertOutput(new ExtractorFactory() { + ExtractorAsserts.assertBehavior(new ExtractorFactory() { @Override public Extractor create() { return new FlacExtractor(); diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java index d13194793e..7b71b5c743 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java @@ -159,13 +159,17 @@ public final class FlacExtractor implements Extractor { if (position == 0) { metadataParsed = false; } - decoderJni.reset(position); + if (decoderJni != null) { + decoderJni.reset(position); + } } @Override public void release() { - decoderJni.release(); - decoderJni = null; + if (decoderJni != null) { + decoderJni.release(); + decoderJni = null; + } } } diff --git a/library/core/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 index 4587c98317..fc8d181eac 100644 --- a/library/core/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 @@ -26,7 +26,7 @@ import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; public final class FlvExtractorTest extends InstrumentationTestCase { public void testSample() throws Exception { - ExtractorAsserts.assertOutput(new ExtractorFactory() { + ExtractorAsserts.assertBehavior(new ExtractorFactory() { @Override public Extractor create() { return new FlvExtractor(); diff --git a/library/core/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 index 57beec3ac6..624a5ccb7e 100644 --- a/library/core/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 @@ -26,7 +26,7 @@ import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; public final class MatroskaExtractorTest extends InstrumentationTestCase { public void testMkvSample() throws Exception { - ExtractorAsserts.assertOutput(new ExtractorFactory() { + ExtractorAsserts.assertBehavior(new ExtractorFactory() { @Override public Extractor create() { return new MatroskaExtractor(); @@ -35,7 +35,7 @@ public final class MatroskaExtractorTest extends InstrumentationTestCase { } public void testWebmSubsampleEncryption() throws Exception { - ExtractorAsserts.assertOutput(new ExtractorFactory() { + ExtractorAsserts.assertBehavior(new ExtractorFactory() { @Override public Extractor create() { return new MatroskaExtractor(); @@ -44,7 +44,7 @@ public final class MatroskaExtractorTest extends InstrumentationTestCase { } public void testWebmSubsampleEncryptionWithAltrefFrames() throws Exception { - ExtractorAsserts.assertOutput(new ExtractorFactory() { + ExtractorAsserts.assertBehavior(new ExtractorFactory() { @Override public Extractor create() { return new MatroskaExtractor(); diff --git a/library/core/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 index 3ad6a74bc9..0f98624d69 100644 --- a/library/core/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 @@ -26,7 +26,7 @@ import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; public final class Mp3ExtractorTest extends InstrumentationTestCase { public void testMp3Sample() throws Exception { - ExtractorAsserts.assertOutput(new ExtractorFactory() { + ExtractorAsserts.assertBehavior(new ExtractorFactory() { @Override public Extractor create() { return new Mp3Extractor(); @@ -35,7 +35,7 @@ public final class Mp3ExtractorTest extends InstrumentationTestCase { } public void testTrimmedMp3Sample() throws Exception { - ExtractorAsserts.assertOutput(new ExtractorFactory() { + ExtractorAsserts.assertBehavior(new ExtractorFactory() { @Override public Extractor create() { return new Mp3Extractor(); diff --git a/library/core/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 index d8da8760e4..76c13495c1 100644 --- a/library/core/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 @@ -27,13 +27,13 @@ import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; public final class FragmentedMp4ExtractorTest extends InstrumentationTestCase { public void testSample() throws Exception { - ExtractorAsserts - .assertOutput(getExtractorFactory(), "mp4/sample_fragmented.mp4", getInstrumentation()); + ExtractorAsserts.assertBehavior(getExtractorFactory(), "mp4/sample_fragmented.mp4", + getInstrumentation()); } public void testSampleWithSeiPayloadParsing() throws Exception { // Enabling the CEA-608 track enables SEI payload parsing. - ExtractorAsserts.assertOutput( + ExtractorAsserts.assertBehavior( getExtractorFactory(FragmentedMp4Extractor.FLAG_ENABLE_CEA608_TRACK), "mp4/sample_fragmented_sei.mp4", getInstrumentation()); } diff --git a/library/core/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 index a534d6dd24..5e327e5502 100644 --- a/library/core/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 @@ -28,7 +28,7 @@ import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; public final class Mp4ExtractorTest extends InstrumentationTestCase { public void testMp4Sample() throws Exception { - ExtractorAsserts.assertOutput(new ExtractorFactory() { + ExtractorAsserts.assertBehavior(new ExtractorFactory() { @Override public Extractor create() { return new Mp4Extractor(); diff --git a/library/core/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 index 26b7991869..3be23422cc 100644 --- a/library/core/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 @@ -36,20 +36,21 @@ public final class OggExtractorTest extends InstrumentationTestCase { }; public void testOpus() throws Exception { - ExtractorAsserts.assertOutput(OGG_EXTRACTOR_FACTORY, "ogg/bear.opus", getInstrumentation()); + ExtractorAsserts.assertBehavior(OGG_EXTRACTOR_FACTORY, "ogg/bear.opus", getInstrumentation()); } public void testFlac() throws Exception { - ExtractorAsserts.assertOutput(OGG_EXTRACTOR_FACTORY, "ogg/bear_flac.ogg", getInstrumentation()); + ExtractorAsserts.assertBehavior(OGG_EXTRACTOR_FACTORY, "ogg/bear_flac.ogg", + getInstrumentation()); } public void testFlacNoSeektable() throws Exception { - ExtractorAsserts.assertOutput(OGG_EXTRACTOR_FACTORY, "ogg/bear_flac_noseektable.ogg", + ExtractorAsserts.assertBehavior(OGG_EXTRACTOR_FACTORY, "ogg/bear_flac_noseektable.ogg", getInstrumentation()); } public void testVorbis() throws Exception { - ExtractorAsserts.assertOutput(OGG_EXTRACTOR_FACTORY, "ogg/bear_vorbis.ogg", + ExtractorAsserts.assertBehavior(OGG_EXTRACTOR_FACTORY, "ogg/bear_vorbis.ogg", getInstrumentation()); } diff --git a/library/core/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 index 5a9d60512c..18050f48a3 100644 --- a/library/core/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 @@ -30,7 +30,7 @@ import com.google.android.exoplayer2.util.MimeTypes; public final class RawCcExtractorTest extends InstrumentationTestCase { public void testRawCcSample() throws Exception { - ExtractorAsserts.assertOutput( + ExtractorAsserts.assertBehavior( new ExtractorFactory() { @Override public Extractor create() { diff --git a/library/core/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 index 1c18e44373..31633361db 100644 --- a/library/core/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 @@ -26,7 +26,7 @@ import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; public final class Ac3ExtractorTest extends InstrumentationTestCase { public void testSample() throws Exception { - ExtractorAsserts.assertOutput(new ExtractorFactory() { + ExtractorAsserts.assertBehavior(new ExtractorFactory() { @Override public Extractor create() { return new Ac3Extractor(); diff --git a/library/core/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 index bc05be6fa8..9eb65d2091 100644 --- a/library/core/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 @@ -26,7 +26,7 @@ import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; public final class AdtsExtractorTest extends InstrumentationTestCase { public void testSample() throws Exception { - ExtractorAsserts.assertOutput(new ExtractorFactory() { + ExtractorAsserts.assertBehavior(new ExtractorFactory() { @Override public Extractor create() { return new AdtsExtractor(); diff --git a/library/core/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 index e6937ccbc8..78ef05a769 100644 --- a/library/core/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 @@ -26,7 +26,7 @@ import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; public final class PsExtractorTest extends InstrumentationTestCase { public void testSample() throws Exception { - ExtractorAsserts.assertOutput(new ExtractorFactory() { + ExtractorAsserts.assertBehavior(new ExtractorFactory() { @Override public Extractor create() { return new PsExtractor(); diff --git a/library/core/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 index 09c9facab0..b6eddb5112 100644 --- a/library/core/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 @@ -45,7 +45,7 @@ public final class TsExtractorTest extends InstrumentationTestCase { private static final int TS_SYNC_BYTE = 0x47; // First byte of each TS packet. public void testSample() throws Exception { - ExtractorAsserts.assertOutput(new ExtractorFactory() { + ExtractorAsserts.assertBehavior(new ExtractorFactory() { @Override public Extractor create() { return new TsExtractor(); diff --git a/library/core/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 index 7c969fd386..36c05aa72e 100644 --- a/library/core/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 @@ -26,7 +26,7 @@ import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; public final class WavExtractorTest extends InstrumentationTestCase { public void testSample() throws Exception { - ExtractorAsserts.assertOutput(new ExtractorFactory() { + ExtractorAsserts.assertBehavior(new ExtractorFactory() { @Override public Extractor create() { return new WavExtractor(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/Extractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/Extractor.java index de3dfd5266..7a2bc15da9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/Extractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/Extractor.java @@ -63,7 +63,8 @@ public interface Extractor { void init(ExtractorOutput output); /** - * Extracts data read from a provided {@link ExtractorInput}. + * Extracts data read from a provided {@link ExtractorInput}. Must not be called before + * {@link #init(ExtractorOutput)}. *

      * A single call to this method will block until some progress has been made, but will not block * for longer than this. Hence each call will consume only a small amount of input data. diff --git a/library/core/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 index cc3c5de311..54e168c665 100644 --- a/library/core/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 @@ -45,30 +45,14 @@ public class OggExtractor implements Extractor { private static final int MAX_VERIFICATION_BYTES = 8; + private ExtractorOutput output; private StreamReader streamReader; + private boolean streamReaderInitialized; @Override public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { try { - OggPageHeader header = new OggPageHeader(); - if (!header.populate(input, true) || (header.type & 0x02) != 0x02) { - return false; - } - - int length = Math.min(header.bodySize, MAX_VERIFICATION_BYTES); - ParsableByteArray scratch = new ParsableByteArray(length); - input.peekFully(scratch.data, 0, length); - - if (FlacReader.verifyBitstreamType(resetPosition(scratch))) { - streamReader = new FlacReader(); - } else if (VorbisReader.verifyBitstreamType(resetPosition(scratch))) { - streamReader = new VorbisReader(); - } else if (OpusReader.verifyBitstreamType(resetPosition(scratch))) { - streamReader = new OpusReader(); - } else { - return false; - } - return true; + return sniffInternal(input); } catch (ParserException e) { return false; } @@ -76,15 +60,14 @@ public class OggExtractor implements Extractor { @Override public void init(ExtractorOutput output) { - TrackOutput trackOutput = output.track(0, C.TRACK_TYPE_AUDIO); - output.endTracks(); - // TODO: fix the case if sniff() isn't called - streamReader.init(output, trackOutput); + this.output = output; } @Override public void seek(long position, long timeUs) { - streamReader.seek(position, timeUs); + if (streamReader != null) { + streamReader.seek(position, timeUs); + } } @Override @@ -95,12 +78,41 @@ public class OggExtractor implements Extractor { @Override public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException, InterruptedException { + if (streamReader == null) { + if (!sniffInternal(input)) { + throw new ParserException("Failed to determine bitstream type"); + } + input.resetPeekPosition(); + } + if (!streamReaderInitialized) { + TrackOutput trackOutput = output.track(0, C.TRACK_TYPE_AUDIO); + output.endTracks(); + streamReader.init(output, trackOutput); + streamReaderInitialized = true; + } return streamReader.read(input, seekPosition); } - //@VisibleForTesting - /* package */ StreamReader getStreamReader() { - return streamReader; + private boolean sniffInternal(ExtractorInput input) throws IOException, InterruptedException { + OggPageHeader header = new OggPageHeader(); + if (!header.populate(input, true) || (header.type & 0x02) != 0x02) { + return false; + } + + int length = Math.min(header.bodySize, MAX_VERIFICATION_BYTES); + ParsableByteArray scratch = new ParsableByteArray(length); + input.peekFully(scratch.data, 0, length); + + if (FlacReader.verifyBitstreamType(resetPosition(scratch))) { + streamReader = new FlacReader(); + } else if (VorbisReader.verifyBitstreamType(resetPosition(scratch))) { + streamReader = new VorbisReader(); + } else if (OpusReader.verifyBitstreamType(resetPosition(scratch))) { + streamReader = new OpusReader(); + } else { + return false; + } + return true; } private static ParsableByteArray resetPosition(ParsableByteArray scratch) { diff --git a/library/core/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 index c203b0c6bd..d136468faa 100644 --- a/library/core/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 @@ -41,7 +41,8 @@ import java.io.IOException; OggSeeker oggSeeker; } - private OggPacket oggPacket; + private final OggPacket oggPacket; + private TrackOutput trackOutput; private ExtractorOutput extractorOutput; private OggSeeker oggSeeker; @@ -55,11 +56,13 @@ import java.io.IOException; private boolean seekMapSet; private boolean formatSet; + public StreamReader() { + oggPacket = new OggPacket(); + } + void init(ExtractorOutput output, TrackOutput trackOutput) { this.extractorOutput = output; this.trackOutput = trackOutput; - this.oggPacket = new OggPacket(); - reset(true); } diff --git a/library/core/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 index 9c9536beec..8bab6b7ed1 100644 --- a/library/core/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 @@ -56,9 +56,9 @@ public final class Ac3Extractor implements Extractor { private static final int ID3_TAG = Util.getIntegerCodeForString("ID3"); private final long firstSampleTimestampUs; + private final Ac3Reader reader; private final ParsableByteArray sampleData; - private Ac3Reader reader; private boolean startedPacket; public Ac3Extractor() { @@ -67,6 +67,7 @@ public final class Ac3Extractor implements Extractor { public Ac3Extractor(long firstSampleTimestampUs) { this.firstSampleTimestampUs = firstSampleTimestampUs; + reader = new Ac3Reader(); sampleData = new ParsableByteArray(MAX_SYNC_FRAME_SIZE); } @@ -117,7 +118,6 @@ public final class Ac3Extractor implements Extractor { @Override public void init(ExtractorOutput output) { - reader = new Ac3Reader(); // TODO: Add support for embedded ID3. reader.createTracks(output, new TrackIdGenerator(0, 1)); output.endTracks(); output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET)); diff --git a/library/core/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 index f7dadd51b2..a1851aa0ea 100644 --- a/library/core/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 @@ -55,10 +55,9 @@ public final class AdtsExtractor implements Extractor { private static final int MAX_SNIFF_BYTES = 8 * 1024; private final long firstSampleTimestampUs; + private final AdtsReader reader; private final ParsableByteArray packetBuffer; - // Accessed only by the loading thread. - private AdtsReader reader; private boolean startedPacket; public AdtsExtractor() { @@ -67,6 +66,7 @@ public final class AdtsExtractor implements Extractor { public AdtsExtractor(long firstSampleTimestampUs) { this.firstSampleTimestampUs = firstSampleTimestampUs; + reader = new AdtsReader(true); packetBuffer = new ParsableByteArray(MAX_PACKET_SIZE); } @@ -127,7 +127,6 @@ public final class AdtsExtractor implements Extractor { @Override public void init(ExtractorOutput output) { - reader = new AdtsReader(true); reader.createTracks(output, new TrackIdGenerator(0, 1)); output.endTracks(); output.seekMap(new SeekMap.Unseekable(C.TIME_UNSET)); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExtractorAsserts.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExtractorAsserts.java index fb78b3a634..db63662c45 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExtractorAsserts.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExtractorAsserts.java @@ -18,6 +18,8 @@ package com.google.android.exoplayer2.testutil; import android.app.Instrumentation; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.Extractor; +import com.google.android.exoplayer2.extractor.ExtractorInput; +import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.testutil.FakeExtractorInput.SimulatedIOException; @@ -42,46 +44,57 @@ public final class ExtractorAsserts { private static final String UNKNOWN_LENGTH_EXTENSION = ".unklen" + DUMP_EXTENSION; /** - * Calls {@link #assertOutput(Extractor, String, byte[], Instrumentation, boolean, boolean, - * boolean)} with all possible combinations of "simulate" parameters. + * Asserts that an extractor behaves correctly given valid input data: + *

        + *
      • Calls {@link Extractor#seek(long, long)} and {@link Extractor#release()} without calling + * {@link Extractor#init(ExtractorOutput)} to check these calls do not fail.
      • + *
      • Calls {@link #assertOutput(Extractor, String, byte[], Instrumentation, boolean, boolean, + * boolean, boolean)} with all possible combinations of "simulate" parameters.
      • + *
      * * @param factory An {@link ExtractorFactory} which creates instances of the {@link Extractor} * class which is to be tested. - * @param sampleFile The path to the input sample. + * @param file The path to the input sample. * @param instrumentation To be used to load the sample file. * @throws IOException If reading from the input fails. * @throws InterruptedException If interrupted while reading from the input. - * @see #assertOutput(Extractor, String, byte[], Instrumentation, boolean, boolean, boolean) */ - public static void assertOutput(ExtractorFactory factory, String sampleFile, + public static void assertBehavior(ExtractorFactory factory, String file, Instrumentation instrumentation) throws IOException, InterruptedException { - byte[] fileData = TestUtil.getByteArray(instrumentation, sampleFile); - assertOutput(factory, sampleFile, fileData, instrumentation); + // Check behavior prior to initialization. + Extractor extractor = factory.create(); + extractor.seek(0, 0); + extractor.release(); + // Assert output. + byte[] fileData = TestUtil.getByteArray(instrumentation, file); + assertOutput(factory, file, fileData, instrumentation); } /** * Calls {@link #assertOutput(Extractor, String, byte[], Instrumentation, boolean, boolean, - * boolean)} with all possible combinations of "simulate" parameters. + * boolean, boolean)} with all possible combinations of "simulate" parameters with + * {@code sniffFirst} set to true, and makes one additional call with the "simulate" and + * {@code sniffFirst} parameters all set to false. * * @param factory An {@link ExtractorFactory} which creates instances of the {@link Extractor} * class which is to be tested. - * @param sampleFile The path to the input sample. - * @param fileData Content of the input file. + * @param file The path to the input sample. + * @param data Content of the input file. * @param instrumentation To be used to load the sample file. * @throws IOException If reading from the input fails. * @throws InterruptedException If interrupted while reading from the input. - * @see #assertOutput(Extractor, String, byte[], Instrumentation, boolean, boolean, boolean) */ - public static void assertOutput(ExtractorFactory factory, String sampleFile, byte[] fileData, + public static void assertOutput(ExtractorFactory factory, String file, byte[] data, Instrumentation instrumentation) throws IOException, InterruptedException { - assertOutput(factory.create(), sampleFile, fileData, instrumentation, false, false, false); - assertOutput(factory.create(), sampleFile, fileData, instrumentation, true, false, false); - assertOutput(factory.create(), sampleFile, fileData, instrumentation, false, true, false); - assertOutput(factory.create(), sampleFile, fileData, instrumentation, true, true, false); - assertOutput(factory.create(), sampleFile, fileData, instrumentation, false, false, true); - assertOutput(factory.create(), sampleFile, fileData, instrumentation, true, false, true); - assertOutput(factory.create(), sampleFile, fileData, instrumentation, false, true, true); - assertOutput(factory.create(), sampleFile, fileData, instrumentation, true, true, true); + assertOutput(factory.create(), file, data, instrumentation, true, false, false, false); + assertOutput(factory.create(), file, data, instrumentation, true, false, false, true); + assertOutput(factory.create(), file, data, instrumentation, true, false, true, false); + assertOutput(factory.create(), file, data, instrumentation, true, false, true, true); + assertOutput(factory.create(), file, data, instrumentation, true, true, false, false); + assertOutput(factory.create(), file, data, instrumentation, true, true, false, true); + assertOutput(factory.create(), file, data, instrumentation, true, true, true, false); + assertOutput(factory.create(), file, data, instrumentation, true, true, true, true); + assertOutput(factory.create(), file, data, instrumentation, false, false, false, false); } /** @@ -91,34 +104,38 @@ public final class ExtractorAsserts { * #UNKNOWN_LENGTH_EXTENSION}" exists, it's preferred. * * @param extractor The {@link Extractor} to be tested. - * @param sampleFile The path to the input sample. - * @param fileData Content of the input file. + * @param file The path to the input sample. + * @param data Content of the input file. * @param instrumentation To be used to load the sample file. - * @param simulateIOErrors If true simulates IOErrors. - * @param simulateUnknownLength If true simulates unknown input length. - * @param simulatePartialReads If true simulates partial reads. + * @param sniffFirst Whether to sniff the data by calling {@link Extractor#sniff(ExtractorInput)} + * prior to consuming it. + * @param simulateIOErrors Whether to simulate IO errors. + * @param simulateUnknownLength Whether to simulate unknown input length. + * @param simulatePartialReads Whether to simulate partial reads. * @return The {@link FakeExtractorOutput} used in the test. * @throws IOException If reading from the input fails. * @throws InterruptedException If interrupted while reading from the input. */ - public static FakeExtractorOutput assertOutput(Extractor extractor, String sampleFile, - byte[] fileData, Instrumentation instrumentation, boolean simulateIOErrors, + public static FakeExtractorOutput assertOutput(Extractor extractor, String file, byte[] data, + Instrumentation instrumentation, boolean sniffFirst, boolean simulateIOErrors, boolean simulateUnknownLength, boolean simulatePartialReads) throws IOException, InterruptedException { - FakeExtractorInput input = new FakeExtractorInput.Builder().setData(fileData) + FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data) .setSimulateIOErrors(simulateIOErrors) .setSimulateUnknownLength(simulateUnknownLength) .setSimulatePartialReads(simulatePartialReads).build(); - Assert.assertTrue(TestUtil.sniffTestData(extractor, input)); - input.resetPeekPosition(); - FakeExtractorOutput extractorOutput = consumeTestData(extractor, input, 0, true); + if (sniffFirst) { + Assert.assertTrue(TestUtil.sniffTestData(extractor, input)); + input.resetPeekPosition(); + } + FakeExtractorOutput extractorOutput = consumeTestData(extractor, input, 0, true); if (simulateUnknownLength - && assetExists(instrumentation, sampleFile + UNKNOWN_LENGTH_EXTENSION)) { - extractorOutput.assertOutput(instrumentation, sampleFile + UNKNOWN_LENGTH_EXTENSION); + && assetExists(instrumentation, file + UNKNOWN_LENGTH_EXTENSION)) { + extractorOutput.assertOutput(instrumentation, file + UNKNOWN_LENGTH_EXTENSION); } else { - extractorOutput.assertOutput(instrumentation, sampleFile + ".0" + DUMP_EXTENSION); + extractorOutput.assertOutput(instrumentation, file + ".0" + DUMP_EXTENSION); } SeekMap seekMap = extractorOutput.seekMap; @@ -133,7 +150,7 @@ public final class ExtractorAsserts { } consumeTestData(extractor, input, timeUs, extractorOutput, false); - extractorOutput.assertOutput(instrumentation, sampleFile + '.' + j + DUMP_EXTENSION); + extractorOutput.assertOutput(instrumentation, file + '.' + j + DUMP_EXTENSION); } } From 19aa69e0f4a0f57816942fef0eeddf33f6c16a7e Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 2 Aug 2017 09:26:58 -0700 Subject: [PATCH 0166/2472] Remove child data holder helper from AbstractConcatenatedTimeline. This helper class required a scratch instance to write on. Such a scratch instance may violate the immuatability of the timelines if used by multiple threads simultaneously. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=163992458 --- .../source/AbstractConcatenatedTimeline.java | 141 ++++++++---------- .../source/ConcatenatingMediaSource.java | 61 ++++---- .../DynamicConcatenatingMediaSource.java | 45 +++--- .../exoplayer2/source/LoopingMediaSource.java | 40 +++-- 4 files changed, 148 insertions(+), 139 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java b/library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java index 3bee3cc73f..42ac938677 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java @@ -25,68 +25,25 @@ import com.google.android.exoplayer2.Timeline; */ /* package */ abstract class AbstractConcatenatedTimeline extends Timeline { - /** - * Meta data of a child timeline. - */ - protected static final class ChildDataHolder { + private final int childCount; - /** - * Child timeline. - */ - public Timeline timeline; - - /** - * First period index belonging to the child timeline. - */ - public int firstPeriodIndexInChild; - - /** - * First window index belonging to the child timeline. - */ - public int firstWindowIndexInChild; - - /** - * UID of child timeline. - */ - public Object uid; - - /** - * Set child holder data. - * - * @param timeline Child timeline. - * @param firstPeriodIndexInChild First period index belonging to the child timeline. - * @param firstWindowIndexInChild First window index belonging to the child timeline. - * @param uid UID of child timeline. - */ - public void setData(Timeline timeline, int firstPeriodIndexInChild, int firstWindowIndexInChild, - Object uid) { - this.timeline = timeline; - this.firstPeriodIndexInChild = firstPeriodIndexInChild; - this.firstWindowIndexInChild = firstWindowIndexInChild; - this.uid = uid; - } - - } - - private final ChildDataHolder childDataHolder; - - public AbstractConcatenatedTimeline() { - childDataHolder = new ChildDataHolder(); + public AbstractConcatenatedTimeline(int childCount) { + this.childCount = childCount; } @Override public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) { - getChildDataByWindowIndex(windowIndex, childDataHolder); - int firstWindowIndexInChild = childDataHolder.firstWindowIndexInChild; - int nextWindowIndexInChild = childDataHolder.timeline.getNextWindowIndex( + int childIndex = getChildIndexByWindowIndex(windowIndex); + int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex); + int nextWindowIndexInChild = getTimelineByChildIndex(childIndex).getNextWindowIndex( windowIndex - firstWindowIndexInChild, repeatMode == Player.REPEAT_MODE_ALL ? Player.REPEAT_MODE_OFF : repeatMode); if (nextWindowIndexInChild != C.INDEX_UNSET) { return firstWindowIndexInChild + nextWindowIndexInChild; } else { - firstWindowIndexInChild += childDataHolder.timeline.getWindowCount(); - if (firstWindowIndexInChild < getWindowCount()) { - return firstWindowIndexInChild; + int nextChildIndex = childIndex + 1; + if (nextChildIndex < childCount) { + return getFirstWindowIndexByChildIndex(nextChildIndex); } else if (repeatMode == Player.REPEAT_MODE_ALL) { return 0; } else { @@ -97,9 +54,9 @@ import com.google.android.exoplayer2.Timeline; @Override public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) { - getChildDataByWindowIndex(windowIndex, childDataHolder); - int firstWindowIndexInChild = childDataHolder.firstWindowIndexInChild; - int previousWindowIndexInChild = childDataHolder.timeline.getPreviousWindowIndex( + int childIndex = getChildIndexByWindowIndex(windowIndex); + int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex); + int previousWindowIndexInChild = getTimelineByChildIndex(childIndex).getPreviousWindowIndex( windowIndex - firstWindowIndexInChild, repeatMode == Player.REPEAT_MODE_ALL ? Player.REPEAT_MODE_OFF : repeatMode); if (previousWindowIndexInChild != C.INDEX_UNSET) { @@ -118,11 +75,11 @@ import com.google.android.exoplayer2.Timeline; @Override public final Window getWindow(int windowIndex, Window window, boolean setIds, long defaultPositionProjectionUs) { - getChildDataByWindowIndex(windowIndex, childDataHolder); - int firstWindowIndexInChild = childDataHolder.firstWindowIndexInChild; - int firstPeriodIndexInChild = childDataHolder.firstPeriodIndexInChild; - childDataHolder.timeline.getWindow(windowIndex - firstWindowIndexInChild, window, setIds, - defaultPositionProjectionUs); + int childIndex = getChildIndexByWindowIndex(windowIndex); + int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex); + int firstPeriodIndexInChild = getFirstPeriodIndexByChildIndex(childIndex); + getTimelineByChildIndex(childIndex).getWindow(windowIndex - firstWindowIndexInChild, window, + setIds, defaultPositionProjectionUs); window.firstPeriodIndex += firstPeriodIndexInChild; window.lastPeriodIndex += firstPeriodIndexInChild; return window; @@ -130,13 +87,14 @@ import com.google.android.exoplayer2.Timeline; @Override public final Period getPeriod(int periodIndex, Period period, boolean setIds) { - getChildDataByPeriodIndex(periodIndex, childDataHolder); - int firstWindowIndexInChild = childDataHolder.firstWindowIndexInChild; - int firstPeriodIndexInChild = childDataHolder.firstPeriodIndexInChild; - childDataHolder.timeline.getPeriod(periodIndex - firstPeriodIndexInChild, period, setIds); + int childIndex = getChildIndexByPeriodIndex(periodIndex); + int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex); + int firstPeriodIndexInChild = getFirstPeriodIndexByChildIndex(childIndex); + getTimelineByChildIndex(childIndex).getPeriod(periodIndex - firstPeriodIndexInChild, period, + setIds); period.windowIndex += firstWindowIndexInChild; if (setIds) { - period.uid = Pair.create(childDataHolder.uid, period.uid); + period.uid = Pair.create(getChildUidByChildIndex(childIndex), period.uid); } return period; } @@ -149,37 +107,64 @@ import com.google.android.exoplayer2.Timeline; Pair childUidAndPeriodUid = (Pair) uid; Object childUid = childUidAndPeriodUid.first; Object periodUid = childUidAndPeriodUid.second; - if (!getChildDataByChildUid(childUid, childDataHolder)) { + int childIndex = getChildIndexByChildUid(childUid); + if (childIndex == C.INDEX_UNSET) { return C.INDEX_UNSET; } - int periodIndexInChild = childDataHolder.timeline.getIndexOfPeriod(periodUid); + int periodIndexInChild = getTimelineByChildIndex(childIndex).getIndexOfPeriod(periodUid); return periodIndexInChild == C.INDEX_UNSET ? C.INDEX_UNSET - : childDataHolder.firstPeriodIndexInChild + periodIndexInChild; + : getFirstPeriodIndexByChildIndex(childIndex) + periodIndexInChild; } /** - * Populates {@link ChildDataHolder} for the child timeline containing the given period index. + * Returns the index of the child timeline containing the given period index. * * @param periodIndex A valid period index within the bounds of the timeline. - * @param childData A data holder to be populated. */ - protected abstract void getChildDataByPeriodIndex(int periodIndex, ChildDataHolder childData); + protected abstract int getChildIndexByPeriodIndex(int periodIndex); /** - * Populates {@link ChildDataHolder} for the child timeline containing the given window index. + * Returns the index of the child timeline containing the given window index. * * @param windowIndex A valid window index within the bounds of the timeline. - * @param childData A data holder to be populated. */ - protected abstract void getChildDataByWindowIndex(int windowIndex, ChildDataHolder childData); + protected abstract int getChildIndexByWindowIndex(int windowIndex); /** - * Populates {@link ChildDataHolder} for the child timeline with the given UID. + * Returns the index of the child timeline with the given UID or {@link C#INDEX_UNSET} if not + * found. * * @param childUid A child UID. - * @param childData A data holder to be populated. - * @return Whether a child with the given UID was found. + * @return Index of child timeline or {@link C#INDEX_UNSET} if UID was not found. */ - protected abstract boolean getChildDataByChildUid(Object childUid, ChildDataHolder childData); + protected abstract int getChildIndexByChildUid(Object childUid); + + /** + * Returns the child timeline for the child with the given index. + * + * @param childIndex A valid child index within the bounds of the timeline. + */ + protected abstract Timeline getTimelineByChildIndex(int childIndex); + + /** + * Returns the first period index belonging to the child timeline with the given index. + * + * @param childIndex A valid child index within the bounds of the timeline. + */ + protected abstract int getFirstPeriodIndexByChildIndex(int childIndex); + + /** + * Returns the first window index belonging to the child timeline with the given index. + * + * @param childIndex A valid child index within the bounds of the timeline. + */ + protected abstract int getFirstWindowIndexByChildIndex(int childIndex); + + /** + * Returns the UID of the child timeline with the given index. + * + * @param childIndex A valid child index within the bounds of the timeline. + */ + protected abstract Object getChildUidByChildIndex(int childIndex); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java index 2c998e8a06..5d2bbcc33e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; @@ -97,7 +98,7 @@ public final class ConcatenatingMediaSource implements MediaSource { public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { int sourceIndex = timeline.getChildIndexByPeriodIndex(id.periodIndex); MediaPeriodId periodIdInSource = - new MediaPeriodId(id.periodIndex - timeline.getFirstPeriodIndexInChild(sourceIndex)); + new MediaPeriodId(id.periodIndex - timeline.getFirstPeriodIndexByChildIndex(sourceIndex)); MediaPeriod mediaPeriod = mediaSources[sourceIndex].createPeriod(periodIdInSource, allocator); sourceIndexByMediaPeriod.put(mediaPeriod, sourceIndex); return mediaPeriod; @@ -166,6 +167,7 @@ public final class ConcatenatingMediaSource implements MediaSource { private final boolean isRepeatOneAtomic; public ConcatenatedTimeline(Timeline[] timelines, boolean isRepeatOneAtomic) { + super(timelines.length); int[] sourcePeriodOffsets = new int[timelines.length]; int[] sourceWindowOffsets = new int[timelines.length]; long periodCount = 0; @@ -212,44 +214,43 @@ public final class ConcatenatingMediaSource implements MediaSource { } @Override - protected void getChildDataByPeriodIndex(int periodIndex, ChildDataHolder childData) { - int childIndex = getChildIndexByPeriodIndex(periodIndex); - getChildDataByChildIndex(childIndex, childData); - } - - @Override - protected void getChildDataByWindowIndex(int windowIndex, ChildDataHolder childData) { - int childIndex = Util.binarySearchFloor(sourceWindowOffsets, windowIndex, true, false) + 1; - getChildDataByChildIndex(childIndex, childData); - } - - @Override - protected boolean getChildDataByChildUid(Object childUid, ChildDataHolder childData) { - if (!(childUid instanceof Integer)) { - return false; - } - int childIndex = (Integer) childUid; - getChildDataByChildIndex(childIndex, childData); - return true; - } - - private void getChildDataByChildIndex(int childIndex, ChildDataHolder childData) { - childData.setData(timelines[childIndex], getFirstPeriodIndexInChild(childIndex), - getFirstWindowIndexInChild(childIndex), childIndex); - } - - private int getChildIndexByPeriodIndex(int periodIndex) { + protected int getChildIndexByPeriodIndex(int periodIndex) { return Util.binarySearchFloor(sourcePeriodOffsets, periodIndex, true, false) + 1; } - private int getFirstPeriodIndexInChild(int childIndex) { + @Override + protected int getChildIndexByWindowIndex(int windowIndex) { + return Util.binarySearchFloor(sourceWindowOffsets, windowIndex, true, false) + 1; + } + + @Override + protected int getChildIndexByChildUid(Object childUid) { + if (!(childUid instanceof Integer)) { + return C.INDEX_UNSET; + } + return (Integer) childUid; + } + + @Override + protected Timeline getTimelineByChildIndex(int childIndex) { + return timelines[childIndex]; + } + + @Override + protected int getFirstPeriodIndexByChildIndex(int childIndex) { return childIndex == 0 ? 0 : sourcePeriodOffsets[childIndex - 1]; } - private int getFirstWindowIndexInChild(int childIndex) { + @Override + protected int getFirstWindowIndexByChildIndex(int childIndex) { return childIndex == 0 ? 0 : sourceWindowOffsets[childIndex - 1]; } + @Override + protected Object getChildUidByChildIndex(int childIndex) { + return childIndex; + } + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java index ad2e154f6d..b00732e839 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java @@ -397,6 +397,7 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl public ConcatenatedTimeline(Collection mediaSourceHolders, int windowCount, int periodCount) { + super(mediaSourceHolders.size()); this.windowCount = windowCount; this.periodCount = periodCount; int childCount = mediaSourceHolders.size(); @@ -416,28 +417,42 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl } @Override - protected void getChildDataByPeriodIndex(int periodIndex, ChildDataHolder childDataHolder) { - int index = Util.binarySearchFloor(firstPeriodInChildIndices, periodIndex, true, false); - setChildData(index, childDataHolder); + protected int getChildIndexByPeriodIndex(int periodIndex) { + return Util.binarySearchFloor(firstPeriodInChildIndices, periodIndex, true, false); } @Override - protected void getChildDataByWindowIndex(int windowIndex, ChildDataHolder childDataHolder) { - int index = Util.binarySearchFloor(firstWindowInChildIndices, windowIndex, true, false); - setChildData(index, childDataHolder); + protected int getChildIndexByWindowIndex(int windowIndex) { + return Util.binarySearchFloor(firstWindowInChildIndices, windowIndex, true, false); } @Override - protected boolean getChildDataByChildUid(Object childUid, ChildDataHolder childDataHolder) { + protected int getChildIndexByChildUid(Object childUid) { if (!(childUid instanceof Integer)) { - return false; + return C.INDEX_UNSET; } int index = childIndexByUid.get((int) childUid, -1); - if (index == -1) { - return false; - } - setChildData(index, childDataHolder); - return true; + return index == -1 ? C.INDEX_UNSET : index; + } + + @Override + protected Timeline getTimelineByChildIndex(int childIndex) { + return timelines[childIndex]; + } + + @Override + protected int getFirstPeriodIndexByChildIndex(int childIndex) { + return firstPeriodInChildIndices[childIndex]; + } + + @Override + protected int getFirstWindowIndexByChildIndex(int childIndex) { + return firstWindowInChildIndices[childIndex]; + } + + @Override + protected Object getChildUidByChildIndex(int childIndex) { + return uids[childIndex]; } @Override @@ -450,10 +465,6 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl return periodCount; } - private void setChildData(int srcIndex, ChildDataHolder dest) { - dest.setData(timelines[srcIndex], firstPeriodInChildIndices[srcIndex], - firstWindowInChildIndices[srcIndex], uids[srcIndex]); - } } private static final class DeferredTimeline extends Timeline { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java index f0032e0ee0..a6e93a92b9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java @@ -101,6 +101,7 @@ public final class LoopingMediaSource implements MediaSource { private final int loopCount; public LoopingTimeline(Timeline childTimeline, int loopCount) { + super(loopCount); this.childTimeline = childTimeline; childPeriodCount = childTimeline.getPeriodCount(); childWindowCount = childTimeline.getWindowCount(); @@ -120,30 +121,41 @@ public final class LoopingMediaSource implements MediaSource { } @Override - protected void getChildDataByPeriodIndex(int periodIndex, ChildDataHolder childData) { - int childIndex = periodIndex / childPeriodCount; - getChildDataByChildIndex(childIndex, childData); + protected int getChildIndexByPeriodIndex(int periodIndex) { + return periodIndex / childPeriodCount; } @Override - protected void getChildDataByWindowIndex(int windowIndex, ChildDataHolder childData) { - int childIndex = windowIndex / childWindowCount; - getChildDataByChildIndex(childIndex, childData); + protected int getChildIndexByWindowIndex(int windowIndex) { + return windowIndex / childWindowCount; } @Override - protected boolean getChildDataByChildUid(Object childUid, ChildDataHolder childData) { + protected int getChildIndexByChildUid(Object childUid) { if (!(childUid instanceof Integer)) { - return false; + return C.INDEX_UNSET; } - int childIndex = (Integer) childUid; - getChildDataByChildIndex(childIndex, childData); - return true; + return (Integer) childUid; } - private void getChildDataByChildIndex(int childIndex, ChildDataHolder childData) { - childData.setData(childTimeline, childIndex * childPeriodCount, childIndex * childWindowCount, - childIndex); + @Override + protected Timeline getTimelineByChildIndex(int childIndex) { + return childTimeline; + } + + @Override + protected int getFirstPeriodIndexByChildIndex(int childIndex) { + return childIndex * childPeriodCount; + } + + @Override + protected int getFirstWindowIndexByChildIndex(int childIndex) { + return childIndex * childWindowCount; + } + + @Override + protected Object getChildUidByChildIndex(int childIndex) { + return childIndex; } } From a5f9f40bf0650adafac8ba69b98f8bbb3124d803 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 3 Aug 2017 00:50:47 -0700 Subject: [PATCH 0167/2472] Replace README reference to source with reference to javadoc ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164090619 --- extensions/ima/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/ima/README.md b/extensions/ima/README.md index b5afcec94a..f328bb44cb 100644 --- a/extensions/ima/README.md +++ b/extensions/ima/README.md @@ -7,7 +7,7 @@ The IMA extension is a [MediaSource][] implementation wrapping the alongside content. [IMA]: https://developers.google.com/interactive-media-ads/docs/sdks/android/ -[MediaSource]: https://github.com/google/ExoPlayer/blob/release-v2/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java +[MediaSource]: https://google.github.io/ExoPlayer/doc/reference/index.html?com/google/android/exoplayer2/source/MediaSource.html ## Getting the extension ## From 81cbd7348fab7a2ed2468beba7eb11cafacf0f38 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 3 Aug 2017 01:51:50 -0700 Subject: [PATCH 0168/2472] Fix path to constants.gradle ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164095350 --- demos/main/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demos/main/build.gradle b/demos/main/build.gradle index 7eea25478f..029a44326e 100644 --- a/demos/main/build.gradle +++ b/demos/main/build.gradle @@ -11,7 +11,7 @@ // 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 from: '../constants.gradle' +apply from: '../../constants.gradle' apply plugin: 'com.android.application' android { From 147d5bb3b21432d364820582d7f1b167ded65240 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 3 Aug 2017 03:33:22 -0700 Subject: [PATCH 0169/2472] Fix targetSdkVersion to be consistent with gradle builds The manifest value is always overridden in gradle builds, so this is for internal builds only. The value should be the same (i.e. 25!). Also fix IMA build to force the right support library version, attempt 2! ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164103183 --- demos/main/src/main/AndroidManifest.xml | 2 +- extensions/ima/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/demos/main/src/main/AndroidManifest.xml b/demos/main/src/main/AndroidManifest.xml index a39023353a..0e04d9a435 100644 --- a/demos/main/src/main/AndroidManifest.xml +++ b/demos/main/src/main/AndroidManifest.xml @@ -23,7 +23,7 @@ - + 11.0.2 // |-- com.google.android.gms:play-services-basement:[11.0.2] -> 11.0.2 // |-- com.android.support:support-v4:25.2.0 - compile 'com.android.support:support-annotations:' + supportLibraryVersion + compile 'com.android.support:support-v4:' + supportLibraryVersion compile 'com.google.ads.interactivemedia.v3:interactivemedia:3.7.4' compile 'com.google.android.gms:play-services-ads:11.0.2' androidTestCompile project(modulePrefix + 'library') From 78cdd08684ae7ad1f8cfe2793f5f9af52687f862 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 3 Aug 2017 04:10:10 -0700 Subject: [PATCH 0170/2472] Add some missing @param Javadoc to extensions ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164105607 --- .../android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java | 4 ++++ .../google/android/exoplayer2/ext/flac/FlacLibrary.java | 2 ++ .../android/exoplayer2/ext/gvr/GvrAudioProcessor.java | 5 +++++ .../android/exoplayer2/ext/ima/ImaAdsMediaSource.java | 2 ++ .../android/exoplayer2/ext/opus/LibopusAudioRenderer.java | 7 +++++++ .../google/android/exoplayer2/ext/opus/OpusLibrary.java | 2 ++ .../com/google/android/exoplayer2/ext/vp9/VpxLibrary.java | 2 ++ .../exoplayer2/ext/vp9/VpxOutputBufferRenderer.java | 2 ++ 8 files changed, 26 insertions(+) diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java index 0c065549ca..9b3bbbb6ab 100644 --- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java +++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java @@ -37,6 +37,8 @@ public final class FfmpegLibrary { * Override the names of the FFmpeg native libraries. If an application wishes to call this * method, it must do so before calling any other method defined by this class, and before * instantiating a {@link FfmpegAudioRenderer} instance. + * + * @param libraries The names of the FFmpeg native libraries. */ public static void setLibraries(String... libraries) { LOADER.setLibraries(libraries); @@ -58,6 +60,8 @@ public final class FfmpegLibrary { /** * Returns whether the underlying library supports the specified MIME type. + * + * @param mimeType The MIME type to check. */ public static boolean supportsFormat(String mimeType) { if (!isAvailable()) { diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacLibrary.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacLibrary.java index 4130c27c59..d8b9b808a6 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacLibrary.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacLibrary.java @@ -35,6 +35,8 @@ public final class FlacLibrary { * Override the names of the Flac native libraries. If an application wishes to call this method, * it must do so before calling any other method defined by this class, and before instantiating * any {@link LibflacAudioRenderer} and {@link FlacExtractor} instances. + * + * @param libraries The names of the Flac native libraries. */ public static void setLibraries(String... libraries) { LOADER.setLibraries(libraries); 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 c6e09cf4cc..5750f5f04d 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 @@ -61,6 +61,11 @@ public final class GvrAudioProcessor implements AudioProcessor { /** * Updates the listener head orientation. May be called on any thread. See * {@code GvrAudioSurround.updateNativeOrientation}. + * + * @param w The w component of the quaternion. + * @param x The x component of the quaternion. + * @param y The y component of the quaternion. + * @param z The z component of the quaternion. */ public synchronized void updateOrientation(float w, float x, float y, float z) { this.w = w; diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java index 0bf5773d2c..d56a3ad41f 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java @@ -50,6 +50,8 @@ public final class ImaAdsMediaSource implements MediaSource { * Called if there was an error loading ads. The media source will load the content without ads * if ads can't be loaded, so listen for this event if you need to implement additional handling * (for example, stopping the player). + * + * @param error The error. */ void onAdLoadError(IOException error); diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java index 93fe033a31..730473ddad 100644 --- a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java +++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java @@ -54,6 +54,13 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer { * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be * null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. + * @param drmSessionManager For use with encrypted media. May be null if support for encrypted + * media is not required. + * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions. + * For example a media file may start with a short clear region so as to allow playback to + * begin in parallel with key acquisition. This parameter specifies whether the renderer is + * permitted to play clear regions of encrypted media files before {@code drmSessionManager} + * has obtained the keys necessary to decrypt encrypted regions of the media. * @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output. */ public LibopusAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java index fb8fb738ff..22985ea497 100644 --- a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java +++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java @@ -35,6 +35,8 @@ public final class OpusLibrary { * Override the names of the Opus native libraries. If an application wishes to call this method, * it must do so before calling any other method defined by this class, and before instantiating a * {@link LibopusAudioRenderer} instance. + * + * @param libraries The names of the Opus native libraries. */ public static void setLibraries(String... libraries) { LOADER.setLibraries(libraries); diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxLibrary.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxLibrary.java index 09f242f1ea..854576b4b2 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxLibrary.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxLibrary.java @@ -35,6 +35,8 @@ public final class VpxLibrary { * Override the names of the Vpx native libraries. If an application wishes to call this method, * it must do so before calling any other method defined by this class, and before instantiating a * {@link LibvpxVideoRenderer} instance. + * + * @param libraries The names of the Vpx native libraries. */ public static void setLibraries(String... libraries) { LOADER.setLibraries(libraries); diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBufferRenderer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBufferRenderer.java index 8f43a0207b..d07e24d920 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBufferRenderer.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBufferRenderer.java @@ -22,6 +22,8 @@ public interface VpxOutputBufferRenderer { /** * Sets the output buffer to be rendered. The renderer is responsible for releasing the buffer. + * + * @param outputBuffer The output buffer to be rendered. */ void setOutputBuffer(VpxOutputBuffer outputBuffer); From db991873819d3bf6ab8b6cbe2514b89d03c1ef0e Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 3 Aug 2017 04:11:08 -0700 Subject: [PATCH 0171/2472] Fix some lint warnings ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164105662 --- .../main/java/com/google/android/exoplayer2/Timeline.java | 2 +- .../com/google/android/exoplayer2/text/ssa/SsaDecoder.java | 3 ++- .../com/google/android/exoplayer2/text/ttml/TtmlDecoder.java | 2 +- .../main/java/com/google/android/exoplayer2/util/Util.java | 2 +- .../google/android/exoplayer2/source/hls/HlsChunkSource.java | 3 +-- .../google/android/exoplayer2/ui/DebugTextViewHelper.java | 5 ++++- 6 files changed, 10 insertions(+), 7 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java index 7ce23e67ec..414c0804ad 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java @@ -95,7 +95,7 @@ import com.google.android.exoplayer2.util.Assertions; * of the on-demand stream ends, playback of the live stream will start from its default position * near the live edge. * - *

      On-demand stream with mid-roll ads

      + *

      On-demand stream with mid-roll ads

      *

      * Example timeline for an on-demand
  *       stream with mid-roll ad groups diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java index 11489e7b35..d2f5a67c27 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java @@ -23,6 +23,7 @@ import com.google.android.exoplayer2.text.SimpleSubtitleDecoder; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.LongArray; import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.Util; import java.util.ArrayList; import java.util.List; import java.util.regex.Matcher; @@ -132,7 +133,7 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { formatEndIndex = C.INDEX_UNSET; formatTextIndex = C.INDEX_UNSET; for (int i = 0; i < formatKeyCount; i++) { - String key = values[i].trim().toLowerCase(); + String key = Util.toLowerInvariant(values[i].trim()); switch (key) { case "start": formatStartIndex = i; diff --git a/library/core/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 index e438aa1837..a215bf3cc9 100644 --- a/library/core/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 @@ -290,7 +290,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { String displayAlign = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_TTS_DISPLAY_ALIGN); if (displayAlign != null) { - switch (displayAlign.toLowerCase()) { + switch (Util.toLowerInvariant(displayAlign)) { case "center": lineAnchor = Cue.ANCHOR_TYPE_MIDDLE; line += height / 2; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java index c00d7fa36c..b958a54244 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -895,7 +895,7 @@ public final class Util { */ @C.ContentType public static int inferContentType(String fileName) { - fileName = fileName.toLowerCase(); + fileName = Util.toLowerInvariant(fileName); if (fileName.endsWith(".mpd")) { return C.TYPE_DASH; } else if (fileName.endsWith(".m3u8")) { diff --git a/library/hls/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 index 38c3da8194..bca62ed230 100644 --- a/library/hls/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 @@ -39,7 +39,6 @@ import java.io.IOException; import java.math.BigInteger; import java.util.Arrays; import java.util.List; -import java.util.Locale; /** * Source of Hls (possibly adaptive) chunks. @@ -366,7 +365,7 @@ import java.util.Locale; private void setEncryptionData(Uri keyUri, String iv, byte[] secretKey) { String trimmedIv; - if (iv.toLowerCase(Locale.getDefault()).startsWith("0x")) { + if (Util.toLowerInvariant(iv).startsWith("0x")) { trimmedIv = iv.substring(2); } else { trimmedIv = iv; diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java index 8c4bad1862..060780eda2 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.ui; +import android.annotation.SuppressLint; import android.widget.TextView; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; @@ -25,6 +26,7 @@ import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import java.util.Locale; /** * A helper class for periodically updating a {@link TextView} with debug information obtained from @@ -125,6 +127,7 @@ public final class DebugTextViewHelper implements Runnable, Player.EventListener // Private methods. + @SuppressLint("SetTextI18n") private void updateAndPost() { textView.setText(getPlayerStateString() + getPlayerWindowIndexString() + getVideoString() + getAudioString()); @@ -192,7 +195,7 @@ public final class DebugTextViewHelper implements Runnable, Player.EventListener private static String getPixelAspectRatioString(float pixelAspectRatio) { return pixelAspectRatio == Format.NO_VALUE || pixelAspectRatio == 1f ? "" - : (" par:" + String.format("%.02f", pixelAspectRatio)); + : (" par:" + String.format(Locale.US, "%.02f", pixelAspectRatio)); } } From cb00b0209f551eefe7968633715ed3067b55aa08 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 3 Aug 2017 05:19:02 -0700 Subject: [PATCH 0172/2472] Take into account init data size for input buffer size Issue: #2900 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164110904 --- .../video/MediaCodecVideoRenderer.java | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index 07c45dcd25..9a2927cc3f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -477,7 +477,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { Format oldFormat, Format newFormat) { return areAdaptationCompatible(codecIsAdaptive, oldFormat, newFormat) && newFormat.width <= codecMaxValues.width && newFormat.height <= codecMaxValues.height - && newFormat.maxInputSize <= codecMaxValues.inputSize; + && getMaxInputSize(newFormat) <= codecMaxValues.inputSize; } @Override @@ -854,18 +854,27 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } /** - * Returns a maximum input size for a given format. + * Returns a maximum input buffer size for a given format. * * @param format The format. - * @return A maximum input size in bytes, or {@link Format#NO_VALUE} if a maximum could not be - * determined. + * @return A maximum input buffer size in bytes, or {@link Format#NO_VALUE} if a maximum could not + * be determined. */ private static int getMaxInputSize(Format format) { if (format.maxInputSize != Format.NO_VALUE) { - // The format defines an explicit maximum input size. - return format.maxInputSize; + // The format defines an explicit maximum input size. Add the total size of initialization + // data buffers, as they may need to be queued in the same input buffer as the largest sample. + int totalInitializationDataSize = 0; + int initializationDataCount = format.initializationData.size(); + for (int i = 0; i < initializationDataCount; i++) { + totalInitializationDataSize += format.initializationData.get(i).length; + } + return format.maxInputSize + totalInitializationDataSize; + } else { + // Calculated maximum input sizes are overestimates, so it's not necessary to add the size of + // initialization data. + return getMaxInputSize(format.sampleMimeType, format.width, format.height); } - return getMaxInputSize(format.sampleMimeType, format.width, format.height); } /** From 3c6ad40481c3b4f2b1a757b7e182c1343a545d03 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 3 Aug 2017 06:32:22 -0700 Subject: [PATCH 0173/2472] Add shuffle order interface and default implementation. These classes maintain a shuffled order of indices allowing to query the next, previous, first, and last indices. And also support inserting and removing elements without changing the shuffled order of the rest. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164116287 --- .../exoplayer2/source/ShuffleOrderTest.java | 134 +++++++++ .../exoplayer2/source/ShuffleOrder.java | 256 ++++++++++++++++++ 2 files changed, 390 insertions(+) create mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/source/ShuffleOrderTest.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/source/ShuffleOrder.java diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ShuffleOrderTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ShuffleOrderTest.java new file mode 100644 index 0000000000..5de6bdf3e1 --- /dev/null +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ShuffleOrderTest.java @@ -0,0 +1,134 @@ +/* + * 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; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder; +import com.google.android.exoplayer2.source.ShuffleOrder.UnshuffledShuffleOrder; +import junit.framework.TestCase; + +/** + * Unit test for {@link ShuffleOrder}. + */ +public final class ShuffleOrderTest extends TestCase { + + public static final long RANDOM_SEED = 1234567890L; + + public void testDefaultShuffleOrder() { + assertShuffleOrderCorrectness(new DefaultShuffleOrder(0, RANDOM_SEED), 0); + assertShuffleOrderCorrectness(new DefaultShuffleOrder(1, RANDOM_SEED), 1); + assertShuffleOrderCorrectness(new DefaultShuffleOrder(5, RANDOM_SEED), 5); + for (int initialLength = 0; initialLength < 4; initialLength++) { + for (int insertionPoint = 0; insertionPoint <= initialLength; insertionPoint += 2) { + testCloneAndInsert(new DefaultShuffleOrder(initialLength, RANDOM_SEED), insertionPoint, 0); + testCloneAndInsert(new DefaultShuffleOrder(initialLength, RANDOM_SEED), insertionPoint, 1); + testCloneAndInsert(new DefaultShuffleOrder(initialLength, RANDOM_SEED), insertionPoint, 5); + } + } + testCloneAndRemove(new DefaultShuffleOrder(5, RANDOM_SEED), 0); + testCloneAndRemove(new DefaultShuffleOrder(5, RANDOM_SEED), 2); + testCloneAndRemove(new DefaultShuffleOrder(5, RANDOM_SEED), 4); + testCloneAndRemove(new DefaultShuffleOrder(1, RANDOM_SEED), 0); + } + + public void testUnshuffledShuffleOrder() { + assertShuffleOrderCorrectness(new UnshuffledShuffleOrder(0), 0); + assertShuffleOrderCorrectness(new UnshuffledShuffleOrder(1), 1); + assertShuffleOrderCorrectness(new UnshuffledShuffleOrder(5), 5); + for (int initialLength = 0; initialLength < 4; initialLength++) { + for (int insertionPoint = 0; insertionPoint <= initialLength; insertionPoint += 2) { + testCloneAndInsert(new UnshuffledShuffleOrder(initialLength), insertionPoint, 0); + testCloneAndInsert(new UnshuffledShuffleOrder(initialLength), insertionPoint, 1); + testCloneAndInsert(new UnshuffledShuffleOrder(initialLength), insertionPoint, 5); + } + } + testCloneAndRemove(new UnshuffledShuffleOrder(5), 0); + testCloneAndRemove(new UnshuffledShuffleOrder(5), 2); + testCloneAndRemove(new UnshuffledShuffleOrder(5), 4); + testCloneAndRemove(new UnshuffledShuffleOrder(1), 0); + } + + public void testUnshuffledShuffleOrderIsUnshuffled() { + ShuffleOrder shuffleOrder = new UnshuffledShuffleOrder(5); + assertEquals(0, shuffleOrder.getFirstIndex()); + assertEquals(4, shuffleOrder.getLastIndex()); + for (int i = 0; i < 4; i++) { + assertEquals(i + 1, shuffleOrder.getNextIndex(i)); + } + } + + private static void assertShuffleOrderCorrectness(ShuffleOrder shuffleOrder, int length) { + assertEquals(length, shuffleOrder.getLength()); + if (length == 0) { + assertEquals(C.INDEX_UNSET, shuffleOrder.getFirstIndex()); + assertEquals(C.INDEX_UNSET, shuffleOrder.getLastIndex()); + } else { + int[] indices = new int[length]; + indices[0] = shuffleOrder.getFirstIndex(); + assertEquals(C.INDEX_UNSET, shuffleOrder.getPreviousIndex(indices[0])); + for (int i = 1; i < length; i++) { + indices[i] = shuffleOrder.getNextIndex(indices[i - 1]); + assertEquals(indices[i - 1], shuffleOrder.getPreviousIndex(indices[i])); + for (int j = 0; j < i; j++) { + assertTrue(indices[i] != indices[j]); + } + } + assertEquals(indices[length - 1], shuffleOrder.getLastIndex()); + assertEquals(C.INDEX_UNSET, shuffleOrder.getNextIndex(indices[length - 1])); + for (int i = 0; i < length; i++) { + assertTrue(indices[i] >= 0 && indices[i] < length); + } + } + } + + private static void testCloneAndInsert(ShuffleOrder shuffleOrder, int position, int count) { + ShuffleOrder newOrder = shuffleOrder.cloneAndInsert(position, count); + assertShuffleOrderCorrectness(newOrder, shuffleOrder.getLength() + count); + // Assert all elements still have the relative same order + for (int i = 0; i < shuffleOrder.getLength(); i++) { + int expectedNextIndex = shuffleOrder.getNextIndex(i); + if (expectedNextIndex != C.INDEX_UNSET && expectedNextIndex >= position) { + expectedNextIndex += count; + } + int newNextIndex = newOrder.getNextIndex(i < position ? i : i + count); + while (newNextIndex >= position && newNextIndex < position + count) { + newNextIndex = newOrder.getNextIndex(newNextIndex); + } + assertEquals(expectedNextIndex, newNextIndex); + } + } + + private static void testCloneAndRemove(ShuffleOrder shuffleOrder, int position) { + ShuffleOrder newOrder = shuffleOrder.cloneAndRemove(position); + assertShuffleOrderCorrectness(newOrder, shuffleOrder.getLength() - 1); + // Assert all elements still have the relative same order + for (int i = 0; i < shuffleOrder.getLength(); i++) { + if (i == position) { + continue; + } + int expectedNextIndex = shuffleOrder.getNextIndex(i); + if (expectedNextIndex == position) { + expectedNextIndex = shuffleOrder.getNextIndex(expectedNextIndex); + } + if (expectedNextIndex != C.INDEX_UNSET && expectedNextIndex >= position) { + expectedNextIndex--; + } + int newNextIndex = newOrder.getNextIndex(i < position ? i : i - 1); + assertEquals(expectedNextIndex, newNextIndex); + } + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ShuffleOrder.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ShuffleOrder.java new file mode 100644 index 0000000000..4307fd2c19 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ShuffleOrder.java @@ -0,0 +1,256 @@ +/* + * 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; + +import com.google.android.exoplayer2.C; +import java.util.Arrays; +import java.util.Random; + +/** + * Shuffled order of indices. + */ +public interface ShuffleOrder { + + /** + * The default {@link ShuffleOrder} implementation for random shuffle order. + */ + class DefaultShuffleOrder implements ShuffleOrder { + + private final Random random; + private final int[] shuffled; + private final int[] indexInShuffled; + + /** + * Creates an instance with a specified length. + * + * @param length The length of the shuffle order. + */ + public DefaultShuffleOrder(int length) { + this(length, new Random()); + } + + /** + * Creates an instance with a specified length and the specified random seed. Shuffle orders of + * the same length initialized with the same random seed are guaranteed to be equal. + * + * @param length The length of the shuffle order. + * @param randomSeed A random seed. + */ + public DefaultShuffleOrder(int length, long randomSeed) { + this(length, new Random(randomSeed)); + } + + private DefaultShuffleOrder(int length, Random random) { + this(createShuffledList(length, random), random); + } + + private DefaultShuffleOrder(int[] shuffled, Random random) { + this.shuffled = shuffled; + this.random = random; + this.indexInShuffled = new int[shuffled.length]; + for (int i = 0; i < shuffled.length; i++) { + indexInShuffled[shuffled[i]] = i; + } + } + + @Override + public int getLength() { + return shuffled.length; + } + + @Override + public int getNextIndex(int index) { + int shuffledIndex = indexInShuffled[index]; + return ++shuffledIndex < shuffled.length ? shuffled[shuffledIndex] : C.INDEX_UNSET; + } + + @Override + public int getPreviousIndex(int index) { + int shuffledIndex = indexInShuffled[index]; + return --shuffledIndex >= 0 ? shuffled[shuffledIndex] : C.INDEX_UNSET; + } + + @Override + public int getLastIndex() { + return shuffled.length > 0 ? shuffled[shuffled.length - 1] : C.INDEX_UNSET; + } + + @Override + public int getFirstIndex() { + return shuffled.length > 0 ? shuffled[0] : C.INDEX_UNSET; + } + + @Override + public ShuffleOrder cloneAndInsert(int insertionIndex, int insertionCount) { + int[] insertionPoints = new int[insertionCount]; + int[] insertionValues = new int[insertionCount]; + for (int i = 0; i < insertionCount; i++) { + insertionPoints[i] = random.nextInt(shuffled.length + 1); + int swapIndex = random.nextInt(i + 1); + insertionValues[i] = insertionValues[swapIndex]; + insertionValues[swapIndex] = i + insertionIndex; + } + Arrays.sort(insertionPoints); + int[] newShuffled = new int[shuffled.length + insertionCount]; + int indexInOldShuffled = 0; + int indexInInsertionList = 0; + for (int i = 0; i < shuffled.length + insertionCount; i++) { + if (indexInInsertionList < insertionCount + && indexInOldShuffled == insertionPoints[indexInInsertionList]) { + newShuffled[i] = insertionValues[indexInInsertionList++]; + } else { + newShuffled[i] = shuffled[indexInOldShuffled++]; + if (newShuffled[i] >= insertionIndex) { + newShuffled[i] += insertionCount; + } + } + } + return new DefaultShuffleOrder(newShuffled, new Random(random.nextLong())); + } + + @Override + public ShuffleOrder cloneAndRemove(int removalIndex) { + int[] newShuffled = new int[shuffled.length - 1]; + boolean foundRemovedElement = false; + for (int i = 0; i < shuffled.length; i++) { + if (shuffled[i] == removalIndex) { + foundRemovedElement = true; + } else { + newShuffled[foundRemovedElement ? i - 1 : i] = shuffled[i] > removalIndex + ? shuffled[i] - 1 : shuffled[i]; + } + } + return new DefaultShuffleOrder(newShuffled, new Random(random.nextLong())); + } + + private static int[] createShuffledList(int length, Random random) { + int[] shuffled = new int[length]; + for (int i = 0; i < length; i++) { + int swapIndex = random.nextInt(i + 1); + shuffled[i] = shuffled[swapIndex]; + shuffled[swapIndex] = i; + } + return shuffled; + } + + } + + /** + * A {@link ShuffleOrder} implementation which does not shuffle. + */ + final class UnshuffledShuffleOrder implements ShuffleOrder { + + private final int length; + + /** + * Creates an instance with a specified length. + * + * @param length The length of the shuffle order. + */ + public UnshuffledShuffleOrder(int length) { + this.length = length; + } + + @Override + public int getLength() { + return length; + } + + @Override + public int getNextIndex(int index) { + return ++index < length ? index : C.INDEX_UNSET; + } + + @Override + public int getPreviousIndex(int index) { + return --index >= 0 ? index : C.INDEX_UNSET; + } + + @Override + public int getLastIndex() { + return length > 0 ? length - 1 : C.INDEX_UNSET; + } + + @Override + public int getFirstIndex() { + return length > 0 ? 0 : C.INDEX_UNSET; + } + + @Override + public ShuffleOrder cloneAndInsert(int insertionIndex, int insertionCount) { + return new UnshuffledShuffleOrder(length + insertionCount); + } + + @Override + public ShuffleOrder cloneAndRemove(int removalIndex) { + return new UnshuffledShuffleOrder(length - 1); + } + + } + + /** + * Returns length of shuffle order. + */ + int getLength(); + + /** + * Returns the next index in the shuffle order. + * + * @param index An index. + * @return The index after {@code index}, or {@link C#INDEX_UNSET} if {@code index} is the last + * element. + */ + int getNextIndex(int index); + + /** + * Returns the previous index in the shuffle order. + * + * @param index An index. + * @return The index before {@code index}, or {@link C#INDEX_UNSET} if {@code index} is the first + * element. + */ + int getPreviousIndex(int index); + + /** + * Returns the last index in the shuffle order, or {@link C#INDEX_UNSET} if the shuffle order is + * empty. + */ + int getLastIndex(); + + /** + * Returns the first index in the shuffle order, or {@link C#INDEX_UNSET} if the shuffle order is + * empty. + */ + int getFirstIndex(); + + /** + * Return a copy of the shuffle order with newly inserted elements. + * + * @param insertionIndex The index in the unshuffled order at which elements are inserted. + * @param insertionCount The number of elements inserted at {@code insertionIndex}. + * @return A copy of this {@link ShuffleOrder} with newly inserted elements. + */ + ShuffleOrder cloneAndInsert(int insertionIndex, int insertionCount); + + /** + * Return a copy of the shuffle order with one element removed. + * + * @param removalIndex The index of the element in the unshuffled order which is to be removed. + * @return A copy of this {@link ShuffleOrder} without the removed element. + */ + ShuffleOrder cloneAndRemove(int removalIndex); + +} From 768a73b377d842aeefdd466cf7a5904f858cc8a4 Mon Sep 17 00:00:00 2001 From: "J. Oliva" Date: Fri, 4 Aug 2017 09:47:54 +0200 Subject: [PATCH 0174/2472] Add possibility of forcing a specific license URL in HttpMediaDrmCallback - Renamed some license URL related variables to keep consistency across the code. - Added a new parameter to HttpMediaDrmCallback that enables forcing defaultLicenseURL as the license acquisition URL. --- .../exoplayer2/drm/HttpMediaDrmCallback.java | 28 ++++++------- .../exoplayer2/drm/OfflineLicenseHelper.java | 39 +++++++++++++++---- 2 files changed, 45 insertions(+), 22 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java index f9d5efffb1..5a56497680 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java @@ -47,29 +47,29 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback { } private final HttpDataSource.Factory dataSourceFactory; - private final String defaultUrl; + private final String defaultLicenseUrl; + private final boolean forceDefaultLicenseUrl; private final Map keyRequestProperties; /** - * @param defaultUrl The default license URL. + * @param defaultLicenseUrl The default license URL. * @param dataSourceFactory A factory from which to obtain {@link HttpDataSource} instances. */ - public HttpMediaDrmCallback(String defaultUrl, HttpDataSource.Factory dataSourceFactory) { - this(defaultUrl, dataSourceFactory, null); + public HttpMediaDrmCallback(String defaultLicenseUrl, HttpDataSource.Factory dataSourceFactory) { + this(defaultLicenseUrl, false, dataSourceFactory, null); } /** - * @deprecated Use {@link HttpMediaDrmCallback#HttpMediaDrmCallback(String, Factory)}. Request - * properties can be set by calling {@link #setKeyRequestProperty(String, String)}. - * @param defaultUrl The default license URL. + * @param defaultLicenseUrl The default license URL. + * @param forceDefaultLicenseUrl Whether to force use of {@code defaultLicenseUrl} even for key + * requests that include their own license URL. * @param dataSourceFactory A factory from which to obtain {@link HttpDataSource} instances. - * @param keyRequestProperties Request properties to set when making key requests, or null. */ - @Deprecated - public HttpMediaDrmCallback(String defaultUrl, HttpDataSource.Factory dataSourceFactory, - Map keyRequestProperties) { + public HttpMediaDrmCallback(String defaultLicenseUrl, boolean forceDefaultLicenseUrl, + HttpDataSource.Factory dataSourceFactory, Map keyRequestProperties) { this.dataSourceFactory = dataSourceFactory; - this.defaultUrl = defaultUrl; + this.defaultLicenseUrl = defaultLicenseUrl; + this.forceDefaultLicenseUrl = forceDefaultLicenseUrl; this.keyRequestProperties = new HashMap<>(); if (keyRequestProperties != null) { this.keyRequestProperties.putAll(keyRequestProperties); @@ -120,8 +120,8 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback { @Override public byte[] executeKeyRequest(UUID uuid, KeyRequest request) throws Exception { String url = request.getDefaultUrl(); - if (TextUtils.isEmpty(url)) { - url = defaultUrl; + if (forceDefaultLicenseUrl || TextUtils.isEmpty(url)) { + url = defaultLicenseUrl; } Map requestProperties = new HashMap<>(); requestProperties.put("Content-Type", "application/octet-stream"); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java index 2eb3463b3d..5ae06b7691 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java @@ -43,23 +43,44 @@ public final class OfflineLicenseHelper { * 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 defaultLicenseUrl The default license URL. * @param httpDataSourceFactory A factory from which to obtain {@link HttpDataSource} instances. * @return A new instance which uses Widevine CDM. * @throws UnsupportedDrmException If the Widevine DRM scheme is unsupported or cannot be * instantiated. */ public static OfflineLicenseHelper newWidevineInstance( - String licenseUrl, Factory httpDataSourceFactory) throws UnsupportedDrmException { - return newWidevineInstance( - new HttpMediaDrmCallback(licenseUrl, httpDataSourceFactory), null); + String defaultLicenseUrl, Factory httpDataSourceFactory) + throws UnsupportedDrmException { + return newWidevineInstance(defaultLicenseUrl, false, httpDataSourceFactory, null); } /** * 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 defaultLicenseUrl The default license URL. + * @param forceDefaultLicenseUrl Whether to force use of {@code defaultLicenseUrl} even for key + * requests that include their own license URL. + * @param httpDataSourceFactory A factory from which to obtain {@link HttpDataSource} instances. + * @return A new instance which uses Widevine CDM. + * @throws UnsupportedDrmException If the Widevine DRM scheme is unsupported or cannot be + * instantiated. + */ + public static OfflineLicenseHelper newWidevineInstance( + String defaultLicenseUrl, boolean forceDefaultLicenseUrl, Factory httpDataSourceFactory) + throws UnsupportedDrmException { + return newWidevineInstance(defaultLicenseUrl, forceDefaultLicenseUrl, httpDataSourceFactory, + null); + } + + /** + * Instantiates a new instance which uses Widevine CDM. Call {@link #release()} when the instance + * is no longer required. + * + * @param defaultLicenseUrl The default license URL. + * @param forceDefaultLicenseUrl Whether to force use of {@code defaultLicenseUrl} even for key + * requests that include their own license URL. * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument * to {@link MediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null. * @return A new instance which uses Widevine CDM. @@ -69,10 +90,12 @@ public final class OfflineLicenseHelper { * MediaDrmCallback, HashMap, Handler, EventListener) */ public static OfflineLicenseHelper newWidevineInstance( - MediaDrmCallback callback, HashMap optionalKeyRequestParameters) + String defaultLicenseUrl, boolean forceDefaultLicenseUrl, Factory httpDataSourceFactory, + HashMap optionalKeyRequestParameters) throws UnsupportedDrmException { - return new OfflineLicenseHelper<>(FrameworkMediaDrm.newInstance(C.WIDEVINE_UUID), callback, - optionalKeyRequestParameters); + return new OfflineLicenseHelper<>(FrameworkMediaDrm.newInstance(C.WIDEVINE_UUID), + new HttpMediaDrmCallback(defaultLicenseUrl, forceDefaultLicenseUrl, httpDataSourceFactory, + null), optionalKeyRequestParameters); } /** From 6f7dc974c963169130a8d59d4890f771883d66d7 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 3 Aug 2017 06:45:18 -0700 Subject: [PATCH 0175/2472] Add forwarding timeline to prevent repetition of boiler-plate code. Multiple timelines work as a wrapper around another timeline and forward most of the method calls to the wrapped timeline. Added a base class meant to be overridden which handles all the boiler-plate forwarding. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164117264 --- .../ext/ima/SinglePeriodAdTimeline.java | 29 ++------ .../source/ClippingMediaSource.java | 31 +-------- .../exoplayer2/source/ForwardingTimeline.java | 68 +++++++++++++++++++ .../exoplayer2/source/LoopingMediaSource.java | 38 ++--------- 4 files changed, 79 insertions(+), 87 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/source/ForwardingTimeline.java diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/SinglePeriodAdTimeline.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/SinglePeriodAdTimeline.java index c93f1e8f28..e3eef6a2b7 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/SinglePeriodAdTimeline.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/SinglePeriodAdTimeline.java @@ -17,14 +17,14 @@ package com.google.android.exoplayer2.ext.ima; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.ForwardingTimeline; import com.google.android.exoplayer2.util.Assertions; /** * A {@link Timeline} for sources that have ads. */ -public final class SinglePeriodAdTimeline extends Timeline { +public final class SinglePeriodAdTimeline extends ForwardingTimeline { - private final Timeline contentTimeline; private final long[] adGroupTimesUs; private final int[] adCounts; private final int[] adsLoadedCounts; @@ -52,9 +52,9 @@ public final class SinglePeriodAdTimeline extends Timeline { public SinglePeriodAdTimeline(Timeline contentTimeline, long[] adGroupTimesUs, int[] adCounts, int[] adsLoadedCounts, int[] adsPlayedCounts, long[][] adDurationsUs, long adResumePositionUs) { + super(contentTimeline); Assertions.checkState(contentTimeline.getPeriodCount() == 1); Assertions.checkState(contentTimeline.getWindowCount() == 1); - this.contentTimeline = contentTimeline; this.adGroupTimesUs = adGroupTimesUs; this.adCounts = adCounts; this.adsLoadedCounts = adsLoadedCounts; @@ -63,34 +63,13 @@ public final class SinglePeriodAdTimeline extends Timeline { this.adResumePositionUs = adResumePositionUs; } - @Override - public int getWindowCount() { - return 1; - } - - @Override - public Window getWindow(int windowIndex, Window window, boolean setIds, - long defaultPositionProjectionUs) { - return contentTimeline.getWindow(windowIndex, window, setIds, defaultPositionProjectionUs); - } - - @Override - public int getPeriodCount() { - return 1; - } - @Override public Period getPeriod(int periodIndex, Period period, boolean setIds) { - contentTimeline.getPeriod(periodIndex, period, setIds); + timeline.getPeriod(periodIndex, period, setIds); period.set(period.id, period.uid, period.windowIndex, period.durationUs, period.getPositionInWindowUs(), adGroupTimesUs, adCounts, adsLoadedCounts, adsPlayedCounts, adDurationsUs, adResumePositionUs); return period; } - @Override - public int getIndexOfPeriod(Object uid) { - return contentTimeline.getIndexOfPeriod(uid); - } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java index 32c4eb6c73..4caafa3110 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.source; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; -import com.google.android.exoplayer2.Player.RepeatMode; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.util.Assertions; @@ -128,9 +127,8 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste /** * Provides a clipped view of a specified timeline. */ - private static final class ClippingTimeline extends Timeline { + private static final class ClippingTimeline extends ForwardingTimeline { - private final Timeline timeline; private final long startUs; private final long endUs; @@ -143,6 +141,7 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste * of {@code timeline}, or {@link C#TIME_END_OF_SOURCE} to clip no samples from the end. */ public ClippingTimeline(Timeline timeline, long startUs, long endUs) { + super(timeline); Assertions.checkArgument(timeline.getWindowCount() == 1); Assertions.checkArgument(timeline.getPeriodCount() == 1); Window window = timeline.getWindow(0, new Window(), false); @@ -155,26 +154,10 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste } Period period = timeline.getPeriod(0, new Period()); Assertions.checkArgument(period.getPositionInWindowUs() == 0); - this.timeline = timeline; this.startUs = startUs; this.endUs = resolvedEndUs; } - @Override - public int getWindowCount() { - return 1; - } - - @Override - public int getNextWindowIndex(int windowIndex, @RepeatMode int repeatMode) { - return timeline.getNextWindowIndex(windowIndex, repeatMode); - } - - @Override - public int getPreviousWindowIndex(int windowIndex, @RepeatMode int repeatMode) { - return timeline.getPreviousWindowIndex(windowIndex, repeatMode); - } - @Override public Window getWindow(int windowIndex, Window window, boolean setIds, long defaultPositionProjectionUs) { @@ -196,11 +179,6 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste return window; } - @Override - public int getPeriodCount() { - return 1; - } - @Override public Period getPeriod(int periodIndex, Period period, boolean setIds) { period = timeline.getPeriod(0, period, setIds); @@ -208,11 +186,6 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste return period; } - @Override - public int getIndexOfPeriod(Object uid) { - return timeline.getIndexOfPeriod(uid); - } - } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ForwardingTimeline.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ForwardingTimeline.java new file mode 100644 index 0000000000..4203abbf39 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ForwardingTimeline.java @@ -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. + */ +package com.google.android.exoplayer2.source; + +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Timeline; + +/** + * An overridable {@link Timeline} implementation forwarding all methods to another timeline. + */ +public abstract class ForwardingTimeline extends Timeline { + + protected final Timeline timeline; + + public ForwardingTimeline(Timeline timeline) { + this.timeline = timeline; + } + + @Override + public int getWindowCount() { + return timeline.getWindowCount(); + } + + @Override + public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) { + return timeline.getNextWindowIndex(windowIndex, repeatMode); + } + + @Override + public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) { + return timeline.getPreviousWindowIndex(windowIndex, repeatMode); + } + + @Override + public Window getWindow(int windowIndex, Window window, boolean setIds, + long defaultPositionProjectionUs) { + return timeline.getWindow(windowIndex, window, setIds, defaultPositionProjectionUs); + } + + @Override + public int getPeriodCount() { + return timeline.getPeriodCount(); + } + + @Override + public Period getPeriod(int periodIndex, Period period, boolean setIds) { + return timeline.getPeriod(periodIndex, period, setIds); + } + + @Override + public int getIndexOfPeriod(Object uid) { + return timeline.getIndexOfPeriod(uid); + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java index a6e93a92b9..1795fe8045 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java @@ -160,53 +160,25 @@ public final class LoopingMediaSource implements MediaSource { } - private static final class InfinitelyLoopingTimeline extends Timeline { + private static final class InfinitelyLoopingTimeline extends ForwardingTimeline { - private final Timeline childTimeline; - - public InfinitelyLoopingTimeline(Timeline childTimeline) { - this.childTimeline = childTimeline; - } - - @Override - public int getWindowCount() { - return childTimeline.getWindowCount(); + public InfinitelyLoopingTimeline(Timeline timeline) { + super(timeline); } @Override public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) { - int childNextWindowIndex = childTimeline.getNextWindowIndex(windowIndex, repeatMode); + int childNextWindowIndex = timeline.getNextWindowIndex(windowIndex, repeatMode); return childNextWindowIndex == C.INDEX_UNSET ? 0 : childNextWindowIndex; } @Override public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) { - int childPreviousWindowIndex = childTimeline.getPreviousWindowIndex(windowIndex, repeatMode); + int childPreviousWindowIndex = timeline.getPreviousWindowIndex(windowIndex, repeatMode); return childPreviousWindowIndex == C.INDEX_UNSET ? getWindowCount() - 1 : childPreviousWindowIndex; } - @Override - public Window getWindow(int windowIndex, Window window, boolean setIds, - long defaultPositionProjectionUs) { - return childTimeline.getWindow(windowIndex, window, setIds, defaultPositionProjectionUs); - } - - @Override - public int getPeriodCount() { - return childTimeline.getPeriodCount(); - } - - @Override - public Period getPeriod(int periodIndex, Period period, boolean setIds) { - return childTimeline.getPeriod(periodIndex, period, setIds); - } - - @Override - public int getIndexOfPeriod(Object uid) { - return childTimeline.getIndexOfPeriod(uid); - } - } } From ccd05cbd04ca62659e4c1992dfeda912e2dbf7b9 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 3 Aug 2017 07:22:15 -0700 Subject: [PATCH 0176/2472] Add fake simple exo player. This implementation runs as fast as possible by triggering a simplified playback loop continuously without waiting. The class only supports a basic use case with a single-period timeline, no timeline update, no seeks or other user-initiated actions. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164120420 --- .../android/exoplayer2/SimpleExoPlayer.java | 15 +- .../testutil/FakeSimpleExoPlayer.java | 521 ++++++++++++++++++ 2 files changed, 535 insertions(+), 1 deletion(-) create mode 100644 testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index ebfe380b6b..9dea83cff2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -141,7 +141,7 @@ public class SimpleExoPlayer implements ExoPlayer { videoScalingMode = C.VIDEO_SCALING_MODE_DEFAULT; // Build the player and associated objects. - player = new ExoPlayerImpl(renderers, trackSelector, loadControl); + player = createExoPlayerImpl(renderers, trackSelector, loadControl); } /** @@ -722,6 +722,19 @@ public class SimpleExoPlayer implements ExoPlayer { // Internal methods. + /** + * Creates the ExoPlayer implementation used by this {@link SimpleExoPlayer}. + * + * @param renderers The {@link Renderer}s that will 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. + * @return A new {@link ExoPlayer} instance. + */ + protected ExoPlayer createExoPlayerImpl(Renderer[] renderers, TrackSelector trackSelector, + LoadControl loadControl) { + return new ExoPlayerImpl(renderers, trackSelector, loadControl); + } + private void removeSurfaceCallbacks() { if (textureView != null) { if (textureView.getSurfaceTextureListener() != componentListener) { diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java new file mode 100644 index 0000000000..cf88d10bc8 --- /dev/null +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java @@ -0,0 +1,521 @@ +/* + * 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.testutil; + +import android.os.ConditionVariable; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.support.annotation.Nullable; +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.LoadControl; +import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Renderer; +import com.google.android.exoplayer2.RendererCapabilities; +import com.google.android.exoplayer2.RenderersFactory; +import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.Timeline.Period; +import com.google.android.exoplayer2.source.MediaPeriod; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import com.google.android.exoplayer2.source.SampleStream; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.trackselection.TrackSelection; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import com.google.android.exoplayer2.trackselection.TrackSelector; +import com.google.android.exoplayer2.trackselection.TrackSelector.InvalidationListener; +import com.google.android.exoplayer2.trackselection.TrackSelectorResult; +import com.google.android.exoplayer2.util.Assertions; +import java.util.Arrays; +import java.util.concurrent.CopyOnWriteArraySet; + +/** + * Fake {@link SimpleExoPlayer} which runs a simplified copy of the playback loop as fast as + * possible without waiting. It does only support single period timelines and does not support + * updates during playback (like seek, timeline changes, repeat mode changes). + */ +public class FakeSimpleExoPlayer extends SimpleExoPlayer { + + private FakeExoPlayer player; + + public FakeSimpleExoPlayer(RenderersFactory renderersFactory, TrackSelector trackSelector, + LoadControl loadControl, FakeClock clock) { + super (renderersFactory, trackSelector, loadControl); + player.setFakeClock(clock); + } + + @Override + protected ExoPlayer createExoPlayerImpl(Renderer[] renderers, TrackSelector trackSelector, + LoadControl loadControl) { + this.player = new FakeExoPlayer(renderers, trackSelector, loadControl); + return player; + } + + private class FakeExoPlayer implements ExoPlayer, MediaSource.Listener, MediaPeriod.Callback, + Runnable { + + private final Renderer[] renderers; + private final TrackSelector trackSelector; + private final LoadControl loadControl; + private final CopyOnWriteArraySet eventListeners; + private final HandlerThread playbackThread; + private final Handler playbackHandler; + private final Handler eventListenerHandler; + + private FakeClock clock; + private MediaSource mediaSource; + private Timeline timeline; + private Object manifest; + private MediaPeriod mediaPeriod; + private TrackSelectorResult selectorResult; + + private boolean isStartingUp; + private boolean isLoading; + private int playbackState; + private long rendererPositionUs; + private long durationUs; + private volatile long currentPositionMs; + private volatile long bufferedPositionMs; + + public FakeExoPlayer(Renderer[] renderers, TrackSelector trackSelector, + LoadControl loadControl) { + this.renderers = renderers; + this.trackSelector = trackSelector; + this.loadControl = loadControl; + this.eventListeners = new CopyOnWriteArraySet<>(); + Looper eventListenerLooper = Looper.myLooper(); + this.eventListenerHandler = new Handler(eventListenerLooper != null ? eventListenerLooper + : Looper.getMainLooper()); + this.playbackThread = new HandlerThread("FakeExoPlayer Thread"); + playbackThread.start(); + this.playbackHandler = new Handler(playbackThread.getLooper()); + this.isStartingUp = true; + this.isLoading = false; + this.playbackState = Player.STATE_IDLE; + this.durationUs = C.TIME_UNSET; + } + + public void setFakeClock(FakeClock clock) { + this.clock = clock; + } + + @Override + public void addListener(Player.EventListener listener) { + eventListeners.add(listener); + } + + @Override + public void removeListener(Player.EventListener listener) { + eventListeners.remove(listener); + } + + @Override + public int getPlaybackState() { + return playbackState; + } + + @Override + public void setPlayWhenReady(boolean playWhenReady) { + if (playWhenReady != true) { + throw new UnsupportedOperationException(); + } + } + + @Override + public boolean getPlayWhenReady() { + return true; + } + + @Override + public void setRepeatMode(@RepeatMode int repeatMode) { + throw new UnsupportedOperationException(); + } + + @Override + public int getRepeatMode() { + return Player.REPEAT_MODE_OFF; + } + + @Override + public boolean isLoading() { + return isLoading; + } + + @Override + public void seekToDefaultPosition() { + throw new UnsupportedOperationException(); + } + + @Override + public void seekToDefaultPosition(int windowIndex) { + throw new UnsupportedOperationException(); + } + + @Override + public void seekTo(long positionMs) { + throw new UnsupportedOperationException(); + } + + @Override + public void seekTo(int windowIndex, long positionMs) { + throw new UnsupportedOperationException(); + } + + @Override + public void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters) { + throw new UnsupportedOperationException(); + } + + @Override + public PlaybackParameters getPlaybackParameters() { + return PlaybackParameters.DEFAULT; + } + + @Override + public void stop() { + throw new UnsupportedOperationException(); + } + + @Override + public void release() { + playbackThread.quit(); + } + + @Override + public int getRendererCount() { + return renderers.length; + } + + @Override + public int getRendererType(int index) { + return renderers[index].getTrackType(); + } + + @Override + public TrackGroupArray getCurrentTrackGroups() { + return selectorResult != null ? selectorResult.groups : null; + } + + @Override + public TrackSelectionArray getCurrentTrackSelections() { + return selectorResult != null ? selectorResult.selections : null; + } + + @Nullable + @Override + public Object getCurrentManifest() { + return manifest; + } + + @Override + public Timeline getCurrentTimeline() { + return timeline; + } + + @Override + public int getCurrentPeriodIndex() { + return 0; + } + + @Override + public int getCurrentWindowIndex() { + return 0; + } + + @Override + public long getDuration() { + return C.usToMs(durationUs); + } + + @Override + public long getCurrentPosition() { + return currentPositionMs; + } + + @Override + public long getBufferedPosition() { + return bufferedPositionMs == C.TIME_END_OF_SOURCE ? getDuration() : bufferedPositionMs; + } + + @Override + public int getBufferedPercentage() { + long duration = getDuration(); + return duration == C.TIME_UNSET ? 0 : (int) (getBufferedPosition() * 100 / duration); + } + + @Override + public boolean isCurrentWindowDynamic() { + return false; + } + + @Override + public boolean isCurrentWindowSeekable() { + return false; + } + + @Override + public boolean isPlayingAd() { + return false; + } + + @Override + public int getCurrentAdGroupIndex() { + return 0; + } + + @Override + public int getCurrentAdIndexInAdGroup() { + return 0; + } + + @Override + public long getContentPosition() { + return getCurrentPosition(); + } + + @Override + public Looper getPlaybackLooper() { + return playbackThread.getLooper(); + } + + @Override + public void prepare(MediaSource mediaSource) { + prepare(mediaSource, true, true); + } + + @Override + public void prepare(final MediaSource mediaSource, boolean resetPosition, boolean resetState) { + if (resetPosition != true || resetState != true) { + throw new UnsupportedOperationException(); + } + this.mediaSource = mediaSource; + playbackHandler.post(new Runnable() { + @Override + public void run() { + mediaSource.prepareSource(FakeExoPlayer.this, true, FakeExoPlayer.this); + } + }); + } + + @Override + public void sendMessages(ExoPlayerMessage... messages) { + throw new UnsupportedOperationException(); + } + + @Override + public void blockingSendMessages(ExoPlayerMessage... messages) { + throw new UnsupportedOperationException(); + } + + // MediaSource.Listener + + @Override + public void onSourceInfoRefreshed(final Timeline timeline, final @Nullable Object manifest) { + if (this.timeline != null) { + throw new UnsupportedOperationException(); + } + Assertions.checkArgument(timeline.getPeriodCount() == 1); + Assertions.checkArgument(timeline.getWindowCount() == 1); + final ConditionVariable waitForNotification = new ConditionVariable(); + eventListenerHandler.post(new Runnable() { + @Override + public void run() { + for (Player.EventListener eventListener : eventListeners) { + FakeExoPlayer.this.durationUs = timeline.getPeriod(0, new Period()).durationUs; + FakeExoPlayer.this.timeline = timeline; + FakeExoPlayer.this.manifest = manifest; + eventListener.onTimelineChanged(timeline, manifest); + waitForNotification.open(); + } + } + }); + waitForNotification.block(); + this.mediaPeriod = mediaSource.createPeriod(new MediaPeriodId(0), loadControl.getAllocator()); + mediaPeriod.prepare(this, 0); + } + + // MediaPeriod.Callback + + @Override + public void onContinueLoadingRequested(MediaPeriod source) { + maybeContinueLoading(); + } + + @Override + public void onPrepared(MediaPeriod mediaPeriod) { + try { + initializePlaybackLoop(); + } catch (ExoPlaybackException e) { + handlePlayerError(e); + } + } + + // Runnable (Playback loop). + + @Override + public void run() { + try { + maybeContinueLoading(); + boolean allRenderersEnded = true; + boolean allRenderersReadyOrEnded = true; + if (playbackState == Player.STATE_READY) { + for (Renderer renderer : renderers) { + renderer.render(rendererPositionUs, C.msToUs(clock.elapsedRealtime())); + if (!renderer.isEnded()) { + allRenderersEnded = false; + } + if (!(renderer.isReady() || renderer.isEnded())) { + allRenderersReadyOrEnded = false; + } + } + } + if (rendererPositionUs >= durationUs && allRenderersEnded) { + changePlaybackState(Player.STATE_ENDED); + return; + } + long bufferedPositionUs = mediaPeriod.getBufferedPositionUs(); + if (playbackState == Player.STATE_BUFFERING && allRenderersReadyOrEnded + && haveSufficientBuffer(!isStartingUp, rendererPositionUs, bufferedPositionUs)) { + changePlaybackState(Player.STATE_READY); + isStartingUp = false; + } else if (playbackState == Player.STATE_READY && !allRenderersReadyOrEnded) { + changePlaybackState(Player.STATE_BUFFERING); + } + // Advance simulated time by 10ms. + clock.advanceTime(10); + if (playbackState == Player.STATE_READY) { + rendererPositionUs += 10000; + } + this.currentPositionMs = C.usToMs(rendererPositionUs); + this.bufferedPositionMs = C.usToMs(bufferedPositionUs); + playbackHandler.post(this); + } catch (ExoPlaybackException e) { + handlePlayerError(e); + } + } + + // Internal logic + + private void initializePlaybackLoop() throws ExoPlaybackException { + Assertions.checkNotNull(clock); + trackSelector.init(new InvalidationListener() { + @Override + public void onTrackSelectionsInvalidated() { + throw new IllegalStateException(); + } + }); + RendererCapabilities[] rendererCapabilities = new RendererCapabilities[renderers.length]; + for (int i = 0; i < renderers.length; i++) { + rendererCapabilities[i] = renderers[i].getCapabilities(); + } + selectorResult = trackSelector.selectTracks(rendererCapabilities, + mediaPeriod.getTrackGroups()); + SampleStream[] sampleStreams = new SampleStream[renderers.length]; + boolean[] mayRetainStreamFlags = new boolean[renderers.length]; + Arrays.fill(mayRetainStreamFlags, true); + mediaPeriod.selectTracks(selectorResult.selections.getAll(), mayRetainStreamFlags, + sampleStreams, new boolean[renderers.length], 0); + eventListenerHandler.post(new Runnable() { + @Override + public void run() { + for (Player.EventListener eventListener : eventListeners) { + eventListener.onTracksChanged(selectorResult.groups, selectorResult.selections); + } + } + }); + + loadControl.onPrepared(); + loadControl.onTracksSelected(renderers, selectorResult.groups, selectorResult.selections); + + for (int i = 0; i < renderers.length; i++) { + TrackSelection selection = selectorResult.selections.get(i); + Format[] formats = new Format[selection.length()]; + for (int j = 0; j < formats.length; j++) { + formats[j] = selection.getFormat(j); + } + renderers[i].enable(selectorResult.rendererConfigurations[i], formats, sampleStreams[i], 0, + false, 0); + renderers[i].setCurrentStreamFinal(); + } + + rendererPositionUs = 0; + changePlaybackState(Player.STATE_BUFFERING); + playbackHandler.post(this); + } + + private void maybeContinueLoading() { + boolean newIsLoading = false; + long nextLoadPositionUs = mediaPeriod.getNextLoadPositionUs(); + if (nextLoadPositionUs != C.TIME_END_OF_SOURCE) { + long bufferedDurationUs = nextLoadPositionUs - rendererPositionUs; + if (loadControl.shouldContinueLoading(bufferedDurationUs)) { + newIsLoading = true; + mediaPeriod.continueLoading(rendererPositionUs); + } + } + if (newIsLoading != isLoading) { + isLoading = newIsLoading; + eventListenerHandler.post(new Runnable() { + @Override + public void run() { + for (Player.EventListener eventListener : eventListeners) { + eventListener.onLoadingChanged(isLoading); + } + } + }); + } + } + + private boolean haveSufficientBuffer(boolean rebuffering, long rendererPositionUs, + long bufferedPositionUs) { + if (bufferedPositionUs == C.TIME_END_OF_SOURCE) { + return true; + } + return loadControl.shouldStartPlayback(bufferedPositionUs - rendererPositionUs, rebuffering); + } + + private void handlePlayerError(final ExoPlaybackException e) { + eventListenerHandler.post(new Runnable() { + @Override + public void run() { + for (Player.EventListener listener : eventListeners) { + listener.onPlayerError(e); + } + } + }); + changePlaybackState(Player.STATE_ENDED); + } + + private void changePlaybackState(final int playbackState) { + this.playbackState = playbackState; + eventListenerHandler.post(new Runnable() { + @Override + public void run() { + for (Player.EventListener listener : eventListeners) { + listener.onPlayerStateChanged(true, playbackState); + } + } + }); + } + + } + +} From 72daef7a4c3ae5a9375f7d064d88bbc9f6f96da6 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 3 Aug 2017 08:17:26 -0700 Subject: [PATCH 0177/2472] Don't read GaSpecificConfig unless required ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164125579 --- .../exoplayer2/extractor/ts/LatmReader.java | 2 +- .../util/CodecSpecificDataUtil.java | 70 ++++++++++--------- 2 files changed, 38 insertions(+), 34 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java index d21943eae8..d06c6f0cb4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java @@ -256,7 +256,7 @@ public final class LatmReader implements ElementaryStreamReader { private int parseAudioSpecificConfig(ParsableBitArray data) throws ParserException { int bitsLeft = data.bitsLeft(); - Pair config = CodecSpecificDataUtil.parseAacAudioSpecificConfig(data); + Pair config = CodecSpecificDataUtil.parseAacAudioSpecificConfig(data, true); sampleRateHz = config.first; channelCount = config.second; return bitsLeft - data.bitsLeft(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/CodecSpecificDataUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/util/CodecSpecificDataUtil.java index c9884abe78..0514d9dbdc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/CodecSpecificDataUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/CodecSpecificDataUtil.java @@ -90,7 +90,7 @@ public final class CodecSpecificDataUtil { */ public static Pair parseAacAudioSpecificConfig(byte[] audioSpecificConfig) throws ParserException { - return parseAacAudioSpecificConfig(new ParsableBitArray(audioSpecificConfig)); + return parseAacAudioSpecificConfig(new ParsableBitArray(audioSpecificConfig), false); } /** @@ -98,11 +98,13 @@ public final class CodecSpecificDataUtil { * * @param bitArray A {@link ParsableBitArray} containing the AudioSpecificConfig to parse. The * position is advanced to the end of the AudioSpecificConfig. + * @param forceReadToEnd Whether the entire AudioSpecificConfig should be read. Required for + * knowing the length of the configuration payload. * @return A pair consisting of the sample rate in Hz and the channel count. * @throws ParserException If the AudioSpecificConfig cannot be parsed as it's not supported. */ - public static Pair parseAacAudioSpecificConfig(ParsableBitArray bitArray) - throws ParserException { + public static Pair parseAacAudioSpecificConfig(ParsableBitArray bitArray, + boolean forceReadToEnd) throws ParserException { int audioObjectType = getAacAudioObjectType(bitArray); int sampleRate = getAacSamplingFrequency(bitArray); int channelConfiguration = bitArray.readBits(4); @@ -120,36 +122,38 @@ public final class CodecSpecificDataUtil { } } - switch (audioObjectType) { - case 1: - case 2: - case 3: - case 4: - case 6: - case 7: - case 17: - case 19: - case 20: - case 21: - case 22: - case 23: - parseGaSpecificConfig(bitArray, audioObjectType, channelConfiguration); - break; - default: - throw new ParserException("Unsupported audio object type: " + audioObjectType); - } - switch (audioObjectType) { - case 17: - case 19: - case 20: - case 21: - case 22: - case 23: - int epConfig = bitArray.readBits(2); - if (epConfig == 2 || epConfig == 3) { - throw new ParserException("Unsupported epConfig: " + epConfig); - } - break; + if (forceReadToEnd) { + switch (audioObjectType) { + case 1: + case 2: + case 3: + case 4: + case 6: + case 7: + case 17: + case 19: + case 20: + case 21: + case 22: + case 23: + parseGaSpecificConfig(bitArray, audioObjectType, channelConfiguration); + break; + default: + throw new ParserException("Unsupported audio object type: " + audioObjectType); + } + switch (audioObjectType) { + case 17: + case 19: + case 20: + case 21: + case 22: + case 23: + int epConfig = bitArray.readBits(2); + if (epConfig == 2 || epConfig == 3) { + throw new ParserException("Unsupported epConfig: " + epConfig); + } + break; + } } // For supported containers, bits_to_decode() is always 0. int channelCount = AUDIO_SPECIFIC_CONFIG_CHANNEL_COUNT_TABLE[channelConfiguration]; From 53bc59b9d214a13b44a08fc088cb69971c40f2d8 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 3 Aug 2017 08:22:45 -0700 Subject: [PATCH 0178/2472] Add UI resources for shuffle mode. Including strings, icon, and button. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164126101 --- .../exo_controls_shuffle.xml | 26 ++++++++++++++++++ .../drawable-hdpi/exo_controls_shuffle.png | Bin 0 -> 268 bytes .../drawable-ldpi/exo_controls_shuffle.png | Bin 0 -> 187 bytes .../drawable-mdpi/exo_controls_shuffle.png | Bin 0 -> 230 bytes .../drawable-xhdpi/exo_controls_shuffle.png | Bin 0 -> 342 bytes .../drawable-xxhdpi/exo_controls_shuffle.png | Bin 0 -> 436 bytes .../res/layout/exo_playback_control_view.xml | 3 ++ library/ui/src/main/res/values-af/strings.xml | 1 + library/ui/src/main/res/values-am/strings.xml | 1 + library/ui/src/main/res/values-ar/strings.xml | 1 + .../ui/src/main/res/values-az-rAZ/strings.xml | 1 + .../src/main/res/values-b+sr+Latn/strings.xml | 1 + .../ui/src/main/res/values-be-rBY/strings.xml | 1 + library/ui/src/main/res/values-bg/strings.xml | 1 + .../ui/src/main/res/values-bn-rBD/strings.xml | 1 + .../ui/src/main/res/values-bs-rBA/strings.xml | 1 + library/ui/src/main/res/values-ca/strings.xml | 1 + library/ui/src/main/res/values-cs/strings.xml | 1 + library/ui/src/main/res/values-da/strings.xml | 1 + library/ui/src/main/res/values-de/strings.xml | 1 + library/ui/src/main/res/values-el/strings.xml | 1 + .../ui/src/main/res/values-en-rAU/strings.xml | 1 + .../ui/src/main/res/values-en-rGB/strings.xml | 1 + .../ui/src/main/res/values-en-rIN/strings.xml | 1 + .../ui/src/main/res/values-es-rUS/strings.xml | 1 + library/ui/src/main/res/values-es/strings.xml | 1 + .../ui/src/main/res/values-et-rEE/strings.xml | 1 + .../ui/src/main/res/values-eu-rES/strings.xml | 1 + library/ui/src/main/res/values-fa/strings.xml | 1 + library/ui/src/main/res/values-fi/strings.xml | 1 + .../ui/src/main/res/values-fr-rCA/strings.xml | 1 + library/ui/src/main/res/values-fr/strings.xml | 1 + .../ui/src/main/res/values-gl-rES/strings.xml | 1 + .../ui/src/main/res/values-gu-rIN/strings.xml | 1 + library/ui/src/main/res/values-hi/strings.xml | 1 + library/ui/src/main/res/values-hr/strings.xml | 1 + library/ui/src/main/res/values-hu/strings.xml | 1 + .../ui/src/main/res/values-hy-rAM/strings.xml | 1 + library/ui/src/main/res/values-in/strings.xml | 1 + .../ui/src/main/res/values-is-rIS/strings.xml | 1 + library/ui/src/main/res/values-it/strings.xml | 1 + library/ui/src/main/res/values-iw/strings.xml | 1 + library/ui/src/main/res/values-ja/strings.xml | 1 + .../ui/src/main/res/values-ka-rGE/strings.xml | 1 + .../ui/src/main/res/values-kk-rKZ/strings.xml | 1 + .../ui/src/main/res/values-km-rKH/strings.xml | 1 + .../ui/src/main/res/values-kn-rIN/strings.xml | 1 + library/ui/src/main/res/values-ko/strings.xml | 1 + .../ui/src/main/res/values-ky-rKG/strings.xml | 1 + .../ui/src/main/res/values-lo-rLA/strings.xml | 1 + library/ui/src/main/res/values-lt/strings.xml | 1 + library/ui/src/main/res/values-lv/strings.xml | 1 + .../ui/src/main/res/values-mk-rMK/strings.xml | 1 + .../ui/src/main/res/values-ml-rIN/strings.xml | 1 + .../ui/src/main/res/values-mn-rMN/strings.xml | 1 + .../ui/src/main/res/values-mr-rIN/strings.xml | 1 + .../ui/src/main/res/values-ms-rMY/strings.xml | 1 + .../ui/src/main/res/values-my-rMM/strings.xml | 1 + library/ui/src/main/res/values-nb/strings.xml | 1 + .../ui/src/main/res/values-ne-rNP/strings.xml | 1 + library/ui/src/main/res/values-nl/strings.xml | 1 + .../ui/src/main/res/values-pa-rIN/strings.xml | 1 + library/ui/src/main/res/values-pl/strings.xml | 1 + .../ui/src/main/res/values-pt-rBR/strings.xml | 1 + .../ui/src/main/res/values-pt-rPT/strings.xml | 1 + library/ui/src/main/res/values-pt/strings.xml | 1 + library/ui/src/main/res/values-ro/strings.xml | 1 + library/ui/src/main/res/values-ru/strings.xml | 1 + .../ui/src/main/res/values-si-rLK/strings.xml | 1 + library/ui/src/main/res/values-sk/strings.xml | 1 + library/ui/src/main/res/values-sl/strings.xml | 1 + .../ui/src/main/res/values-sq-rAL/strings.xml | 1 + library/ui/src/main/res/values-sr/strings.xml | 1 + library/ui/src/main/res/values-sv/strings.xml | 1 + library/ui/src/main/res/values-sw/strings.xml | 1 + .../ui/src/main/res/values-ta-rIN/strings.xml | 1 + .../ui/src/main/res/values-te-rIN/strings.xml | 1 + library/ui/src/main/res/values-th/strings.xml | 1 + library/ui/src/main/res/values-tl/strings.xml | 1 + library/ui/src/main/res/values-tr/strings.xml | 1 + library/ui/src/main/res/values-uk/strings.xml | 1 + .../ui/src/main/res/values-ur-rPK/strings.xml | 1 + .../ui/src/main/res/values-uz-rUZ/strings.xml | 1 + library/ui/src/main/res/values-vi/strings.xml | 1 + .../ui/src/main/res/values-zh-rCN/strings.xml | 1 + .../ui/src/main/res/values-zh-rHK/strings.xml | 1 + .../ui/src/main/res/values-zh-rTW/strings.xml | 1 + library/ui/src/main/res/values-zu/strings.xml | 1 + library/ui/src/main/res/values/ids.xml | 1 + library/ui/src/main/res/values/strings.xml | 1 + library/ui/src/main/res/values/styles.xml | 5 ++++ 91 files changed, 117 insertions(+) create mode 100644 library/ui/src/main/res/drawable-anydpi-v21/exo_controls_shuffle.xml create mode 100644 library/ui/src/main/res/drawable-hdpi/exo_controls_shuffle.png create mode 100644 library/ui/src/main/res/drawable-ldpi/exo_controls_shuffle.png create mode 100644 library/ui/src/main/res/drawable-mdpi/exo_controls_shuffle.png create mode 100644 library/ui/src/main/res/drawable-xhdpi/exo_controls_shuffle.png create mode 100644 library/ui/src/main/res/drawable-xxhdpi/exo_controls_shuffle.png diff --git a/library/ui/src/main/res/drawable-anydpi-v21/exo_controls_shuffle.xml b/library/ui/src/main/res/drawable-anydpi-v21/exo_controls_shuffle.xml new file mode 100644 index 0000000000..28ac6a5786 --- /dev/null +++ b/library/ui/src/main/res/drawable-anydpi-v21/exo_controls_shuffle.xml @@ -0,0 +1,26 @@ + + + + + diff --git a/library/ui/src/main/res/drawable-hdpi/exo_controls_shuffle.png b/library/ui/src/main/res/drawable-hdpi/exo_controls_shuffle.png new file mode 100644 index 0000000000000000000000000000000000000000..52a805aac1b49335b507086aea99d9fb813ef0ba GIT binary patch literal 268 zcmV+n0rUQeP)4mffJ0+6L|cR#qfu^z8=xV=jnM!Na5#kbz~|tV9=-_Q3o3bjQ2(1-tyWW` zm-++mJ!N(zKS}xk0s@mH0RjV~#0UsbLIVI05%~=OfN*&OaKh&|Jv2Fh6Fxu0=ZyeQ z_U> z$6!27$vYyRV56m=AfxF%$&wwCimfr_zwb~E-7#5fO S8wt(;0000XkOu&a%t_Iz(lrVI~F;eUBxBp z#}dj^lPK&c(r>L5WwvzhrpvY;SLX+1e`U;j>OZIN6ZgH%zmNL-vHWy%_7CsEgvZaO ls~x-?zbLiqaX`uxX0MyCcfUOJfFI}@22WQ%mvv4FO#oRXM=1aR literal 0 HcmV?d00001 diff --git a/library/ui/src/main/res/drawable-mdpi/exo_controls_shuffle.png b/library/ui/src/main/res/drawable-mdpi/exo_controls_shuffle.png new file mode 100644 index 0000000000000000000000000000000000000000..0924b2cb69b3d5c8acd3efbfc833d36e1896a9d8 GIT binary patch literal 230 zcmVkdg00023Nkl4l>LI}_?I>7M1LkH*xJIan3AcQcy;RUEq4{rwFJAlpeGt#zQuh;AKs_6{R zKWY@miRNcLHNk5ZeczyYNm0V!`6vMZbdi@O@H>zpa6o{dD+iZ*kTIHAb<7eN7>P~=UZ08S|KCK!+-+;#XpZ$b&+{|_YZ=?G#af#-A6%=6*t z%=77WzVn?g@;YprpLEx>W&TMYU-S9ud13$0{7Dbh&kM6gHea7%g6F(A0OU)8^BjQk z1;Kd^Ao+seyaBO1xZ)*Y0LbTG%|M%-P0 z+t->RuQji1eYfM?ywc;PdLpk^UQS9}xS=4b*2MSRl5(@1)6?V59si$~8GF4Hg6GO_+)>tvRSrW5t|-tKt3gkhe-r+RY^rUVY9 zdI1&(L6?6D5e*7E{x}*iINs>~<0!!3XmRR~hnBtlJePmd zw*CF{%zXdPv-x$OXP^JK?Q{Q}$~{kO4hVkwIcviC>n$~Qr&;)-Pu0(e*B7>Mu&e*s z?>OV9z$x*TiTpEbfXoRe#am_~GaGA4Uv^u5`uQ_GZ1Q`~hc*5j&#MomeLns*pIhK( zos0v+wz>P+9w^V;&z5jLUF|+Io8pA`jLiZkzBf2>JgGjQP_t%cJ|jHT>^BNrb3E|o Sniw!P7(8A5T-G@yGywp@636lY literal 0 HcmV?d00001 diff --git a/library/ui/src/main/res/layout/exo_playback_control_view.xml b/library/ui/src/main/res/layout/exo_playback_control_view.xml index 407329890d..159844c234 100644 --- a/library/ui/src/main/res/layout/exo_playback_control_view.xml +++ b/library/ui/src/main/res/layout/exo_playback_control_view.xml @@ -34,6 +34,9 @@ + + diff --git a/library/ui/src/main/res/values-af/strings.xml b/library/ui/src/main/res/values-af/strings.xml index 103877f1e6..2510552c0c 100644 --- a/library/ui/src/main/res/values-af/strings.xml +++ b/library/ui/src/main/res/values-af/strings.xml @@ -25,4 +25,5 @@ "Herhaal alles" "Herhaal niks" "Herhaal een" + "Skommel" diff --git a/library/ui/src/main/res/values-am/strings.xml b/library/ui/src/main/res/values-am/strings.xml index 356566cb87..165b5eee62 100644 --- a/library/ui/src/main/res/values-am/strings.xml +++ b/library/ui/src/main/res/values-am/strings.xml @@ -25,4 +25,5 @@ "ሁሉንም ድገም" "ምንም አትድገም" "አንዱን ድገም" + "በው" diff --git a/library/ui/src/main/res/values-ar/strings.xml b/library/ui/src/main/res/values-ar/strings.xml index 4bdbda061c..239f01be6b 100644 --- a/library/ui/src/main/res/values-ar/strings.xml +++ b/library/ui/src/main/res/values-ar/strings.xml @@ -25,4 +25,5 @@ "تكرار الكل" "عدم التكرار" "تكرار مقطع واحد" + "ترتيب عشوائي" diff --git a/library/ui/src/main/res/values-az-rAZ/strings.xml b/library/ui/src/main/res/values-az-rAZ/strings.xml index 771335952f..1071cd5542 100644 --- a/library/ui/src/main/res/values-az-rAZ/strings.xml +++ b/library/ui/src/main/res/values-az-rAZ/strings.xml @@ -25,4 +25,5 @@ "Bütün təkrarlayın" "Təkrar bir" "Heç bir təkrar" + "Qarışdır" diff --git a/library/ui/src/main/res/values-b+sr+Latn/strings.xml b/library/ui/src/main/res/values-b+sr+Latn/strings.xml index 7c373b5b55..a9d35e5cb6 100644 --- a/library/ui/src/main/res/values-b+sr+Latn/strings.xml +++ b/library/ui/src/main/res/values-b+sr+Latn/strings.xml @@ -25,4 +25,5 @@ "Ponovi sve" "Ne ponavljaj nijednu" "Ponovi jednu" + "Pusti nasumično" diff --git a/library/ui/src/main/res/values-be-rBY/strings.xml b/library/ui/src/main/res/values-be-rBY/strings.xml index 7790a7887f..69b24ad5e9 100644 --- a/library/ui/src/main/res/values-be-rBY/strings.xml +++ b/library/ui/src/main/res/values-be-rBY/strings.xml @@ -25,4 +25,5 @@ "Паўтарыць усё" "Паўтараць ні" "Паўтарыць адзін" + "Перамяшаць" diff --git a/library/ui/src/main/res/values-bg/strings.xml b/library/ui/src/main/res/values-bg/strings.xml index ce9e3d6943..e350479788 100644 --- a/library/ui/src/main/res/values-bg/strings.xml +++ b/library/ui/src/main/res/values-bg/strings.xml @@ -25,4 +25,5 @@ "Повтаряне на всички" "Без повтаряне" "Повтаряне на един елемент" + "Разбъркване" diff --git a/library/ui/src/main/res/values-bn-rBD/strings.xml b/library/ui/src/main/res/values-bn-rBD/strings.xml index 5f8ebfa98e..446ef982a3 100644 --- a/library/ui/src/main/res/values-bn-rBD/strings.xml +++ b/library/ui/src/main/res/values-bn-rBD/strings.xml @@ -25,4 +25,5 @@ "সবগুলির পুনরাবৃত্তি করুন" "একটিরও পুনরাবৃত্তি করবেন না" "একটির পুনরাবৃত্তি করুন" + "অদলবদল" diff --git a/library/ui/src/main/res/values-bs-rBA/strings.xml b/library/ui/src/main/res/values-bs-rBA/strings.xml index ef47099760..186b1058d9 100644 --- a/library/ui/src/main/res/values-bs-rBA/strings.xml +++ b/library/ui/src/main/res/values-bs-rBA/strings.xml @@ -25,4 +25,5 @@ "Ponovite sve" "Ne ponavljaju" "Ponovite jedan" + "Izmiješaj" diff --git a/library/ui/src/main/res/values-ca/strings.xml b/library/ui/src/main/res/values-ca/strings.xml index a42fe3b9cb..fd76a8e08e 100644 --- a/library/ui/src/main/res/values-ca/strings.xml +++ b/library/ui/src/main/res/values-ca/strings.xml @@ -25,4 +25,5 @@ "Repeteix-ho tot" "No en repeteixis cap" "Repeteix-ne un" + "Reprodueix aleatòriament" diff --git a/library/ui/src/main/res/values-cs/strings.xml b/library/ui/src/main/res/values-cs/strings.xml index 9c1e50ce27..087ab79c25 100644 --- a/library/ui/src/main/res/values-cs/strings.xml +++ b/library/ui/src/main/res/values-cs/strings.xml @@ -25,4 +25,5 @@ "Opakovat vše" "Neopakovat" "Opakovat jednu položku" + "Náhodně" diff --git a/library/ui/src/main/res/values-da/strings.xml b/library/ui/src/main/res/values-da/strings.xml index 3ec132ebb7..0ae23ee288 100644 --- a/library/ui/src/main/res/values-da/strings.xml +++ b/library/ui/src/main/res/values-da/strings.xml @@ -25,4 +25,5 @@ "Gentag alle" "Gentag ingen" "Gentag en" + "Bland" diff --git a/library/ui/src/main/res/values-de/strings.xml b/library/ui/src/main/res/values-de/strings.xml index a1dc749864..37ca6c44ac 100644 --- a/library/ui/src/main/res/values-de/strings.xml +++ b/library/ui/src/main/res/values-de/strings.xml @@ -25,4 +25,5 @@ "Alle wiederholen" "Keinen Titel wiederholen" "Einen Titel wiederholen" + "Zufallsmix" diff --git a/library/ui/src/main/res/values-el/strings.xml b/library/ui/src/main/res/values-el/strings.xml index 845011fe55..534192e185 100644 --- a/library/ui/src/main/res/values-el/strings.xml +++ b/library/ui/src/main/res/values-el/strings.xml @@ -25,4 +25,5 @@ "Επανάληψη όλων" "Καμία επανάληψη" "Επανάληψη ενός στοιχείου" + "Τυχαία αναπαραγωγή" diff --git a/library/ui/src/main/res/values-en-rAU/strings.xml b/library/ui/src/main/res/values-en-rAU/strings.xml index 8a1742c8ca..0b4c465853 100644 --- a/library/ui/src/main/res/values-en-rAU/strings.xml +++ b/library/ui/src/main/res/values-en-rAU/strings.xml @@ -25,4 +25,5 @@ "Repeat all" "Repeat none" "Repeat one" + "Shuffle" diff --git a/library/ui/src/main/res/values-en-rGB/strings.xml b/library/ui/src/main/res/values-en-rGB/strings.xml index 8a1742c8ca..0b4c465853 100644 --- a/library/ui/src/main/res/values-en-rGB/strings.xml +++ b/library/ui/src/main/res/values-en-rGB/strings.xml @@ -25,4 +25,5 @@ "Repeat all" "Repeat none" "Repeat one" + "Shuffle" diff --git a/library/ui/src/main/res/values-en-rIN/strings.xml b/library/ui/src/main/res/values-en-rIN/strings.xml index 8a1742c8ca..0b4c465853 100644 --- a/library/ui/src/main/res/values-en-rIN/strings.xml +++ b/library/ui/src/main/res/values-en-rIN/strings.xml @@ -25,4 +25,5 @@ "Repeat all" "Repeat none" "Repeat one" + "Shuffle" diff --git a/library/ui/src/main/res/values-es-rUS/strings.xml b/library/ui/src/main/res/values-es-rUS/strings.xml index f2ec848fb6..e6cf3fc6f2 100644 --- a/library/ui/src/main/res/values-es-rUS/strings.xml +++ b/library/ui/src/main/res/values-es-rUS/strings.xml @@ -25,4 +25,5 @@ "Repetir todo" "No repetir" "Repetir uno" + "Reproducir aleatoriamente" diff --git a/library/ui/src/main/res/values-es/strings.xml b/library/ui/src/main/res/values-es/strings.xml index 116f064223..04e1ea038c 100644 --- a/library/ui/src/main/res/values-es/strings.xml +++ b/library/ui/src/main/res/values-es/strings.xml @@ -25,4 +25,5 @@ "Repetir todo" "No repetir" "Repetir uno" + "Reproducción aleatoria" diff --git a/library/ui/src/main/res/values-et-rEE/strings.xml b/library/ui/src/main/res/values-et-rEE/strings.xml index 153611ece4..004ec7e6c3 100644 --- a/library/ui/src/main/res/values-et-rEE/strings.xml +++ b/library/ui/src/main/res/values-et-rEE/strings.xml @@ -25,4 +25,5 @@ "Korda kõike" "Ära korda midagi" "Korda ühte" + "Esita juhuslikus järjekorras" diff --git a/library/ui/src/main/res/values-eu-rES/strings.xml b/library/ui/src/main/res/values-eu-rES/strings.xml index 1128572d9a..6a3345303a 100644 --- a/library/ui/src/main/res/values-eu-rES/strings.xml +++ b/library/ui/src/main/res/values-eu-rES/strings.xml @@ -25,4 +25,5 @@ "Errepikatu guztiak" "Ez errepikatu" "Errepikatu bat" + "Erreproduzitu ausaz" diff --git a/library/ui/src/main/res/values-fa/strings.xml b/library/ui/src/main/res/values-fa/strings.xml index d6be77323b..101fcdbfb5 100644 --- a/library/ui/src/main/res/values-fa/strings.xml +++ b/library/ui/src/main/res/values-fa/strings.xml @@ -25,4 +25,5 @@ "تکرار همه" "تکرار هیچ‌کدام" "یک‌بار تکرار" + "پخش درهم" diff --git a/library/ui/src/main/res/values-fi/strings.xml b/library/ui/src/main/res/values-fi/strings.xml index 10e4b0bbe3..92feb86683 100644 --- a/library/ui/src/main/res/values-fi/strings.xml +++ b/library/ui/src/main/res/values-fi/strings.xml @@ -25,4 +25,5 @@ "Toista kaikki" "Toista ei mitään" "Toista yksi" + "Toista satunnaisesti" diff --git a/library/ui/src/main/res/values-fr-rCA/strings.xml b/library/ui/src/main/res/values-fr-rCA/strings.xml index d8852b5d3f..45fc0a86f9 100644 --- a/library/ui/src/main/res/values-fr-rCA/strings.xml +++ b/library/ui/src/main/res/values-fr-rCA/strings.xml @@ -25,4 +25,5 @@ "Tout lire en boucle" "Aucune répétition" "Répéter un élément" + "Lecture aléatoire" diff --git a/library/ui/src/main/res/values-fr/strings.xml b/library/ui/src/main/res/values-fr/strings.xml index acf3670fa4..82b5a40626 100644 --- a/library/ui/src/main/res/values-fr/strings.xml +++ b/library/ui/src/main/res/values-fr/strings.xml @@ -25,4 +25,5 @@ "Tout lire en boucle" "Ne rien lire en boucle" "Lire en boucle un élément" + "Lire en mode aléatoire" diff --git a/library/ui/src/main/res/values-gl-rES/strings.xml b/library/ui/src/main/res/values-gl-rES/strings.xml index 81b854cafe..7062d8d023 100644 --- a/library/ui/src/main/res/values-gl-rES/strings.xml +++ b/library/ui/src/main/res/values-gl-rES/strings.xml @@ -25,4 +25,5 @@ "Repetir todo" "Non repetir" "Repetir un" + "Reprodución aleatoria" diff --git a/library/ui/src/main/res/values-gu-rIN/strings.xml b/library/ui/src/main/res/values-gu-rIN/strings.xml index 6d51c29f97..ed78b1ee30 100644 --- a/library/ui/src/main/res/values-gu-rIN/strings.xml +++ b/library/ui/src/main/res/values-gu-rIN/strings.xml @@ -25,4 +25,5 @@ "બધા પુનરાવર્તન કરો" "કંઈ પુનરાવર્તન કરો" "એક પુનરાવર્તન કરો" + "શફલ કરો" diff --git a/library/ui/src/main/res/values-hi/strings.xml b/library/ui/src/main/res/values-hi/strings.xml index eadb0519df..ec624b1f35 100644 --- a/library/ui/src/main/res/values-hi/strings.xml +++ b/library/ui/src/main/res/values-hi/strings.xml @@ -25,4 +25,5 @@ "सभी को दोहराएं" "कुछ भी न दोहराएं" "एक दोहराएं" + "शफ़ल करें" diff --git a/library/ui/src/main/res/values-hr/strings.xml b/library/ui/src/main/res/values-hr/strings.xml index cb49965640..7cb23e11dd 100644 --- a/library/ui/src/main/res/values-hr/strings.xml +++ b/library/ui/src/main/res/values-hr/strings.xml @@ -25,4 +25,5 @@ "Ponovi sve" "Bez ponavljanja" "Ponovi jedno" + "Reproduciraj nasumično" diff --git a/library/ui/src/main/res/values-hu/strings.xml b/library/ui/src/main/res/values-hu/strings.xml index 43ac8f51ff..cf3d34c88f 100644 --- a/library/ui/src/main/res/values-hu/strings.xml +++ b/library/ui/src/main/res/values-hu/strings.xml @@ -25,4 +25,5 @@ "Összes ismétlése" "Nincs ismétlés" "Egy ismétlése" + "Véletlenszerű lejátszás" diff --git a/library/ui/src/main/res/values-hy-rAM/strings.xml b/library/ui/src/main/res/values-hy-rAM/strings.xml index 3b09f9a507..13a489baf5 100644 --- a/library/ui/src/main/res/values-hy-rAM/strings.xml +++ b/library/ui/src/main/res/values-hy-rAM/strings.xml @@ -25,4 +25,5 @@ "կրկնել այն ամենը" "Չկրկնել" "Կրկնել մեկը" + "Խառնել" diff --git a/library/ui/src/main/res/values-in/strings.xml b/library/ui/src/main/res/values-in/strings.xml index 928be5945a..09b05815e6 100644 --- a/library/ui/src/main/res/values-in/strings.xml +++ b/library/ui/src/main/res/values-in/strings.xml @@ -25,4 +25,5 @@ "Ulangi Semua" "Jangan Ulangi" "Ulangi Satu" + "Acak" diff --git a/library/ui/src/main/res/values-is-rIS/strings.xml b/library/ui/src/main/res/values-is-rIS/strings.xml index 75be2aeb17..12c4632cdf 100644 --- a/library/ui/src/main/res/values-is-rIS/strings.xml +++ b/library/ui/src/main/res/values-is-rIS/strings.xml @@ -25,4 +25,5 @@ "Endurtaka allt" "Endurtaka ekkert" "Endurtaka eitt" + "Stokka" diff --git a/library/ui/src/main/res/values-it/strings.xml b/library/ui/src/main/res/values-it/strings.xml index 59117a6b75..aea20db82e 100644 --- a/library/ui/src/main/res/values-it/strings.xml +++ b/library/ui/src/main/res/values-it/strings.xml @@ -25,4 +25,5 @@ "Ripeti tutti" "Non ripetere nessuno" "Ripeti uno" + "Riproduci casualmente" diff --git a/library/ui/src/main/res/values-iw/strings.xml b/library/ui/src/main/res/values-iw/strings.xml index 347b137cf2..dd973af50b 100644 --- a/library/ui/src/main/res/values-iw/strings.xml +++ b/library/ui/src/main/res/values-iw/strings.xml @@ -25,4 +25,5 @@ "חזור על הכל" "אל תחזור על כלום" "חזור על פריט אחד" + "ערבב" diff --git a/library/ui/src/main/res/values-ja/strings.xml b/library/ui/src/main/res/values-ja/strings.xml index cf2cc49b67..d6ce751d5c 100644 --- a/library/ui/src/main/res/values-ja/strings.xml +++ b/library/ui/src/main/res/values-ja/strings.xml @@ -25,4 +25,5 @@ "全曲を繰り返し" "繰り返しなし" "1曲を繰り返し" + "シャッフル" diff --git a/library/ui/src/main/res/values-ka-rGE/strings.xml b/library/ui/src/main/res/values-ka-rGE/strings.xml index 75da8dde18..252e52f151 100644 --- a/library/ui/src/main/res/values-ka-rGE/strings.xml +++ b/library/ui/src/main/res/values-ka-rGE/strings.xml @@ -25,4 +25,5 @@ "გამეორება ყველა" "გაიმეორეთ არცერთი" "გაიმეორეთ ერთი" + "არეულად დაკვრა" diff --git a/library/ui/src/main/res/values-kk-rKZ/strings.xml b/library/ui/src/main/res/values-kk-rKZ/strings.xml index b1ab22ecf6..43eb3dd030 100644 --- a/library/ui/src/main/res/values-kk-rKZ/strings.xml +++ b/library/ui/src/main/res/values-kk-rKZ/strings.xml @@ -25,4 +25,5 @@ "Барлығын қайталау" "Ешқайсысын қайталамау" "Біреуін қайталау" + "Кездейсоқ ретпен ойнату" diff --git a/library/ui/src/main/res/values-km-rKH/strings.xml b/library/ui/src/main/res/values-km-rKH/strings.xml index dfd9f7d863..653c9f051d 100644 --- a/library/ui/src/main/res/values-km-rKH/strings.xml +++ b/library/ui/src/main/res/values-km-rKH/strings.xml @@ -25,4 +25,5 @@ "ធ្វើ​ម្ដង​ទៀត​ទាំងអស់" "មិន​ធ្វើ​ឡើង​វិញ" "ធ្វើ​​ឡើងវិញ​ម្ដង" + "ច្របល់" diff --git a/library/ui/src/main/res/values-kn-rIN/strings.xml b/library/ui/src/main/res/values-kn-rIN/strings.xml index 868af17a65..7368fc8ad3 100644 --- a/library/ui/src/main/res/values-kn-rIN/strings.xml +++ b/library/ui/src/main/res/values-kn-rIN/strings.xml @@ -25,4 +25,5 @@ "ಎಲ್ಲವನ್ನು ಪುನರಾವರ್ತಿಸಿ" "ಯಾವುದನ್ನೂ ಪುನರಾವರ್ತಿಸಬೇಡಿ" "ಒಂದನ್ನು ಪುನರಾವರ್ತಿಸಿ" + "ಬೆರೆಸು" diff --git a/library/ui/src/main/res/values-ko/strings.xml b/library/ui/src/main/res/values-ko/strings.xml index 89636ac8a0..99d4a2f9a4 100644 --- a/library/ui/src/main/res/values-ko/strings.xml +++ b/library/ui/src/main/res/values-ko/strings.xml @@ -25,4 +25,5 @@ "전체 반복" "반복 안함" "한 항목 반복" + "셔플" diff --git a/library/ui/src/main/res/values-ky-rKG/strings.xml b/library/ui/src/main/res/values-ky-rKG/strings.xml index 15fd50468a..9b903a124e 100644 --- a/library/ui/src/main/res/values-ky-rKG/strings.xml +++ b/library/ui/src/main/res/values-ky-rKG/strings.xml @@ -25,4 +25,5 @@ "Баарын кайталоо" "Эч бирин кайталабоо" "Бирөөнү кайталоо" + "Аралаштыруу" diff --git a/library/ui/src/main/res/values-lo-rLA/strings.xml b/library/ui/src/main/res/values-lo-rLA/strings.xml index 405d0c64fe..702cd54396 100644 --- a/library/ui/src/main/res/values-lo-rLA/strings.xml +++ b/library/ui/src/main/res/values-lo-rLA/strings.xml @@ -25,4 +25,5 @@ "ຫຼິ້ນ​ຊ້ຳ​ທັງ​ໝົດ" "​ບໍ່ຫຼິ້ນ​ຊ້ຳ" "ຫຼິ້ນ​ຊ້ຳ" + "ຫຼີ້ນແບບສຸ່ມ" diff --git a/library/ui/src/main/res/values-lt/strings.xml b/library/ui/src/main/res/values-lt/strings.xml index bd7d4142fc..d6073f42e3 100644 --- a/library/ui/src/main/res/values-lt/strings.xml +++ b/library/ui/src/main/res/values-lt/strings.xml @@ -25,4 +25,5 @@ "Kartoti viską" "Nekartoti nieko" "Kartoti vieną" + "Maišyti" diff --git a/library/ui/src/main/res/values-lv/strings.xml b/library/ui/src/main/res/values-lv/strings.xml index c2ebc70cbd..64393d679a 100644 --- a/library/ui/src/main/res/values-lv/strings.xml +++ b/library/ui/src/main/res/values-lv/strings.xml @@ -25,4 +25,5 @@ "Atkārtot visu" "Neatkārtot nevienu" "Atkārtot vienu" + "Atskaņot jauktā secībā" diff --git a/library/ui/src/main/res/values-mk-rMK/strings.xml b/library/ui/src/main/res/values-mk-rMK/strings.xml index 14ce7111a4..60858df8b1 100644 --- a/library/ui/src/main/res/values-mk-rMK/strings.xml +++ b/library/ui/src/main/res/values-mk-rMK/strings.xml @@ -25,4 +25,5 @@ "Повтори ги сите" "Не повторувај ниту една" "Повтори една" + "По случаен избор" diff --git a/library/ui/src/main/res/values-ml-rIN/strings.xml b/library/ui/src/main/res/values-ml-rIN/strings.xml index 17fe7a1655..4e5eddb93e 100644 --- a/library/ui/src/main/res/values-ml-rIN/strings.xml +++ b/library/ui/src/main/res/values-ml-rIN/strings.xml @@ -25,4 +25,5 @@ "എല്ലാം ആവർത്തിക്കുക" "ഒന്നും ആവർത്തിക്കരുത്" "ഒന്ന് ആവർത്തിക്കുക" + "ഷഫിൾ ചെയ്യുക" diff --git a/library/ui/src/main/res/values-mn-rMN/strings.xml b/library/ui/src/main/res/values-mn-rMN/strings.xml index bf9a7e03bf..4ab26a7f62 100644 --- a/library/ui/src/main/res/values-mn-rMN/strings.xml +++ b/library/ui/src/main/res/values-mn-rMN/strings.xml @@ -25,4 +25,5 @@ "Бүгдийг давтах" "Алийг нь ч давтахгүй" "Нэгийг давтах" + "Холих" diff --git a/library/ui/src/main/res/values-mr-rIN/strings.xml b/library/ui/src/main/res/values-mr-rIN/strings.xml index df4ac9de6b..7869355b59 100644 --- a/library/ui/src/main/res/values-mr-rIN/strings.xml +++ b/library/ui/src/main/res/values-mr-rIN/strings.xml @@ -25,4 +25,5 @@ "सर्व पुनरावृत्ती करा" "काहीही पुनरावृत्ती करू नका" "एक पुनरावृत्ती करा" + "शफल करा" diff --git a/library/ui/src/main/res/values-ms-rMY/strings.xml b/library/ui/src/main/res/values-ms-rMY/strings.xml index 33dfcb40f0..fdde3de079 100644 --- a/library/ui/src/main/res/values-ms-rMY/strings.xml +++ b/library/ui/src/main/res/values-ms-rMY/strings.xml @@ -25,4 +25,5 @@ "Ulang semua" "Tiada ulangan" "Ulangan" + "Rombak" diff --git a/library/ui/src/main/res/values-my-rMM/strings.xml b/library/ui/src/main/res/values-my-rMM/strings.xml index b4ea5b1155..3d7918d953 100644 --- a/library/ui/src/main/res/values-my-rMM/strings.xml +++ b/library/ui/src/main/res/values-my-rMM/strings.xml @@ -25,4 +25,5 @@ "အားလုံး ထပ်တလဲလဲဖွင့်ရန်" "ထပ်တလဲလဲမဖွင့်ရန်" "တစ်ခုအား ထပ်တလဲလဲဖွင့်ရန်" + "မွှေနှောက်ဖွင့်ရန်" diff --git a/library/ui/src/main/res/values-nb/strings.xml b/library/ui/src/main/res/values-nb/strings.xml index 679bf1134c..370c759b84 100644 --- a/library/ui/src/main/res/values-nb/strings.xml +++ b/library/ui/src/main/res/values-nb/strings.xml @@ -25,4 +25,5 @@ "Gjenta alle" "Ikke gjenta noen" "Gjenta én" + "Spill av i tilfeldig rekkefølge" diff --git a/library/ui/src/main/res/values-ne-rNP/strings.xml b/library/ui/src/main/res/values-ne-rNP/strings.xml index 43730c1880..19f43d0392 100644 --- a/library/ui/src/main/res/values-ne-rNP/strings.xml +++ b/library/ui/src/main/res/values-ne-rNP/strings.xml @@ -25,4 +25,5 @@ "सबै दोहोर्याउनुहोस्" "कुनै पनि नदोहोर्याउनुहोस्" "एउटा दोहोर्याउनुहोस्" + "मिसाउनुहोस्" diff --git a/library/ui/src/main/res/values-nl/strings.xml b/library/ui/src/main/res/values-nl/strings.xml index 6383c977fc..a67ab2968c 100644 --- a/library/ui/src/main/res/values-nl/strings.xml +++ b/library/ui/src/main/res/values-nl/strings.xml @@ -25,4 +25,5 @@ "Alles herhalen" "Niet herhalen" "Eén herhalen" + "Shuffle" diff --git a/library/ui/src/main/res/values-pa-rIN/strings.xml b/library/ui/src/main/res/values-pa-rIN/strings.xml index ddf60b0394..6250b90514 100644 --- a/library/ui/src/main/res/values-pa-rIN/strings.xml +++ b/library/ui/src/main/res/values-pa-rIN/strings.xml @@ -25,4 +25,5 @@ "ਸਭ ਨੂੰ ਦੁਹਰਾਓ" "ਕੋਈ ਵੀ ਨਹੀਂ ਦੁਹਰਾਓ" "ਇੱਕ ਦੁਹਰਾਓ" + "ਸ਼ੱਫਲ" diff --git a/library/ui/src/main/res/values-pl/strings.xml b/library/ui/src/main/res/values-pl/strings.xml index 113c568f85..ff1d77fdd5 100644 --- a/library/ui/src/main/res/values-pl/strings.xml +++ b/library/ui/src/main/res/values-pl/strings.xml @@ -25,4 +25,5 @@ "Powtórz wszystkie" "Nie powtarzaj" "Powtórz jeden" + "Odtwarzaj losowo" diff --git a/library/ui/src/main/res/values-pt-rBR/strings.xml b/library/ui/src/main/res/values-pt-rBR/strings.xml index 87c54358ba..86a91b0677 100644 --- a/library/ui/src/main/res/values-pt-rBR/strings.xml +++ b/library/ui/src/main/res/values-pt-rBR/strings.xml @@ -25,4 +25,5 @@ "Repetir tudo" "Não repetir" "Repetir um" + "Reproduzir aleatoriamente" diff --git a/library/ui/src/main/res/values-pt-rPT/strings.xml b/library/ui/src/main/res/values-pt-rPT/strings.xml index ca34afec3c..5a7144e36b 100644 --- a/library/ui/src/main/res/values-pt-rPT/strings.xml +++ b/library/ui/src/main/res/values-pt-rPT/strings.xml @@ -25,4 +25,5 @@ "Repetir tudo" "Não repetir" "Repetir um" + "Reproduzir aleatoriamente" diff --git a/library/ui/src/main/res/values-pt/strings.xml b/library/ui/src/main/res/values-pt/strings.xml index 2fc3191738..8441e4e1cc 100644 --- a/library/ui/src/main/res/values-pt/strings.xml +++ b/library/ui/src/main/res/values-pt/strings.xml @@ -25,4 +25,5 @@ "Repetir tudo" "Não repetir" "Repetir uma" + "Reproduzir aleatoriamente" diff --git a/library/ui/src/main/res/values-ro/strings.xml b/library/ui/src/main/res/values-ro/strings.xml index 0b2ce540f7..6b8644e30a 100644 --- a/library/ui/src/main/res/values-ro/strings.xml +++ b/library/ui/src/main/res/values-ro/strings.xml @@ -25,4 +25,5 @@ "Repetați toate" "Repetați niciuna" "Repetați unul" + "Redați aleatoriu" diff --git a/library/ui/src/main/res/values-ru/strings.xml b/library/ui/src/main/res/values-ru/strings.xml index 1d179e028c..51d11d6371 100644 --- a/library/ui/src/main/res/values-ru/strings.xml +++ b/library/ui/src/main/res/values-ru/strings.xml @@ -25,4 +25,5 @@ "Повторять все" "Не повторять" "Повторять один элемент" + "Перемешать" diff --git a/library/ui/src/main/res/values-si-rLK/strings.xml b/library/ui/src/main/res/values-si-rLK/strings.xml index bc37d98eed..eb8453b156 100644 --- a/library/ui/src/main/res/values-si-rLK/strings.xml +++ b/library/ui/src/main/res/values-si-rLK/strings.xml @@ -25,4 +25,5 @@ "සියලු නැවත" "කිසිවක් නැවත" "නැවත නැවත එක්" + "කලවම් කරන්න" diff --git a/library/ui/src/main/res/values-sk/strings.xml b/library/ui/src/main/res/values-sk/strings.xml index a6ea26bdf0..2428dbdcce 100644 --- a/library/ui/src/main/res/values-sk/strings.xml +++ b/library/ui/src/main/res/values-sk/strings.xml @@ -25,4 +25,5 @@ "Opakovať všetko" "Neopakovať" "Opakovať jednu položku" + "Náhodne prehrávať" diff --git a/library/ui/src/main/res/values-sl/strings.xml b/library/ui/src/main/res/values-sl/strings.xml index 39813fa385..8ed731b0d3 100644 --- a/library/ui/src/main/res/values-sl/strings.xml +++ b/library/ui/src/main/res/values-sl/strings.xml @@ -25,4 +25,5 @@ "Ponovi vse" "Ne ponovi" "Ponovi eno" + "Naključno predvajaj" diff --git a/library/ui/src/main/res/values-sq-rAL/strings.xml b/library/ui/src/main/res/values-sq-rAL/strings.xml index 0bdc2e5f84..e2d209e10b 100644 --- a/library/ui/src/main/res/values-sq-rAL/strings.xml +++ b/library/ui/src/main/res/values-sq-rAL/strings.xml @@ -25,4 +25,5 @@ "Përsërit të gjithë" "Përsëritni asnjë" "Përsëritni një" + "Përziej" diff --git a/library/ui/src/main/res/values-sr/strings.xml b/library/ui/src/main/res/values-sr/strings.xml index 0d54de5f6a..8e43a03079 100644 --- a/library/ui/src/main/res/values-sr/strings.xml +++ b/library/ui/src/main/res/values-sr/strings.xml @@ -25,4 +25,5 @@ "Понови све" "Понављање је искључено" "Понови једну" + "Пусти насумично" diff --git a/library/ui/src/main/res/values-sv/strings.xml b/library/ui/src/main/res/values-sv/strings.xml index 0f7f16f91d..5ff1100632 100644 --- a/library/ui/src/main/res/values-sv/strings.xml +++ b/library/ui/src/main/res/values-sv/strings.xml @@ -25,4 +25,5 @@ "Upprepa alla" "Upprepa inga" "Upprepa en" + "Blanda" diff --git a/library/ui/src/main/res/values-sw/strings.xml b/library/ui/src/main/res/values-sw/strings.xml index b48af88659..d1d5978f9c 100644 --- a/library/ui/src/main/res/values-sw/strings.xml +++ b/library/ui/src/main/res/values-sw/strings.xml @@ -25,4 +25,5 @@ "Rudia zote" "Usirudie Yoyote" "Rudia Moja" + "Changanya" diff --git a/library/ui/src/main/res/values-ta-rIN/strings.xml b/library/ui/src/main/res/values-ta-rIN/strings.xml index 3dd64f52f7..43a925aa2e 100644 --- a/library/ui/src/main/res/values-ta-rIN/strings.xml +++ b/library/ui/src/main/res/values-ta-rIN/strings.xml @@ -25,4 +25,5 @@ "அனைத்தையும் மீண்டும் இயக்கு" "எதையும் மீண்டும் இயக்காதே" "ஒன்றை மட்டும் மீண்டும் இயக்கு" + "குலை" diff --git a/library/ui/src/main/res/values-te-rIN/strings.xml b/library/ui/src/main/res/values-te-rIN/strings.xml index daf337a931..8541a44553 100644 --- a/library/ui/src/main/res/values-te-rIN/strings.xml +++ b/library/ui/src/main/res/values-te-rIN/strings.xml @@ -25,4 +25,5 @@ "అన్నీ పునరావృతం చేయి" "ఏదీ పునరావృతం చేయవద్దు" "ఒకదాన్ని పునరావృతం చేయి" + "షఫుల్ చేయి" diff --git a/library/ui/src/main/res/values-th/strings.xml b/library/ui/src/main/res/values-th/strings.xml index ff89b8d5f5..cd97712b67 100644 --- a/library/ui/src/main/res/values-th/strings.xml +++ b/library/ui/src/main/res/values-th/strings.xml @@ -25,4 +25,5 @@ "เล่นซ้ำทั้งหมด" "ไม่เล่นซ้ำ" "เล่นซ้ำรายการเดียว" + "สุ่มเพลง" diff --git a/library/ui/src/main/res/values-tl/strings.xml b/library/ui/src/main/res/values-tl/strings.xml index 89cf2ef400..e8cb87acdd 100644 --- a/library/ui/src/main/res/values-tl/strings.xml +++ b/library/ui/src/main/res/values-tl/strings.xml @@ -25,4 +25,5 @@ "Ulitin Lahat" "Walang Uulitin" "Ulitin ang Isa" + "I-shuffle" diff --git a/library/ui/src/main/res/values-tr/strings.xml b/library/ui/src/main/res/values-tr/strings.xml index 87dba7204c..cd1bfc5444 100644 --- a/library/ui/src/main/res/values-tr/strings.xml +++ b/library/ui/src/main/res/values-tr/strings.xml @@ -25,4 +25,5 @@ "Tümünü Tekrarla" "Hiçbirini Tekrarlama" "Birini Tekrarla" + "Karıştır" diff --git a/library/ui/src/main/res/values-uk/strings.xml b/library/ui/src/main/res/values-uk/strings.xml index 1fdfe2bce5..1b0278ae94 100644 --- a/library/ui/src/main/res/values-uk/strings.xml +++ b/library/ui/src/main/res/values-uk/strings.xml @@ -25,4 +25,5 @@ "Повторити все" "Не повторювати" "Повторити один елемент" + "Перемішати" diff --git a/library/ui/src/main/res/values-ur-rPK/strings.xml b/library/ui/src/main/res/values-ur-rPK/strings.xml index 956374b26a..f253e56c00 100644 --- a/library/ui/src/main/res/values-ur-rPK/strings.xml +++ b/library/ui/src/main/res/values-ur-rPK/strings.xml @@ -25,4 +25,5 @@ "سبھی کو دہرائیں" "کسی کو نہ دہرائیں" "ایک کو دہرائیں" + "شفل کریں" diff --git a/library/ui/src/main/res/values-uz-rUZ/strings.xml b/library/ui/src/main/res/values-uz-rUZ/strings.xml index 286d4d01ab..a322690b2d 100644 --- a/library/ui/src/main/res/values-uz-rUZ/strings.xml +++ b/library/ui/src/main/res/values-uz-rUZ/strings.xml @@ -25,4 +25,5 @@ "Barchasini takrorlash" "Takrorlamaslik" "Bir marta takrorlash" + "Tasodifiy tartibda" diff --git a/library/ui/src/main/res/values-vi/strings.xml b/library/ui/src/main/res/values-vi/strings.xml index 4dea58d494..cff19eca7e 100644 --- a/library/ui/src/main/res/values-vi/strings.xml +++ b/library/ui/src/main/res/values-vi/strings.xml @@ -25,4 +25,5 @@ "Lặp lại tất cả" "Không lặp lại" "Lặp lại một mục" + "Trộn bài" diff --git a/library/ui/src/main/res/values-zh-rCN/strings.xml b/library/ui/src/main/res/values-zh-rCN/strings.xml index e15d84e777..cf3fe5e88b 100644 --- a/library/ui/src/main/res/values-zh-rCN/strings.xml +++ b/library/ui/src/main/res/values-zh-rCN/strings.xml @@ -25,4 +25,5 @@ "重复播放全部" "不重复播放" "重复播放单个视频" + "随机播放" diff --git a/library/ui/src/main/res/values-zh-rHK/strings.xml b/library/ui/src/main/res/values-zh-rHK/strings.xml index ba793e98a8..78fe4ad995 100644 --- a/library/ui/src/main/res/values-zh-rHK/strings.xml +++ b/library/ui/src/main/res/values-zh-rHK/strings.xml @@ -25,4 +25,5 @@ "重複播放所有媒體項目" "不重複播放任何媒體項目" "重複播放一個媒體項目" + "隨機播放" diff --git a/library/ui/src/main/res/values-zh-rTW/strings.xml b/library/ui/src/main/res/values-zh-rTW/strings.xml index bf3364d5cf..3632742904 100644 --- a/library/ui/src/main/res/values-zh-rTW/strings.xml +++ b/library/ui/src/main/res/values-zh-rTW/strings.xml @@ -25,4 +25,5 @@ "重複播放所有媒體項目" "不重複播放" "重複播放單一媒體項目" + "隨機播放" diff --git a/library/ui/src/main/res/values-zu/strings.xml b/library/ui/src/main/res/values-zu/strings.xml index d7bebaaa2a..42dd59c97f 100644 --- a/library/ui/src/main/res/values-zu/strings.xml +++ b/library/ui/src/main/res/values-zu/strings.xml @@ -25,4 +25,5 @@ "Phinda konke" "Ungaphindi lutho" "Phida okukodwa" + "Shova" diff --git a/library/ui/src/main/res/values/ids.xml b/library/ui/src/main/res/values/ids.xml index b16b1729da..b90d2329b3 100644 --- a/library/ui/src/main/res/values/ids.xml +++ b/library/ui/src/main/res/values/ids.xml @@ -28,6 +28,7 @@ + diff --git a/library/ui/src/main/res/values/strings.xml b/library/ui/src/main/res/values/strings.xml index c5d11eeadb..ee8cd78be7 100644 --- a/library/ui/src/main/res/values/strings.xml +++ b/library/ui/src/main/res/values/strings.xml @@ -24,4 +24,5 @@ Repeat none Repeat one Repeat all + Shuffle diff --git a/library/ui/src/main/res/values/styles.xml b/library/ui/src/main/res/values/styles.xml index a67cffe420..4ef8971ccd 100644 --- a/library/ui/src/main/res/values/styles.xml +++ b/library/ui/src/main/res/values/styles.xml @@ -51,4 +51,9 @@ @string/exo_controls_pause_description + + From 1109bf50dd9cb9ed5108cf325559e139364e9872 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 3 Aug 2017 08:26:44 -0700 Subject: [PATCH 0179/2472] Bump version ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164126494 --- RELEASENOTES.md | 2 ++ constants.gradle | 2 +- .../com/google/android/exoplayer2/ExoPlayerLibraryInfo.java | 4 ++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 379b84b4e7..e96cd9ddab 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -52,6 +52,8 @@ ([#2891](https://github.com/google/ExoPlayer/issues/2891)). * Cronet extension: Support for a user-defined fallback if Cronet library is not present. +* Fix buffer too small IllegalStateException issue affecting some composite + media playbacks ([#2900](https://github.com/google/ExoPlayer/issues/2900)). * Misc bugfixes. ### r2.4.4 ### diff --git a/constants.gradle b/constants.gradle index 0db74945c4..93284fd897 100644 --- a/constants.gradle +++ b/constants.gradle @@ -24,7 +24,7 @@ project.ext { supportLibraryVersion = '25.4.0' dexmakerVersion = '1.2' mockitoVersion = '1.9.5' - releaseVersion = 'r2.5.0-beta2' + releaseVersion = 'r2.5.0-beta3' modulePrefix = ':' if (gradle.ext.has('exoplayerModulePrefix')) { modulePrefix += gradle.ext.exoplayerModulePrefix diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java index c04a777e14..153c6cda9c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java @@ -31,13 +31,13 @@ public final class ExoPlayerLibraryInfo { * The version of the library expressed as a string, for example "1.2.3". */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. - public static final String VERSION = "2.5.0-beta2"; + public static final String VERSION = "2.5.0-beta3"; /** * The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final String VERSION_SLASHY = "ExoPlayerLib/2.5.0-beta2"; + public static final String VERSION_SLASHY = "ExoPlayerLib/2.5.0-beta3"; /** * The version of the library expressed as an integer, for example 1002003. From 95dd590016d064a47f142159afa70286ba63ec4c Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 3 Aug 2017 10:06:28 -0700 Subject: [PATCH 0180/2472] Remove dead sample link Issue: #3135 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164138761 --- demos/main/src/main/assets/media.exolist.json | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/demos/main/src/main/assets/media.exolist.json b/demos/main/src/main/assets/media.exolist.json index e0110df80b..59d8259d37 100644 --- a/demos/main/src/main/assets/media.exolist.json +++ b/demos/main/src/main/assets/media.exolist.json @@ -325,18 +325,6 @@ } ] }, - { - "name": "ClearKey DASH", - "samples": [ - { - "name": "Big Buck Bunny (CENC ClearKey)", - "uri": "http://html5.cablelabs.com:8100/cenc/ck/dash.mpd", - "extension": "mpd", - "drm_scheme": "cenc", - "drm_license_url": "https://wasabeef.jp/demos/cenc-ck-dash.json" - } - ] - }, { "name": "SmoothStreaming", "samples": [ From b664e92bb4084af281d8ab116bcd4cb3a7671030 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 3 Aug 2017 10:18:54 -0700 Subject: [PATCH 0181/2472] Remove shuffle button since it doesn't work yet ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164141326 --- library/ui/src/main/res/layout/exo_playback_control_view.xml | 3 --- 1 file changed, 3 deletions(-) diff --git a/library/ui/src/main/res/layout/exo_playback_control_view.xml b/library/ui/src/main/res/layout/exo_playback_control_view.xml index 159844c234..407329890d 100644 --- a/library/ui/src/main/res/layout/exo_playback_control_view.xml +++ b/library/ui/src/main/res/layout/exo_playback_control_view.xml @@ -34,9 +34,6 @@ - - From 40f3f1cb784343657057bee8c212ba9e508e270b Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 4 Aug 2017 00:57:05 -0700 Subject: [PATCH 0182/2472] Decrease number of Vpx input buffers I think they're excessively sized also, but changing that is a little more risky. And we should look at investigating the input buffer size for all our decoder extensions, rather than just this one. Issue: #3120 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164234087 --- .../ext/ffmpeg/FfmpegAudioRenderer.java | 7 +++++++ .../ext/vp9/LibvpxVideoRenderer.java | 18 +++++++++++++----- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java index 8d75ca3dbb..453a18476e 100644 --- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java +++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java @@ -30,7 +30,14 @@ import com.google.android.exoplayer2.util.MimeTypes; */ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer { + /** + * The number of input and output buffers. + */ private static final int NUM_BUFFERS = 16; + /** + * The initial input buffer size. Input buffers are reallocated dynamically if this value is + * insufficient. + */ private static final int INITIAL_INPUT_BUFFER_SIZE = 960 * 6; private FfmpegDecoder decoder; 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 9b0355a9e7..a947378de5 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 @@ -77,11 +77,18 @@ public final class LibvpxVideoRenderer extends BaseRenderer { public static final int MSG_SET_OUTPUT_BUFFER_RENDERER = C.MSG_CUSTOM_BASE; /** - * The number of input buffers and the number of output buffers. The renderer may limit the - * minimum possible value due to requiring multiple output buffers to be dequeued at a time for it - * to make progress. + * The number of input buffers. + */ + private static final int NUM_INPUT_BUFFERS = 8; + /** + * The number of output buffers. The renderer may limit the minimum possible value due to + * requiring multiple output buffers to be dequeued at a time for it to make progress. + */ + private static final int NUM_OUTPUT_BUFFERS = 16; + /** + * The initial input buffer size. Input buffers are reallocated dynamically if this value is + * insufficient. */ - private static final int NUM_BUFFERS = 16; private static final int INITIAL_INPUT_BUFFER_SIZE = 768 * 1024; // Value based on cs/SoftVpx.cpp. private final boolean scaleToFit; @@ -564,7 +571,8 @@ public final class LibvpxVideoRenderer extends BaseRenderer { try { long codecInitializingTimestamp = SystemClock.elapsedRealtime(); TraceUtil.beginSection("createVpxDecoder"); - decoder = new VpxDecoder(NUM_BUFFERS, NUM_BUFFERS, INITIAL_INPUT_BUFFER_SIZE, mediaCrypto); + decoder = new VpxDecoder(NUM_INPUT_BUFFERS, NUM_OUTPUT_BUFFERS, INITIAL_INPUT_BUFFER_SIZE, + mediaCrypto); decoder.setOutputMode(outputMode); TraceUtil.endSection(); long codecInitializedTimestamp = SystemClock.elapsedRealtime(); From fbee0c85212088212a3c1e1db217263b1807e639 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 3 Aug 2017 00:50:47 -0700 Subject: [PATCH 0183/2472] Replace README reference to source with reference to javadoc ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164090619 --- extensions/ima/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/ima/README.md b/extensions/ima/README.md index b5afcec94a..f328bb44cb 100644 --- a/extensions/ima/README.md +++ b/extensions/ima/README.md @@ -7,7 +7,7 @@ The IMA extension is a [MediaSource][] implementation wrapping the alongside content. [IMA]: https://developers.google.com/interactive-media-ads/docs/sdks/android/ -[MediaSource]: https://github.com/google/ExoPlayer/blob/release-v2/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java +[MediaSource]: https://google.github.io/ExoPlayer/doc/reference/index.html?com/google/android/exoplayer2/source/MediaSource.html ## Getting the extension ## From 838c4414eb63ce4a90119793b46b510e8acee639 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 3 Aug 2017 03:33:22 -0700 Subject: [PATCH 0184/2472] Fix targetSdkVersion to be consistent with gradle builds The manifest value is always overridden in gradle builds, so this is for internal builds only. The value should be the same (i.e. 25!). Also fix IMA build to force the right support library version, attempt 2! ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164103183 --- demo/src/main/AndroidManifest.xml | 2 +- extensions/ima/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/demo/src/main/AndroidManifest.xml b/demo/src/main/AndroidManifest.xml index a39023353a..0e04d9a435 100644 --- a/demo/src/main/AndroidManifest.xml +++ b/demo/src/main/AndroidManifest.xml @@ -23,7 +23,7 @@ - + 11.0.2 // |-- com.google.android.gms:play-services-basement:[11.0.2] -> 11.0.2 // |-- com.android.support:support-v4:25.2.0 - compile 'com.android.support:support-annotations:' + supportLibraryVersion + compile 'com.android.support:support-v4:' + supportLibraryVersion compile 'com.google.ads.interactivemedia.v3:interactivemedia:3.7.4' compile 'com.google.android.gms:play-services-ads:11.0.2' androidTestCompile project(modulePrefix + 'library') From 587704a2a0d9a4c4ce1a2281e0918f5615248b93 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 3 Aug 2017 04:10:10 -0700 Subject: [PATCH 0185/2472] Add some missing @param Javadoc to extensions ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164105607 --- .../android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java | 4 ++++ .../google/android/exoplayer2/ext/flac/FlacLibrary.java | 2 ++ .../android/exoplayer2/ext/gvr/GvrAudioProcessor.java | 5 +++++ .../android/exoplayer2/ext/ima/ImaAdsMediaSource.java | 2 ++ .../android/exoplayer2/ext/opus/LibopusAudioRenderer.java | 7 +++++++ .../google/android/exoplayer2/ext/opus/OpusLibrary.java | 2 ++ .../com/google/android/exoplayer2/ext/vp9/VpxLibrary.java | 2 ++ .../exoplayer2/ext/vp9/VpxOutputBufferRenderer.java | 2 ++ 8 files changed, 26 insertions(+) diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java index 0c065549ca..9b3bbbb6ab 100644 --- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java +++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java @@ -37,6 +37,8 @@ public final class FfmpegLibrary { * Override the names of the FFmpeg native libraries. If an application wishes to call this * method, it must do so before calling any other method defined by this class, and before * instantiating a {@link FfmpegAudioRenderer} instance. + * + * @param libraries The names of the FFmpeg native libraries. */ public static void setLibraries(String... libraries) { LOADER.setLibraries(libraries); @@ -58,6 +60,8 @@ public final class FfmpegLibrary { /** * Returns whether the underlying library supports the specified MIME type. + * + * @param mimeType The MIME type to check. */ public static boolean supportsFormat(String mimeType) { if (!isAvailable()) { diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacLibrary.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacLibrary.java index 4130c27c59..d8b9b808a6 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacLibrary.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacLibrary.java @@ -35,6 +35,8 @@ public final class FlacLibrary { * Override the names of the Flac native libraries. If an application wishes to call this method, * it must do so before calling any other method defined by this class, and before instantiating * any {@link LibflacAudioRenderer} and {@link FlacExtractor} instances. + * + * @param libraries The names of the Flac native libraries. */ public static void setLibraries(String... libraries) { LOADER.setLibraries(libraries); 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 c6e09cf4cc..5750f5f04d 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 @@ -61,6 +61,11 @@ public final class GvrAudioProcessor implements AudioProcessor { /** * Updates the listener head orientation. May be called on any thread. See * {@code GvrAudioSurround.updateNativeOrientation}. + * + * @param w The w component of the quaternion. + * @param x The x component of the quaternion. + * @param y The y component of the quaternion. + * @param z The z component of the quaternion. */ public synchronized void updateOrientation(float w, float x, float y, float z) { this.w = w; diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java index 0bf5773d2c..d56a3ad41f 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java @@ -50,6 +50,8 @@ public final class ImaAdsMediaSource implements MediaSource { * Called if there was an error loading ads. The media source will load the content without ads * if ads can't be loaded, so listen for this event if you need to implement additional handling * (for example, stopping the player). + * + * @param error The error. */ void onAdLoadError(IOException error); diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java index 93fe033a31..730473ddad 100644 --- a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java +++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java @@ -54,6 +54,13 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer { * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be * null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. + * @param drmSessionManager For use with encrypted media. May be null if support for encrypted + * media is not required. + * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions. + * For example a media file may start with a short clear region so as to allow playback to + * begin in parallel with key acquisition. This parameter specifies whether the renderer is + * permitted to play clear regions of encrypted media files before {@code drmSessionManager} + * has obtained the keys necessary to decrypt encrypted regions of the media. * @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output. */ public LibopusAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java index fb8fb738ff..22985ea497 100644 --- a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java +++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusLibrary.java @@ -35,6 +35,8 @@ public final class OpusLibrary { * Override the names of the Opus native libraries. If an application wishes to call this method, * it must do so before calling any other method defined by this class, and before instantiating a * {@link LibopusAudioRenderer} instance. + * + * @param libraries The names of the Opus native libraries. */ public static void setLibraries(String... libraries) { LOADER.setLibraries(libraries); diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxLibrary.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxLibrary.java index 09f242f1ea..854576b4b2 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxLibrary.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxLibrary.java @@ -35,6 +35,8 @@ public final class VpxLibrary { * Override the names of the Vpx native libraries. If an application wishes to call this method, * it must do so before calling any other method defined by this class, and before instantiating a * {@link LibvpxVideoRenderer} instance. + * + * @param libraries The names of the Vpx native libraries. */ public static void setLibraries(String... libraries) { LOADER.setLibraries(libraries); diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBufferRenderer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBufferRenderer.java index 8f43a0207b..d07e24d920 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBufferRenderer.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBufferRenderer.java @@ -22,6 +22,8 @@ public interface VpxOutputBufferRenderer { /** * Sets the output buffer to be rendered. The renderer is responsible for releasing the buffer. + * + * @param outputBuffer The output buffer to be rendered. */ void setOutputBuffer(VpxOutputBuffer outputBuffer); From c2049ba1a7946c3e4c49b02098104a283ca13167 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 3 Aug 2017 04:11:08 -0700 Subject: [PATCH 0186/2472] Fix some lint warnings ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164105662 --- .../main/java/com/google/android/exoplayer2/Timeline.java | 2 +- .../com/google/android/exoplayer2/text/ssa/SsaDecoder.java | 3 ++- .../com/google/android/exoplayer2/text/ttml/TtmlDecoder.java | 2 +- .../main/java/com/google/android/exoplayer2/util/Util.java | 2 +- .../google/android/exoplayer2/source/hls/HlsChunkSource.java | 3 +-- .../google/android/exoplayer2/ui/DebugTextViewHelper.java | 5 ++++- 6 files changed, 10 insertions(+), 7 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java index 7ce23e67ec..414c0804ad 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java @@ -95,7 +95,7 @@ import com.google.android.exoplayer2.util.Assertions; * of the on-demand stream ends, playback of the live stream will start from its default position * near the live edge. * - *

      On-demand stream with mid-roll ads

      + *

      On-demand stream with mid-roll ads

      *

      * Example timeline for an on-demand
  *       stream with mid-roll ad groups diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java index 11489e7b35..d2f5a67c27 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java @@ -23,6 +23,7 @@ import com.google.android.exoplayer2.text.SimpleSubtitleDecoder; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.LongArray; import com.google.android.exoplayer2.util.ParsableByteArray; +import com.google.android.exoplayer2.util.Util; import java.util.ArrayList; import java.util.List; import java.util.regex.Matcher; @@ -132,7 +133,7 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { formatEndIndex = C.INDEX_UNSET; formatTextIndex = C.INDEX_UNSET; for (int i = 0; i < formatKeyCount; i++) { - String key = values[i].trim().toLowerCase(); + String key = Util.toLowerInvariant(values[i].trim()); switch (key) { case "start": formatStartIndex = i; diff --git a/library/core/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 index e438aa1837..a215bf3cc9 100644 --- a/library/core/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 @@ -290,7 +290,7 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { String displayAlign = XmlPullParserUtil.getAttributeValue(xmlParser, TtmlNode.ATTR_TTS_DISPLAY_ALIGN); if (displayAlign != null) { - switch (displayAlign.toLowerCase()) { + switch (Util.toLowerInvariant(displayAlign)) { case "center": lineAnchor = Cue.ANCHOR_TYPE_MIDDLE; line += height / 2; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java index c00d7fa36c..b958a54244 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -895,7 +895,7 @@ public final class Util { */ @C.ContentType public static int inferContentType(String fileName) { - fileName = fileName.toLowerCase(); + fileName = Util.toLowerInvariant(fileName); if (fileName.endsWith(".mpd")) { return C.TYPE_DASH; } else if (fileName.endsWith(".m3u8")) { diff --git a/library/hls/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 index 38c3da8194..bca62ed230 100644 --- a/library/hls/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 @@ -39,7 +39,6 @@ import java.io.IOException; import java.math.BigInteger; import java.util.Arrays; import java.util.List; -import java.util.Locale; /** * Source of Hls (possibly adaptive) chunks. @@ -366,7 +365,7 @@ import java.util.Locale; private void setEncryptionData(Uri keyUri, String iv, byte[] secretKey) { String trimmedIv; - if (iv.toLowerCase(Locale.getDefault()).startsWith("0x")) { + if (Util.toLowerInvariant(iv).startsWith("0x")) { trimmedIv = iv.substring(2); } else { trimmedIv = iv; diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java index 2b8705bb74..cb5e3465f8 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.ui; +import android.annotation.SuppressLint; import android.widget.TextView; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; @@ -25,6 +26,7 @@ import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import java.util.Locale; /** * A helper class for periodically updating a {@link TextView} with debug information obtained from @@ -125,6 +127,7 @@ public final class DebugTextViewHelper implements Runnable, Player.EventListener // Private methods. + @SuppressLint("SetTextI18n") private void updateAndPost() { textView.setText(getPlayerStateString() + getPlayerWindowIndexString() + getVideoString() + getAudioString()); @@ -191,7 +194,7 @@ public final class DebugTextViewHelper implements Runnable, Player.EventListener private static String getPixelAspectRatioString(float pixelAspectRatio) { return pixelAspectRatio == Format.NO_VALUE || pixelAspectRatio == 1f ? "" - : (" par:" + String.format("%.02f", pixelAspectRatio)); + : (" par:" + String.format(Locale.US, "%.02f", pixelAspectRatio)); } } From 5fbb58c62f77707eef49709348815f5b5f46afaa Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 3 Aug 2017 05:19:02 -0700 Subject: [PATCH 0187/2472] Take into account init data size for input buffer size Issue: #2900 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164110904 --- .../video/MediaCodecVideoRenderer.java | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index 07c45dcd25..9a2927cc3f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -477,7 +477,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { Format oldFormat, Format newFormat) { return areAdaptationCompatible(codecIsAdaptive, oldFormat, newFormat) && newFormat.width <= codecMaxValues.width && newFormat.height <= codecMaxValues.height - && newFormat.maxInputSize <= codecMaxValues.inputSize; + && getMaxInputSize(newFormat) <= codecMaxValues.inputSize; } @Override @@ -854,18 +854,27 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } /** - * Returns a maximum input size for a given format. + * Returns a maximum input buffer size for a given format. * * @param format The format. - * @return A maximum input size in bytes, or {@link Format#NO_VALUE} if a maximum could not be - * determined. + * @return A maximum input buffer size in bytes, or {@link Format#NO_VALUE} if a maximum could not + * be determined. */ private static int getMaxInputSize(Format format) { if (format.maxInputSize != Format.NO_VALUE) { - // The format defines an explicit maximum input size. - return format.maxInputSize; + // The format defines an explicit maximum input size. Add the total size of initialization + // data buffers, as they may need to be queued in the same input buffer as the largest sample. + int totalInitializationDataSize = 0; + int initializationDataCount = format.initializationData.size(); + for (int i = 0; i < initializationDataCount; i++) { + totalInitializationDataSize += format.initializationData.get(i).length; + } + return format.maxInputSize + totalInitializationDataSize; + } else { + // Calculated maximum input sizes are overestimates, so it's not necessary to add the size of + // initialization data. + return getMaxInputSize(format.sampleMimeType, format.width, format.height); } - return getMaxInputSize(format.sampleMimeType, format.width, format.height); } /** From de1f538e14738e0e8e84cc14156d005f264918d9 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 3 Aug 2017 08:26:44 -0700 Subject: [PATCH 0188/2472] Bump version ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164126494 --- RELEASENOTES.md | 2 ++ constants.gradle | 2 +- .../com/google/android/exoplayer2/ExoPlayerLibraryInfo.java | 4 ++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 379b84b4e7..e96cd9ddab 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -52,6 +52,8 @@ ([#2891](https://github.com/google/ExoPlayer/issues/2891)). * Cronet extension: Support for a user-defined fallback if Cronet library is not present. +* Fix buffer too small IllegalStateException issue affecting some composite + media playbacks ([#2900](https://github.com/google/ExoPlayer/issues/2900)). * Misc bugfixes. ### r2.4.4 ### diff --git a/constants.gradle b/constants.gradle index 0db74945c4..93284fd897 100644 --- a/constants.gradle +++ b/constants.gradle @@ -24,7 +24,7 @@ project.ext { supportLibraryVersion = '25.4.0' dexmakerVersion = '1.2' mockitoVersion = '1.9.5' - releaseVersion = 'r2.5.0-beta2' + releaseVersion = 'r2.5.0-beta3' modulePrefix = ':' if (gradle.ext.has('exoplayerModulePrefix')) { modulePrefix += gradle.ext.exoplayerModulePrefix diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java index c04a777e14..153c6cda9c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java @@ -31,13 +31,13 @@ public final class ExoPlayerLibraryInfo { * The version of the library expressed as a string, for example "1.2.3". */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. - public static final String VERSION = "2.5.0-beta2"; + public static final String VERSION = "2.5.0-beta3"; /** * The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final String VERSION_SLASHY = "ExoPlayerLib/2.5.0-beta2"; + public static final String VERSION_SLASHY = "ExoPlayerLib/2.5.0-beta3"; /** * The version of the library expressed as an integer, for example 1002003. From 6bf967263ba5816467516fae1bc86e796a08ac60 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 3 Aug 2017 10:06:28 -0700 Subject: [PATCH 0189/2472] Remove dead sample link Issue: #3135 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164138761 --- demo/src/main/assets/media.exolist.json | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/demo/src/main/assets/media.exolist.json b/demo/src/main/assets/media.exolist.json index e0110df80b..59d8259d37 100644 --- a/demo/src/main/assets/media.exolist.json +++ b/demo/src/main/assets/media.exolist.json @@ -325,18 +325,6 @@ } ] }, - { - "name": "ClearKey DASH", - "samples": [ - { - "name": "Big Buck Bunny (CENC ClearKey)", - "uri": "http://html5.cablelabs.com:8100/cenc/ck/dash.mpd", - "extension": "mpd", - "drm_scheme": "cenc", - "drm_license_url": "https://wasabeef.jp/demos/cenc-ck-dash.json" - } - ] - }, { "name": "SmoothStreaming", "samples": [ From 1203b534062b4b9ceefa59549fd72e796e0146ca Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 4 Aug 2017 00:57:05 -0700 Subject: [PATCH 0190/2472] Decrease number of Vpx input buffers I think they're excessively sized also, but changing that is a little more risky. And we should look at investigating the input buffer size for all our decoder extensions, rather than just this one. Issue: #3120 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164234087 --- .../ext/ffmpeg/FfmpegAudioRenderer.java | 7 +++++++ .../ext/vp9/LibvpxVideoRenderer.java | 18 +++++++++++++----- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java index 8d75ca3dbb..453a18476e 100644 --- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java +++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java @@ -30,7 +30,14 @@ import com.google.android.exoplayer2.util.MimeTypes; */ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer { + /** + * The number of input and output buffers. + */ private static final int NUM_BUFFERS = 16; + /** + * The initial input buffer size. Input buffers are reallocated dynamically if this value is + * insufficient. + */ private static final int INITIAL_INPUT_BUFFER_SIZE = 960 * 6; private FfmpegDecoder decoder; 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 9b0355a9e7..a947378de5 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 @@ -77,11 +77,18 @@ public final class LibvpxVideoRenderer extends BaseRenderer { public static final int MSG_SET_OUTPUT_BUFFER_RENDERER = C.MSG_CUSTOM_BASE; /** - * The number of input buffers and the number of output buffers. The renderer may limit the - * minimum possible value due to requiring multiple output buffers to be dequeued at a time for it - * to make progress. + * The number of input buffers. + */ + private static final int NUM_INPUT_BUFFERS = 8; + /** + * The number of output buffers. The renderer may limit the minimum possible value due to + * requiring multiple output buffers to be dequeued at a time for it to make progress. + */ + private static final int NUM_OUTPUT_BUFFERS = 16; + /** + * The initial input buffer size. Input buffers are reallocated dynamically if this value is + * insufficient. */ - private static final int NUM_BUFFERS = 16; private static final int INITIAL_INPUT_BUFFER_SIZE = 768 * 1024; // Value based on cs/SoftVpx.cpp. private final boolean scaleToFit; @@ -564,7 +571,8 @@ public final class LibvpxVideoRenderer extends BaseRenderer { try { long codecInitializingTimestamp = SystemClock.elapsedRealtime(); TraceUtil.beginSection("createVpxDecoder"); - decoder = new VpxDecoder(NUM_BUFFERS, NUM_BUFFERS, INITIAL_INPUT_BUFFER_SIZE, mediaCrypto); + decoder = new VpxDecoder(NUM_INPUT_BUFFERS, NUM_OUTPUT_BUFFERS, INITIAL_INPUT_BUFFER_SIZE, + mediaCrypto); decoder.setOutputMode(outputMode); TraceUtil.endSection(); long codecInitializedTimestamp = SystemClock.elapsedRealtime(); From 08e58af6e7ac8707e59fd2c80c48c5d4569ba48e Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 4 Aug 2017 06:20:38 -0700 Subject: [PATCH 0191/2472] Separate handling of oldTimeline == null case ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164254522 --- .../exoplayer2/ExoPlayerImplInternal.java | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 633250a784..f77b32082b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -986,26 +986,29 @@ import java.io.IOException; // The seek position was valid for the timeline that it was performed into, but the // timeline has changed and a suitable seek position could not be resolved in the new one. handleSourceInfoRefreshEndedPlayback(manifest, processedInitialSeekCount); - return; + } else { + int periodIndex = periodPosition.first; + long positionUs = periodPosition.second; + MediaPeriodId periodId = + mediaPeriodInfoSequence.resolvePeriodPositionForAds(periodIndex, positionUs); + playbackInfo = new PlaybackInfo(periodId, periodId.isAd() ? 0 : positionUs, positionUs); + notifySourceInfoRefresh(manifest, processedInitialSeekCount); } - int periodIndex = periodPosition.first; - long positionUs = periodPosition.second; - MediaPeriodId periodId = - mediaPeriodInfoSequence.resolvePeriodPositionForAds(periodIndex, positionUs); - playbackInfo = new PlaybackInfo(periodId, periodId.isAd() ? 0 : positionUs, positionUs); } else if (playbackInfo.startPositionUs == C.TIME_UNSET) { if (timeline.isEmpty()) { handleSourceInfoRefreshEndedPlayback(manifest, processedInitialSeekCount); - return; + } else { + Pair defaultPosition = getPeriodPosition(0, C.TIME_UNSET); + int periodIndex = defaultPosition.first; + long startPositionUs = defaultPosition.second; + MediaPeriodId periodId = mediaPeriodInfoSequence.resolvePeriodPositionForAds(periodIndex, + startPositionUs); + playbackInfo = new PlaybackInfo(periodId, periodId.isAd() ? 0 : startPositionUs, + startPositionUs); + notifySourceInfoRefresh(manifest, processedInitialSeekCount); } - Pair defaultPosition = getPeriodPosition(0, C.TIME_UNSET); - int periodIndex = defaultPosition.first; - long startPositionUs = defaultPosition.second; - MediaPeriodId periodId = mediaPeriodInfoSequence.resolvePeriodPositionForAds(periodIndex, - startPositionUs); - playbackInfo = new PlaybackInfo(periodId, periodId.isAd() ? 0 : startPositionUs, - startPositionUs); } + return; } MediaPeriodHolder periodHolder = playingPeriodHolder != null ? playingPeriodHolder From 576f362912924cff21e24ed9f7c52389c50a982c Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 4 Aug 2017 06:51:38 -0700 Subject: [PATCH 0192/2472] Update playbackInfo even if there's no period holder This is required to correctly update the playbackInfo.periodId when seeking close to the end of a period with ads, as the seek operation leads to an immediate source info refresh when midroll ads are marked as played. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164257099 --- .../exoplayer2/ExoPlayerImplInternal.java | 56 +++++++++++-------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index f77b32082b..cb04501fc0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -1011,20 +1011,20 @@ import java.io.IOException; return; } + int playingPeriodIndex = playbackInfo.periodId.periodIndex; MediaPeriodHolder periodHolder = playingPeriodHolder != null ? playingPeriodHolder : loadingPeriodHolder; - if (periodHolder == null) { - // We don't have any period holders, so we're done. + if (periodHolder == null && playingPeriodIndex >= oldTimeline.getPeriodCount()) { notifySourceInfoRefresh(manifest, processedInitialSeekCount); return; } - - int periodIndex = timeline.getIndexOfPeriod(periodHolder.uid); + Object playingPeriodUid = periodHolder == null + ? oldTimeline.getPeriod(playingPeriodIndex, period, true).uid : periodHolder.uid; + int periodIndex = timeline.getIndexOfPeriod(playingPeriodUid); if (periodIndex == C.INDEX_UNSET) { // We didn't find the current period in the new timeline. Attempt to resolve a subsequent // period whose window we can restart from. - int newPeriodIndex = resolveSubsequentPeriod(periodHolder.info.id.periodIndex, oldTimeline, - timeline); + int newPeriodIndex = resolveSubsequentPeriod(playingPeriodIndex, oldTimeline, timeline); if (newPeriodIndex == C.INDEX_UNSET) { // We failed to resolve a suitable restart position. handleSourceInfoRefreshEndedPlayback(manifest, processedInitialSeekCount); @@ -1036,17 +1036,19 @@ import java.io.IOException; newPeriodIndex = defaultPosition.first; long newPositionUs = defaultPosition.second; timeline.getPeriod(newPeriodIndex, period, true); - // Clear the index of each holder that doesn't contain the default position. If a holder - // contains the default position then update its index so it can be re-used when seeking. - Object newPeriodUid = period.uid; - periodHolder.info = periodHolder.info.copyWithPeriodIndex(C.INDEX_UNSET); - while (periodHolder.next != null) { - periodHolder = periodHolder.next; - if (periodHolder.uid.equals(newPeriodUid)) { - periodHolder.info = mediaPeriodInfoSequence.getUpdatedMediaPeriodInfo(periodHolder.info, - newPeriodIndex); - } else { - periodHolder.info = periodHolder.info.copyWithPeriodIndex(C.INDEX_UNSET); + if (periodHolder != null) { + // Clear the index of each holder that doesn't contain the default position. If a holder + // contains the default position then update its index so it can be re-used when seeking. + Object newPeriodUid = period.uid; + periodHolder.info = periodHolder.info.copyWithPeriodIndex(C.INDEX_UNSET); + while (periodHolder.next != null) { + periodHolder = periodHolder.next; + if (periodHolder.uid.equals(newPeriodUid)) { + periodHolder.info = mediaPeriodInfoSequence.getUpdatedMediaPeriodInfo(periodHolder.info, + newPeriodIndex); + } else { + periodHolder.info = periodHolder.info.copyWithPeriodIndex(C.INDEX_UNSET); + } } } // Actually do the seek. @@ -1057,8 +1059,13 @@ import java.io.IOException; return; } - // If playing an ad, check that it hasn't been marked as played. If it has, skip forward. + // The current period is in the new timeline. Update the playback info. + if (periodIndex != playingPeriodIndex) { + playbackInfo = playbackInfo.copyWithPeriodIndex(periodIndex); + } + if (playbackInfo.periodId.isAd()) { + // Check that the playing ad hasn't been marked as played. If it has, skip forward. MediaPeriodId periodId = mediaPeriodInfoSequence.resolvePeriodPositionForAds(periodIndex, playbackInfo.contentPositionUs); if (!periodId.isAd() || periodId.adIndexInAdGroup != playbackInfo.periodId.adIndexInAdGroup) { @@ -1070,14 +1077,15 @@ import java.io.IOException; } } - // The current period is in the new timeline. Update the holder and playbackInfo. - periodHolder = updatePeriodInfo(periodHolder, periodIndex); - if (periodIndex != playbackInfo.periodId.periodIndex) { - playbackInfo = playbackInfo.copyWithPeriodIndex(periodIndex); + if (periodHolder == null) { + // We don't have any period holders, so we're done. + notifySourceInfoRefresh(manifest, processedInitialSeekCount); + return; } - // If there are subsequent holders, update the index for each of them. If we find a holder - // that's inconsistent with the new timeline then take appropriate action. + // Update the holder indices. If we find a subsequent holder that's inconsistent with the new + // timeline then take appropriate action. + periodHolder = updatePeriodInfo(periodHolder, periodIndex); while (periodHolder.next != null) { MediaPeriodHolder previousPeriodHolder = periodHolder; periodHolder = periodHolder.next; From c28ebf10f7f89b4b0a4b908a09788f533b762b00 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 4 Aug 2017 07:26:19 -0700 Subject: [PATCH 0193/2472] Fix playing local content after permission granted. After maybeRequestReadExternalStoragePermission and the subsequent granting of the permission, the media source would never be created. I can't see a case where initializePlayer shouldn't create a new MediaSource, so I've just removed the condition. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164260074 --- .../exoplayer2/demo/PlayerActivity.java | 108 +++++++++--------- 1 file changed, 53 insertions(+), 55 deletions(-) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index 40e77452ea..a98ab599ff 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -119,7 +119,7 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi private DefaultTrackSelector trackSelector; private TrackSelectionHelper trackSelectionHelper; private DebugTextViewHelper debugViewHelper; - private boolean needRetrySource; + private boolean inErrorState; private TrackGroupArray lastSeenTrackGroupArray; private boolean shouldAutoPlay; @@ -297,60 +297,58 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi debugViewHelper = new DebugTextViewHelper(player, debugTextView); debugViewHelper.start(); } - if (needNewPlayer || needRetrySource) { - String action = intent.getAction(); - Uri[] uris; - String[] extensions; - if (ACTION_VIEW.equals(action)) { - uris = new Uri[] {intent.getData()}; - extensions = new String[] {intent.getStringExtra(EXTENSION_EXTRA)}; - } else if (ACTION_VIEW_LIST.equals(action)) { - String[] uriStrings = intent.getStringArrayExtra(URI_LIST_EXTRA); - uris = new Uri[uriStrings.length]; - for (int i = 0; i < uriStrings.length; i++) { - uris[i] = Uri.parse(uriStrings[i]); - } - extensions = intent.getStringArrayExtra(EXTENSION_LIST_EXTRA); - if (extensions == null) { - extensions = new String[uriStrings.length]; - } - } else { - showToast(getString(R.string.unexpected_intent_action, action)); - return; + String action = intent.getAction(); + Uri[] uris; + String[] extensions; + if (ACTION_VIEW.equals(action)) { + uris = new Uri[]{intent.getData()}; + extensions = new String[]{intent.getStringExtra(EXTENSION_EXTRA)}; + } else if (ACTION_VIEW_LIST.equals(action)) { + String[] uriStrings = intent.getStringArrayExtra(URI_LIST_EXTRA); + uris = new Uri[uriStrings.length]; + for (int i = 0; i < uriStrings.length; i++) { + uris[i] = Uri.parse(uriStrings[i]); } - if (Util.maybeRequestReadExternalStoragePermission(this, uris)) { - // The player will be reinitialized if the permission is granted. - return; + extensions = intent.getStringArrayExtra(EXTENSION_LIST_EXTRA); + if (extensions == null) { + extensions = new String[uriStrings.length]; } - MediaSource[] mediaSources = new MediaSource[uris.length]; - for (int i = 0; i < uris.length; i++) { - mediaSources[i] = buildMediaSource(uris[i], extensions[i]); - } - MediaSource mediaSource = mediaSources.length == 1 ? mediaSources[0] - : new ConcatenatingMediaSource(mediaSources); - String adTagUriString = intent.getStringExtra(AD_TAG_URI_EXTRA); - if (adTagUriString != null) { - Uri adTagUri = Uri.parse(adTagUriString); - if (!adTagUri.equals(loadedAdTagUri)) { - releaseAdsLoader(); - loadedAdTagUri = adTagUri; - } - try { - mediaSource = createAdsMediaSource(mediaSource, Uri.parse(adTagUriString)); - } catch (Exception e) { - showToast(R.string.ima_not_loaded); - } - } else { - releaseAdsLoader(); - } - boolean haveResumePosition = resumeWindow != C.INDEX_UNSET; - if (haveResumePosition) { - player.seekTo(resumeWindow, resumePosition); - } - player.prepare(mediaSource, !haveResumePosition, false); - needRetrySource = false; - updateButtonVisibilities(); + } else { + showToast(getString(R.string.unexpected_intent_action, action)); + return; } + if (Util.maybeRequestReadExternalStoragePermission(this, uris)) { + // The player will be reinitialized if the permission is granted. + return; + } + MediaSource[] mediaSources = new MediaSource[uris.length]; + for (int i = 0; i < uris.length; i++) { + mediaSources[i] = buildMediaSource(uris[i], extensions[i]); + } + MediaSource mediaSource = mediaSources.length == 1 ? mediaSources[0] + : new ConcatenatingMediaSource(mediaSources); + String adTagUriString = intent.getStringExtra(AD_TAG_URI_EXTRA); + if (adTagUriString != null) { + Uri adTagUri = Uri.parse(adTagUriString); + if (!adTagUri.equals(loadedAdTagUri)) { + releaseAdsLoader(); + loadedAdTagUri = adTagUri; + } + try { + mediaSource = createAdsMediaSource(mediaSource, Uri.parse(adTagUriString)); + } catch (Exception e) { + showToast(R.string.ima_not_loaded); + } + } else { + releaseAdsLoader(); + } + boolean haveResumePosition = resumeWindow != C.INDEX_UNSET; + if (haveResumePosition) { + player.seekTo(resumeWindow, resumePosition); + } + player.prepare(mediaSource, !haveResumePosition, false); + inErrorState = false; + updateButtonVisibilities(); } private MediaSource buildMediaSource(Uri uri, String overrideExtension) { @@ -502,7 +500,7 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi @Override public void onPositionDiscontinuity() { - if (needRetrySource) { + if (inErrorState) { // This will only occur if the user has performed a seek whilst in the error state. Update the // resume position so that if the user then retries, playback will resume from the position to // which they seeked. @@ -548,7 +546,7 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi if (errorString != null) { showToast(errorString); } - needRetrySource = true; + inErrorState = true; if (isBehindLiveWindow(e)) { clearResumePosition(); initializePlayer(); @@ -584,7 +582,7 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi private void updateButtonVisibilities() { debugRootView.removeAllViews(); - retryButton.setVisibility(needRetrySource ? View.VISIBLE : View.GONE); + retryButton.setVisibility(inErrorState ? View.VISIBLE : View.GONE); debugRootView.addView(retryButton); if (player == null) { From 287b999723b2b73b9822ec897af84233cae6e5c8 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 13 Jul 2017 08:03:45 +0100 Subject: [PATCH 0194/2472] Fix detection of postroll when seeking to duration Also mark all ads as played when the postroll plays, in the case the player is backgrounded then resumed and the user seeks back. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164262738 --- .../exoplayer2/ext/ima/ImaAdsLoader.java | 28 +++++++++++++------ .../ext/ima/SinglePeriodAdTimeline.java | 2 +- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index a1db09b3d8..90593a6937 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -572,15 +572,25 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, return; } if (!playingAd && !player.isPlayingAd()) { - long positionUs = C.msToUs(player.getCurrentPosition()); - int adGroupIndex = timeline.getPeriod(0, period).getAdGroupIndexForPositionUs(positionUs); - if (adGroupIndex != C.INDEX_UNSET) { - sentPendingContentPositionMs = false; - pendingContentPositionMs = player.getCurrentPosition(); + checkForContentComplete(); + if (sentContentComplete) { + for (int i = 0; i < adPlaybackState.adGroupCount; i++) { + if (adPlaybackState.adGroupTimesUs[i] != C.TIME_END_OF_SOURCE) { + adPlaybackState.playedAdGroup(i); + } + } + updateAdPlaybackState(); + } else { + long positionMs = player.getCurrentPosition(); + timeline.getPeriod(0, period); + if (period.getAdGroupIndexForPositionUs(C.msToUs(positionMs)) != C.INDEX_UNSET) { + sentPendingContentPositionMs = false; + pendingContentPositionMs = positionMs; + } } - return; + } else { + updateImaStateForPlayerState(); } - updateImaStateForPlayerState(); } @Override @@ -672,8 +682,8 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, } private void checkForContentComplete() { - if (contentDurationMs != C.TIME_UNSET - && player.getCurrentPosition() + END_OF_CONTENT_POSITION_THRESHOLD_MS >= contentDurationMs + if (contentDurationMs != C.TIME_UNSET && pendingContentPositionMs == C.TIME_UNSET + && player.getContentPosition() + END_OF_CONTENT_POSITION_THRESHOLD_MS >= contentDurationMs && !sentContentComplete) { adsLoader.contentComplete(); if (DEBUG) { diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/SinglePeriodAdTimeline.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/SinglePeriodAdTimeline.java index e3eef6a2b7..0162d22c34 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/SinglePeriodAdTimeline.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/SinglePeriodAdTimeline.java @@ -23,7 +23,7 @@ import com.google.android.exoplayer2.util.Assertions; /** * A {@link Timeline} for sources that have ads. */ -public final class SinglePeriodAdTimeline extends ForwardingTimeline { +/* package */ final class SinglePeriodAdTimeline extends ForwardingTimeline { private final long[] adGroupTimesUs; private final int[] adCounts; From c449bae51aed45fcf28f95818a79a9670584ff23 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 4 Aug 2017 08:55:43 -0700 Subject: [PATCH 0195/2472] Log IMA LOG AdEvent ad data ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164267555 --- .../android/exoplayer2/ext/ima/ImaAdsLoader.java | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index 90593a6937..9bfe33e988 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -26,6 +26,7 @@ import com.google.ads.interactivemedia.v3.api.AdErrorEvent; import com.google.ads.interactivemedia.v3.api.AdErrorEvent.AdErrorListener; import com.google.ads.interactivemedia.v3.api.AdEvent; import com.google.ads.interactivemedia.v3.api.AdEvent.AdEventListener; +import com.google.ads.interactivemedia.v3.api.AdEvent.AdEventType; import com.google.ads.interactivemedia.v3.api.AdPodInfo; import com.google.ads.interactivemedia.v3.api.AdsLoader; import com.google.ads.interactivemedia.v3.api.AdsLoader.AdsLoadedListener; @@ -51,6 +52,7 @@ import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.Map; /** * Loads ads using the IMA SDK. All methods are called on the main thread. @@ -324,14 +326,21 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, @Override public void onAdEvent(AdEvent adEvent) { - Ad ad = adEvent.getAd(); - if (DEBUG) { - Log.d(TAG, "onAdEvent " + adEvent.getType()); + AdEventType adEventType = adEvent.getType(); + boolean isLogAdEvent = adEventType == AdEventType.LOG; + if (DEBUG || isLogAdEvent) { + Log.w(TAG, "onAdEvent: " + adEventType); + if (isLogAdEvent) { + for (Map.Entry entry : adEvent.getAdData().entrySet()) { + Log.w(TAG, " " + entry.getKey() + ": " + entry.getValue()); + } + } } if (adsManager == null) { Log.w(TAG, "Dropping ad event after release: " + adEvent); return; } + Ad ad = adEvent.getAd(); switch (adEvent.getType()) { case LOADED: // The ad position is not always accurate when using preloading. See [Internal: b/62613240]. From f502171e5fe78382e3972e8832b690c6eba97d71 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Fri, 4 Aug 2017 09:32:51 -0700 Subject: [PATCH 0196/2472] Remove unnecessary API level check in PlayerActivity of the demo app ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164271226 --- .../exoplayer2/demo/PlayerActivity.java | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index a98ab599ff..9e53dff857 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -264,13 +264,19 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi if (drmSchemeUuid != null) { String drmLicenseUrl = intent.getStringExtra(DRM_LICENSE_URL); String[] keyRequestPropertiesArray = intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES); - try { - drmSessionManager = buildDrmSessionManager(drmSchemeUuid, drmLicenseUrl, - keyRequestPropertiesArray); - } catch (UnsupportedDrmException e) { - int errorStringId = Util.SDK_INT < 18 ? R.string.error_drm_not_supported - : (e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME - ? R.string.error_drm_unsupported_scheme : R.string.error_drm_unknown); + int errorStringId = R.string.error_drm_unknown; + if (Util.SDK_INT < 18) { + errorStringId = R.string.error_drm_not_supported; + } else { + try { + drmSessionManager = buildDrmSessionManagerV18(drmSchemeUuid, drmLicenseUrl, + keyRequestPropertiesArray); + } catch (UnsupportedDrmException e) { + errorStringId = e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME + ? R.string.error_drm_unsupported_scheme : R.string.error_drm_unknown; + } + } + if (drmSessionManager == null) { showToast(errorStringId); return; } @@ -372,11 +378,8 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi } } - private DrmSessionManager buildDrmSessionManager(UUID uuid, + private DrmSessionManager buildDrmSessionManagerV18(UUID uuid, String licenseUrl, String[] keyRequestPropertiesArray) throws UnsupportedDrmException { - if (Util.SDK_INT < 18) { - return null; - } HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(licenseUrl, buildHttpDataSourceFactory(false)); if (keyRequestPropertiesArray != null) { From c72278d23eb7da24f4f33d1a1f280499d316c8d9 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 4 Aug 2017 06:20:38 -0700 Subject: [PATCH 0197/2472] Separate handling of oldTimeline == null case ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164254522 --- .../exoplayer2/ExoPlayerImplInternal.java | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 633250a784..f77b32082b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -986,26 +986,29 @@ import java.io.IOException; // The seek position was valid for the timeline that it was performed into, but the // timeline has changed and a suitable seek position could not be resolved in the new one. handleSourceInfoRefreshEndedPlayback(manifest, processedInitialSeekCount); - return; + } else { + int periodIndex = periodPosition.first; + long positionUs = periodPosition.second; + MediaPeriodId periodId = + mediaPeriodInfoSequence.resolvePeriodPositionForAds(periodIndex, positionUs); + playbackInfo = new PlaybackInfo(periodId, periodId.isAd() ? 0 : positionUs, positionUs); + notifySourceInfoRefresh(manifest, processedInitialSeekCount); } - int periodIndex = periodPosition.first; - long positionUs = periodPosition.second; - MediaPeriodId periodId = - mediaPeriodInfoSequence.resolvePeriodPositionForAds(periodIndex, positionUs); - playbackInfo = new PlaybackInfo(periodId, periodId.isAd() ? 0 : positionUs, positionUs); } else if (playbackInfo.startPositionUs == C.TIME_UNSET) { if (timeline.isEmpty()) { handleSourceInfoRefreshEndedPlayback(manifest, processedInitialSeekCount); - return; + } else { + Pair defaultPosition = getPeriodPosition(0, C.TIME_UNSET); + int periodIndex = defaultPosition.first; + long startPositionUs = defaultPosition.second; + MediaPeriodId periodId = mediaPeriodInfoSequence.resolvePeriodPositionForAds(periodIndex, + startPositionUs); + playbackInfo = new PlaybackInfo(periodId, periodId.isAd() ? 0 : startPositionUs, + startPositionUs); + notifySourceInfoRefresh(manifest, processedInitialSeekCount); } - Pair defaultPosition = getPeriodPosition(0, C.TIME_UNSET); - int periodIndex = defaultPosition.first; - long startPositionUs = defaultPosition.second; - MediaPeriodId periodId = mediaPeriodInfoSequence.resolvePeriodPositionForAds(periodIndex, - startPositionUs); - playbackInfo = new PlaybackInfo(periodId, periodId.isAd() ? 0 : startPositionUs, - startPositionUs); } + return; } MediaPeriodHolder periodHolder = playingPeriodHolder != null ? playingPeriodHolder From 42eaee3db8cee10802ffdfae1fb22f655b10c961 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 4 Aug 2017 06:51:38 -0700 Subject: [PATCH 0198/2472] Update playbackInfo even if there's no period holder This is required to correctly update the playbackInfo.periodId when seeking close to the end of a period with ads, as the seek operation leads to an immediate source info refresh when midroll ads are marked as played. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164257099 --- .../exoplayer2/ExoPlayerImplInternal.java | 56 +++++++++++-------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index f77b32082b..cb04501fc0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -1011,20 +1011,20 @@ import java.io.IOException; return; } + int playingPeriodIndex = playbackInfo.periodId.periodIndex; MediaPeriodHolder periodHolder = playingPeriodHolder != null ? playingPeriodHolder : loadingPeriodHolder; - if (periodHolder == null) { - // We don't have any period holders, so we're done. + if (periodHolder == null && playingPeriodIndex >= oldTimeline.getPeriodCount()) { notifySourceInfoRefresh(manifest, processedInitialSeekCount); return; } - - int periodIndex = timeline.getIndexOfPeriod(periodHolder.uid); + Object playingPeriodUid = periodHolder == null + ? oldTimeline.getPeriod(playingPeriodIndex, period, true).uid : periodHolder.uid; + int periodIndex = timeline.getIndexOfPeriod(playingPeriodUid); if (periodIndex == C.INDEX_UNSET) { // We didn't find the current period in the new timeline. Attempt to resolve a subsequent // period whose window we can restart from. - int newPeriodIndex = resolveSubsequentPeriod(periodHolder.info.id.periodIndex, oldTimeline, - timeline); + int newPeriodIndex = resolveSubsequentPeriod(playingPeriodIndex, oldTimeline, timeline); if (newPeriodIndex == C.INDEX_UNSET) { // We failed to resolve a suitable restart position. handleSourceInfoRefreshEndedPlayback(manifest, processedInitialSeekCount); @@ -1036,17 +1036,19 @@ import java.io.IOException; newPeriodIndex = defaultPosition.first; long newPositionUs = defaultPosition.second; timeline.getPeriod(newPeriodIndex, period, true); - // Clear the index of each holder that doesn't contain the default position. If a holder - // contains the default position then update its index so it can be re-used when seeking. - Object newPeriodUid = period.uid; - periodHolder.info = periodHolder.info.copyWithPeriodIndex(C.INDEX_UNSET); - while (periodHolder.next != null) { - periodHolder = periodHolder.next; - if (periodHolder.uid.equals(newPeriodUid)) { - periodHolder.info = mediaPeriodInfoSequence.getUpdatedMediaPeriodInfo(periodHolder.info, - newPeriodIndex); - } else { - periodHolder.info = periodHolder.info.copyWithPeriodIndex(C.INDEX_UNSET); + if (periodHolder != null) { + // Clear the index of each holder that doesn't contain the default position. If a holder + // contains the default position then update its index so it can be re-used when seeking. + Object newPeriodUid = period.uid; + periodHolder.info = periodHolder.info.copyWithPeriodIndex(C.INDEX_UNSET); + while (periodHolder.next != null) { + periodHolder = periodHolder.next; + if (periodHolder.uid.equals(newPeriodUid)) { + periodHolder.info = mediaPeriodInfoSequence.getUpdatedMediaPeriodInfo(periodHolder.info, + newPeriodIndex); + } else { + periodHolder.info = periodHolder.info.copyWithPeriodIndex(C.INDEX_UNSET); + } } } // Actually do the seek. @@ -1057,8 +1059,13 @@ import java.io.IOException; return; } - // If playing an ad, check that it hasn't been marked as played. If it has, skip forward. + // The current period is in the new timeline. Update the playback info. + if (periodIndex != playingPeriodIndex) { + playbackInfo = playbackInfo.copyWithPeriodIndex(periodIndex); + } + if (playbackInfo.periodId.isAd()) { + // Check that the playing ad hasn't been marked as played. If it has, skip forward. MediaPeriodId periodId = mediaPeriodInfoSequence.resolvePeriodPositionForAds(periodIndex, playbackInfo.contentPositionUs); if (!periodId.isAd() || periodId.adIndexInAdGroup != playbackInfo.periodId.adIndexInAdGroup) { @@ -1070,14 +1077,15 @@ import java.io.IOException; } } - // The current period is in the new timeline. Update the holder and playbackInfo. - periodHolder = updatePeriodInfo(periodHolder, periodIndex); - if (periodIndex != playbackInfo.periodId.periodIndex) { - playbackInfo = playbackInfo.copyWithPeriodIndex(periodIndex); + if (periodHolder == null) { + // We don't have any period holders, so we're done. + notifySourceInfoRefresh(manifest, processedInitialSeekCount); + return; } - // If there are subsequent holders, update the index for each of them. If we find a holder - // that's inconsistent with the new timeline then take appropriate action. + // Update the holder indices. If we find a subsequent holder that's inconsistent with the new + // timeline then take appropriate action. + periodHolder = updatePeriodInfo(periodHolder, periodIndex); while (periodHolder.next != null) { MediaPeriodHolder previousPeriodHolder = periodHolder; periodHolder = periodHolder.next; From ba46e472af2b6c5f5fad59337a72b5050880a093 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 4 Aug 2017 07:26:19 -0700 Subject: [PATCH 0199/2472] Fix playing local content after permission granted. After maybeRequestReadExternalStoragePermission and the subsequent granting of the permission, the media source would never be created. I can't see a case where initializePlayer shouldn't create a new MediaSource, so I've just removed the condition. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164260074 --- .../exoplayer2/demo/PlayerActivity.java | 108 +++++++++--------- 1 file changed, 53 insertions(+), 55 deletions(-) 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 40e77452ea..a98ab599ff 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 @@ -119,7 +119,7 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi private DefaultTrackSelector trackSelector; private TrackSelectionHelper trackSelectionHelper; private DebugTextViewHelper debugViewHelper; - private boolean needRetrySource; + private boolean inErrorState; private TrackGroupArray lastSeenTrackGroupArray; private boolean shouldAutoPlay; @@ -297,60 +297,58 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi debugViewHelper = new DebugTextViewHelper(player, debugTextView); debugViewHelper.start(); } - if (needNewPlayer || needRetrySource) { - String action = intent.getAction(); - Uri[] uris; - String[] extensions; - if (ACTION_VIEW.equals(action)) { - uris = new Uri[] {intent.getData()}; - extensions = new String[] {intent.getStringExtra(EXTENSION_EXTRA)}; - } else if (ACTION_VIEW_LIST.equals(action)) { - String[] uriStrings = intent.getStringArrayExtra(URI_LIST_EXTRA); - uris = new Uri[uriStrings.length]; - for (int i = 0; i < uriStrings.length; i++) { - uris[i] = Uri.parse(uriStrings[i]); - } - extensions = intent.getStringArrayExtra(EXTENSION_LIST_EXTRA); - if (extensions == null) { - extensions = new String[uriStrings.length]; - } - } else { - showToast(getString(R.string.unexpected_intent_action, action)); - return; + String action = intent.getAction(); + Uri[] uris; + String[] extensions; + if (ACTION_VIEW.equals(action)) { + uris = new Uri[]{intent.getData()}; + extensions = new String[]{intent.getStringExtra(EXTENSION_EXTRA)}; + } else if (ACTION_VIEW_LIST.equals(action)) { + String[] uriStrings = intent.getStringArrayExtra(URI_LIST_EXTRA); + uris = new Uri[uriStrings.length]; + for (int i = 0; i < uriStrings.length; i++) { + uris[i] = Uri.parse(uriStrings[i]); } - if (Util.maybeRequestReadExternalStoragePermission(this, uris)) { - // The player will be reinitialized if the permission is granted. - return; + extensions = intent.getStringArrayExtra(EXTENSION_LIST_EXTRA); + if (extensions == null) { + extensions = new String[uriStrings.length]; } - MediaSource[] mediaSources = new MediaSource[uris.length]; - for (int i = 0; i < uris.length; i++) { - mediaSources[i] = buildMediaSource(uris[i], extensions[i]); - } - MediaSource mediaSource = mediaSources.length == 1 ? mediaSources[0] - : new ConcatenatingMediaSource(mediaSources); - String adTagUriString = intent.getStringExtra(AD_TAG_URI_EXTRA); - if (adTagUriString != null) { - Uri adTagUri = Uri.parse(adTagUriString); - if (!adTagUri.equals(loadedAdTagUri)) { - releaseAdsLoader(); - loadedAdTagUri = adTagUri; - } - try { - mediaSource = createAdsMediaSource(mediaSource, Uri.parse(adTagUriString)); - } catch (Exception e) { - showToast(R.string.ima_not_loaded); - } - } else { - releaseAdsLoader(); - } - boolean haveResumePosition = resumeWindow != C.INDEX_UNSET; - if (haveResumePosition) { - player.seekTo(resumeWindow, resumePosition); - } - player.prepare(mediaSource, !haveResumePosition, false); - needRetrySource = false; - updateButtonVisibilities(); + } else { + showToast(getString(R.string.unexpected_intent_action, action)); + return; } + if (Util.maybeRequestReadExternalStoragePermission(this, uris)) { + // The player will be reinitialized if the permission is granted. + return; + } + MediaSource[] mediaSources = new MediaSource[uris.length]; + for (int i = 0; i < uris.length; i++) { + mediaSources[i] = buildMediaSource(uris[i], extensions[i]); + } + MediaSource mediaSource = mediaSources.length == 1 ? mediaSources[0] + : new ConcatenatingMediaSource(mediaSources); + String adTagUriString = intent.getStringExtra(AD_TAG_URI_EXTRA); + if (adTagUriString != null) { + Uri adTagUri = Uri.parse(adTagUriString); + if (!adTagUri.equals(loadedAdTagUri)) { + releaseAdsLoader(); + loadedAdTagUri = adTagUri; + } + try { + mediaSource = createAdsMediaSource(mediaSource, Uri.parse(adTagUriString)); + } catch (Exception e) { + showToast(R.string.ima_not_loaded); + } + } else { + releaseAdsLoader(); + } + boolean haveResumePosition = resumeWindow != C.INDEX_UNSET; + if (haveResumePosition) { + player.seekTo(resumeWindow, resumePosition); + } + player.prepare(mediaSource, !haveResumePosition, false); + inErrorState = false; + updateButtonVisibilities(); } private MediaSource buildMediaSource(Uri uri, String overrideExtension) { @@ -502,7 +500,7 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi @Override public void onPositionDiscontinuity() { - if (needRetrySource) { + if (inErrorState) { // This will only occur if the user has performed a seek whilst in the error state. Update the // resume position so that if the user then retries, playback will resume from the position to // which they seeked. @@ -548,7 +546,7 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi if (errorString != null) { showToast(errorString); } - needRetrySource = true; + inErrorState = true; if (isBehindLiveWindow(e)) { clearResumePosition(); initializePlayer(); @@ -584,7 +582,7 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi private void updateButtonVisibilities() { debugRootView.removeAllViews(); - retryButton.setVisibility(needRetrySource ? View.VISIBLE : View.GONE); + retryButton.setVisibility(inErrorState ? View.VISIBLE : View.GONE); debugRootView.addView(retryButton); if (player == null) { From fdcee8f1b6a13dc22972ae3cc28279edcebdc219 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 13 Jul 2017 08:03:45 +0100 Subject: [PATCH 0200/2472] Fix detection of postroll when seeking to duration Also mark all ads as played when the postroll plays, in the case the player is backgrounded then resumed and the user seeks back. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164262738 --- .../exoplayer2/ext/ima/ImaAdsLoader.java | 28 +++++++++++++------ .../ext/ima/SinglePeriodAdTimeline.java | 2 +- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index 6541dad0ac..c00e0731ee 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -572,15 +572,25 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, return; } if (!playingAd && !player.isPlayingAd()) { - long positionUs = C.msToUs(player.getCurrentPosition()); - int adGroupIndex = timeline.getPeriod(0, period).getAdGroupIndexForPositionUs(positionUs); - if (adGroupIndex != C.INDEX_UNSET) { - sentPendingContentPositionMs = false; - pendingContentPositionMs = player.getCurrentPosition(); + checkForContentComplete(); + if (sentContentComplete) { + for (int i = 0; i < adPlaybackState.adGroupCount; i++) { + if (adPlaybackState.adGroupTimesUs[i] != C.TIME_END_OF_SOURCE) { + adPlaybackState.playedAdGroup(i); + } + } + updateAdPlaybackState(); + } else { + long positionMs = player.getCurrentPosition(); + timeline.getPeriod(0, period); + if (period.getAdGroupIndexForPositionUs(C.msToUs(positionMs)) != C.INDEX_UNSET) { + sentPendingContentPositionMs = false; + pendingContentPositionMs = positionMs; + } } - return; + } else { + updateImaStateForPlayerState(); } - updateImaStateForPlayerState(); } @Override @@ -672,8 +682,8 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, } private void checkForContentComplete() { - if (contentDurationMs != C.TIME_UNSET - && player.getCurrentPosition() + END_OF_CONTENT_POSITION_THRESHOLD_MS >= contentDurationMs + if (contentDurationMs != C.TIME_UNSET && pendingContentPositionMs == C.TIME_UNSET + && player.getContentPosition() + END_OF_CONTENT_POSITION_THRESHOLD_MS >= contentDurationMs && !sentContentComplete) { adsLoader.contentComplete(); if (DEBUG) { diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/SinglePeriodAdTimeline.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/SinglePeriodAdTimeline.java index c93f1e8f28..1d73234286 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/SinglePeriodAdTimeline.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/SinglePeriodAdTimeline.java @@ -22,7 +22,7 @@ import com.google.android.exoplayer2.util.Assertions; /** * A {@link Timeline} for sources that have ads. */ -public final class SinglePeriodAdTimeline extends Timeline { +/* package */ final class SinglePeriodAdTimeline extends Timeline { private final Timeline contentTimeline; private final long[] adGroupTimesUs; From b407b19296a75f34764ede59ed430a252f248b5c Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 4 Aug 2017 08:55:43 -0700 Subject: [PATCH 0201/2472] Log IMA LOG AdEvent ad data ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164267555 --- .../android/exoplayer2/ext/ima/ImaAdsLoader.java | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index c00e0731ee..8c4fb4c51c 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -26,6 +26,7 @@ import com.google.ads.interactivemedia.v3.api.AdErrorEvent; import com.google.ads.interactivemedia.v3.api.AdErrorEvent.AdErrorListener; import com.google.ads.interactivemedia.v3.api.AdEvent; import com.google.ads.interactivemedia.v3.api.AdEvent.AdEventListener; +import com.google.ads.interactivemedia.v3.api.AdEvent.AdEventType; import com.google.ads.interactivemedia.v3.api.AdPodInfo; import com.google.ads.interactivemedia.v3.api.AdsLoader; import com.google.ads.interactivemedia.v3.api.AdsLoader.AdsLoadedListener; @@ -51,6 +52,7 @@ import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.Map; /** * Loads ads using the IMA SDK. All methods are called on the main thread. @@ -324,14 +326,21 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, @Override public void onAdEvent(AdEvent adEvent) { - Ad ad = adEvent.getAd(); - if (DEBUG) { - Log.d(TAG, "onAdEvent " + adEvent.getType()); + AdEventType adEventType = adEvent.getType(); + boolean isLogAdEvent = adEventType == AdEventType.LOG; + if (DEBUG || isLogAdEvent) { + Log.w(TAG, "onAdEvent: " + adEventType); + if (isLogAdEvent) { + for (Map.Entry entry : adEvent.getAdData().entrySet()) { + Log.w(TAG, " " + entry.getKey() + ": " + entry.getValue()); + } + } } if (adsManager == null) { Log.w(TAG, "Dropping ad event after release: " + adEvent); return; } + Ad ad = adEvent.getAd(); switch (adEvent.getType()) { case LOADED: // The ad position is not always accurate when using preloading. See [Internal: b/62613240]. From b0da1f98f6d24a852968841833fb904d1af71c4b Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Fri, 4 Aug 2017 09:32:51 -0700 Subject: [PATCH 0202/2472] Remove unnecessary API level check in PlayerActivity of the demo app ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164271226 --- .../exoplayer2/demo/PlayerActivity.java | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) 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 a98ab599ff..9e53dff857 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 @@ -264,13 +264,19 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi if (drmSchemeUuid != null) { String drmLicenseUrl = intent.getStringExtra(DRM_LICENSE_URL); String[] keyRequestPropertiesArray = intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES); - try { - drmSessionManager = buildDrmSessionManager(drmSchemeUuid, drmLicenseUrl, - keyRequestPropertiesArray); - } catch (UnsupportedDrmException e) { - int errorStringId = Util.SDK_INT < 18 ? R.string.error_drm_not_supported - : (e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME - ? R.string.error_drm_unsupported_scheme : R.string.error_drm_unknown); + int errorStringId = R.string.error_drm_unknown; + if (Util.SDK_INT < 18) { + errorStringId = R.string.error_drm_not_supported; + } else { + try { + drmSessionManager = buildDrmSessionManagerV18(drmSchemeUuid, drmLicenseUrl, + keyRequestPropertiesArray); + } catch (UnsupportedDrmException e) { + errorStringId = e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME + ? R.string.error_drm_unsupported_scheme : R.string.error_drm_unknown; + } + } + if (drmSessionManager == null) { showToast(errorStringId); return; } @@ -372,11 +378,8 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi } } - private DrmSessionManager buildDrmSessionManager(UUID uuid, + private DrmSessionManager buildDrmSessionManagerV18(UUID uuid, String licenseUrl, String[] keyRequestPropertiesArray) throws UnsupportedDrmException { - if (Util.SDK_INT < 18) { - return null; - } HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(licenseUrl, buildHttpDataSourceFactory(false)); if (keyRequestPropertiesArray != null) { From 9ecec92b568c0c73fbb259ffd0ba4561a705a3f0 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 7 Aug 2017 01:48:44 -0700 Subject: [PATCH 0203/2472] Finalize r2.5.0 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164434615 --- RELEASENOTES.md | 8 +++----- constants.gradle | 2 +- .../google/android/exoplayer2/ExoPlayerLibraryInfo.java | 4 ++-- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index e96cd9ddab..4101caad47 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,14 +1,13 @@ # Release notes # -### r2.5.0 (beta) ### +### r2.5.0 ### * IMA extension: Wraps the Google Interactive Media Ads (IMA) SDK to provide an easy and seamless way of incorporating display ads into ExoPlayer playbacks. You can read more about the IMA extension [here](https://medium.com/google-exoplayer/playing-ads-with-exoplayer-and-ima-868dfd767ea). * MediaSession extension: Provides an easy to to connect ExoPlayer with - MediaSessionCompat in the Android Support Library. *A link to a blog post - about this extension will be added here prior to the stable 2.5.0 release.* + MediaSessionCompat in the Android Support Library. * RTMP extension: An extension for playing streams over RTMP. * Build: Made it easier for application developers to depend on a local checkout of ExoPlayer. You can learn how to do this @@ -18,8 +17,7 @@ playback of progressive streams ([#2926](https://github.com/google/ExoPlayer/issues/2926)). * New DynamicConcatenatingMediaSource class to support playback of dynamic - playlists. *A link to a blog post about DynamicConcatenatingMediaSource will - be added here prior to the stable 2.5.0 release.* + playlists. * New ExoPlayer.setRepeatMode method for dynamic toggling of repeat mode during playback. Use of setRepeatMode should be preferred to LoopingMediaSource for most looping use cases. You can read more about diff --git a/constants.gradle b/constants.gradle index 93284fd897..7d126ccd89 100644 --- a/constants.gradle +++ b/constants.gradle @@ -24,7 +24,7 @@ project.ext { supportLibraryVersion = '25.4.0' dexmakerVersion = '1.2' mockitoVersion = '1.9.5' - releaseVersion = 'r2.5.0-beta3' + releaseVersion = 'r2.5.0' modulePrefix = ':' if (gradle.ext.has('exoplayerModulePrefix')) { modulePrefix += gradle.ext.exoplayerModulePrefix diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java index 153c6cda9c..fd5ead5c85 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java @@ -31,13 +31,13 @@ public final class ExoPlayerLibraryInfo { * The version of the library expressed as a string, for example "1.2.3". */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. - public static final String VERSION = "2.5.0-beta3"; + public static final String VERSION = "2.5.0"; /** * The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final String VERSION_SLASHY = "ExoPlayerLib/2.5.0-beta3"; + public static final String VERSION_SLASHY = "ExoPlayerLib/2.5.0"; /** * The version of the library expressed as an integer, for example 1002003. From d4c45861f49c943836f8bae6300358bee34a32ad Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 7 Aug 2017 01:51:26 -0700 Subject: [PATCH 0204/2472] Clean up extension READMEs Issue: #1157 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164434768 --- extensions/cronet/README.md | 33 ++++++++++++++++++++++++++----- extensions/ffmpeg/README.md | 2 +- extensions/flac/README.md | 6 +++--- extensions/gvr/README.md | 2 +- extensions/mediasession/README.md | 4 ++-- extensions/okhttp/README.md | 29 +++++++++++++++++++++++++-- extensions/opus/README.md | 6 +++--- extensions/rtmp/README.md | 4 ++-- extensions/vp9/README.md | 6 +++--- 9 files changed, 70 insertions(+), 22 deletions(-) diff --git a/extensions/cronet/README.md b/extensions/cronet/README.md index 30409fa99e..2287c4c19b 100644 --- a/extensions/cronet/README.md +++ b/extensions/cronet/README.md @@ -1,15 +1,13 @@ -# ExoPlayer Cronet Extension # +# ExoPlayer Cronet extension # ## Description ## -[Cronet][] is Chromium's Networking stack packaged as a library. - -The Cronet Extension is an [HttpDataSource][] implementation using [Cronet][]. +The Cronet extension is an [HttpDataSource][] implementation using [Cronet][]. [HttpDataSource]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer/upstream/HttpDataSource.html [Cronet]: https://chromium.googlesource.com/chromium/src/+/master/components/cronet?autodive=0%2F%2F -## Build Instructions ## +## Build instructions ## To use this extension you need to clone the ExoPlayer repository and depend on its modules locally. Instructions for doing this can be found in ExoPlayer's @@ -33,3 +31,28 @@ gradle.ext.exoplayerIncludeCronetExtension = true; [top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md [here]: https://console.cloud.google.com/storage/browser/chromium-cronet/android + +## Using the extension ## + +ExoPlayer requests data through `DataSource` instances. These instances are +either instantiated and injected from application code, or obtained from +instances of `DataSource.Factory` that are instantiated and injected from +application code. + +If your application only needs to play http(s) content, using the Cronet +extension is as simple as updating any `DataSource`s and `DataSource.Factory` +instantiations in your application code to use `CronetDataSource` and +`CronetDataSourceFactory` respectively. If your application also needs to play +non-http(s) content such as local files, use +``` +new DefaultDataSource( + ... + new CronetDataSource(...) /* baseDataSource argument */); +``` +and +``` +new DefaultDataSourceFactory( + ... + new CronetDataSourceFactory(...) /* baseDataSourceFactory argument */); +``` +respectively. diff --git a/extensions/ffmpeg/README.md b/extensions/ffmpeg/README.md index ab3e5ffb94..b4514effbc 100644 --- a/extensions/ffmpeg/README.md +++ b/extensions/ffmpeg/README.md @@ -1,4 +1,4 @@ -# FfmpegAudioRenderer # +# ExoPlayer FFmpeg extension # ## Description ## diff --git a/extensions/flac/README.md b/extensions/flac/README.md index a35dac7858..9db2e5727d 100644 --- a/extensions/flac/README.md +++ b/extensions/flac/README.md @@ -1,14 +1,14 @@ -# ExoPlayer Flac Extension # +# ExoPlayer Flac extension # ## Description ## -The Flac Extension is a [Renderer][] implementation that helps you bundle +The Flac extension is a [Renderer][] implementation that helps you bundle libFLAC (the Flac decoding library) into your app and use it along with ExoPlayer to play Flac audio on Android devices. [Renderer]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/Renderer.html -## Build Instructions ## +## Build instructions ## To use this extension you need to clone the ExoPlayer repository and depend on its modules locally. Instructions for doing this can be found in ExoPlayer's diff --git a/extensions/gvr/README.md b/extensions/gvr/README.md index ad28569121..7e072d070c 100644 --- a/extensions/gvr/README.md +++ b/extensions/gvr/README.md @@ -1,4 +1,4 @@ -# ExoPlayer GVR Extension # +# ExoPlayer GVR extension # ## Description ## diff --git a/extensions/mediasession/README.md b/extensions/mediasession/README.md index 7515cf9eef..3acf8e4c79 100644 --- a/extensions/mediasession/README.md +++ b/extensions/mediasession/README.md @@ -1,8 +1,8 @@ -# ExoPlayer MediaSession Extension # +# ExoPlayer MediaSession extension # ## Description ## -The MediaSession Extension mediates between an ExoPlayer instance and a +The MediaSession extension mediates between an ExoPlayer instance and a [MediaSession][]. It automatically retrieves and implements playback actions and syncs the player state with the state of the media session. The behaviour can be extended to support other playback and custom actions. diff --git a/extensions/okhttp/README.md b/extensions/okhttp/README.md index 52d5fabf38..b10c4ba629 100644 --- a/extensions/okhttp/README.md +++ b/extensions/okhttp/README.md @@ -1,8 +1,8 @@ -# ExoPlayer OkHttp Extension # +# ExoPlayer OkHttp extension # ## Description ## -The OkHttp Extension is an [HttpDataSource][] implementation using Square's +The OkHttp extension is an [HttpDataSource][] implementation using Square's [OkHttp][]. [HttpDataSource]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/upstream/HttpDataSource.html @@ -24,3 +24,28 @@ locally. Instructions for doing this can be found in ExoPlayer's [top level README][]. [top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md + +## Using the extension ## + +ExoPlayer requests data through `DataSource` instances. These instances are +either instantiated and injected from application code, or obtained from +instances of `DataSource.Factory` that are instantiated and injected from +application code. + +If your application only needs to play http(s) content, using the OkHttp +extension is as simple as updating any `DataSource`s and `DataSource.Factory` +instantiations in your application code to use `OkHttpDataSource` and +`OkHttpDataSourceFactory` respectively. If your application also needs to play +non-http(s) content such as local files, use +``` +new DefaultDataSource( + ... + new OkHttpDataSource(...) /* baseDataSource argument */); +``` +and +``` +new DefaultDataSourceFactory( + ... + new OkHttpDataSourceFactory(...) /* baseDataSourceFactory argument */); +``` +respectively. diff --git a/extensions/opus/README.md b/extensions/opus/README.md index ae42a9c310..e5f5bcb168 100644 --- a/extensions/opus/README.md +++ b/extensions/opus/README.md @@ -1,14 +1,14 @@ -# ExoPlayer Opus Extension # +# ExoPlayer Opus extension # ## Description ## -The Opus Extension is a [Renderer][] implementation that helps you bundle +The Opus extension is a [Renderer][] implementation that helps you bundle libopus (the Opus decoding library) into your app and use it along with ExoPlayer to play Opus audio on Android devices. [Renderer]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/Renderer.html -## Build Instructions ## +## Build instructions ## To use this extension you need to clone the ExoPlayer repository and depend on its modules locally. Instructions for doing this can be found in ExoPlayer's diff --git a/extensions/rtmp/README.md b/extensions/rtmp/README.md index 2cfa6b8ff4..042d7078dc 100644 --- a/extensions/rtmp/README.md +++ b/extensions/rtmp/README.md @@ -1,8 +1,8 @@ -# ExoPlayer RTMP Extension # +# ExoPlayer RTMP extension # ## Description ## -The RTMP Extension is a [DataSource][] implementation for playing [RTMP][] +The RTMP extension is a [DataSource][] implementation for playing [RTMP][] streams using [LibRtmp Client for Android][]. [DataSource]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/upstream/DataSource.html diff --git a/extensions/vp9/README.md b/extensions/vp9/README.md index 8bdfe652e6..87c5c8d54f 100644 --- a/extensions/vp9/README.md +++ b/extensions/vp9/README.md @@ -1,14 +1,14 @@ -# ExoPlayer VP9 Extension # +# ExoPlayer VP9 extension # ## Description ## -The VP9 Extension is a [Renderer][] implementation that helps you bundle libvpx +The VP9 extension is a [Renderer][] implementation that helps you bundle libvpx (the VP9 decoding library) into your app and use it along with ExoPlayer to play VP9 video on Android devices. [Renderer]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/Renderer.html -## Build Instructions ## +## Build instructions ## To use this extension you need to clone the ExoPlayer repository and depend on its modules locally. Instructions for doing this can be found in ExoPlayer's From 05179b4f1b36cb61e3ddee1653abe1239c9f986b Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 7 Aug 2017 01:53:09 -0700 Subject: [PATCH 0205/2472] Set correct Content-Type for ClearKey requests Issue: #3138 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164434858 --- .../exoplayer2/drm/HttpMediaDrmCallback.java | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java index f9d5efffb1..f08d9b59b5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java @@ -38,14 +38,6 @@ import java.util.UUID; @TargetApi(18) public final class HttpMediaDrmCallback implements MediaDrmCallback { - private static final Map PLAYREADY_KEY_REQUEST_PROPERTIES; - static { - PLAYREADY_KEY_REQUEST_PROPERTIES = new HashMap<>(); - PLAYREADY_KEY_REQUEST_PROPERTIES.put("Content-Type", "text/xml"); - PLAYREADY_KEY_REQUEST_PROPERTIES.put("SOAPAction", - "http://schemas.microsoft.com/DRM/2007/03/protocols/AcquireLicense"); - } - private final HttpDataSource.Factory dataSourceFactory; private final String defaultUrl; private final Map keyRequestProperties; @@ -124,10 +116,15 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback { url = defaultUrl; } Map requestProperties = new HashMap<>(); - requestProperties.put("Content-Type", "application/octet-stream"); + // Add standard request properties for supported schemes. + String contentType = C.PLAYREADY_UUID.equals(uuid) ? "text/xml" + : (C.CLEARKEY_UUID.equals(uuid) ? "application/json" : "application/octet-stream"); + requestProperties.put("Content-Type", contentType); if (C.PLAYREADY_UUID.equals(uuid)) { - requestProperties.putAll(PLAYREADY_KEY_REQUEST_PROPERTIES); + requestProperties.put("SOAPAction", + "http://schemas.microsoft.com/DRM/2007/03/protocols/AcquireLicense"); } + // Add additional request properties. synchronized (keyRequestProperties) { requestProperties.putAll(keyRequestProperties); } From 865d74cde0cf3b135404a6e951de0d6efb8011ed Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 7 Aug 2017 01:54:51 -0700 Subject: [PATCH 0206/2472] Don't use TextureView's SurfaceTexture unless available ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164434943 --- .../java/com/google/android/exoplayer2/SimpleExoPlayer.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 9dea83cff2..db1b1a13f1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -276,7 +276,8 @@ public class SimpleExoPlayer implements ExoPlayer { Log.w(TAG, "Replacing existing SurfaceTextureListener."); } textureView.setSurfaceTextureListener(componentListener); - SurfaceTexture surfaceTexture = textureView.getSurfaceTexture(); + SurfaceTexture surfaceTexture = textureView.isAvailable() ? textureView.getSurfaceTexture() + : null; setVideoSurfaceInternal(surfaceTexture == null ? null : new Surface(surfaceTexture), true); } } From 87012f22f27337d3074839bbe3881621f4a81875 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 7 Aug 2017 03:45:10 -0700 Subject: [PATCH 0207/2472] Avoid rollover calculating sample offsets I considered using Util.scaleLargeTimestamp for this, but given sample offsets are relative and should always be small (<<1s), it really shouldn't be necessary. Issue: #3139 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164443795 --- .../exoplayer2/extractor/mp4/FragmentedMp4Extractor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/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 index 6dcae9c2d6..6b2077ef76 100644 --- a/library/core/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 @@ -815,7 +815,7 @@ public final class FragmentedMp4Extractor implements Extractor { // here, because unsigned integers will still be parsed correctly (unless their top bit is // set, which is never true in practice because sample offsets are always small). int sampleOffset = trun.readInt(); - sampleCompositionTimeOffsetTable[i] = (int) ((sampleOffset * 1000) / timescale); + sampleCompositionTimeOffsetTable[i] = (int) ((sampleOffset * 1000L) / timescale); } else { sampleCompositionTimeOffsetTable[i] = 0; } From 427411befbff873b87fbc4846ab2953eafe902e7 Mon Sep 17 00:00:00 2001 From: eguven Date: Mon, 7 Aug 2017 05:22:15 -0700 Subject: [PATCH 0208/2472] Disable test coverage again https://issuetracker.google.com/issues/37019591 causes local variables can't be found while debugging. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164449443 --- library/core/build.gradle | 8 +++++--- library/dash/build.gradle | 8 +++++--- library/hls/build.gradle | 8 +++++--- library/smoothstreaming/build.gradle | 8 +++++--- library/ui/build.gradle | 8 +++++--- 5 files changed, 25 insertions(+), 15 deletions(-) diff --git a/library/core/build.gradle b/library/core/build.gradle index 65a7353607..ecad1e58b5 100644 --- a/library/core/build.gradle +++ b/library/core/build.gradle @@ -31,9 +31,11 @@ android { } buildTypes { - debug { - testCoverageEnabled = true - } + // Re-enable test coverage when the following issue is fixed: + // https://issuetracker.google.com/issues/37019591 + // debug { + // testCoverageEnabled = true + // } } } diff --git a/library/dash/build.gradle b/library/dash/build.gradle index aa8031467e..2220e5b250 100644 --- a/library/dash/build.gradle +++ b/library/dash/build.gradle @@ -24,9 +24,11 @@ android { } buildTypes { - debug { - testCoverageEnabled = true - } + // Re-enable test coverage when the following issue is fixed: + // https://issuetracker.google.com/issues/37019591 + // debug { + // testCoverageEnabled = true + // } } } diff --git a/library/hls/build.gradle b/library/hls/build.gradle index 14a26b0e12..5471eacec6 100644 --- a/library/hls/build.gradle +++ b/library/hls/build.gradle @@ -24,9 +24,11 @@ android { } buildTypes { - debug { - testCoverageEnabled = true - } + // Re-enable test coverage when the following issue is fixed: + // https://issuetracker.google.com/issues/37019591 + // debug { + // testCoverageEnabled = true + // } } } diff --git a/library/smoothstreaming/build.gradle b/library/smoothstreaming/build.gradle index b5f918075f..ee5a8c4e73 100644 --- a/library/smoothstreaming/build.gradle +++ b/library/smoothstreaming/build.gradle @@ -24,9 +24,11 @@ android { } buildTypes { - debug { - testCoverageEnabled = true - } + // Re-enable test coverage when the following issue is fixed: + // https://issuetracker.google.com/issues/37019591 + // debug { + // testCoverageEnabled = true + // } } } diff --git a/library/ui/build.gradle b/library/ui/build.gradle index c036bc9819..89734ed806 100644 --- a/library/ui/build.gradle +++ b/library/ui/build.gradle @@ -24,9 +24,11 @@ android { } buildTypes { - debug { - testCoverageEnabled = true - } + // Re-enable test coverage when the following issue is fixed: + // https://issuetracker.google.com/issues/37019591 + // debug { + // testCoverageEnabled = true + // } } } From 13a580fdbca297f1406b8d9ab6f8e37bd4eb1a07 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 7 Aug 2017 01:48:44 -0700 Subject: [PATCH 0209/2472] Finalize r2.5.0 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164434615 --- RELEASENOTES.md | 8 +++----- constants.gradle | 2 +- .../google/android/exoplayer2/ExoPlayerLibraryInfo.java | 4 ++-- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index e96cd9ddab..4101caad47 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,14 +1,13 @@ # Release notes # -### r2.5.0 (beta) ### +### r2.5.0 ### * IMA extension: Wraps the Google Interactive Media Ads (IMA) SDK to provide an easy and seamless way of incorporating display ads into ExoPlayer playbacks. You can read more about the IMA extension [here](https://medium.com/google-exoplayer/playing-ads-with-exoplayer-and-ima-868dfd767ea). * MediaSession extension: Provides an easy to to connect ExoPlayer with - MediaSessionCompat in the Android Support Library. *A link to a blog post - about this extension will be added here prior to the stable 2.5.0 release.* + MediaSessionCompat in the Android Support Library. * RTMP extension: An extension for playing streams over RTMP. * Build: Made it easier for application developers to depend on a local checkout of ExoPlayer. You can learn how to do this @@ -18,8 +17,7 @@ playback of progressive streams ([#2926](https://github.com/google/ExoPlayer/issues/2926)). * New DynamicConcatenatingMediaSource class to support playback of dynamic - playlists. *A link to a blog post about DynamicConcatenatingMediaSource will - be added here prior to the stable 2.5.0 release.* + playlists. * New ExoPlayer.setRepeatMode method for dynamic toggling of repeat mode during playback. Use of setRepeatMode should be preferred to LoopingMediaSource for most looping use cases. You can read more about diff --git a/constants.gradle b/constants.gradle index 93284fd897..7d126ccd89 100644 --- a/constants.gradle +++ b/constants.gradle @@ -24,7 +24,7 @@ project.ext { supportLibraryVersion = '25.4.0' dexmakerVersion = '1.2' mockitoVersion = '1.9.5' - releaseVersion = 'r2.5.0-beta3' + releaseVersion = 'r2.5.0' modulePrefix = ':' if (gradle.ext.has('exoplayerModulePrefix')) { modulePrefix += gradle.ext.exoplayerModulePrefix diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java index 153c6cda9c..fd5ead5c85 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java @@ -31,13 +31,13 @@ public final class ExoPlayerLibraryInfo { * The version of the library expressed as a string, for example "1.2.3". */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. - public static final String VERSION = "2.5.0-beta3"; + public static final String VERSION = "2.5.0"; /** * The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final String VERSION_SLASHY = "ExoPlayerLib/2.5.0-beta3"; + public static final String VERSION_SLASHY = "ExoPlayerLib/2.5.0"; /** * The version of the library expressed as an integer, for example 1002003. From 1f66f30ccdb51eea086619c6933cad0f73de4d15 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 7 Aug 2017 01:51:26 -0700 Subject: [PATCH 0210/2472] Clean up extension READMEs Issue: #1157 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164434768 --- extensions/cronet/README.md | 33 ++++++++++++++++++++++++++----- extensions/ffmpeg/README.md | 2 +- extensions/flac/README.md | 6 +++--- extensions/gvr/README.md | 2 +- extensions/mediasession/README.md | 4 ++-- extensions/okhttp/README.md | 29 +++++++++++++++++++++++++-- extensions/opus/README.md | 6 +++--- extensions/rtmp/README.md | 4 ++-- extensions/vp9/README.md | 6 +++--- 9 files changed, 70 insertions(+), 22 deletions(-) diff --git a/extensions/cronet/README.md b/extensions/cronet/README.md index 30409fa99e..2287c4c19b 100644 --- a/extensions/cronet/README.md +++ b/extensions/cronet/README.md @@ -1,15 +1,13 @@ -# ExoPlayer Cronet Extension # +# ExoPlayer Cronet extension # ## Description ## -[Cronet][] is Chromium's Networking stack packaged as a library. - -The Cronet Extension is an [HttpDataSource][] implementation using [Cronet][]. +The Cronet extension is an [HttpDataSource][] implementation using [Cronet][]. [HttpDataSource]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer/upstream/HttpDataSource.html [Cronet]: https://chromium.googlesource.com/chromium/src/+/master/components/cronet?autodive=0%2F%2F -## Build Instructions ## +## Build instructions ## To use this extension you need to clone the ExoPlayer repository and depend on its modules locally. Instructions for doing this can be found in ExoPlayer's @@ -33,3 +31,28 @@ gradle.ext.exoplayerIncludeCronetExtension = true; [top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md [here]: https://console.cloud.google.com/storage/browser/chromium-cronet/android + +## Using the extension ## + +ExoPlayer requests data through `DataSource` instances. These instances are +either instantiated and injected from application code, or obtained from +instances of `DataSource.Factory` that are instantiated and injected from +application code. + +If your application only needs to play http(s) content, using the Cronet +extension is as simple as updating any `DataSource`s and `DataSource.Factory` +instantiations in your application code to use `CronetDataSource` and +`CronetDataSourceFactory` respectively. If your application also needs to play +non-http(s) content such as local files, use +``` +new DefaultDataSource( + ... + new CronetDataSource(...) /* baseDataSource argument */); +``` +and +``` +new DefaultDataSourceFactory( + ... + new CronetDataSourceFactory(...) /* baseDataSourceFactory argument */); +``` +respectively. diff --git a/extensions/ffmpeg/README.md b/extensions/ffmpeg/README.md index ab3e5ffb94..b4514effbc 100644 --- a/extensions/ffmpeg/README.md +++ b/extensions/ffmpeg/README.md @@ -1,4 +1,4 @@ -# FfmpegAudioRenderer # +# ExoPlayer FFmpeg extension # ## Description ## diff --git a/extensions/flac/README.md b/extensions/flac/README.md index a35dac7858..9db2e5727d 100644 --- a/extensions/flac/README.md +++ b/extensions/flac/README.md @@ -1,14 +1,14 @@ -# ExoPlayer Flac Extension # +# ExoPlayer Flac extension # ## Description ## -The Flac Extension is a [Renderer][] implementation that helps you bundle +The Flac extension is a [Renderer][] implementation that helps you bundle libFLAC (the Flac decoding library) into your app and use it along with ExoPlayer to play Flac audio on Android devices. [Renderer]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/Renderer.html -## Build Instructions ## +## Build instructions ## To use this extension you need to clone the ExoPlayer repository and depend on its modules locally. Instructions for doing this can be found in ExoPlayer's diff --git a/extensions/gvr/README.md b/extensions/gvr/README.md index ad28569121..7e072d070c 100644 --- a/extensions/gvr/README.md +++ b/extensions/gvr/README.md @@ -1,4 +1,4 @@ -# ExoPlayer GVR Extension # +# ExoPlayer GVR extension # ## Description ## diff --git a/extensions/mediasession/README.md b/extensions/mediasession/README.md index 7515cf9eef..3acf8e4c79 100644 --- a/extensions/mediasession/README.md +++ b/extensions/mediasession/README.md @@ -1,8 +1,8 @@ -# ExoPlayer MediaSession Extension # +# ExoPlayer MediaSession extension # ## Description ## -The MediaSession Extension mediates between an ExoPlayer instance and a +The MediaSession extension mediates between an ExoPlayer instance and a [MediaSession][]. It automatically retrieves and implements playback actions and syncs the player state with the state of the media session. The behaviour can be extended to support other playback and custom actions. diff --git a/extensions/okhttp/README.md b/extensions/okhttp/README.md index 52d5fabf38..b10c4ba629 100644 --- a/extensions/okhttp/README.md +++ b/extensions/okhttp/README.md @@ -1,8 +1,8 @@ -# ExoPlayer OkHttp Extension # +# ExoPlayer OkHttp extension # ## Description ## -The OkHttp Extension is an [HttpDataSource][] implementation using Square's +The OkHttp extension is an [HttpDataSource][] implementation using Square's [OkHttp][]. [HttpDataSource]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/upstream/HttpDataSource.html @@ -24,3 +24,28 @@ locally. Instructions for doing this can be found in ExoPlayer's [top level README][]. [top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md + +## Using the extension ## + +ExoPlayer requests data through `DataSource` instances. These instances are +either instantiated and injected from application code, or obtained from +instances of `DataSource.Factory` that are instantiated and injected from +application code. + +If your application only needs to play http(s) content, using the OkHttp +extension is as simple as updating any `DataSource`s and `DataSource.Factory` +instantiations in your application code to use `OkHttpDataSource` and +`OkHttpDataSourceFactory` respectively. If your application also needs to play +non-http(s) content such as local files, use +``` +new DefaultDataSource( + ... + new OkHttpDataSource(...) /* baseDataSource argument */); +``` +and +``` +new DefaultDataSourceFactory( + ... + new OkHttpDataSourceFactory(...) /* baseDataSourceFactory argument */); +``` +respectively. diff --git a/extensions/opus/README.md b/extensions/opus/README.md index ae42a9c310..e5f5bcb168 100644 --- a/extensions/opus/README.md +++ b/extensions/opus/README.md @@ -1,14 +1,14 @@ -# ExoPlayer Opus Extension # +# ExoPlayer Opus extension # ## Description ## -The Opus Extension is a [Renderer][] implementation that helps you bundle +The Opus extension is a [Renderer][] implementation that helps you bundle libopus (the Opus decoding library) into your app and use it along with ExoPlayer to play Opus audio on Android devices. [Renderer]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/Renderer.html -## Build Instructions ## +## Build instructions ## To use this extension you need to clone the ExoPlayer repository and depend on its modules locally. Instructions for doing this can be found in ExoPlayer's diff --git a/extensions/rtmp/README.md b/extensions/rtmp/README.md index 2cfa6b8ff4..042d7078dc 100644 --- a/extensions/rtmp/README.md +++ b/extensions/rtmp/README.md @@ -1,8 +1,8 @@ -# ExoPlayer RTMP Extension # +# ExoPlayer RTMP extension # ## Description ## -The RTMP Extension is a [DataSource][] implementation for playing [RTMP][] +The RTMP extension is a [DataSource][] implementation for playing [RTMP][] streams using [LibRtmp Client for Android][]. [DataSource]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/upstream/DataSource.html diff --git a/extensions/vp9/README.md b/extensions/vp9/README.md index 8bdfe652e6..87c5c8d54f 100644 --- a/extensions/vp9/README.md +++ b/extensions/vp9/README.md @@ -1,14 +1,14 @@ -# ExoPlayer VP9 Extension # +# ExoPlayer VP9 extension # ## Description ## -The VP9 Extension is a [Renderer][] implementation that helps you bundle libvpx +The VP9 extension is a [Renderer][] implementation that helps you bundle libvpx (the VP9 decoding library) into your app and use it along with ExoPlayer to play VP9 video on Android devices. [Renderer]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/Renderer.html -## Build Instructions ## +## Build instructions ## To use this extension you need to clone the ExoPlayer repository and depend on its modules locally. Instructions for doing this can be found in ExoPlayer's From df05195f5fdaf12f83fa3e615d3cdc20e4dce401 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 7 Aug 2017 01:53:09 -0700 Subject: [PATCH 0211/2472] Set correct Content-Type for ClearKey requests Issue: #3138 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164434858 --- .../exoplayer2/drm/HttpMediaDrmCallback.java | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java index f9d5efffb1..f08d9b59b5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java @@ -38,14 +38,6 @@ import java.util.UUID; @TargetApi(18) public final class HttpMediaDrmCallback implements MediaDrmCallback { - private static final Map PLAYREADY_KEY_REQUEST_PROPERTIES; - static { - PLAYREADY_KEY_REQUEST_PROPERTIES = new HashMap<>(); - PLAYREADY_KEY_REQUEST_PROPERTIES.put("Content-Type", "text/xml"); - PLAYREADY_KEY_REQUEST_PROPERTIES.put("SOAPAction", - "http://schemas.microsoft.com/DRM/2007/03/protocols/AcquireLicense"); - } - private final HttpDataSource.Factory dataSourceFactory; private final String defaultUrl; private final Map keyRequestProperties; @@ -124,10 +116,15 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback { url = defaultUrl; } Map requestProperties = new HashMap<>(); - requestProperties.put("Content-Type", "application/octet-stream"); + // Add standard request properties for supported schemes. + String contentType = C.PLAYREADY_UUID.equals(uuid) ? "text/xml" + : (C.CLEARKEY_UUID.equals(uuid) ? "application/json" : "application/octet-stream"); + requestProperties.put("Content-Type", contentType); if (C.PLAYREADY_UUID.equals(uuid)) { - requestProperties.putAll(PLAYREADY_KEY_REQUEST_PROPERTIES); + requestProperties.put("SOAPAction", + "http://schemas.microsoft.com/DRM/2007/03/protocols/AcquireLicense"); } + // Add additional request properties. synchronized (keyRequestProperties) { requestProperties.putAll(keyRequestProperties); } From 15bcdf3b71f02b4ce2f8328625fb43a5b0f25fe6 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 7 Aug 2017 01:54:51 -0700 Subject: [PATCH 0212/2472] Don't use TextureView's SurfaceTexture unless available ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164434943 --- .../java/com/google/android/exoplayer2/SimpleExoPlayer.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index ebfe380b6b..08e178878b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -276,7 +276,8 @@ public class SimpleExoPlayer implements ExoPlayer { Log.w(TAG, "Replacing existing SurfaceTextureListener."); } textureView.setSurfaceTextureListener(componentListener); - SurfaceTexture surfaceTexture = textureView.getSurfaceTexture(); + SurfaceTexture surfaceTexture = textureView.isAvailable() ? textureView.getSurfaceTexture() + : null; setVideoSurfaceInternal(surfaceTexture == null ? null : new Surface(surfaceTexture), true); } } From f88149385a63c407f0589f496b87a864a1aa32cd Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 7 Aug 2017 03:45:10 -0700 Subject: [PATCH 0213/2472] Avoid rollover calculating sample offsets I considered using Util.scaleLargeTimestamp for this, but given sample offsets are relative and should always be small (<<1s), it really shouldn't be necessary. Issue: #3139 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164443795 --- .../exoplayer2/extractor/mp4/FragmentedMp4Extractor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/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 index 6dcae9c2d6..6b2077ef76 100644 --- a/library/core/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 @@ -815,7 +815,7 @@ public final class FragmentedMp4Extractor implements Extractor { // here, because unsigned integers will still be parsed correctly (unless their top bit is // set, which is never true in practice because sample offsets are always small). int sampleOffset = trun.readInt(); - sampleCompositionTimeOffsetTable[i] = (int) ((sampleOffset * 1000) / timescale); + sampleCompositionTimeOffsetTable[i] = (int) ((sampleOffset * 1000L) / timescale); } else { sampleCompositionTimeOffsetTable[i] = 0; } From e713ddc22d6976b55aa4e178e4cd1343e7219df0 Mon Sep 17 00:00:00 2001 From: eguven Date: Mon, 7 Aug 2017 05:22:15 -0700 Subject: [PATCH 0214/2472] Disable test coverage again https://issuetracker.google.com/issues/37019591 causes local variables can't be found while debugging. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164449443 --- library/core/build.gradle | 8 +++++--- library/dash/build.gradle | 8 +++++--- library/hls/build.gradle | 8 +++++--- library/smoothstreaming/build.gradle | 8 +++++--- library/ui/build.gradle | 8 +++++--- 5 files changed, 25 insertions(+), 15 deletions(-) diff --git a/library/core/build.gradle b/library/core/build.gradle index 65a7353607..ecad1e58b5 100644 --- a/library/core/build.gradle +++ b/library/core/build.gradle @@ -31,9 +31,11 @@ android { } buildTypes { - debug { - testCoverageEnabled = true - } + // Re-enable test coverage when the following issue is fixed: + // https://issuetracker.google.com/issues/37019591 + // debug { + // testCoverageEnabled = true + // } } } diff --git a/library/dash/build.gradle b/library/dash/build.gradle index aa8031467e..2220e5b250 100644 --- a/library/dash/build.gradle +++ b/library/dash/build.gradle @@ -24,9 +24,11 @@ android { } buildTypes { - debug { - testCoverageEnabled = true - } + // Re-enable test coverage when the following issue is fixed: + // https://issuetracker.google.com/issues/37019591 + // debug { + // testCoverageEnabled = true + // } } } diff --git a/library/hls/build.gradle b/library/hls/build.gradle index 77680569f0..ac77725ca5 100644 --- a/library/hls/build.gradle +++ b/library/hls/build.gradle @@ -24,9 +24,11 @@ android { } buildTypes { - debug { - testCoverageEnabled = true - } + // Re-enable test coverage when the following issue is fixed: + // https://issuetracker.google.com/issues/37019591 + // debug { + // testCoverageEnabled = true + // } } } diff --git a/library/smoothstreaming/build.gradle b/library/smoothstreaming/build.gradle index b5f918075f..ee5a8c4e73 100644 --- a/library/smoothstreaming/build.gradle +++ b/library/smoothstreaming/build.gradle @@ -24,9 +24,11 @@ android { } buildTypes { - debug { - testCoverageEnabled = true - } + // Re-enable test coverage when the following issue is fixed: + // https://issuetracker.google.com/issues/37019591 + // debug { + // testCoverageEnabled = true + // } } } diff --git a/library/ui/build.gradle b/library/ui/build.gradle index c036bc9819..89734ed806 100644 --- a/library/ui/build.gradle +++ b/library/ui/build.gradle @@ -24,9 +24,11 @@ android { } buildTypes { - debug { - testCoverageEnabled = true - } + // Re-enable test coverage when the following issue is fixed: + // https://issuetracker.google.com/issues/37019591 + // debug { + // testCoverageEnabled = true + // } } } From 38360f626729a75a016ba20a546b40405b30ea80 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Mon, 7 Aug 2017 11:05:12 -0700 Subject: [PATCH 0215/2472] Add support for HLS's FRAME-RATE attribute ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164482135 --- .../hls/playlist/HlsMasterPlaylistParserTest.java | 7 +++++-- .../source/hls/playlist/HlsPlaylistParser.java | 11 ++++++++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/library/hls/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 index f835c87466..35cd7f03d8 100644 --- a/library/hls/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 @@ -42,10 +42,10 @@ public class HlsMasterPlaylistParserTest extends TestCase { + "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2 , avc1.66.30 \"\n" + "http://example.com/spaces_in_codecs.m3u8\n" + "\n" - + "#EXT-X-STREAM-INF:BANDWIDTH=2560000,RESOLUTION=384x160\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=2560000,FRAME-RATE=25,RESOLUTION=384x160\n" + "http://example.com/mid.m3u8\n" + "\n" - + "#EXT-X-STREAM-INF:BANDWIDTH=7680000\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=7680000,FRAME-RATE=29.997\n" + "http://example.com/hi.m3u8\n" + "\n" + "#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"mp4a.40.5\"\n" @@ -96,18 +96,21 @@ public class HlsMasterPlaylistParserTest extends TestCase { assertNull(variants.get(2).format.codecs); assertEquals(384, variants.get(2).format.width); assertEquals(160, variants.get(2).format.height); + assertEquals(25.0f, variants.get(2).format.frameRate); assertEquals("http://example.com/mid.m3u8", variants.get(2).url); assertEquals(7680000, variants.get(3).format.bitrate); assertNull(variants.get(3).format.codecs); assertEquals(Format.NO_VALUE, variants.get(3).format.width); assertEquals(Format.NO_VALUE, variants.get(3).format.height); + assertEquals(29.997f, variants.get(3).format.frameRate); assertEquals("http://example.com/hi.m3u8", variants.get(3).url); assertEquals(65000, variants.get(4).format.bitrate); assertEquals("mp4a.40.5", variants.get(4).format.codecs); assertEquals(Format.NO_VALUE, variants.get(4).format.width); assertEquals(Format.NO_VALUE, variants.get(4).format.height); + assertEquals((float) Format.NO_VALUE, variants.get(4).format.frameRate); assertEquals("http://example.com/audio-only.m3u8", variants.get(4).url); } diff --git a/library/hls/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 index 09d6fcfa18..c5d3302eca 100644 --- a/library/hls/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 @@ -81,6 +81,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser Date: Mon, 7 Aug 2017 13:28:01 -0700 Subject: [PATCH 0216/2472] Fix missing source info refresh notification ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164502580 --- .../exoplayer2/ExoPlayerImplInternal.java | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index cb04501fc0..a789dbc1b2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -975,11 +975,10 @@ import java.io.IOException; mediaPeriodInfoSequence.setTimeline(timeline); Object manifest = timelineAndManifest.second; - int processedInitialSeekCount = 0; if (oldTimeline == null) { if (pendingInitialSeekCount > 0) { Pair periodPosition = resolveSeekPosition(pendingSeekPosition); - processedInitialSeekCount = pendingInitialSeekCount; + int processedInitialSeekCount = pendingInitialSeekCount; pendingInitialSeekCount = 0; pendingSeekPosition = null; if (periodPosition == null) { @@ -996,7 +995,7 @@ import java.io.IOException; } } else if (playbackInfo.startPositionUs == C.TIME_UNSET) { if (timeline.isEmpty()) { - handleSourceInfoRefreshEndedPlayback(manifest, processedInitialSeekCount); + handleSourceInfoRefreshEndedPlayback(manifest); } else { Pair defaultPosition = getPeriodPosition(0, C.TIME_UNSET); int periodIndex = defaultPosition.first; @@ -1005,8 +1004,10 @@ import java.io.IOException; startPositionUs); playbackInfo = new PlaybackInfo(periodId, periodId.isAd() ? 0 : startPositionUs, startPositionUs); - notifySourceInfoRefresh(manifest, processedInitialSeekCount); + notifySourceInfoRefresh(manifest); } + } else { + notifySourceInfoRefresh(manifest); } return; } @@ -1015,7 +1016,7 @@ import java.io.IOException; MediaPeriodHolder periodHolder = playingPeriodHolder != null ? playingPeriodHolder : loadingPeriodHolder; if (periodHolder == null && playingPeriodIndex >= oldTimeline.getPeriodCount()) { - notifySourceInfoRefresh(manifest, processedInitialSeekCount); + notifySourceInfoRefresh(manifest); return; } Object playingPeriodUid = periodHolder == null @@ -1027,7 +1028,7 @@ import java.io.IOException; int newPeriodIndex = resolveSubsequentPeriod(playingPeriodIndex, oldTimeline, timeline); if (newPeriodIndex == C.INDEX_UNSET) { // We failed to resolve a suitable restart position. - handleSourceInfoRefreshEndedPlayback(manifest, processedInitialSeekCount); + handleSourceInfoRefreshEndedPlayback(manifest); return; } // We resolved a subsequent period. Seek to the default position in the corresponding window. @@ -1055,7 +1056,7 @@ import java.io.IOException; MediaPeriodId periodId = new MediaPeriodId(newPeriodIndex); newPositionUs = seekToPeriodPosition(periodId, newPositionUs); playbackInfo = new PlaybackInfo(periodId, newPositionUs); - notifySourceInfoRefresh(manifest, processedInitialSeekCount); + notifySourceInfoRefresh(manifest); return; } @@ -1072,14 +1073,14 @@ import java.io.IOException; long newPositionUs = seekToPeriodPosition(periodId, playbackInfo.contentPositionUs); long contentPositionUs = periodId.isAd() ? playbackInfo.contentPositionUs : C.TIME_UNSET; playbackInfo = new PlaybackInfo(periodId, newPositionUs, contentPositionUs); - notifySourceInfoRefresh(manifest, processedInitialSeekCount); + notifySourceInfoRefresh(manifest); return; } } if (periodHolder == null) { // We don't have any period holders, so we're done. - notifySourceInfoRefresh(manifest, processedInitialSeekCount); + notifySourceInfoRefresh(manifest); return; } @@ -1117,7 +1118,7 @@ import java.io.IOException; } } - notifySourceInfoRefresh(manifest, processedInitialSeekCount); + notifySourceInfoRefresh(manifest); } private MediaPeriodHolder updatePeriodInfo(MediaPeriodHolder periodHolder, int periodIndex) { @@ -1131,6 +1132,10 @@ import java.io.IOException; } } + private void handleSourceInfoRefreshEndedPlayback(Object manifest) { + handleSourceInfoRefreshEndedPlayback(manifest, 0); + } + private void handleSourceInfoRefreshEndedPlayback(Object manifest, int processedInitialSeekCount) { // Set the playback position to (0,0) for notifying the eventHandler. @@ -1143,6 +1148,10 @@ import java.io.IOException; resetInternal(false); } + private void notifySourceInfoRefresh(Object manifest) { + notifySourceInfoRefresh(manifest, 0); + } + private void notifySourceInfoRefresh(Object manifest, int processedInitialSeekCount) { eventHandler.obtainMessage(MSG_SOURCE_INFO_REFRESHED, new SourceInfo(timeline, manifest, playbackInfo, processedInitialSeekCount)).sendToTarget(); From 24bf2aa0e25041203a25b7172b88a3a01e591076 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 8 Aug 2017 04:07:13 -0700 Subject: [PATCH 0217/2472] Support clip end greater than the child's duration ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164577001 --- .../exoplayer2/source/ClippingMediaSource.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java index 4caafa3110..2387b43d5e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java @@ -47,7 +47,9 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste * start providing samples, in microseconds. * @param endPositionUs The end position within {@code mediaSource}'s timeline at which to stop * providing samples, in microseconds. Specify {@link C#TIME_END_OF_SOURCE} to provide samples - * from the specified start point up to the end of the source. + * from the specified start point up to the end of the source. Specifying a position that + * exceeds the {@code mediaSource}'s duration will also result in the end of the source not + * being clipped. */ public ClippingMediaSource(MediaSource mediaSource, long startPositionUs, long endPositionUs) { this(mediaSource, startPositionUs, endPositionUs, true); @@ -65,7 +67,9 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste * start providing samples, in microseconds. * @param endPositionUs The end position within {@code mediaSource}'s timeline at which to stop * providing samples, in microseconds. Specify {@link C#TIME_END_OF_SOURCE} to provide samples - * from the specified start point up to the end of the source. + * from the specified start point up to the end of the source. Specifying a position that + * exceeds the {@code mediaSource}'s duration will also result in the end of the source not + * being clipped. * @param enableInitialDiscontinuity Whether the initial discontinuity should be enabled. */ public ClippingMediaSource(MediaSource mediaSource, long startPositionUs, long endPositionUs, @@ -148,8 +152,10 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste Assertions.checkArgument(!window.isDynamic); long resolvedEndUs = endUs == C.TIME_END_OF_SOURCE ? window.durationUs : endUs; if (window.durationUs != C.TIME_UNSET) { + if (resolvedEndUs > window.durationUs) { + resolvedEndUs = window.durationUs; + } Assertions.checkArgument(startUs == 0 || window.isSeekable); - Assertions.checkArgument(resolvedEndUs <= window.durationUs); Assertions.checkArgument(startUs <= resolvedEndUs); } Period period = timeline.getPeriod(0, new Period()); From 072788c88aa7f53ec2b671d7b95efd712fe0e2d8 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 8 Aug 2017 07:36:31 -0700 Subject: [PATCH 0218/2472] Support building of version 1 PSSH atoms Issue: #3138 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164590835 --- .../extractor/mp4/PsshAtomUtilTest.java | 47 +++++++++++++++++++ .../extractor/mp4/PsshAtomUtil.java | 45 ++++++++++++++---- 2 files changed, 83 insertions(+), 9 deletions(-) create mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtilTest.java diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtilTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtilTest.java new file mode 100644 index 0000000000..5ac3979746 --- /dev/null +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtilTest.java @@ -0,0 +1,47 @@ +/* + * 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.mp4; + +import android.test.MoreAsserts; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.util.ParsableByteArray; +import java.util.UUID; +import junit.framework.TestCase; + +/** + * Tests for {@link PsshAtomUtil}. + */ +public class PsshAtomUtilTest extends TestCase { + + public void testBuildPsshAtom() { + byte[] schemeData = new byte[]{0, 1, 2, 3, 4, 5}; + byte[] psshAtom = PsshAtomUtil.buildPsshAtom(C.WIDEVINE_UUID, schemeData); + // Read the PSSH atom back and assert its content is as expected. + ParsableByteArray parsablePsshAtom = new ParsableByteArray(psshAtom); + assertEquals(psshAtom.length, parsablePsshAtom.readUnsignedIntToInt()); // length + assertEquals(Atom.TYPE_pssh, parsablePsshAtom.readInt()); // type + int fullAtomInt = parsablePsshAtom.readInt(); // version + flags + assertEquals(0, Atom.parseFullAtomVersion(fullAtomInt)); + assertEquals(0, Atom.parseFullAtomFlags(fullAtomInt)); + UUID systemId = new UUID(parsablePsshAtom.readLong(), parsablePsshAtom.readLong()); + assertEquals(C.WIDEVINE_UUID, systemId); + assertEquals(schemeData.length, parsablePsshAtom.readUnsignedIntToInt()); + byte[] psshSchemeData = new byte[schemeData.length]; + parsablePsshAtom.readBytes(psshSchemeData, 0, schemeData.length); + MoreAsserts.assertEquals(schemeData, psshSchemeData); + } + +} diff --git a/library/core/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 index 6d5c372619..cfca015348 100644 --- a/library/core/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 @@ -31,22 +31,48 @@ public final class PsshAtomUtil { private PsshAtomUtil() {} /** - * Builds a PSSH atom for a given {@link UUID} containing the given scheme specific data. + * Builds a version 0 PSSH atom for a given system id, containing the given data. * - * @param uuid The UUID of the scheme. + * @param systemId The system id of the scheme. * @param data The scheme specific data. * @return The PSSH atom. */ - public static byte[] buildPsshAtom(UUID uuid, byte[] data) { - int psshBoxLength = Atom.FULL_HEADER_SIZE + 16 /* UUID */ + 4 /* DataSize */ + data.length; + public static byte[] buildPsshAtom(UUID systemId, byte[] data) { + return buildPsshAtom(systemId, null, data); + } + + /** + * Builds a PSSH atom for the given system id, containing the given key ids and data. + * + * @param systemId The system id of the scheme. + * @param keyIds The key ids for a version 1 PSSH atom, or null for a version 0 PSSH atom. + * @param data The scheme specific data. + * @return The PSSH atom. + */ + public static byte[] buildPsshAtom(UUID systemId, UUID[] keyIds, byte[] data) { + boolean buildV1Atom = keyIds != null; + int dataLength = data != null ? data.length : 0; + int psshBoxLength = Atom.FULL_HEADER_SIZE + 16 /* SystemId */ + 4 /* DataSize */ + dataLength; + if (buildV1Atom) { + psshBoxLength += 4 /* KID_count */ + (keyIds.length * 16) /* KIDs */; + } ByteBuffer psshBox = ByteBuffer.allocate(psshBoxLength); psshBox.putInt(psshBoxLength); psshBox.putInt(Atom.TYPE_pssh); - psshBox.putInt(0 /* version=0, flags=0 */); - psshBox.putLong(uuid.getMostSignificantBits()); - psshBox.putLong(uuid.getLeastSignificantBits()); - psshBox.putInt(data.length); - psshBox.put(data); + psshBox.putInt(buildV1Atom ? 0x01000000 : 0 /* version=(buildV1Atom ? 1 : 0), flags=0 */); + psshBox.putLong(systemId.getMostSignificantBits()); + psshBox.putLong(systemId.getLeastSignificantBits()); + if (buildV1Atom) { + psshBox.putInt(keyIds.length); + for (UUID keyId : keyIds) { + psshBox.putLong(keyId.getMostSignificantBits()); + psshBox.putLong(keyId.getLeastSignificantBits()); + } + } + if (dataLength != 0) { + psshBox.putInt(data.length); + psshBox.put(data); + } // Else the last 4 bytes are a 0 DataSize. return psshBox.array(); } @@ -98,6 +124,7 @@ public final class PsshAtomUtil { * @return A pair consisting of the parsed UUID and scheme specific data. Null if the input is * not a valid PSSH atom, or if the PSSH atom has an unsupported version. */ + // TODO: Support parsing of the key ids for version 1 PSSH atoms. private static Pair parsePsshAtom(byte[] atom) { ParsableByteArray atomData = new ParsableByteArray(atom); if (atomData.limit() < Atom.FULL_HEADER_SIZE + 16 /* UUID */ + 4 /* DataSize */) { From b8c6ed670138ee4f864dc9cd7ba9cb5ff8a43094 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 8 Aug 2017 08:30:17 -0700 Subject: [PATCH 0219/2472] Bump to 2.5.1 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164595874 --- RELEASENOTES.md | 7 +++++++ constants.gradle | 2 +- demos/main/src/main/AndroidManifest.xml | 4 ++-- .../google/android/exoplayer2/ExoPlayerLibraryInfo.java | 6 +++--- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 4101caad47..8ad866395e 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,12 @@ # Release notes # +### r2.5.1 ### + +* Fix an issue that could cause the reported playback position to stop advancing + in some cases. +* Fix an issue where a Surface could be released whilst still in use by the + player. + ### r2.5.0 ### * IMA extension: Wraps the Google Interactive Media Ads (IMA) SDK to provide an diff --git a/constants.gradle b/constants.gradle index 7d126ccd89..b7cc8b6906 100644 --- a/constants.gradle +++ b/constants.gradle @@ -24,7 +24,7 @@ project.ext { supportLibraryVersion = '25.4.0' dexmakerVersion = '1.2' mockitoVersion = '1.9.5' - releaseVersion = 'r2.5.0' + releaseVersion = 'r2.5.1' modulePrefix = ':' if (gradle.ext.has('exoplayerModulePrefix')) { modulePrefix += gradle.ext.exoplayerModulePrefix diff --git a/demos/main/src/main/AndroidManifest.xml b/demos/main/src/main/AndroidManifest.xml index 0e04d9a435..1f66822dc7 100644 --- a/demos/main/src/main/AndroidManifest.xml +++ b/demos/main/src/main/AndroidManifest.xml @@ -16,8 +16,8 @@ + android:versionCode="2501" + android:versionName="2.5.1"> diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java index fd5ead5c85..33f992964a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java @@ -31,13 +31,13 @@ public final class ExoPlayerLibraryInfo { * The version of the library expressed as a string, for example "1.2.3". */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. - public static final String VERSION = "2.5.0"; + public static final String VERSION = "2.5.1"; /** * The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final String VERSION_SLASHY = "ExoPlayerLib/2.5.0"; + public static final String VERSION_SLASHY = "ExoPlayerLib/2.5.1"; /** * The version of the library expressed as an integer, for example 1002003. @@ -47,7 +47,7 @@ public final class ExoPlayerLibraryInfo { * integer version 123045006 (123-045-006). */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final int VERSION_INT = 2005000; + public static final int VERSION_INT = 2005001; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} From 026ac1d69b8c0afb8236029a914196abf6e4e2c3 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 8 Aug 2017 08:31:52 -0700 Subject: [PATCH 0220/2472] Don't release a surface until we've stopped using it ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164596062 --- .../java/com/google/android/exoplayer2/SimpleExoPlayer.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index db1b1a13f1..16a44aa016 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -762,12 +762,12 @@ public class SimpleExoPlayer implements ExoPlayer { } } if (this.surface != null && this.surface != surface) { - // If we created this surface, we are responsible for releasing it. + // We're replacing a surface. Block to ensure that it's not accessed after the method returns. + player.blockingSendMessages(messages); + // If we created the previous surface, we are responsible for releasing it. if (this.ownsSurface) { this.surface.release(); } - // We're replacing a surface. Block to ensure that it's not accessed after the method returns. - player.blockingSendMessages(messages); } else { player.sendMessages(messages); } From 371f675ae8d9076a859e28399c55d4aa7da2273a Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 7 Aug 2017 13:28:01 -0700 Subject: [PATCH 0221/2472] Fix missing source info refresh notification ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164502580 --- .../exoplayer2/ExoPlayerImplInternal.java | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index cb04501fc0..a789dbc1b2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -975,11 +975,10 @@ import java.io.IOException; mediaPeriodInfoSequence.setTimeline(timeline); Object manifest = timelineAndManifest.second; - int processedInitialSeekCount = 0; if (oldTimeline == null) { if (pendingInitialSeekCount > 0) { Pair periodPosition = resolveSeekPosition(pendingSeekPosition); - processedInitialSeekCount = pendingInitialSeekCount; + int processedInitialSeekCount = pendingInitialSeekCount; pendingInitialSeekCount = 0; pendingSeekPosition = null; if (periodPosition == null) { @@ -996,7 +995,7 @@ import java.io.IOException; } } else if (playbackInfo.startPositionUs == C.TIME_UNSET) { if (timeline.isEmpty()) { - handleSourceInfoRefreshEndedPlayback(manifest, processedInitialSeekCount); + handleSourceInfoRefreshEndedPlayback(manifest); } else { Pair defaultPosition = getPeriodPosition(0, C.TIME_UNSET); int periodIndex = defaultPosition.first; @@ -1005,8 +1004,10 @@ import java.io.IOException; startPositionUs); playbackInfo = new PlaybackInfo(periodId, periodId.isAd() ? 0 : startPositionUs, startPositionUs); - notifySourceInfoRefresh(manifest, processedInitialSeekCount); + notifySourceInfoRefresh(manifest); } + } else { + notifySourceInfoRefresh(manifest); } return; } @@ -1015,7 +1016,7 @@ import java.io.IOException; MediaPeriodHolder periodHolder = playingPeriodHolder != null ? playingPeriodHolder : loadingPeriodHolder; if (periodHolder == null && playingPeriodIndex >= oldTimeline.getPeriodCount()) { - notifySourceInfoRefresh(manifest, processedInitialSeekCount); + notifySourceInfoRefresh(manifest); return; } Object playingPeriodUid = periodHolder == null @@ -1027,7 +1028,7 @@ import java.io.IOException; int newPeriodIndex = resolveSubsequentPeriod(playingPeriodIndex, oldTimeline, timeline); if (newPeriodIndex == C.INDEX_UNSET) { // We failed to resolve a suitable restart position. - handleSourceInfoRefreshEndedPlayback(manifest, processedInitialSeekCount); + handleSourceInfoRefreshEndedPlayback(manifest); return; } // We resolved a subsequent period. Seek to the default position in the corresponding window. @@ -1055,7 +1056,7 @@ import java.io.IOException; MediaPeriodId periodId = new MediaPeriodId(newPeriodIndex); newPositionUs = seekToPeriodPosition(periodId, newPositionUs); playbackInfo = new PlaybackInfo(periodId, newPositionUs); - notifySourceInfoRefresh(manifest, processedInitialSeekCount); + notifySourceInfoRefresh(manifest); return; } @@ -1072,14 +1073,14 @@ import java.io.IOException; long newPositionUs = seekToPeriodPosition(periodId, playbackInfo.contentPositionUs); long contentPositionUs = periodId.isAd() ? playbackInfo.contentPositionUs : C.TIME_UNSET; playbackInfo = new PlaybackInfo(periodId, newPositionUs, contentPositionUs); - notifySourceInfoRefresh(manifest, processedInitialSeekCount); + notifySourceInfoRefresh(manifest); return; } } if (periodHolder == null) { // We don't have any period holders, so we're done. - notifySourceInfoRefresh(manifest, processedInitialSeekCount); + notifySourceInfoRefresh(manifest); return; } @@ -1117,7 +1118,7 @@ import java.io.IOException; } } - notifySourceInfoRefresh(manifest, processedInitialSeekCount); + notifySourceInfoRefresh(manifest); } private MediaPeriodHolder updatePeriodInfo(MediaPeriodHolder periodHolder, int periodIndex) { @@ -1131,6 +1132,10 @@ import java.io.IOException; } } + private void handleSourceInfoRefreshEndedPlayback(Object manifest) { + handleSourceInfoRefreshEndedPlayback(manifest, 0); + } + private void handleSourceInfoRefreshEndedPlayback(Object manifest, int processedInitialSeekCount) { // Set the playback position to (0,0) for notifying the eventHandler. @@ -1143,6 +1148,10 @@ import java.io.IOException; resetInternal(false); } + private void notifySourceInfoRefresh(Object manifest) { + notifySourceInfoRefresh(manifest, 0); + } + private void notifySourceInfoRefresh(Object manifest, int processedInitialSeekCount) { eventHandler.obtainMessage(MSG_SOURCE_INFO_REFRESHED, new SourceInfo(timeline, manifest, playbackInfo, processedInitialSeekCount)).sendToTarget(); From c1827b10b6501b18bf68277b572e985c1315b76a Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 8 Aug 2017 08:30:17 -0700 Subject: [PATCH 0222/2472] Bump to 2.5.1 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164595874 --- RELEASENOTES.md | 7 +++++++ constants.gradle | 2 +- demo/src/main/AndroidManifest.xml | 4 ++-- .../google/android/exoplayer2/ExoPlayerLibraryInfo.java | 6 +++--- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 4101caad47..8ad866395e 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,12 @@ # Release notes # +### r2.5.1 ### + +* Fix an issue that could cause the reported playback position to stop advancing + in some cases. +* Fix an issue where a Surface could be released whilst still in use by the + player. + ### r2.5.0 ### * IMA extension: Wraps the Google Interactive Media Ads (IMA) SDK to provide an diff --git a/constants.gradle b/constants.gradle index 7d126ccd89..b7cc8b6906 100644 --- a/constants.gradle +++ b/constants.gradle @@ -24,7 +24,7 @@ project.ext { supportLibraryVersion = '25.4.0' dexmakerVersion = '1.2' mockitoVersion = '1.9.5' - releaseVersion = 'r2.5.0' + releaseVersion = 'r2.5.1' modulePrefix = ':' if (gradle.ext.has('exoplayerModulePrefix')) { modulePrefix += gradle.ext.exoplayerModulePrefix diff --git a/demo/src/main/AndroidManifest.xml b/demo/src/main/AndroidManifest.xml index 0e04d9a435..1f66822dc7 100644 --- a/demo/src/main/AndroidManifest.xml +++ b/demo/src/main/AndroidManifest.xml @@ -16,8 +16,8 @@ + android:versionCode="2501" + android:versionName="2.5.1"> diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java index fd5ead5c85..33f992964a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java @@ -31,13 +31,13 @@ public final class ExoPlayerLibraryInfo { * The version of the library expressed as a string, for example "1.2.3". */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. - public static final String VERSION = "2.5.0"; + public static final String VERSION = "2.5.1"; /** * The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final String VERSION_SLASHY = "ExoPlayerLib/2.5.0"; + public static final String VERSION_SLASHY = "ExoPlayerLib/2.5.1"; /** * The version of the library expressed as an integer, for example 1002003. @@ -47,7 +47,7 @@ public final class ExoPlayerLibraryInfo { * integer version 123045006 (123-045-006). */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final int VERSION_INT = 2005000; + public static final int VERSION_INT = 2005001; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} From 13bcc0062e44f823296968fe040442aaacfd3515 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 8 Aug 2017 08:31:52 -0700 Subject: [PATCH 0223/2472] Don't release a surface until we've stopped using it ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164596062 --- .../java/com/google/android/exoplayer2/SimpleExoPlayer.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 08e178878b..3a3768bcc2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -749,12 +749,12 @@ public class SimpleExoPlayer implements ExoPlayer { } } if (this.surface != null && this.surface != surface) { - // If we created this surface, we are responsible for releasing it. + // We're replacing a surface. Block to ensure that it's not accessed after the method returns. + player.blockingSendMessages(messages); + // If we created the previous surface, we are responsible for releasing it. if (this.ownsSurface) { this.surface.release(); } - // We're replacing a surface. Block to ensure that it's not accessed after the method returns. - player.blockingSendMessages(messages); } else { player.sendMessages(messages); } From a6fd4bdaf078375bc67820ec774ac0d1360c2dac Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 8 Aug 2017 04:07:13 -0700 Subject: [PATCH 0224/2472] Support clip end greater than the child's duration ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164577001 --- .../exoplayer2/source/ClippingMediaSource.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java index 32c4eb6c73..8be6bf028f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java @@ -48,7 +48,9 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste * start providing samples, in microseconds. * @param endPositionUs The end position within {@code mediaSource}'s timeline at which to stop * providing samples, in microseconds. Specify {@link C#TIME_END_OF_SOURCE} to provide samples - * from the specified start point up to the end of the source. + * from the specified start point up to the end of the source. Specifying a position that + * exceeds the {@code mediaSource}'s duration will also result in the end of the source not + * being clipped. */ public ClippingMediaSource(MediaSource mediaSource, long startPositionUs, long endPositionUs) { this(mediaSource, startPositionUs, endPositionUs, true); @@ -66,7 +68,9 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste * start providing samples, in microseconds. * @param endPositionUs The end position within {@code mediaSource}'s timeline at which to stop * providing samples, in microseconds. Specify {@link C#TIME_END_OF_SOURCE} to provide samples - * from the specified start point up to the end of the source. + * from the specified start point up to the end of the source. Specifying a position that + * exceeds the {@code mediaSource}'s duration will also result in the end of the source not + * being clipped. * @param enableInitialDiscontinuity Whether the initial discontinuity should be enabled. */ public ClippingMediaSource(MediaSource mediaSource, long startPositionUs, long endPositionUs, @@ -149,8 +153,10 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste Assertions.checkArgument(!window.isDynamic); long resolvedEndUs = endUs == C.TIME_END_OF_SOURCE ? window.durationUs : endUs; if (window.durationUs != C.TIME_UNSET) { + if (resolvedEndUs > window.durationUs) { + resolvedEndUs = window.durationUs; + } Assertions.checkArgument(startUs == 0 || window.isSeekable); - Assertions.checkArgument(resolvedEndUs <= window.durationUs); Assertions.checkArgument(startUs <= resolvedEndUs); } Period period = timeline.getPeriod(0, new Period()); From 65e212c409800f49d823d66ed55fb799799c77f1 Mon Sep 17 00:00:00 2001 From: Dulmandakh Date: Wed, 9 Aug 2017 15:42:58 +0900 Subject: [PATCH 0225/2472] expose setPropertyByteArray, setPropertyString export setPropertyByteArray, setPropertyString of DefaultDrmSessionManager for easy customization. --- .../exoplayer2/drm/OfflineLicenseHelper.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java index 040ca50c76..b5927dcd95 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java @@ -185,6 +185,22 @@ public final class OfflineLicenseHelper { } return licenseDurationRemainingSec; } + + public byte[] getPropertyByteArray(String key) { + return drmSessionManager.getPropertyByteArray(key); + } + + public void setPropertyByteArray(String key, byte[] value) { + drmSessionManager.setPropertyByteArray(key, value); + } + + public String getPropertyString(String key) { + return drmSessionManager.getPropertyString(key); + } + + public void setPropertyString(String key, String value) { + drmSessionManager.setPropertyString(key, value); + } private byte[] blockingKeyRequest(@Mode int licenseMode, byte[] offlineLicenseKeySetId, DrmInitData drmInitData) throws DrmSessionException { From ceb1e87219666f6c47f87f8f46df5de6f580a612 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 10 Aug 2017 12:57:06 +0100 Subject: [PATCH 0226/2472] Revert "Add possibility of forcing a specific license URL in HttpMediaDrmCallback" This reverts commit 768a73b377d842aeefdd466cf7a5904f858cc8a4. --- .../exoplayer2/drm/HttpMediaDrmCallback.java | 28 ++++++------- .../exoplayer2/drm/OfflineLicenseHelper.java | 39 ++++--------------- 2 files changed, 22 insertions(+), 45 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java index 5fef8b34be..f08d9b59b5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java @@ -39,29 +39,29 @@ import java.util.UUID; public final class HttpMediaDrmCallback implements MediaDrmCallback { private final HttpDataSource.Factory dataSourceFactory; - private final String defaultLicenseUrl; - private final boolean forceDefaultLicenseUrl; + private final String defaultUrl; private final Map keyRequestProperties; /** - * @param defaultLicenseUrl The default license URL. + * @param defaultUrl The default license URL. * @param dataSourceFactory A factory from which to obtain {@link HttpDataSource} instances. */ - public HttpMediaDrmCallback(String defaultLicenseUrl, HttpDataSource.Factory dataSourceFactory) { - this(defaultLicenseUrl, false, dataSourceFactory, null); + public HttpMediaDrmCallback(String defaultUrl, HttpDataSource.Factory dataSourceFactory) { + this(defaultUrl, dataSourceFactory, null); } /** - * @param defaultLicenseUrl The default license URL. - * @param forceDefaultLicenseUrl Whether to force use of {@code defaultLicenseUrl} even for key - * requests that include their own license URL. + * @deprecated Use {@link HttpMediaDrmCallback#HttpMediaDrmCallback(String, Factory)}. Request + * properties can be set by calling {@link #setKeyRequestProperty(String, String)}. + * @param defaultUrl The default license URL. * @param dataSourceFactory A factory from which to obtain {@link HttpDataSource} instances. + * @param keyRequestProperties Request properties to set when making key requests, or null. */ - public HttpMediaDrmCallback(String defaultLicenseUrl, boolean forceDefaultLicenseUrl, - HttpDataSource.Factory dataSourceFactory, Map keyRequestProperties) { + @Deprecated + public HttpMediaDrmCallback(String defaultUrl, HttpDataSource.Factory dataSourceFactory, + Map keyRequestProperties) { this.dataSourceFactory = dataSourceFactory; - this.defaultLicenseUrl = defaultLicenseUrl; - this.forceDefaultLicenseUrl = forceDefaultLicenseUrl; + this.defaultUrl = defaultUrl; this.keyRequestProperties = new HashMap<>(); if (keyRequestProperties != null) { this.keyRequestProperties.putAll(keyRequestProperties); @@ -112,8 +112,8 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback { @Override public byte[] executeKeyRequest(UUID uuid, KeyRequest request) throws Exception { String url = request.getDefaultUrl(); - if (forceDefaultLicenseUrl || TextUtils.isEmpty(url)) { - url = defaultLicenseUrl; + if (TextUtils.isEmpty(url)) { + url = defaultUrl; } Map requestProperties = new HashMap<>(); // Add standard request properties for supported schemes. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java index 5ae06b7691..2eb3463b3d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java @@ -43,44 +43,23 @@ public final class OfflineLicenseHelper { * Instantiates a new instance which uses Widevine CDM. Call {@link #release()} when the instance * is no longer required. * - * @param defaultLicenseUrl The default license URL. + * @param licenseUrl The default license URL. * @param httpDataSourceFactory A factory from which to obtain {@link HttpDataSource} instances. * @return A new instance which uses Widevine CDM. * @throws UnsupportedDrmException If the Widevine DRM scheme is unsupported or cannot be * instantiated. */ public static OfflineLicenseHelper newWidevineInstance( - String defaultLicenseUrl, Factory httpDataSourceFactory) - throws UnsupportedDrmException { - return newWidevineInstance(defaultLicenseUrl, false, httpDataSourceFactory, null); + String licenseUrl, Factory httpDataSourceFactory) throws UnsupportedDrmException { + return newWidevineInstance( + new HttpMediaDrmCallback(licenseUrl, httpDataSourceFactory), null); } /** * Instantiates a new instance which uses Widevine CDM. Call {@link #release()} when the instance * is no longer required. * - * @param defaultLicenseUrl The default license URL. - * @param forceDefaultLicenseUrl Whether to force use of {@code defaultLicenseUrl} even for key - * requests that include their own license URL. - * @param httpDataSourceFactory A factory from which to obtain {@link HttpDataSource} instances. - * @return A new instance which uses Widevine CDM. - * @throws UnsupportedDrmException If the Widevine DRM scheme is unsupported or cannot be - * instantiated. - */ - public static OfflineLicenseHelper newWidevineInstance( - String defaultLicenseUrl, boolean forceDefaultLicenseUrl, Factory httpDataSourceFactory) - throws UnsupportedDrmException { - return newWidevineInstance(defaultLicenseUrl, forceDefaultLicenseUrl, httpDataSourceFactory, - null); - } - - /** - * Instantiates a new instance which uses Widevine CDM. Call {@link #release()} when the instance - * is no longer required. - * - * @param defaultLicenseUrl The default license URL. - * @param forceDefaultLicenseUrl Whether to force use of {@code defaultLicenseUrl} even for key - * requests that include their own license URL. + * @param callback Performs key and provisioning requests. * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument * to {@link MediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null. * @return A new instance which uses Widevine CDM. @@ -90,12 +69,10 @@ public final class OfflineLicenseHelper { * MediaDrmCallback, HashMap, Handler, EventListener) */ public static OfflineLicenseHelper newWidevineInstance( - String defaultLicenseUrl, boolean forceDefaultLicenseUrl, Factory httpDataSourceFactory, - HashMap optionalKeyRequestParameters) + MediaDrmCallback callback, HashMap optionalKeyRequestParameters) throws UnsupportedDrmException { - return new OfflineLicenseHelper<>(FrameworkMediaDrm.newInstance(C.WIDEVINE_UUID), - new HttpMediaDrmCallback(defaultLicenseUrl, forceDefaultLicenseUrl, httpDataSourceFactory, - null), optionalKeyRequestParameters); + return new OfflineLicenseHelper<>(FrameworkMediaDrm.newInstance(C.WIDEVINE_UUID), callback, + optionalKeyRequestParameters); } /** From 91adba5d1bc8dae54ef62f3276f81df2559c388b Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 8 Aug 2017 10:54:06 -0700 Subject: [PATCH 0227/2472] Minimal change to expose segment indices in DefaultDashChunkSource Issue: #3037 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164614478 --- .../source/dash/DefaultDashChunkSource.java | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/library/dash/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 index 297052f65a..dd62d47621 100644 --- a/library/dash/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 @@ -85,14 +85,14 @@ public class DefaultDashChunkSource implements DashChunkSource { private final int[] adaptationSetIndices; private final TrackSelection trackSelection; private final int trackType; - private final RepresentationHolder[] representationHolders; private final DataSource dataSource; private final long elapsedRealtimeOffsetMs; private final int maxSegmentsPerLoad; + protected final RepresentationHolder[] representationHolders; + private DashManifest manifest; private int periodIndex; - private IOException fatalError; private boolean missingLastSegment; @@ -377,9 +377,12 @@ public class DefaultDashChunkSource implements DashChunkSource { // Protected classes. + /** + * Holds information about a single {@link Representation}. + */ protected static final class RepresentationHolder { - public final ChunkExtractorWrapper extractorWrapper; + /* package */ final ChunkExtractorWrapper extractorWrapper; public Representation representation; public DashSegmentIndex segmentIndex; @@ -387,7 +390,7 @@ public class DefaultDashChunkSource implements DashChunkSource { private long periodDurationUs; private int segmentNumShift; - public RepresentationHolder(long periodDurationUs, Representation representation, + /* package */ RepresentationHolder(long periodDurationUs, Representation representation, boolean enableEventMessageTrack, boolean enableCea608Track) { this.periodDurationUs = periodDurationUs; this.representation = representation; @@ -417,8 +420,8 @@ public class DefaultDashChunkSource implements DashChunkSource { segmentIndex = representation.getIndex(); } - public void updateRepresentation(long newPeriodDurationUs, Representation newRepresentation) - throws BehindLiveWindowException{ + /* package */ void updateRepresentation(long newPeriodDurationUs, + Representation newRepresentation) throws BehindLiveWindowException { DashSegmentIndex oldIndex = representation.getIndex(); DashSegmentIndex newIndex = newRepresentation.getIndex(); From 98d50c13ef9436ab018ee8381ebaf0a0adb6b6dc Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 9 Aug 2017 03:21:25 -0700 Subject: [PATCH 0228/2472] Clean up terminology for MediaSession extension ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164705750 --- extensions/mediasession/README.md | 8 ++++---- .../ext/mediasession/MediaSessionConnector.java | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/extensions/mediasession/README.md b/extensions/mediasession/README.md index 3acf8e4c79..60fec9fb60 100644 --- a/extensions/mediasession/README.md +++ b/extensions/mediasession/README.md @@ -2,10 +2,10 @@ ## Description ## -The MediaSession extension mediates between an ExoPlayer instance and a -[MediaSession][]. It automatically retrieves and implements playback actions -and syncs the player state with the state of the media session. The behaviour -can be extended to support other playback and custom actions. +The MediaSession extension mediates between a Player (or ExoPlayer) instance +and a [MediaSession][]. It automatically retrieves and implements playback +actions and syncs the player state with the state of the media session. The +behaviour can be extended to support other playback and custom actions. [MediaSession]: https://developer.android.com/reference/android/support/v4/media/session/MediaSessionCompat.html diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java index 0e839b8083..33807930a5 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java @@ -47,7 +47,7 @@ import java.util.Map; * Connects a {@link MediaSessionCompat} to a {@link Player}. *

      * The connector listens for actions sent by the media session's controller and implements these - * actions by calling appropriate ExoPlayer methods. The playback state of the media session is + * actions by calling appropriate player methods. The playback state of the media session is * automatically synced with the player. The connector can also be optionally extended by providing * various collaborators: *

        From ebbf9f635bdbb1b7b4ba418955bc5be207afeca6 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 9 Aug 2017 03:26:18 -0700 Subject: [PATCH 0229/2472] Fix minor Javadoc error ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164706078 --- .../android/exoplayer2/ui/SimpleExoPlayerView.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index 2bba9071fd..17923767d1 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -668,10 +668,15 @@ public final class SimpleExoPlayerView extends FrameLayout { } /** - * 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. + * Gets the view onto which video is rendered. This is a: + *
          + *
        • {@link SurfaceView} by default, or if the {@code surface_type} attribute is set to + * {@code surface_view}.
        • + *
        • {@link TextureView} if {@code surface_type} is {@code texture_view}.
        • + *
        • {@code null} if {@code surface_type} is {@code none}.
        • + *
        * - * @return Either a {@link SurfaceView} or a {@link TextureView}. + * @return The {@link SurfaceView}, {@link TextureView} or {@code null}. */ public View getVideoSurfaceView() { return surfaceView; From 55d1abaa2190aaabc1c022aa2e080d018725a824 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 9 Aug 2017 03:27:13 -0700 Subject: [PATCH 0230/2472] Document usage of the RTMP extension ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164706135 --- extensions/rtmp/README.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/extensions/rtmp/README.md b/extensions/rtmp/README.md index 042d7078dc..80074f119c 100644 --- a/extensions/rtmp/README.md +++ b/extensions/rtmp/README.md @@ -9,7 +9,7 @@ streams using [LibRtmp Client for Android][]. [RTMP]: https://en.wikipedia.org/wiki/Real-Time_Messaging_Protocol [LibRtmp Client for Android]: https://github.com/ant-media/LibRtmp-Client-for-Android -## Using the extension ## +## Getting the extension ## The easiest way to use the extension is to add it as a gradle dependency: @@ -25,3 +25,19 @@ locally. Instructions for doing this can be found in ExoPlayer's [top level README][]. [top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md + +## Using the extension ## + +ExoPlayer requests data through `DataSource` instances. These instances are +either instantiated and injected from application code, or obtained from +instances of `DataSource.Factory` that are instantiated and injected from +application code. + +`DefaultDataSource` will automatically use uses the RTMP extension whenever it's +available. Hence if your application is using `DefaultDataSource` or +`DefaultDataSourceFactory`, adding support for RTMP streams is as simple as +adding a dependency to the RTMP extension as described above. No changes to your +application code are required. Alternatively, if you know that your application +doesn't need to handle any other protocols, you can update any `DataSource`s and +`DataSource.Factory` instantiations in your application code to use +`RtmpDataSource` and `RtmpDataSourceFactory` directly. From b6b84839ed25476667abe6cdf6ccf0a41c8ab9b7 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 9 Aug 2017 07:10:42 -0700 Subject: [PATCH 0231/2472] Add ErrorMessageProvider to util package It's needed in multiple places. MediaSessionConnector uses it today. Our leanback connector will also use it. Maybe SimpleExoPlayerView should use one too, to show the message to the user when an error occurs. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164720020 --- .../mediasession/MediaSessionConnector.java | 17 +++------- .../exoplayer2/util/ErrorMessageProvider.java | 33 +++++++++++++++++++ 2 files changed, 37 insertions(+), 13 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/util/ErrorMessageProvider.java diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java index 33807930a5..743b2e066f 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java @@ -38,6 +38,7 @@ import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import com.google.android.exoplayer2.util.ErrorMessageProvider; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -293,17 +294,6 @@ public final class MediaSessionConnector { PlaybackStateCompat.CustomAction getCustomAction(); } - /** - * Converts an exception into an error code and a user readable error message. - */ - public interface ErrorMessageProvider { - /** - * Returns a pair consisting of an error code and a user readable error message for a given - * exception. - */ - Pair getErrorMessage(ExoPlaybackException playbackException); - } - /** * The wrapped {@link MediaSessionCompat}. */ @@ -320,7 +310,7 @@ public final class MediaSessionConnector { private CustomActionProvider[] customActionProviders; private int currentWindowIndex; private Map customActionMap; - private ErrorMessageProvider errorMessageProvider; + private ErrorMessageProvider errorMessageProvider; private PlaybackPreparer playbackPreparer; private QueueNavigator queueNavigator; private QueueEditor queueEditor; @@ -411,7 +401,8 @@ public final class MediaSessionConnector { * * @param errorMessageProvider The error message provider. */ - public void setErrorMessageProvider(ErrorMessageProvider errorMessageProvider) { + public void setErrorMessageProvider( + ErrorMessageProvider errorMessageProvider) { this.errorMessageProvider = errorMessageProvider; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/ErrorMessageProvider.java b/library/core/src/main/java/com/google/android/exoplayer2/util/ErrorMessageProvider.java new file mode 100644 index 0000000000..3d2c043a91 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/ErrorMessageProvider.java @@ -0,0 +1,33 @@ +/* + * 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.util; + +import android.util.Pair; + +/** + * Converts exceptions into error codes and user readable error messages. + */ +public interface ErrorMessageProvider { + + /** + * Returns a pair consisting of an error code and a user readable error message for the given + * exception. + * + * @param exception The exception for which an error code and message should be generated. + */ + Pair getErrorMessage(T exception); + +} From 0d11ebdf2d814eb6dbe0a820ae5c4b7d415a95ba Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 9 Aug 2017 08:25:06 -0700 Subject: [PATCH 0232/2472] Run optimage to optimize demo app's PNGs ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164726319 --- .../src/main/res/drawable-xhdpi/ic_banner.png | Bin 6884 -> 4299 bytes .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 3441 -> 3394 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 2186 -> 2184 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 4951 -> 4886 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 7732 -> 7492 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 11241 -> 10801 bytes 6 files changed, 0 insertions(+), 0 deletions(-) diff --git a/demos/main/src/main/res/drawable-xhdpi/ic_banner.png b/demos/main/src/main/res/drawable-xhdpi/ic_banner.png index 520d83cc3b9e8ee1438d884a94c853373b0f6923..09de177387a3aa03fb29e8dd83bba0b3858b4f31 100644 GIT binary patch literal 4299 zcmZvg2T;>V`^SGF0YZAdXruR;g9Fs{O5h=o$t)5Bh)S|KaxwzR%MjbzF*Zf|>$-wsrwLc*2MlX*g2U;efgac->gX4oJEhdXKu z41j6HYLn7L*2?L9co%;c;i*)~Du%A5jvQynGiJB10WO^52}v34#iIcK zv|GXV6B`qeZ?$i8-Lv<->Ik0(mXkZcnJc1I2`vL6#&tfH3g>HCY~L>Qe!co(Dd*Kb zPSZy@m6uGcbW}~TDBt^-FA<_Khy-BRFR#86E!q>C{0V4S+$HM)ucZ6-o!KK#ZRLq? z=s_9XFLu673U|q0Qw1P7*wQlH{-Av01+_m*b{{VPwNTsYSIZNTA`aTIHNEQux+V=k zOkD?0-jrZv*g4rwUR9f2`iN?}EbcX-TE_*)tRP##;6%MCCdvsLQKfjBy%r5PC z{k_s8UlX@=c!{Qa(Xl}%UC09gKu*2^EQgxu6LBg}s^UM;-gybyp)+6>{Q86nvZhIx zG3cXYN8ygT^_Ta(JkXB72}mP%u@YYMJ8tC*Qj&dIU2wG{&xNN3*02~PxhkhpOu z_Jv8*{pnK;rEn4fO(z{;NawQ&B}LlBCU?3Gagf-KVi}aXULZf!`*Z{FbeldF`4*RVD)y#Yt4M z(KF6xl=v82ufK&+{YgswSPR8Z&tQ+zUVWxqx?&-RieSCHlL6O4e1GXP{HZlBPmUTM zk;tb>XtRJEP6SSUW^*@`C&!4)EPl#m!}0kue5sX-ed(aqmE=<&d>1$RTn4<@Q*c*3 z0N_&#Fg?2E<+Wa8;lHgsBA!UKUipDF8R|FhGu$#e=u-z1NZ13Kk&zCpu= zu)~?B^`fUfGPykQumbz-cs{VH*Vmh(QgItdito~s|!l`a(kQ23QS zM{sX!`RF#t%L7H9|9DFzK9WLkJu3-?hHz##{n#JJ;4E?wPTWH|W~4_+nm%rBy-jpq z`AbSl4msYW(^W7 z9>^14NGL5WO*IOd)kdU{7ntdhMdxw0pCU^}LE8)f+3g-UJ`8t33>M^gB&*LhIy%Zd z+0GYH)<~z6^1Js~b>|w_p~{8k(x7@i-gZ~{Hr(jLh)+2xg;zn(uPZEE3fpB1K#n;) zY!~HUqBegf*%Zg2N+kM z5Pw{SaP1;AVVruL*}#F56*n5@zwJXvO8ecoMX^ScSyH8TmxCR`fri*^O36G*5uS~q z?J!`Yb8;5WbGTl))GSMW0wlwyZ680HshggQX1Pu>zFr=<@r4eGZWQ|+V(+IW?T5jL zOh+c#-MHb*j0eF%`qLg|j#U_@cI!KL2)yi2MWPS|IzB%BNKe14br+%T+9-)YzoV#< zxWSVKaDn~zwwvP1kN)Vmh)Xm*o?B`m^xy*{Ko8cS_Zx@9FAQJ)Dnx4XX}a)5o)xU( z09a){mhFk>mIU+K_Q#b=*4`bj^-`zC-pax$J_T8fW7xT>FS4^|8CaoEuC(Yk9=7B_ z*wfa)*3J~s2G(zgF6kok+}S$Q0s+1D)z9GJSFc3u8N~LevDVh<_X?W+R&mjn69MGE z@bt|dIeW@zJ;Y|JXxvNSeqk6fq|MCZ{#%h|Wwqr!I^tEnCcL(X=f62C}G9$=BB|D|-*B8M!%E2nL|CVDk@Ua&FSX1IeyC z4Zz9D+ZzpzS=T;DOZ4Q=^3ViPs;8+u=hB<#HCT-YKIN+U4_;`oRU!s{;{~D7WfFpd z(JUJGm6X&fuyobqAP{3ah)fLHx-w~CXy01!l202aa1$qKDZOe6xh^M{L=wDn&6;3W zLr-%uLe}x%>QtKaeJsUAQYR|Yk#_bU*@tX2Z;fdm9M)d|hpxMt6MKD{dB4ExU%UX6 z=9-ml?`mdQp*uYu-XP%2)4`ak+8#fDC=3^f9z6HQw9vTd)29Vg6@vtgJQ{F{~(4DW8f&w~zzY|1)qp?jRBo_EyHZO)LN z8WoB#p{0Ci5v!}K32COmGxnjrGhqga#k2@Ik_}`0qIW~;Y2hM+Xg!sb z;Aa&Z{ouVzMIQ{mJDa_>snB6LLYsrBo}1+cvoiQEGz-{LSsnw#?DaHAw_b~<30Iw zq(UmViKl+1#QMgp=>A1>ztMcDppD}#frQ8TA(-g}COs91ERUX&5KN&=akFq58>QhW zCQYv&Z+$_^Sey2_Qp&P$dDx5Q$!nf(F)Q$w*S0a8@SQ>Io-X(eU>e_*Z>+izE;$sg zwtK|)F7Q;@xE+2!D%*FX=i{x^LV(QOO5b-%YC@*Rd7_#6t3C+9%%P)T&rTZOz<3Dn z!#_Ynho%-52bwZEwD7RPP`1|5h2yw+$EMoH0|I6+RU9>1@@yRCux;b_g6*LERGrzE zj#zj)%=1I@#OuswcQ5~a?TMNy;$u&VSS*(UF3HPk|8YA95ymo6c&8=2DbHz$7_;~G z>f@}0s3?Xv3j)z{dhQi}%lPo|LO2JyS}tx|X=Ug?Ul)GM5uJg-g4h}C_s6o2^uAI- zpSZ_XT(mGk?8ZP2=4j8@O^xC6rsT=GDeikYN*G`#^V7C~P*AT~Ngb9{eWl`EVL@SZ zB7ZhSB?0gGLgj+CAf{)g^GW2DOR#6%N+mDA@dXcs0bN)eXxb0UH`4bKye5(6B7Wx4 zpS)OXH$y|^pIZL_2KTC$wMh)QY>K0v=HN*0jo|%}ten?^zjV0$E-LbCd)vBlMK6t; zx5GOmRad;VZ%@^z;za!e@}_1dOYPj_>%4!6jZRaQDVtY4i6>_rr*A23fn{k(47;~f zzP_Gzk_wYwxXk-=@W&2yEPv{DZSmFkzTb0k5f}FaFcXG}KdxSk4bn1q?=50lrh;Ex z5qFaWZ7eOJxA9ldxb%y*z$;m?DRm4StC^5l{P$75HvTtt#QEb#&NgqZWc4?u0QTwT zzdlFj<`$gT+KxcfG$rEPm@K80X;96v6;ND*;Nk(NPKrW~Q8Xn+mkLag)ENmlJMVP* z4hK;7DbJR&YVS?Uinb!Iwd1O$sg5pdjF{xtX`!OZ1$aj@uyLFExWwNQ%4W@4}(edSQT?*z+8j8yA5fN9@ zQPq+-V=k|)RZ47k&S8#Job$W<|Au-<{^AWkm?(=&L==&r4DTd+;n)5$`?m zHfG!N{2>=Pz(T%Bg&k}zjpt)gXgJH|sNCb(6IL3pI{jucj`?^Q;cJISI!l3`n`~G6 z>wPUVp*5k{j45IRyvaV z;$=Y=ha^KF{AF>a#%$Y9GcWCibzi^Q)h8$+K#g#83;N5|S}8fg&$5lN+ml@5WWyW~T6cQ;GC z*Y^*2fBL?6^Uj^Qb7$t9=XuVVkFPb9i3#Wk001C{swij!0EP_szY-4z{9ao#YUh9A1d`HO}sGSz=64P(Wz$2-IT^BN16Yb#zM@t8~OYp2m!l%(>@T z7M#jl^w|TVVMng_+ZW|`~2o}e0yvRrl6@_+ygUuYEAQu2WM$#e_6o_Ql*l<|L z$sq~<GCKL)l94JAtQzb}ap@E{%%BtLt)dJX(k zL*o!j17u1xNku%bNu`NLK15lkMaoG7YxOxxjHc+9wW{|xy+?4IO3w^eHi9{T$~Xm+ zaImw@<$*K8fBiv*2OGfvfYpy`8~vN5+l;M$q@aR6!g7q=UAui^L^dzlS_eKePe+F9rZ;kiAN3hp+`&<}rc-z+q zsPVu`m^e#c#Ax=uJPIK_va{OBDq2h3x^w*te3{?V`V@Z*sXgzlk zBLqMp$kShEB){uUoGcLI=;5;@cDaExUzcsz+8Rnsij=GVKxJ3lQX9n-GCUowTf=k9 zEW*31W4evtfWvkwUZDgL1zXe~n6{L5-67-r%>2JOL`t^2WE++OVTuY4n7E{U6i+T2 z_|xx0%qKAYUigGzsXQBoaUI@iN*pFXYqO_NLyRxA^}ykzd+~v|>qi!U58j55v6?;s<1=T15M8C3fWWXb2CFE$rl;)d5x)mZHhh( z*lyQdn9yAq3x+-pEP*j{UzNPEd5GGYbK1RZ!WDibm;{xdtvjr{FRKzZ(otob6rI~14bBW9w$XqtpaK<%xW#%n0_vq@F5|w4= zm;~yRXX978!@*9M&<;$;Dt+ZBWs9Ory)*nu=Rn9`V=wupdwVj94N?jLKpAa1yFL~a zL;fEo594M*GhJZrfd7GUYJ;cT)G0*eWc_HHP^-Q)7l|Y*_pfT^U}M81q~yL@hssBsY%~SuhawK4dxHn-S^X z$0bxt;Z<~UavG+-IvJ9V-{0TIfbfIQ^7QG`?*4v-w{NEts<}c<6ZwIVtaWqXp^!YOcnrLcBEu`MI)|aBCqXQWg+M6!P zc`{k=!PRupc<{Th4K8k!&xG?8cdQ01!wi@>;oWi7tpr^THYBjc<~ zABtt(_0#C+R21>a%v>_6*zX7X9|VwPdQ_XFlvi?FzJG6iwA`_>O(#GE=}X}|sDS=~ z-v@R*Da*6Hyj+R@^!@ulyD5Mp&5CixN!6{oi1FZot=UNIT--(o&m7_z#8nGtIj1u# zx%^f#b!H*tID}cQ{~LU6os=XW3NtZ@&&$h$VTdL4_NsPSD<~+i2n$yY4HuP^gih=z z`uT|iED{n7;1Z@jw;oJTcEuGhy}9uh5*E%bFQ-6_HMN~=4B;{$9335NU6wGncXmdm zrtkqgLPB|MZHo2&uPb;<*Sb;Jc1t@?J}Jb@dAd=|BczV&Ze3Z!g^*zwb|N z0sg=^utm!|Lbe06W!$c~2~!hI&Gt+7F=&)&)vl%QDajZiXW$Cr)V>apb$1u4c9_C! zM8)tI*H8tpQxkVJdC^Y z)W7lD{{4X=y)&k7QUx&{t%O;5Zw|9kQBk3U)Vl%=lEYeC<=Q(smYUG}^6%cgD|5^g z^B_=ERMh9f(b7^;Q)`=V^mAQAVLXXi-lQA$koQIMO zi=5g?m9W+7*IQnBzw55yOurWe%*AeX?`kjR{mf`_IH1AYeNVW#+dHCZ{2Hqp8t7+g zoFj^gJWXcPgzd@ZES!E=8D0)J2ZkgjCI%|I-ooKvpAJ*yR=RQQ5)%FjAzF_fJ-WO- zfXA<|zkd?-_aKmv6=^@go0OC^iIMvCt1a_q@NTG!jsFUDklcNNnyRW%x#t(>GtMg^ zq*9wFee!r1idw_d(@`e(*AoSMD^nh(P0Gjjw9V{lPX++hVb*N^Ld;K7W6j_WI^}rz|%+yQOJ_yD!*`_Rw1beDf2v&4-g=rtliKf=yp8 zTi7@{dW44Ar`{q#ymVpsR=Ptb$08<1x3sk6NjzmD#T%7`sZIFiA6=V z2Tf>(840&_qQUf+5Jj!s^`l=k^HH<2@02Nj7Zl)ibaW(GJ(R;s2Tjd-D5InQ>vM?I z`rp6W%0S+aA4Cn0(=#&SK##UqY7I*j_u_r5nX$V`Gh1#IJ?D%I|p1ZsIVGqKV^V^SGX zSXoI0`fOx;eAoV5!yW=e&Wi;&yi7I*^f~bx?mjsuC*I?elkYUh9Mi48KNC3c!I|;= zT$+_-It=XEzl}Dd^}fHoaNVDMsoXvIUEFKZs>W{YnZo(=FHZ#pDSUiMsk(WM5uV_yGdSl_`*+kGbxkAX2I3k>gwq>du()c+eGu7 z$IWp{bEW)g_b2vSoGg>KZ|U~yHki=OdV0h?_$#b5h_jy*QrBg8HjerR-hU83K zHaJo^zp%&ui<$EpB+DS!DvsxR@q)Ve&9Bx{;|7*KK`O--@q<4hpkFC}=HcYT6`6IB zFH2MR@Ti(Q6;h`Jxq0v61Uy_17iAlgeUFwE-90>7R*p`MEO?$gLBuF*& z#6Q^IPgLN{^&!D09SVhlZbQO<-(B5|e|v{Mm_ci4X$A4YulgDXp8hCYu>IyTk=sMTt>1@=j@;`LYkbMoYwt!2fMqK`N5aYO<%b5&wWiC9e=Thh6|R9 z23)(Ri+c?=K^tMt%{nR^&}}!DrD=`Z_V#wIx+!rnvEkui1V|V*X*lYmzGu&2Mim47 z{q0+$`HTTUl8{nRM|gO6cp{V(6ilZpZO^Z4?CgSP_HY1i38tjvWU=cT-(FDVh z?}s){%ns%o<+n-5497_feus!|O04=JfnGgux>g~Z(AW)-uWpoV5x z*aOl?>&b88C7^Xa7UzZn0s@``Y-B}6=3_;A-WPLTjOq&@zjr02*ndM!oC}cn3UEbP zXoUwUJ-@}6J!y2Rq5t^tW2&Uzi}H$!J*nR^R1Y3xXJw)0!?_UoK8c?{lQT0rlX8qr zOyvIlJ;G%3gomdper)69W$W*Th6a5%pW0|6BHckE)Q&MkQ2lb&DXGLbI%+1xd<%Zh zm%}&SLMNMVYE3izUlGeQiQ0&uj()=g{Suzm&E0XcDb{2Pry6R@ z4!g$kp{zI^_xb|X8d@5!(5=#KL&NV{L9@Pe#j6=@T(-rB8dL7@9bdoEk;A#c+}U;S zr(q}=FL=_6WX`0IYV^AhQ!>%f)<%91w_5}83r?@r5B|vt*tGFG9=IK;~2%m zA8@@{ocwL!d3ii{)6?U9*n%rN;SY`b#!ilzqMg6A)*OJtTxN(*ZmK-L z_9lo@j~8o>##EIc$vTkWEJB)~aDl4MWhpqf@G&%zJ+-K)==Ga7<2wPLIW;|nwCp1e zcYI-G6L%H1X)3Q?F`TTN`8LL3u-44P;xhXq^)HR-Cm~^#aJ!ghThM_SM-xNI8uPX| zI5ov^fhb3>63>!jKyj zbK4oIbVO1zllF%EgondS^+0K3YTieyzGSnxdJj*P?(!HAVW)531s)w8#br6z*tl?) zyZi!k0m?vcxxca4P8s8$hc?VoSF4{{h0kdCU=A17*Bhd+y5p;rw1*EL1~_p(ecE$Z zVMZ(Jx{O8XEP=bYA#1a|T)CA%ZXo`~pE{VC$uqa;O&C7LPQ}yUWNe%8&sc`k>w?F`S)&SOiL29G{g_s?n!0*Nj$$;w$F?r_yV_x=9B5+b1kCT(1lsS# zj;>w@C>zHPtR2CzjHHNK52o+@jbC%FN1si&mz9<>xqt8(V%d2~`n$ey8%DXBey*e@VVIknQ z!tdIF#|6!!uM{vH)7_0Y-drII5c<~HWG(y8LPAQT91N+~2fy1(6Mq0zfbxAZ2Q;p! z3FHAvJw3hibQCDrcBd-3jQP5jaSZIq{&%a+kE_>>Ab!>&+ zB8Br&{G1BLmQ$$^&`U z#Cj7RvpyT&L!`ov6kQwUKeVCb38mB|F`IS7%!I z%`Ky@N!7VU2~G7RrSMzD;V=^xnBSgoS8URkFJUn;F|abVSFaRcu#5oq-tricdGEFn z2MtZlF3F48mCfN44*aN`)3@(cCpMq57yPEe=r|GQ}bdME?x$W&BzHlYv_k=84?{o8*zITLis`lY){` zcw@Zeg}$C1w^22b*Fr!Iqp18;eoPEem%X-*PK55Lx`syQKePlv91CQVw7i$+Jti5h zuC4}6{yt^O_r6T_Lu*}~kdcuQa&6_44QF^%^?nl{RgcgS>bZ2t(^z%k`AWo%Q4Ast zWj^lEO)5=f>q%E|fY@>-`^PyjQ5H5fwt^JXa&AJ9=fOA=41gk|qP8!#Qd&UpAqI7C zRh$1nnsBN#{0><&IzJzmoJ1;gZ}BX2T#dhw;S=9N-X{+zmpZ;<+SZLKT|_rl$M z2d2Hmr`Q50JSTsD|LziT&QMg+Qyry0jG03bVDPLwDwc3~0=ET2=tDMoXN&?*ulQOM z4FhUZLu3=8r}J;hgk0Ud{hw`{NibisK?~pu-PkbTvaEgzOI~tand1~e(NKp!)zjq=)Z zFx8t4q@T_;XZea__jhBl%KDC=<`0_bF9}01fBzC+WkNa;cq`U{=8yVx#8VYRQiOVX z;CP3pratcPJ9SlbOifKie*9=AD*$|-wlL39jF7iIAlSSev)xMQAIb;t+S-+i^;Ztl z#td#-qa*L!H-~h3;WsR+WA@%*Bgn!cg3KY@%!BQmX36|(P>Y!k(82U`t=_4Jd?JD{ z-}J~QV5Tmcde7RZZ=#oc`Y`AHmR)zJFKPX?$jR0L!jSnox@|%}!J8+h0qPMWLd2u_ znqBPi60Ml##7O7X@?oJz56>#{*|WEAhH(Y4kZx@jNS5IRH^VqX;IsaN*zV~n`=vAAJ#02mTJnQ%Ev287P5!>qI}LX;zrpx2 z7moH)@QJ9F*4~y~FN-(*~;xjUR9W ztcrO2jMhriSK7Z<#N%^?Sy{7~ANra@C8+ajS*gPGDaaxGwV1<6Rth3QU_RkdUb9i>OqQ59_m|ic-pBusy%lKtvX9&J4Ct)9fjfV~T7V~mvO^5&a-~d;6B!;M~L?u6U-+M)w zPgsN@;JE;|>e-@^NEds5N1zm#r^F)P`E-<0qiH71{4Z&Jb@x}nQau9;imX!uQ_n2p zmI}*FR{});stttLfASQ!j$(rjkxg>Ob>M@&j zwBUak*yBWmntu$5-4Yr-dobajG9&wYK6@vBwDHGc0V=)dz29Fb3VEH~5}tcmDVg2+ z-@V5qk8J}^{ql3L#h=Y&J*4Kq=+`_(X2wT8Rs#1$VzI0N%K!at$p2%j<$u2O@~L_t(|0o|Jga2rPyh9yI}VR9JCjNwAiVKju9IdRI&%nV^> zGCR!7Xij2gCY}8H{+Zp*86vPwlG~m6=g#utr}y5%opjDjM5GsTxrOvXI%mnz4eD90 zEH_-OD5ox?|Gxop5rcEN#s6<-)nhedwGnD2KD*sqo&RM(@_$>`sZ^KMh4m_H1?vzi zij`JcTAI%)hQxiE_}?Ls_Z3mLuDVWXS^p(KZspvdA!{OQJ1dz7PLR=Pv`VrZ>R@dX zbv7*LzHZeSkZU=U@4#Bj$|wiLsE8zPjUtt*CGx4WEBIUu40Ve(I+Sxi*XjmH{ms41 z3S+6EC@K|J&JumZ zm9nz3d|6gmk~MH(IXx|gQWH|9L8FS+t0*O;;K|ujsym--*CB|+Zkc(LJgvwdV zae#z^hLGX%63mDWSwto#Chopfz^>G_m}*;LJ=$83e}78)aWGx+8%kFf4yEK@2Ac-8 zbAJpe*F%XnR(3lDAWpmmq0^^NzsRZ(iz;=mqN1X|)z+g0pqy0$X?Is&+SGhl8SVA- zrQEdxOoGbs_oIRfm+WOlNOb-B^%blNZdSpy*xc%K!894|wE$GOwLgV+0FcVH>m^?* z+TOp?au;WN(boHiQQQD8%K7>$G9)G0Wq(G?$jC@vxpHMwR++_P1?$Pl$(bOmN5HWi zQ1}}|0m7uB-y2eL>a?zbkbfDJ zIrLXQGSMAAe7FwhO_Wr%0<^b>7_b=yw;e{CTZ+HU0Fugn^K~i-57)xYu+vZe?z`_i zSpPCn&t8Z>UF5kki?jlCa*Q_yX|>D)5*j*~a)0`f3~6aLt%)$3?cTk+FH5&)&mK3H z4mbb$90#gbqGXWvDrkxbO_$Ats* z3S`zA5OigMmra1;`wsyGieZ|3)Nx4qnw~#&<`|fl9(8)&Wd7VV~;S4+awnWNPqY8%P-%- zCKe$w8#M%kH4awJX5Gx6*E=`*2usmQ-a{$xuRlr2$}$JYNSqQ95~3b_@WFdoItj=H zfR-;`-heYDivpSfiu-a1g|xTKjiNn8oUwy4#*U`qGiS`tWSm}o_0^^<9oYN}C@3hX zB?HRV3Q)$PLA1Y*kHs-&=YM0q7>He!F=iAgd3okm)H&aO|NVokvVZ~t0v=#M1?mlI zRa6*#giej~rp>La3&O|R``8^2RCMBmsf{ZD6)>D9DgfHHZQFxd06GzsLGMmmOluz; zXBD8MZ5;!s_|z%0fWGIDP8?54L4ipZOG``3egFOUA7KeVVix=4lTSSOfIe9(KvGjb-%Y>2{JKSu zBD&izNMr9-a_y=`L3-M>X)Rg0GShs$diCz*b&#CqH3KBI@YRj9zV#@RfVQ_8MlqVs zWMcQfb`31YqG|r-&3~KGHEPtT!zu#^3&p#6C4Eq2RaE6|$;rv9>;>qP(LdR? z)#zvW0y1l%9~ED^Sf#DTsZ*ymf`xjk0l8s$&HIqIK~ZZ!Gk-q}p-nC9zgf+kFpdht zs(gm6;Lt22B;-@DsBE+9791ShSH3N#C7`c{d|}_?4qtD|Teg&x+}x@jm=RKTb~gR? z+iyN#QU16CG00`ah!HKAVuFn7S^@gQ!dYxlXFmjTs{rME_6Zr{Y)s#*bheco!bfEK>CsZ5#K?*-7}cJnF#!hWj2-=B;n z+C9tXuwuk%;@Y)qUk3vQ<<8PUI(Z}9%-z@5_hEi;n13SOObbB1i_X(uofqB`&TlWj zK|YHwRw(DQ=mNbpYdNicWFo~6@}ayxem8M!VVir!#l@xd=+WaTFaV269~`bH;|mo?&6(nKNOM&_*wvxn*7y1dTst%dev_oP5kzN$-E1;MADg?Ire-u zj}0nbrlPmse%lLUm+_}U43e5Xd-m;|u@OIp-6gF^6F^pI!s@FOcqxra6vZJgFPu1W z;sB4?17m0D)RH$+ayGm7r=NcE=7)$XE3!YJVSmdm)1t!(lwF_>z;?AB?B3~v4?Y-; zG5_($AMPxjTH>ax#*G_y2N!63>C&ZbnEqG>djt9~IF@453mp0yo`^kx7%Rri(v_FI zQ48_N6-0**AKn>9kiv?L_5d_{V+6`L4vcE%;xLjkV8DPb7%QLuxUqEQC2lGetf3Vrf3Oh%*rdY^;`-Sl_-veLl_IcaZgK@Ecpy$oIij5oh)5t5;qmaN#Chc zr+fL@n%^f5VRJ+SW?k)ET&_tlK1(mrFZ<&tJ+Bz~odE}~Mf>*c+r?vRBgUv_S)X8b ze5u`}Ns}k}&4(yUe)3ev7BOYrwMyZ<9e)r_5h?jj{c#Jz#f1wOqCGr3p2gUvOqt@& z(%nkp#-#4ziW-g`JGL+H({hN&?9_iXDUYV9AWd5zPWvM>NpbS;HHcGmbaXE776xKW z7#mAx6Ch~h$dNU;!lry~Js~wU)o2MM#Fh&Qxpa|*d!zH=U^`*t1>sa4OEWPhJ%3A+ zq#rbBP%XZ;YBuEsxwf(#N4f5G!HhQ59K))sIyO)i}~cSgmnIW|q= z!|iOb6Rwt!WI$%Yt#aOYR>D^yCj!x-MZD~YpYSCMh&=fGxiEp6=KN@-dz<~pynVFfSa=B6`f6K$k9EJj24m{Q1Eoe)=duDfVrGl z{8S=<-*xJRKA~^uqk!YI@ok5hzIA<2E6B9f8+ zhdVHNXlP6CejW*FX`k=O&>XuyFwQE-crY%4{%Bc&VExp6X z3L$)y8;8?79FU8>9hpghiM;roDPB#X$jHbz-bV$aPShu{G>M= zc{sLhTDEN2S15}*gtef~=4b=L)(+s114i6@?DP``ftMmP%= z;F_~q2sIaN#yUI*#e?=1 zlAkf_yai-a#nb)Xz& zb>6FEz8(WzV}INQe+Qs!Luu84qUPAf4%KYy)e$v2#|f_Q6#}EW**yYu%x}vNS z$FHJvGcdhQ0AmQ0=AO7Is7?CX>Qp$mza61u2sB3JM1LrHg>M7%dzsWmr$Q3InSfG5 z4PlK^yBdy;pW;E)^z`&R>J&)4<5Q_r!KB6lI4}@J&^AS9XXo!7>b!aSc+)5{a)A?u zfh>F*aR=?~?IMS05?>)|&a-C2>23wCK1UqXlhVbSGUS4Q{$7p?(Xg}2uIGx z7Ip>1OZGhAs=fp@r*vwZM1ze?CX-*fbm<{spDhA}TC>m6SjS>OyxHZxtT9h#ni>uL zjg5_mn5hl2$h1&sY(5@Pk5ekhT&4DWkkx;fO5+onb zF%Igh1;Ev2qknQ(SfH67!88OeUcC6UNngaPm_niWk2Tt343Kos^N=%51P4dQYDsRe z2xRGDMnOq45M$)LUsPLL`>9bgFcEP&GC>WJGIiZEe}Td-nqdgGZj6R4QG}w8yIH3w?mf z*G3wI!%Nlr2(2VJ7TjGC7J zL=2QqkA%u+Bcb@2D7gG|l-Y2!{L_Z@pscR8Xs)QHrY0X*4DRE?JEOqBz*|wpD%izp zdjg^uhazWFW|o2RPTlMXt6&(E=t$`N0 z3s7rXIGmjwW44d^YJ$jNaQYxEhyxBsDiaa4bo1uT1%*-5zfaKOBs6y$k%7?;5 z(QwGuGAI1)lQ9klM4~7tFq*g`AUOh>*bmUrqesWv15iO_E4;O08-JvaUt|%WZ~UDD zsHd>dB%n>3Ha*r4(4IYe{8TEH)K(Mwwx$E3x0gcP$Nz;7qBg>TF>}oVbn;=xn^@bD z7cqyE8#S@!=4M%9V&cO>K$=l3DJjWYsZ>hr1&EjB@8`hQz%^z;s+i$8Cr!DV@`p0B zob-wnE5-;%F%6*M!+(bl!#K$1VO|#yFRNA`fc<`PMge^>HWsSwJd$a$|Mbs{X^z$~ zUteEe<>lpdmlhxf=@%GD=h+%48UjSJ-TxJnT6D-K7S28%10@(BmQ9O-i&LWELO?V$ zTMCErvrQj@N+Pi+P%JMm{}x$zX|2&_VfAeiiDZwX042SUVt-qz@tpYqWc#WF=qW9+ zD%B_~EIdFgw5B{*Vm27_kae~utNH-F{B{-`^l^N$+O~KRbf2_(4x6J%v#hMFzp+%K zU$W|P;>3v%YnJGm0<WZEAu+M{Wp^^t~idiN&_gC~6#ZU0q${?Af!&v20%Zl?O)b z_QuA>FKzKd`T+fNR-#Ebn?v5#T)Q|q4?fa!y)K~kSL31QVxe81Y+Sf-AsZRop`|@K zerRZZe*P=A4E6c|t$A&)NkFUKJg8MBetjlvoBUe;Nq>9Hr{ozKpjO)NSw6ZIHO(fb zr>CzW1_;Vi@0&Qd^Aw3hzmm)4P1c&3K0uu7$@yXX;jy(-zj6msl)ro;h99IsA1wLDIiuIUe)uX}s+T(Q*}Nt}ZE0zNb?eqeQQ!6YAK?}s zA5ZUS-B(*%d&(LuG6l#SS-iIlj+HiplH;7Rcz>awpx_+#*_-v<_{InhOG!zIRwxt} zYmqI0Vt0NIY3CcDL#}kj`k-Exy~BnL8(yG3r>3TwxAJw-qD8mj?*7u!(xWu|S+ao2 zT0n1~sDYXmxl@!5-#l=1FQnFoBLw{LgC#(=aTCu+z&R%T; z&c*L*P}{4js$`fgJWqXMeY1U=KYzX#?jA@=N?Ls5#tpSOkjS?bH^`s_#o2zw5Puv@ zC)5}aF2}x%VtumPM+)4+!^7{u{g~wBkJ<^i40-*VlKjOn5*-;vF*i95gHG+O=y*bNm7V0%$_sVK4f+PoF-W zzI@;t7Z(?H<;s;R7Hz9-c@pA?IDd!QMC*MnU%p%w9UVP~+RWPS@VB7dgM)+VSMc%j z@_ORjxpUcVZEYYhxn387p?hmgirRGQ)TytqjsDbD)@G-@BJMV0#teF!cqGb%w6wIO znwpw+`o&xUN(hG9fFViy)~#FL$F|YSydzm#o&MDrw@H&G4fXNyp+%45f`5X7f=`}2 znbFkLq+4HjWAQ_o zxP1BYxMRnT?XRq?Y^292ynj})PgiilZ=B#cHS2hv2>z$JmdfzusV=Eh3RE}UW9ibR zG+jYUR{W?913_yHZG$FHp6tcUjzB$vTTn_$%3pJHbF(TcD#SE?MYQsxQu;o7xpEodqoBy@~UzoD$Gtembv-*#~4&Yc^nEY-oZ zh3Xtl_h1R2Ok_l0VXp)TKhK^<(D-2d3t(2j`9R`ddie3&rX{*Z59a~ z)Bo_?Z|NGkmX)Eh4`)zaR43K#%%R)iu+St1)2~g@?8J!^?-?;-#C2!!{F5r`3xB!5pyL_t(&-o;vJP*YbF7KK`u+WNy4*`Dgw3%!TD58&?O1hc)z+oj#i`>uR)N|mRTcrotzyBbjG$#HB2r+4A|_xG2oOkK za(Yg@FTUhuc_dNa%o!fZefNIfz28~xed*WF&kF#8&<3D|(0^YE?d@&p*?!OJ!omBa z1-tofC|W35I5WB*>ZWZl_4Q1F~p>2qf?zUf=9~F@mUZ0&V3u&LYLAtrMXd>XY{g zwi-c!0zW`-|9>umE*1bMKxWIM3;JlHXYv1H8pGAtfdIb}fpdbvIUhi~G}f1eNsI1R z`b|RkAa=NqpLI5C)4 z=seijegZG-)ILyi5ts@VqAdchAs;N9Igmu`lVva%4u1+r1W6$f%x3-ze&$6^YSe-0 zYz&yr%&?oea0PIhvVfAn@hxVv8FV_`Dj_znaUS*e4)3t8WW#A|9KbseDcug1&ietA z6HOvYiED0de&1uB5b7RT7YiI#+_CnKTLLyaI~&Z^zk$Wn6$lbG40^r(214uaj8!BZ zoc+%P(tok~M1Z$T^MGr-gwJq(MJJLH;sRWSWiF+}Gs3@varSEju^Id#z{^Ut8n}nm ze&q?8R9uC-`T6<7M619v>g?=H7R)|KCD5G}4JAo($ek1qxsw%ebFLhW+0l+f@}t36 zybDa4R-Z`FDxfbweSQ4`hgA@Urkq!@GDj;!1hT>uw(rz? zWh>;oC=Qv zjck^f05ffJB6R2GVE!<9s*|9lrR5r8et#TFFJHbq)Mzx;`b=QoOvo8827W4B0e3!% zwF7LMC36d2X!_-!Lr+PG+sxeB+FG}L`}W~>0%c`ouVJEi&{qN-$D-keLJnD@UC(^w zv{*@i-I-zU9+w2#+uPMCDJfxg0;Q#;V=;kgr4le{YM^RGEaXJQdDOdF7ULHIGJkVZ zqlf_dt1crWW1^ivVPWC;z7VK=V1RWil%Slvz$1YNGXp@N`9GHgQd3hS?F91j@%^Fi>ynd`Ul!JL198)! z@|D2S&FA3cm_)Zy${7=H3#HkkJ^O`r{yZ>Vya2e1@fb!M8ykxe^I%~i7mTqr#TNo| zj#R?YsKx#zvN^HPnVJHe!5}G-RaRF1j+jGi*jXtNR8di}jIY1Fl@me&3x9qpatl@& zxezkK5nTGSFyCx6U^?z~*L<$||zx~8V45V5}Kuod9_ps1*5Irpf%^(Z4} z0*XWBunoiM(YF?(KP-VQNqZpf*9wO=@oBf=sHzD}`gUJ-WLCsz#c~I-v$IJ)!4BI> zetR}`>eM%IiSByaQaTgxX4@{_gGQ~h|v+ey1w z1*rs9X4OLJy>`Ek90jw#kubVE9}+MMy?_6HsV86XMqr-u4&+s~g2~JU>{tc0e+#j_ zP5@7wI5EJ}socO_yLQQNgWt=J6-832)wm`Qms$k}Rn4I5G6m#d3zfi8ECJVT@$Exv@u7k^tR=F;*xC$|ox36hRhJAmJKwgKu|y8?R1OJn0k3oP`pGZ(|fXx>j(>q@W3ODfaty0g8wqkM zifT{by@2fn2aQ3p!DvV`KaMciGOjNddyq(kkq$9O^swU@YoLe-fZO_NYisLCPfy=T zWBLVND5uEC$md9hxNnHBuC6Yn)u0F^AY$p`YbXAVrVTjtMSbFVPwAR9YZ7TJ8ne%` z@#NUp8IhoqfPYEHk01XQ&t~q@QnMAYUHqFBFRxfFg7!AKP+v-=@;mH%GL1oFJ&~Uw zuoz5|dS%3j5mOEyKD@rXyu1*PIrKDxX0{PwtK)F-FA1p)WmS}l9!vN_>Vx{CJ|)HZ zK&%EEWDwd&v#H()WCjxs2wngzFgEtr7X$aLtwsi~{JzIIx4n7=c>zEeS|79s`hP2-y|XPn+v{0fICyWg za4+8tM2kd=W=8h|y|fLdzMd&?{C^DEr)c}_cKcs8o2>>-gZBn9x~I?RJ8Hw)j(O@t zXzAT%IR!zTLo=ZPf|P9RnRCNzlswZ?RbR2TVd34X`sM1JCOho}@iq3>$QJcOO9rVLc)t zbRO(%AAuKk>Kv%42rLB)&=vw$uL2uq4J8rhWSLB+1AhV%VNwW$vzh;bpLvm!nhjt% z6AzZtQ=MilTn?P}R!B+U_%^H63I>B=r4XCnIFI^!hj&;{vf=dAF5q2=lx_oC`~8r~ zi6#-H#5FZFz3($m2z8IFiv3`S*BEZ|lxxh7C#Ampmq7z98aRIKvQjb#N8R6glIQ!Lt*bG4t;AN#=1>D1` zpz;JwDz3s^l}a^8vK%dE=8HZ-N4D%#njRH_nxaDh|xW zJHevW1w?{Y0eu1L>gwjZtb#B!b#--}^QC=n1b?o`WwpY_^^cfn$P6pw@eTcFr>>w z;D33`8zcv=Z{i=YODfIL??n) zHeF1BnLcF}bmpDJ{9*A`CqZ*_^Hs$BIDeF0x^!uv*=()}n84m?aB{R5_=#u*-1#WM z39x0l%qw`I8JB$yT_q)6GqX;otKGJ3+h8YwTeoh#hKb@qPYJXgiG%A3IphrYJoDv~ z6C?rlW~M#6JrZbbZPlcurA0Xjl$Mr`!~~|7O2DG6hKl70aB@tdPrYko@j($Fvwt== zhzOv+8nUvo#yJTT78Z`~34xjiCRn>%3CaoceG+&uEd&Ib{_{v6Jv}|fNgzKze;6h) zjZ^}6H60*NtAOPFH(^WiFK|41mPZ0*9|R$x8PlQtP^w1>VaV6++O=z>lfaraYX*0A zcGmQi0B`etEP~82b43JlF_~4MAAj)9tCdM~;xAW1_dn%cofxxmZAwbY%ffoDFK!yt zff87<=`0){G0UrzPL4=&gwovMzWqW!Z!VZGoCn;+_za^B4GqPJxxcWG3&+@+76^fv zhsxn_?4sZj*~|oJPfr8RWRjG~%FD}tN6Zlp?5vasyM6oiQojE7S562C%zyu>$SYW7 z%mT=YO7=;hZOdkiG>uaC1$OjRIDh{9Dwc^Kml7En851!Vv`Q)Fgaqd7DS@ovNh0tB zeD>ZFSnz!T?E7$?h=6hVQs};Mqet6ID&|n?ZQHkRe~%^LaA7$;5G&FGDdn`g5BxlJ zi-^Dv@jG1FtokYkGGZ5lCVzPjbmr%IEf@Ti)7910g^2Y-b>dplwB zd$h=hCb?WrOZ4isYuA*t2gOTtGW~aB8?4Q*gLS9s;7XklxDcOE)0i}Y80#|$3t^J{ z)`s~5Emd?$Nl6}C3CEH?>Em@?HZoZBjW~gu?LAn6zLFiL>D`rv4@I?0Jx*CmX?;T%*@Ok zG-goXg>s6CiFuB6i2H`5s;a6|S`CU&0wT5^zINi@XgYvXU({!1Wo7B=)vIUGSTtsj zW#h}Su`?n;Cw~Fsj~+exZGCH7LJgYC~=58GLpe z>!Tm_xpCvhXY!i~A~T2qA;%jZ85#NZlqpjt%Ve^T$>^RwqwiRoq3oH6r~YFPPb)%^ zqN1YWIp!z)>d@~WUSKwWeHOv=9YN28Lg)Vj$h_xbTbhQT00000NkvXXu0mjf^-CAl diff --git a/demos/main/src/main/res/mipmap-xhdpi/ic_launcher.png b/demos/main/src/main/res/mipmap-xhdpi/ic_launcher.png index d3251491ceab2edd0085cc3babc6ebd1b46f1782..2101026c9fe1b48ab11a23fd440cf2f80af2afae 100644 GIT binary patch literal 4886 zcmV+x6Y1=UP)NZw)cXxMpfx5T2C&3}PVZZ;p-o4i2PAJ@j z+=Q8)wZ4!Axsm_BXYYOXxj|RrxHs+%BJu*>GCn21kMjaLPsdZ=m^Y{(+<2>%EO28%%wuqUDMu8OH$8%zKu7G4&}qFQk04YA1g*$9*T z-fAmCrB}H$Kq34SU>Gp7@Ek5m5M2v1I8?C_X8FB7VgIvuz5!mq7wBXR;06n?h|Qh^ zpAc-s4G;!&GQPme(+%+E*a@BkO92bdVTL>$4o@VHrSfOt+~Do02C(pgS3oS_4`l^Z zo>0t&81Rby&*~gy9`Es{e^8wD9OKI!Tp)18WVKp2jmH3M;1&be{m1d9 zjuM7eWu?2zR$)RxLBX-u*w`n5Q!~LS@l#V%#hHJEh}<7?G!99^gxuWRqXRF(=;(_=tajB`c+L4k@u+Sx9J@^Gl=@-gH zbtLBlx_R@aA5c{psY<%p+1bzG#bO+-^QBvxg}5s4BkSp3$(EDi>G?o{CL$EY9zA-r z3SiC3!~idWh;ewGB~Da4FkquP1DLI$BwbxbHcOsIw=OV5C|U`vyjeG=4PYlx_v#6Q zW1pr5FuUI*DgFSSOY+EkKo~hWIdtX9mA;@;qG0A=0pN&_+$2GE8k#f#5uZfT(GWp_6rYpFk-=oLzv*V}fW~yFWgb4PcD=P|8`jfTZkf%}Uawq@;u?Q>N4ay5-B4`v4syZWv*J zub3*RVMCPR$CU=e{Td|1QbqL0AaN|E&kCUZuju*rB^tn(-W*7IySeS{`Y?3x;K6=E zTb$e#fsPS33|Bwi=x0n)p8c*g;P&tk)dsAoZ4lKF&m7eTFy@GGD!6pfrR{2T5(xUv z7TPMSp+AJ5)~#FnB5s|~(txB1fh?Gls5gM~L)O^e$(mC3ZEx1__U+qeDpjiVAmA`m zGz4+=bNTY+`Z&M@v@!t6)1THi3UiwQjIpPIa+WV6DKD?g^FT^Uih0qZMJywMn(qh+O?~XvK&=*KE2U;{)V?3 zF$%RZAbUC68A6DLj#Q|19>{a*nYdzQV`uNEueJd~DO z+BhckhB^k?(?U-NTIp$56Fse|8Ae+h7$`L`&@ls;&|u1%GMOZ!(Ww@-NW^E)o?Qfu zeOYgyE0TaQDxw0~?hXUE`L?r*x>>NPu7NK9BZSfdiwXzf`FCZ4u}^QxUB8Y{(m2!| z#USL@z0mkUzI>&K0U|bqhK4?jHfkge(A)s?`~W%>&_k8@+Zq}u$v=oi(ggEIA5i{@ z6Atx-Pzjyy*s;Dj?}UiDUX}Q3Yw9WfbrxTH zGDnRhDLbpw1`r0WBaQy#lTWJse|NOV*5jHrYii@MNnAd;$^Z^G*MA76P0byP`NunQ za&;e>BS)wzMX|q(CdC=xy<^9YhIp{a)dnPv3#47$oU3?lzZC3N@fpkY>!mgz zIXT%papJ^QLTkGQY}vA<84k#Fl>u32w%qA@R(HOfGBDlMZItdC>mzvs$f5t~P-AVCXiQ+4&2H3^>xZ zhuaOvnlN5f5=NURB_blCd9enBhlf|eI^Y(UPOdb7=`s5}ef{ZTTGZq%b?xXH28wyb zO<^b`h_a_m6^k-eW$3M2x1zDusw1@KYlU>U;f0uYtY#JJ5>I+yG`; z_Xt%pVI3B5QQboSkdza!NJ|L_EHmLv@4Q9%`}aHAAr*3pUXW--JPEvUm4ra26F0&D z{1olzhmk1exXXavlTOgWX78}LszcKDMh4p56f>j#o!@P&YjD~GW0-+*mM$SFH`meq z0B*(+zBjNG8b2bmRyN~wVv|5LiZD%nOl%nafdzu)~bNU%tUaz0q4KNMtPkFm`lFe!@^DW7^xH!{i zpMBOlb<%JH2}wK6vtA(iO1-dz)`fadYCE$ z%%6Tt1*cDIa(yT+bR&F|{zIWH^1%H-$A}w7p7him_mC#m{pg?Cg1PNY# z`DHN+p2har68TB2T2w*51dT4W0V8^i7suFbYlMHfoE9{Hr-%W(xt={^I!R`;OGQ`I z|5g!xcJJQ37MeaMv;{gwz;_s7LPb3I@wIE$_7zXUVaU}6^u;1;kE!Rh z(|lYpRaGPn=|7p4*Xbh+po~F3lI7M-w_Ki%|0qLa$BunpXv*>5UJhV53_V!2YSr3^ zT^G17q{T{j;MhYntK(E&g7mY_QN$O^M1@0h+k7a_cpCreQ2H`rl?XAV z68_C4mudT@bh1j?imo{OZRo4fiWMuy3oV7FK&M39DB&>lXy3kl+hI+R#3iW)1~^67 zyo)sVXd-3im5^YyOTH<7etzAAmLGle(S1OtgzprAuMxt}N~cbpnrgkXGW2KzM(l{D z7^B<&mjowY8%|t89-SvNl(qEIsTsmCv2f_!yLTr{rDUE3o@T(OtFO_im`t||uRQzl zzamm`_WxaI*uQ`O`+?3L;>Iq*vCf@4KZ2iC_V3^SD|X7n3wVqH{TE%P4d+s+z^Y8J z?mzoQ<88^3C6k4AvW8wj{lL@_x!_4uL``s2*JWN8xYGdrTx{ALj3;A`R&iea#tXvs zwd&QYw-VZM?Oz;x$1cJ-Oe5i7<(V^QzMqwqMf}BWcNp-~mRocqHAibv)o#2Ku6+1H zXa)@*<+I}L?{wFJX#v?!HTPN=%$$@O+{6|>-FFE&H+fWBnZPd z#&T>M-D}(44PNMMW6jXnw!O9;INP>u1(|%heRnbt14b z%;Lo@rkE*HrZ6tZ?cTk6A5BrbgurH8$W7n8Nx<)a%hSQ~hu;gn{w};1_&F?ie(0fx zUWr5^1Hfwd@Zr|;X&s6kJa}-Lo{P^of zJw)x{Aa-#5_19krHil8cJ{~AWaV@1ohYsv(snrbMVg?%`n*RSiP4T)0OGht#?k~U} zvEl?QV@Ug|tcrm|EgWDaO&Gy75fbYi%YJ9S+CPZ$$ z@y2sCSo==oM$FWPHe2egZQ};S@251qB5eUAlC^2@Uh-&p%9~ z!k37Z{%eEZQZF00a_r^l)2APaZPUW*LBs^)q~fsU%&dzH{T4b zHc%6waF=?K>B_m@V8x0RYpbfNhGG-8brppTd`B_X4eL-*QBkDlDslAGQ%{|)t&h*> z4o#fk90C6nrmiJAp<5tASC#r_wKO{u8?eP}>R>ivYiKD}oLp8`mXn>GU7^?Y>FMeH zZ@&5FOP_e+iFfEJ;Rq29_`#g9$6`D>pbHAIh^{@H?`T@#a&$&_Y`_+5GTYh&xsAS^ z%FD}(tu{o43>h+R@#4id$PsT-%2;cL?7vtbN}^xTks(4RviXdzB|2#p>qf0$O+Z(4 zMt5xR8*==Iq~zS(+$^mS(LmRTqy75z8>>=wk*fKVv=KIvcFy<_qI5#eDR7g3Hn4x6 zmkTlOo`1TP=y2V2*InGFPoMGVhK}fp&gdTQkkf>bv$%t*bB-G4a&tsP!}z`n7cM+e zy9ZWj1JkPj!lH(&8IAzXy0kz1iGDQxl}JO{--GQ+I*w+XB3Z8>6WP+CAG)9ux}hVw zdKuYx0v2&c~z1(GLVH#Wc!ct;U{-SIo<&2pwgMf zfoA&Nga@l36&A8;0Mxk7vAwUcG!^`Y-&!8ICaE@0ri|jx?k-uv5rmFW@bA2pnr1XB_`0cAr~1`(4QCXBG>Tj>W)rr~m)}07*qo IM6N<$f{P}YasU7T literal 4951 zcmV-d6R7NoP)KE;0(O~`)f^ydw=_RzVCU5Zx2sB z^<;UnbhH3~6`D0On+}YxXY~X*?ExnMJ2A7-VLLXW?N~oSw*4MXVA-|@IR0jb)(fpa znlsvPX3p$8`pvEbLTuUVlMy27p;j%v?~FDK%>!*M+HSO1w3}!JXq9NSX!Xo$*>`u@ zIb&T^J=%ISi3oTu7sdd+Ju)6i6(d5z+G^j{u~W(fsNhE!B;30atj8& znek(x_Wb9tQHczE zxrIxPT45`Z(Ae0RR8UaR( z;*?h4*W3enY9PqYF9lirQhdJ^zbmvzzfePjkOjdN4Ut52&k&6+AoTZtPS4#F&tYfa=7YizPtfsvG2A9Uu@?V zgW_&1DDl87gajozWhg2t@;B%x;NBj=e@hhSYZUD$ij&(1Uu5!2t3gqbW)aa5^9PWd zn>)iGkr8(=wY9aqac6h6jq??EqSYqgq0In!$|g|Om72RhP!J|4MFok8iT(8Ilv#0& z^vw$Q6RJzZqquWILjstc@&rZZK~T!1W?j0#g`_9~v9cE7rk~!Aaj%!Cw$o%(0=UJ! z4~oKzxGl5v4?@wXuC5L;GBOtFWfX9SQdwEq1BX}^lOC;0$vP9j{ln$YKv9)#wo+74 zQIQiB74;{boih8g754srFzL~$j;}8Q1dE>aFn^F%niUC1fTE$H0kX5Rza#9lCV>A@ zB9RQjjaIVWuQVG1xSjC4{be$;U1!E9Ue`E&yzG4oRGzR{hAQ)VE-(L za#0OFx9HK{PC4Kn^fq(}O9zbkt>_U~{DA)hW%F53~(~V8WBB0^88^llZG7AE@Jy^FE zWI5UTm?sdj;^gGy^=AeiTDsQKwfP*hgxRH7zFeB;KA1BkJ0t9(F|B6r3J=!~fdpyJ!P z$(oIVlTO}{{^lGgcW(|H0pIdFU95890;oTJ3^1h8sya%iub_X8M~v-Sl_l5*D^E|) z?hOqMzlm!Z#v(wncNY9QYpzD%pC0B7#WOwh2C_OPS@sb$BqwWC7s5#B*2IYusV-#I zEE3|?&+Xf{_r?%3*O&y<9i9QHOXdi1y(ZvC4)lUTH{*gYw(4&;Vm4i)HUZ>_3knMI zmoH!bQmg7HC*bJOqmH<4u^~dxmIRR8WUreICr4_h`Bx@-nhg9$By8NEQ7BqeR8;!S zH{ZCZBEb6W*|WoNm3ZLY#n@ zn3xgxhiXF+P;))zVb!z0?%`$0EDy6FpgtyAlVq$037DXY0Gp_&sF8*w;B0CUeEnV! zggY&IYzUB^KBW->fq{XqveI;G0wN+Jh8t3XPW@gEp1acE-7N|5FSqqNjfC9kW*|Yc z-9eV0uaN`=1O$v#Nx-&k+lI*H@?t|1!0n@do`xNhR%=B-!nnC6C!pqsK#iR6{rmSz z{rvn!sw7~|nl=3}54dBfFtjBBWOGAq!QWT>3Z9!R5zbYC%#2X6#Y+Mo^ zeDOKRva+-5{_X4mpaw6 zl~cZtNm}VFcfA2$wd4b6xNt$+3aKy%Zrr#*r3lJ|t^NJ|o0TH0@CzCe-O*9(G8F*} z{+$f_M=a6GCy*qadD$D{TybPLKm6{5lef+!RQh;BU05h6>g%G`(FOB}70~RaLNS*RBr;W7Pl7s%FGG zZ`rbC42Gf=h6qJf5ioDtbqF7_*yIFMz2^(k*mIzi%k@4bdGFr6s+B8OP9cm{O-Yg_ z(GSm8S63$+lYnJk$C!3f-pdne_wG@_irIWMgE|Q0Lh9^pfM%IDCdV( zR8&B4aPY^3E&72jDhXi8LD%{7=f7NEUw>EMGNg)t1wq%~kmCZ4dcW6>)MndQy5Ssb zdu>g}rzS352=ymV80Cyv|asgAwzaAzWAaeYFC;*>p~r6!tZVV6?RTssS^BcQ`f+v z9}+t(XK?#^-!-UCylSfZP6(K#rlw}R@WKmpv7TLr^23)8Km3qRo#=Jt%9V|56zJt7 zEp=)^UOR7s-|B-xgtL3>$3l+}{NI1Vr+$Y8iBSjm_e0a+%!k2yiHzCu!U2++lR_WTaJ6N{i)Dl-Z2Z8+c388c=~B`lXM zTlTaj7fK4}2;r;4wQJXcl zV8Md_#-UWAk5OO<0#+Q!f@^oHO%8m^-k*_?QHH($4Z_gZ*SCu{7t32snly=S?cVRg zg$tYL!ed=Wfj$ZFJ&+DjH_M<=t}rL?h2Bqvx6shg9|=2#p>}so!J$LGpeIH|E(HYz z>AL0x`XIn-cNzp=Dul{9nb`o}a^eMLJF9>H{*-w2R4evxZxrA#A_n{J*sjps^HDY_19L z3-{fIi0h>wm6`7i{5Tf$nv#-|=j!S@k+6FG_1C-WqxbX0dh+DS^gx4`Hg4SLi|Mso zo2cmK{Tj7^pNl#l7+(OTHH~Hqd`s!c&CQkj`S~p&j2KpWzk8zXh!G=t;p^ZdM~-|) zo?cUbK$+?4$n18(|12sOZWmj|y+cJ8Vnaws$Y#QZVPuf|Nn4?=b?w%z8=coOG%hYK zjFJuFfKqRD8(o0O($etER=|IFFazQ;D?y=9S_=3=xq)i^5fKrG2@}GGU5aY3Cv8{{ z9z6J2HnrxOkdSbOX8XwjX+lDcR1U{(ltI9`eE2T*t`N`HHOeglcn&@tGhO+3>eQ(? z#9{X@q_`xqLU$|r!5k=Qyz#mIzB%B zEO*S*P=#gO`&sQ!Q*<*vIC=6U$qt=nK2YRzjsAos+F@rG7Z-ZM5Iu!%WOQ_Ncu7eK zkB;Pz0|`9NXV-X9eV-gI-Ev?MJNC||0KXXt=zN}MBf)j|?%m&GvL>S&6|s!jB1Z%7 z^~|@?9S~(fK|vI1y3(<-V>T)H&E%+W-@gCVqel-SVfg2tfBrsZg}J1amN**3y`GLC zDJkjBs#U8f$r?__NXKd_*=d&qdwcsH18~9j!-fnQGWo=b6Oo05g}_FGP%1QMB3Q3i zu(r~5y>R&O;p0wDPBg1EjE?O&T+9u8F$olgb#rudd=X!rC_Y}le*LPeSFfg*mzVP= zm@1i*ZZfCPlF5zWXsNB(>offQ{Xb(;Geij>&X>6%#m+$27NYdp1q+9Eu~QzD^rIEzCy#-|riX=v?Ls1osF%@PGkY3`oXJy@!m#&n>Bu9Bm!Bz5W2r_b~K`}ZdY?My>@!h{Lau$OJYVV*`l zf@RX23?+-4_-hjbPE(+P$=suDaf#vg`KPdw+OTuy&cJcw#=XVl#+lkmZDzgR#uCAA zl{u4s;&ASb&Up|c!WGl_@oU$vT^<=3c{DXOHJ<>XNewXQTfTvYayQac7A=u94Mz}0Iy*bR zv2NYERha1PzkK=fH9D)HxVTuD;HKV6*I2QcCs4JpISL`_qB~TPrk9qMDzmb(N)r+i zuFAG9ypC+(ZsKy6_<-;K4=`l-iw;Y8R`JdM^5V{1MtTnHGnanq(vbHjXM`Hmes zHXS>5?BID^l9Q9KqgH39r>7U@gx=w5ma@hsw2q-{yuf= z*s=ff=9_Q2;WCqq-qUyZ+Z0-dtviZox(nOZVA?kAgXw#Bwr`uqO=`3u!kTF=0n!b7 zr#)TJOtCk~0~;ZZOtPFApk%{DHkAEF>p0SX=)Y_meQ29ZVxDIEvi_q3uMQzpYw1eB zQI<_-8aOyO{E7LHK4PQq=r>x2)@5LKW!rcHxQ>KyQmmO|*f3DV#=o}~fo=AH|3B#l VxK7YlkA?sM002ovPDHLkV1knZn^*t< diff --git a/demos/main/src/main/res/mipmap-xxhdpi/ic_launcher.png b/demos/main/src/main/res/mipmap-xxhdpi/ic_launcher.png index b5a12d35f337790ea7c6eb6cd43ac4cf5761efb1..223ec8bd1132aab759469bd5ad2232f35f7be2c2 100644 GIT binary patch literal 7492 zcmV-K9lPR*P)7bFjo3yR6hYmf3aeOcP}{a`bNw*d|0U;cCTESAhdF=p<#BND ze(#)nSIgQ+Boc{4BJn&=2L@r*gF)y(i#kyntnL#ek1(4lg3iwuHMt@n4EL&P$1W2%4KN`sv1kd9&jD}1l8iYZj<4mYg_}n>DmK;k!doC zK-8Ytd#%B2upQh1F)j+>AQoX0DsDZf*n~wmwTKm?d)ZF+)|%ZjwK)+eUDYNJOanW? zn;L|%l_)+zyns-GRb8`&D)ol$bt=dUuPTW^d~&;k)!;FcCKVEfWJxcs63RuGP>*R0 z-T9A11PV@^$>1O;W0l6DU%!{(q++pFS4Lx++;Sp`K)hAW0H;{BQI*EaQfwuY3XO3l zp9c}B;pl8_FcBQ(;;8nNBe+~78uKK!*3(6xI$+}T00;fvkT_nBW3h$OLC}NEkqA^r z)zLAD%kfz3Gl$w#bYMHUVww}3RU;9I_4i5OP5`YcS6bU$*4E6Z3zQEuVm(mt@2CU> z5?Q|aSL4y#5NK~Mg#jwlFZsOPqA&&6XANTp%zN^pJEs3?(0 z%mW&rxy@aHc+cM0GF3;ak!4w%G)>b%=w(ATBu}!%CsIgy2-ZR` zK7IPs8-!AcLXii98jQzpp~)i>ef#|R^Dq#&M1i&~l4lSef75~|BzZ(4RQx~h-n|uaCmfD&_xA;bbBL6fpgL3ffTX{bTL=(&!o5T$ReRGIh+p* z&-^E#W(q)kq$}0MM!=PkkpYpBks35%nVB%rjB%CERL7$UcYC{>eJ0L15>N5JQj0ins zbiIe*L6Nr7Xx+mxDP#))AkHpoK=;T4^ipoAgNS^TnVCtU?BC=&W9vgki{|X??B0Cc zqdpUw{8)5QUN`)lC(a9fDlYz29zjRfg}J?J8x zP#@)}T?^@O1Qzbzy<4P=%;35YimWD_x{u-jirH?7pg4eAbH4e1=l}_OERElh{3n7g zI!N_(q36$^r|#dszb*OP+1a@aniguRYLgZGKH%@q^XN-kNV2ttj2(_3B_^sa zjG>>Yuy_xBUXgrXSdYx0sxuav2FSY)6VHkSDD~Jd@NBQ)fWo)a-8GQuJk(%tQVtJ; zprQ8pfRzEV8b$`mBZot_rw3q)qb4y;?F~3{=FFGmdlJ*d7SXhYQCf%P?v4ELr3`?s zY3#^}M%mpdwuOG3Y`39sXOb<3J_c*S05MCMF&%QRT~$T>2$v2=Uuo21H~GG>Fji>z z+OcCtRs6&cnRr$zK-um?!LyylXn@Z2&_J%w>$AE?KU(A8Dd1x{05MBjwF0!ypDA8U zlODobJ|f1CA78t$Fji>z!rH(``N9_{6`+^DSdjyTSbPClS*)WaWo-P91AvyAwkCNKpWx-{HZ72RD#`O1v<}D;`>egoL!iPnO2AFGcS_^B6$F zO%OmFv$llv4eLRQic(ZI_UzfS4Aj6qi3KVu8eB+Z*%zjB4uFeYL#JSXsAZvAfS4uQ z+d|f{qoB*oP(o2rkL4RUckbLIVgb1*Zzw=a|CGZxxCwCSa48+2EZ3owR;wByW+@ZK zL5{y4=q2fmhA~R#gM)+j5eu)=K?Uj`TU%Soz$##i-%lj~kx|psBmiQTwqz0Hg}$-d zMo0$%%9vo%LBvEsI_S0fXXD0=b&;DFvi{Ml0icwltP*Vk05P+%f{e|ZK#Grl6U|jR zfEYb|`0%E3hn|cJ5DQ;ru&qz8yVi09v?6<9ROoYSVLAXYOa9IQvRs@&my`2P=^zr) zbaqTSc<|uAh>1OW_LM_2%Ww2Ueq#cDS=h)`5V9p!e(`Q6w-NOZ(w_&^~%pq5#p}fmaoRudnYqV!{xB$jXy|#-lqs zn1EIzK-V0^3F@;Q93b>V8%VIUEYO(=R+bPu)JiZU1{jTv?Ai&sq@)s^QpBrZjuI0F z0kmSp3erH8@B_Wn3Q+uROYrI_Mob6et`g6{QK%)48Ufi}o}kam6P}6X;pe=5{W=ZX zS0*;l%<>!k@IUt9haVC^mGO$+Ye=A~LKw~R+!|vG?ybeZbG0`MN$RlFnK(9bSSgg9G#8Qh5LF_K;|)WMDXL zku^WlG^7xKhym=0J!;snVLf7l7ZK%r4qS+YjkX)Xwe|3~02LLWt(!&cKw3ipVnxIXM~)onj__nF1)v=P zufX=uLzuF`ANG8-w8SBL{*~zfl;!4XG(gxF%~`s1Xb<^Kf(X!q{j zo$(W-vn)(005Myz#2t>kJGI0By7w>B5Ybc@LKP1p-g9YzZW$iJJtX@oDw?LyM{$B!Q;jT<+vN1*_*NyM>Z$2P+r$O|O{ z#BA)gE3jqQI&f_%HamOrg}TF7+VW+f%ghu4P-tjq^p{_L*@4)gb_SXmGqe1^U;v@j z!Ex45*2+*aK+LAE@r46}76=3Ad`Ab?L{*pB+d}5ygP_aK7S_tZ;lfAAL?dE@7(p{* zW|sdKxGY+>Y*`VXwXqw2MNI&a*&TcUOXmFsC%R7(R*_DyQV&GwKd%Dq^XEdxS|v%M zbMhg`L_L`e3p36#$DN4&QZ0NdkS81GPzylJM((@;$9qi?0+C-=dx##a?80K|tQpjW zFR{B5FI~FiOPh+!1|J~QLY47Z6VFEPR4YJa8|*g=3uj)~Ho1w@hf%KqiGH6nQ-b9i zm;>_m0zEppBqkEs(Vnws&z>MAWH!vX0L`5{w-P>UU@CsCQhk$%oRL;~tB;eD(=WtCQ2?rrV~iRAP*fq&FnI1$ z)%uN{%6RgXt@>u6IYagMP84u-beu{|7zEJFnKLO1tG0LV-hTLrI1N>00<>oAE+w4{ zQ~qfTeifQ(K)H(-FaCm<7&B%}#X({RZ3aFt4xCi4XWU?0p8f@D+8oc13)`J|G{*Q zHq(jJEt^4#i+hu|*HZ=-Of{g!jT=`cHqZ)P7FZLiiqE=FpFZ6T0IC3JjN>)jeZDgZ zfKnF=I1?`*1H%mj+}+&|5(_dD76t({c<^ANt+v0v|0n=Zg$7!<$W?LIyD0xw4!(fK zW6iK_+qPN60x@AIK;~b4^%V)}8uRAO>m^B266=X68KBjZ_Nf-2WCuIQI)04xu9RSU z0HD`lrj8gfVj!^~7ZJ@30*J&??b@}g!{w6jeaPyAc2nSL~%^s(0_+)tP`cbXk1<`R7$gJRLf8 zXfP>qRu57tK(khy6$21fl_&jr>Ho(@wqAy_!^5{_txt*0?|9SN2k#70& z<->^q3|~w@8@$Z%bSmkfnrJOCjNN6skWvBqZr>en?li_|fR4R44eSm*cd@NC)B3)=f$Wh;04vtwsa% zm?higV zY;U+8qFrk`Y8(1kV{GB-vkfy5mc4iOIE<|a)(g9*N@z+0L<%GRJA!h%; z1+aI}LfAHVHT*Q~FpR_0+}K@bk@sV`AC;jVP&mD>goFgD{Wy`Yu>)V0IOehbm~sIsd7JQC1Y8VD085yUbr`-wLt-g?X#!}S?{j#OEMTxpf$vGZfB(J?r%{KKk2`kkm|r{#0f?HfLrg-hc$;j}ghK5u#m^g9bw|EI- zHhp7yS}Fm^{_sQCcIhR=r?d7$sWDpg{)-%qMq^DrG$8X5z6+H@tKJe9K0CO&x~{`A zIFYRifR_3^fv_0i1MI4q_rbRi!uIXkw~+5-A66%y@^>HcyHG{6+BIs_Xob(t_>%hH zvCUB;S=qkeC?ViXc8UQ1$YjvzOdl3bzl&ZV7f3#neOH@&Y2fZ7UQpMqTbH)KHr={) z>x*rUS2Rx|8luzp6O009#IZ0qaU&KobEySWdf~x0Fwsei=uVwF4I-bR?-~me&PE98 z8Z4BpT)A>AruKT#dMG+Imo!VU#w>opF^i~FHMSqbpYy_L3O(MwGL3wNx~CQSPMB~u zB9v7^tAkbQwzMstIC0_^x)S6sTq3K~y8??A#uj)zf;+F$OFKBM)Baa@csQLh-A+D| zeN~5iCnj_W2xVyXt5>g1-&j|?(aIS|V13I)*@h1>BMQS+ zwe$S492@H$$F`jmV%ufMwr$(CZQHgz_`lTOsaH2nrfvrJR-M(`*Es#pf6%A6<(9na0X9wtD`8XH2P<^wY4pL^UXIuLj4I79oj;)BvePII@3j# z`3coDFKX~ibHr7QZdHsF5aI1tsfK8}exH*{rFyTq=9*iTYy?C+eTiT6-&n^A%4?7s8mBq_$Smy%R4Z-Hgl21k-3lu&q87y`U4Q=fAWBS@gtzGo z=o+5Oq}v{JY+YA}a6Jm1kqV+JC$VN}qFs8a-65O=`1$oa*@WcS3qNF05&9>;uv^n~7=PtYKvW04^-z00Tr!O&Sf7@jB;Y;3lcK-L~3cF2<86jNd0JIGk*(uC4||9pou=mFz9lsBl#1&qMtrMCW%^J#Ym1 zcI{y=jt(0d8rspRbz6wf=ZT+*m{{H$c?|;5Z1!jHi?q>q;KL6;{59EAJw#{`HcA|2 z(jZEQe0|`72Of&f(4Bt}8?4j(umDdC$;O;ylB8C5&`h5+X;KF?yylu~u71T8SKO@& zjs~#rb`k;7m<0|xfSF+(1t0gWyzJaF&X{ztqd`P-^gxV5HH^I6q;-2`sb)^yLXBoN$g3f{UXQ zbR!+jL|4|Co#kN;o*4Y1oOg+ITl3dYk=m6ye)~O#l?Tf|Hel4GXddFLH+I^XDa)a!KIW&;DK6CI!nvrf5nl!<|LMl>5u zuzhcYvZX|lWL+o~C-UTzPd@98JMMU!Y__hN0Yyk3VlWL?w&^$9^nreosBA$n7ro%` z?3GqpX>9|ii#gj|HCY!E64v==zEQ!^MKww+7K^ipJOU$0!3*!V-+p^5YI{)T?H5+k ztI7v>45nb_0M!2tn4be^I2i%k=!%?g82jFP@4Zh&Ma3e4ahM}>HE^n}gTg=ebrLoz zGgTK|2vs9#HX{Yo)-9QP?X}llSMT*a;kxUtdqO7g>2GBhD7@b2pt91t8I%0DjWx!3J=QM9&K#Ia3F8FdWE0>FJ&;gp(X?J^$FfzFKbxQ6 zbUGbai3RFyZ*N~tH?Hq=+;PX9sN>hv`M)f4_XP`!$s^(cSeO;S1*`n&V@Bcsa)6TW za=RDoqL&%1!x= zlC}V*z)Yopk}RdXVIiIOw;4|Go@`-@=;R6pm@U}L8U)81S&s=rYRrj5L{`uF?h7zCq( zUjZ=QhfbiBYxfHY2$Dq*DwC17fhw8%8fkW<>u;^}w$O%YH@%ntKG+B`gGs< z1T-TTOyj(0DLbb*Nx@7oJ^;%MQSQaBG;35xq{^RF`2S7`Kyh0|x1DoeDfc~Ylz=7y zR0LE=%FK{4OXMv1KaiXy>+hrkP$FPS05c{)6#-X7#ziFhITCGW%sqoSto+VsHI1DB O0000W_Vw2BsjWj*oad2B z0R`$%6;wb#P=O+&h|I`5Lr6~8Z?Cuy?~n#a$VqZelJkA*OT^^loV|Zp>%Z0>^2Qtb zLx1QG{h>efhyLK}fe<&~#`C82tC%*v4aU2Wn~VcKBLdrD^4V|=hI*K|2aY5>N&O8%%SYJuVtY(HhAdFT_z)4Jh|x!58%7`W z`{?tCTL7z`T+3c^ZDvx}Y$Oes{!pX0Asz5v*u1c%%K(OAfIfveDxJ1x?hb=)08aQZRR0)eBP)UFy*T%fu1ac2H(q9sNkE;%x$=J?g ztC8|FBJ%A=95*q6i5co@$URNggTyX4%uw}rY`3Mn4eeQ&R@bCgQH5)CI)(MBtz>ko=+k*6KxGu%7(TlQemxwuES%@ zc)o?JSG%f2Rx~ZVe>|~WXw0;PJeoj%8 zEBHA&Vl5D2b5fBw9qR;K#2(5tMhG$+$4^dUetgam$tGdhi|sHo_PpC_^o4<$rY zUKkQR-Hk+H>#(gyqS(ENDu~4YC(^^ifC#cPoVX;1hKBa$MT$=svJw*$AytoLUGr4s zU|`lF;q{p^4Pc^567}#9qrbT2<>k?P_wF_2bg_|9N;kk4?#ntn)pUt2P(rySXkI#PKatL-Sa?I z1EZq&gGvMRS|82A=kh}0`ZApjWkR74<>lo?;6Cf1aXmz83kF!EZDwEhFaV1Eg&lxs zyX}Ff^s7iL66hR6&_^{jH4w^vB6`S3qoTR4u5N-VbdO2_qV4`lL={9yM7pl&q0-V) z6cZCO75ebry?aKQ3SEZSEk>z&PLIj}k{C|=A8kV7$}F7=Wx2VzPjDX^t38y_p$lKc zT2+QF^#DZdrV|o9JAuT4N}UO1RaI3eF){HwwT3cM$u!8%&mVwEE+e5!%zJ=nyE7kA znKvavCOR-tAH90@syrwtXb^$OP+i)`kfNxQ>Y9$KR)FZVKlI;-DvH%=eFS}nH7rU= zNx1}ls`9j+7S5HGmE-9hywowPP!j+(4U0V9AsI%|WRtuRa;w&;^X7l&B*Aw z4;ES0>Z<#&0Z_p~Ck%=^@*B{)g-mrv#mAhK_ede1Au2zd_jRGHtgMQYCr>&+pZoau zbYQFMY*QNM+lyB!Ef7`aj#2;#0zW~i8|I?ijXxq`S{m;F z!rB(Pm%>RBij0ih1bzPa@nd5)#=O9cL?Y2+O@uE+0ebP{9OP#sKRmSg+qt9M-OlZT zCvUGax;y3ddD5r5pi-APsQQo>5|xzjE{wtS*OZi$KXIRTBGqXFrL|8{QPC87H&jh5 zN0t|$iVL&RwZR?=!$aHejvlDSXO`UHl%JiA!e_ZP16Bf%^K6NgE$~Ej*RCR*;_ymL zzk2lwUB7;PI`q9#Vw$+H4s~^PyEVDhoV)-%@^Ec8Jha8HaB1^%>ilM*xaBUA&?o(V zCP1{6ulfQB9!2q@egt6*roP6+#2kgbZ+94@!`Gohhq~euCu%}GD*{lR|15NEkcZL$ zT_5d%1VJrlb?N_^gJ09~`(*({TjiEbNSKw$c`*%o==t;Ksh@uOsd>A@7#+R}3JRt% z!WZHyKsi4D%LR@Wv#v_4w02-V%Zfy6=~C&e_( z6^e+6@B#)D28fY;nFxi#a6SN(u*#J+fF$*y58P1XTvrq@M6s|aFF>?atY3>7;$k_a zj{qQ;vH0xSvlz^Psls859`}lkjU9wfR;j7%OCb-mo?U?2Yl;FSu{lntYS&IA%FE+a zHkO&0S&cRD7+^u6q9UDr;dIWO=pT)4LnjYFFl517fatL1=7MT3oJUkmH5ZDC;-aD= zbo1uT<-h_4$V5^7V~peAUP)eu53U1L>pM$kTFobbn$rGdOHqADFcOPI8VzHR&Odzk z@C2|Rw+@0>=i=f5?y93eAPD9QKwvOvsto`&rTrVeK@CY7+igUmgRqQw2s#LukV^;A z`e*m<-4>XeY@&a}d;qBYymFOjt^p*mxz4D1-(DmtD$>Z{YFb)azNe>WKQcmZC~x@c z5FZ~8KX)zEw18H0EDQ>L?HLQ>EI_nAn&*ycfAc|9eZ9JLkVHu5=H`k{pFaIQFmdeI zF=KfJv{6=8)?CI$&WJAnNk-$po~tp4GH1+H2|!KyXvM#fAUsUvX+IItjUc4`{QN!z zCgc**1R#@=l9CNX|A_ep5LoEPZmh!*IM4%yj&(;dlUz{jdv20P_;;X#y9C(qzqz6k z6~fa@AN}w@NSL0kQh>mBR8&-;J9qBv04C%Dh`gv{Wo6|dE(qxM0`$n8JwbhAh&xJ} zIu{i?ImztIVrM7xYL>HFz-eYMZRE&dM3t7Rbe3XRSlD@BLN0(dZQ2AH$P~XZfUf`* z9d$whL)j7253yG%Y5^RsSlPn)s4n0d5;ruko{2T!b4Eu;!({u;gpJmZsZ*!Ii#pfW z*FORQ&8H9sb9tUDc0vC3?7$N>L46^K4XarF1ro%>u%?fo?;(`M;R7>aM%XZD&HF%_ zX5`@D&`l^5Ci4X#K>!xc-^@ipBiKvfLmb^ui4%X|X*MjfeH$yYA))VqfoIR2rCV89 z!B)XWgpn5YPY3cME0IW)!-X19s{sA}dK$-_N$iSce=LY|K6d6GJj`tE^A$)`T z2xLrZYHI$11q8GP!*D@=*NVYAzHJr2(q>Z5DdE#06coS6#(9aq3*H00>N&nyPd$F*7r> z=7%4Ccw26O@Z%^9Pd%3a5(Lgfso%1zb6PUKhcBkpUcRI>K-t;Zf-PINOawMs1&EZY zA>SW^;c4IsKn4C@==MbQRFeH|JDa>V12Z1}G;dM}z^I3~XS4Oxgs<+uM6A zmjF8TM;hAgvK#r@d+GsF6rlb4*ziC?xd0;WspGkG=NvISbzA}HP-rf4Ig^N1>i}Vq z)EPhaC@3g6%vXTGcDe0iEu00|d!WaYTu@#MAi>{3GZi~&U>;Cl01~TU`xr7KQL?n> z#*G`W?OhMTMyo3`4A!k%2TNJIr>3TU$CW9qiUI_o?AX)|>=zkr+|eJy+$GakBPYCm zgn#>!`PacXdn-{#$W!r5MCO!M_*I`PSFZdFOeg|Sx3I7<4|VUx$_7BLrxPSqYfY`n z$e7{6>$%X?;!bS>MDqMDlO|2F!2;?f7cwz<0b02u82Q?HY860bo*t;y z=Zf0agGs0GadC0Ej*gB{1M2ej+ix4U1rQt^EG;d&;77~y^71iheU;i8r{7fXJD|j`OUt4`*x;(XSJ=v(8a~2D}JNwzQ8(-dU@4|&$ zIhX=EH#Zj@IB;MMut1oQ3m}6TGiHF0?!JEg`mrLBs8rKqV)6jAb@>V20#xSiifS)j zU}NlxwF)#4l$e-UF@OI2cYy^`M3ifnG6N7x=H}+z@N;YI#vW)|NXrAz_N7Nzg8&9* zfB9rP+Og;my7<;g)d9r4oynR20{wIE-aXjjZUC?#uYhh6(vv4oHp7orNl8gN`2f(S z&(E+1(6%Kr;)fNb2+@;&$eI&EZs1lo_ilCO=2QUH}L zTFBYmS(){p^z?M<`|rQ^1O{;Ul3PGGg)s*Q2hc%1u-W1;7OzP!Bo9FIPQ)RfVT+Xp z=)&JVN3Lg{wfH%!w%taj-u+T>fav|9D>?tO;$qmDEG#T61@}GtnsX1rfHy4qvIMsY0BAyFuT#NB=bGAeez>W@gq4!dPKpp~fbd zDKZxJ?;o!>>ri>^%FxAV#m*3gr;UIu+Z%?wW^BUx$wLAV4}YbH-VaEUeFX;x!)_b{ zN#A!=RR1u;m@#o-)*&Gw8)0^uCiaY0bUMenAA-=8A@b`X@LFGBZPOljniVs*-avUr z&LWWtr&*|aZ)hkU9zJ|{C-nLF@#6s?;?!JOuk%gd3zU#o&1Z0$#=eS+i=p;&1^U|2(Xo#*;j39T1{+S{$G#gjY?xkM zUEQGW@iAopQk5 zOZ;e`oSYn>ZZE_ofaaY~LT952P-UHv_uxp6ykEU~6(-NylfE`pHheV`(^w0EnC`cA z>(<#-RaGK&ru19^Xvdu_lwDS@72rt!GEYK60=0VeYN$N-8#;8T(&AYfARw``jg1W~ zE^~;DjrDJMy(1{43jnS4e~zAHS0PHfz-jV6Fmc|;$LALGE$L%=@)#=s(PG-9U%!6f zg$ArzwQ5F5NlC4`ypW~rB1tkkr-{;PqgUk!(N)GU& zDqYwT&oWbbLg;{T_{h%A&Vvh4b#+>u0R4FLC3;a@tF_=rFIx@|4}Udg%ox}_YCzw< zeJ$8X-CIrRflxx~KyPpF??AniUj*khK&yPy(8G)ht#Gjo+?Js3PKUD1n>R1PslAwOJ(OP|kZ6|n zOtbhoBpc;b)axWT((An9;^M^X)~)*-`U>kF@IKwuA)MO^WiaU0t6#r<4&W9qUApu$ zYzabsYn8Ps^^q{e!q`^<=_oF@O4q@mN7@@xQc_@*=|Sit(pSA01EvOVXy|APWiSD$ zTeog-VIyzcxZzXydQ$+_LK%dg?|#o{dm`E86m%;|JEmE%6}mDrGtt?zX9J;cNFVjl zWaw%sl$mwz+_^7V3Ni}E+Iuj&A8vrH2B~WhNd&dWxg?p>EY3vcqpEtL&V$2*2cm(G zk54%66WHf@kg2ID3`Ln~GIYuELWZOS-UA;t5M!~8dHC?*6DU|g;fi8$WdhW0ZY7nV z(-@dDQ3aBTT_x2GdT^wQg^+PwyLRnQ+!yduO+!fE^k58E7`(xegQNDk=?rI+|^W^wv5&Wo2csIpzy{d;2NS7o<-xl*yPERr(uXP zXiuc?Wc9#v>(;HTapT7Q9r^(G1;`2u(lvaqm#8yb#pa12f>e}lOwWTh*m zhp7D_3#Jt(B_*LVXU<&3Z95j)n6$N(RJzU^cv^zUk`%Fq;Ts)&^ytw&uo9rArbe%e z+6fQL!-|QCq4wb#fp^x540(6TYDmyyK<>e$fhRhLej|rR#sL3(I}`>`1|_@ z!yFY@Dx^>KTTjv9F*!LImLT7^wY7a8+OU8B{v)AHN!xbeqvny<6B(0Q1PtW$#}EyN z@fpxX2M!$g>G9*oHBdbTQ3&5Oy*Fwul%Wg~9v)uv(@#I`g*Jn>BVWY+q)j_(BRsN- zjZh7R-Ou`(nVAjl*|X;;aKlbcPO}060>V>MQxU{>A`*2HBpMuO1Mtk?b#U?#@$ttW zJ40JRn?c(_8_8Zy{OMDAHE8r0u$CVn!iXAfm-JxZi&G8#Sa$mo5@-gyZ_V zXV0F!5FH&|RZvhs@1P~p*3icAI3kGQkXS@TMO7U;cI;BWe*NBuHi5RmZ8Q|xinJLm zmURjqS-QxSj1pT(d808iX3Us<>C&aZr%#^>AoCy~Q8Y+=hKX_gl28TUfChZ>4 z!L5TX0Ab0;@Wu>Nkdi}~&dbY#nojM-ix+QtczDbw;5b4XKwA*6V?o-eqpo@#dA(70 z65j`a%f^ieMY7S*s30Owo;-Qs!Gj06P|dEYssf56%T>s-B2L0YyiFtVHj+8YP(+0x z2)LKy$B$q9$3Olthk!F0?wN!u8`2it^0=!LZY-}xD`?hV{w)gJcdkXDw;2z;# z2{=}y4Z7&E)@iRdG9s?JE13ud%ZF@4171BwZivKoqc4suShG%(;D!P5XVfByMrJzOJPtFyE7Jh*mJ&yj>G;&E)ry_%DI z@5o5c_3*F&lEfo{F2KTa6wXW_v`NAnj6~0yH*dlI{reAHy?XUwL_|a()RbUeFaQNw zgK^LuO)76#KeU5O3*=XjT|}h!}aUeuQ_t$ z$Z=m^Uzq2gj>BCgbS#8C$anxO2rFdYN7U8<9tB8a^ z2`FsueDJ{sv)8U&yL#u&ox4t*I(6>y<;&Lt0|O(2gM$-8LqlIiMn>l1G`a}GP>MrN zc|t-$d3=0)DF(0T(W6JXVPRnzAt51&K|w*0zyA8`bvVbaUAuO|IpN$!Mn)6KXN@JF zE%7uAXaTjOqnth44%W^;Qt69;~`u!-rocZ5yFn-Ge^QTz_pNTVnRcQ zDPwXkdT=xVlC)Q1!qkhTzkSI!9)KD|vN;J<7>F^DE;GCp)5b_yv;QYvWi&a51Qs|q zdKj{fAFR^&6x$!D68YtTc(Q?3S7wcyyOM-n-02&+!#EIxIce+u1iBJ_{ZA{D_@u zcDyU!&S*>E@jvsdP}X7OsVqtzrcRKo6p#%(5zG1PuE3<= zA`eXEIYz+nMzu5Nv5#1B0mMV61gJgU$3uY&2C4%-{OZX9z2jO;A z)(PKVr@6=i#BjkurqDkA{#R>AS4A$iGyhO_NMg^dSD)p|oK4QcspI_YP>r^*N z3b&fROcF~|Hj+o*^CBOj#tYu%aH(1amHBswiR^;-Nj#3)SOY8fIc&ArMN=|p@yu{r_2BZ?Or-rRVji`=Ib%+v< z;*cH7+w_z*3JRvf)}LMwM*}~eM;M_Dq#SlTzto*^p{WCIe?7{ryVBlwfdhxa)=7nW z_XK)_SH#hvC@&0Npp{A5&lhr3C^V}$*ZTu_3%_A(ENcDLj}1bEJQJZ5#Y>0scHaj_ z_d6p(=sXaZ0mWy_xYbfvVoAkHI7DT#)#i#U8Pcn}n;RJjd9GQSVCwRm!MJa^ zA_PnC<^*qF(!}xP@UZPIW~F14JyswE)=-79U^+8sr|mSvspCQxIQKTFal^^c(I|rd zLS!qoj60ah&k)=oai)?l>@A|J2i6WRaBvmc?RGn-9?VH&oQ%=-^t@ZU+M8IznIi~t z$7){SyR*!vehy#>y|DhFJ7dLR&~gsYy_p=XFjrRgiMOnhc)vW;ApF&MeLzN4!_d&5 zrc#ihks9`JzcI%=>TrPBjSoCgB}H^bA|nlm)cL0qFzE6%^wrJCbNHjmVT^dQR9=JR zo^(OyDr%iBpkDySsCZwm1J1>CAI}rI8*M>}&SC+O2l%`cd#|u;OjzMGa9NG#dYcKx z6xwe%IXSIG%*Wtj1zLw(TKkuF1X60QRyPA)MJi!}#^r@S1$(IEain=Bd@C50GM!ah zqYHEQVAHsr8}34>{2P`#0Riz}#Y9`JjdROjf2uD%m+;Vnmx-o9KL-W|h|LFL|Dk@M zEoXy(a!X4~8(YuC@Gt^><1Pa#qeDaiYX;>o@3o)MJ6yC7d59#DM=tVD|I!1Ab9}C?WzDw zLS4nH(`DjN9BYiRjQQ+!P;*j^(bRBsRZCnhiPvy>WyO6(&ohG!Rn1S&Af)|W6)Ri1 zWk`FwK7dpMlwNM}>-I@~5%10lc@$!Dg6p_2zb}jdx`EBG|_Tz63 z2I5G&5m+~~nMJV0Q-uPlFa8cMrT9eD-F^TKO;92W5BXX0>>(c@natK}&@z{fWW2kZ zV_}}F*u3Jyp+|Mik$sgSuF(e?Ee+HfS#+H!atf0aHhPPi>=O3(_tP7NKJEl&icI`I z(YyS`ElU{_Y@+pMh*e`>{gj=KWWA9Kl|tA{T&K$5L5Bts=_dvGcE3Jqp)=;*oeLOz z0{u7~EW)&iUmUZXWl}t{4LHw0UWMW>n*LP&%kn-^ogyAm0Gua-A^cjhI01ywVT1*VjLa66 z>lhL(>KsKlqW&1q>}JXk_4o+Di3l_;UNP;{zR1-oI-0LG{@3K7Z%7?>nG5cHd?QcU zBruWY!m|t|iw*Q6!-Bu_x_S z$xV$7>3UNYKSaYs%={Nh#3vtwroS&?_XleOy}#$FAd$2yLu9UhFJVym$PJOCRNN?(%^x z_PU_MyiW6uNjo2lqyVDVyv=@t?KF!5+V`4WFt$qI3n!o7BPkd=Xu`2;%R2Q_feRWH z;d*o;BPR;mm(Pf}DzB(`45=n3fwEQ47@&nk50NAX0)F)MkznH?L?$-2o`Uy|95Q%l;Z&Shi0u6kR_+k~uFOB;k4QiZ0&2ct9P?V98Wg23Q>f|l z&oR!fW*M-9YEKZImyr5`IN{~PpS;`V%R<9sB$tQ~fi0bO;*6*xdvqtUyNV-c~TfqOcq^pV9zyLtX^G~dI(l3PDHt84ESjUJ|G zfmANRK5Z!L`UFea-m2Tqgu}}vW&zj&d3C2UOIOS$yRCSaS6BL&N-Td6@NHic6BFBD zKu39%Kto(A|D}oTt9n4gCCc%>0-EYzAy1${EnjHA;{6~&Ped{|*s z3AmAkoKsf;GETmA2^kg~RuWwYr>dpo*`Dch3o_6`BEOqTG4ara>QXIfaK{E^6~~Sx zLz!y6T+!(YKYP2*=Zv;lB*6~j0Eusx^idN3aol!oDBJ1>XoFvTW5^Enb?PRr*+7xb zwo+KT zd1y*@>p6kNH`}jdux3%5*F~^~7cKwop->)63Vj|HjRAoo<&Xd6{Z%2e4pd8bOfe1M zoPN04^Ve_uqJ^nzIZA^DtA+-?Sff0+-W<+0?_A0`fg`i#W!cYT3;<&dWMX||#N$-a z_xuwo02~K0X76L7;WjU|Y7yWuzJHBbfAmBcdR-bAcdVKw*B%{r9NA(W?yr3Sp%yBn zh5Ph?o9Ts8?!yIwJk3j094gfW)?1iNX^TLoLq-5f2-z8SHnm~7 z^70a)hEb=9q{kKr0)a>_2;0i7LAf4|KR*DmpH@A}-iR2qh^-GB(FmS5{^#Tuf9AeB zrLK25pp*r=!MnKr|JZ@0qGe+LWkdp@^+U3X2rhK~6E;f}QY*!+DrfO;++z-j&=%W-A+-HxhRhgG$H^Emm! z4zf3w3?qQSo(_a>K5>KH`ZCW0{I>UDY8QzD2}ltG#`1i0npH51ODeO(=0x+{&lmA6 z1+?Razxk?rCy8-0ts)i%X6Zpa7n6KF$^#IY2+l4F0P_i=Rsu?{`yx!*$LqbtB_%<} zQ>5@NAkx)NM!*jae_Rj28vY9M`)h;(@v5ofwI4(RevRD5>0{zy!+(%4q6OR{nfjZp z$Gq!`RJ_HddGOjtXYowu4bZ@R63N&4zEl5zX=EB;4Iv06aJ(wxk^{A=#);`su56hGGU~CC z{<2g<(R9@D7rQUh;&3hS^_iSASAzyN3ciPbu=j|)?e@MZ&GCax<)c6Z*2ZKsRGq9< zCNJi_V=tw2qVJc(I0CnnGrByA*HsMmua_Jfbn4`t_K1TLRSX-zZHP@r6sa3qAuy_W zR*8KSr`3*DWX{pK$mBVO-e3T|7tVqB!zQaQt@u5aL$Fxf5PgyMp45c?aTkSN0a@N@M0$2U6K5YX^S{pmG>7cjL z5DN-9;gjuBmiuOxdBMv!)&SY&q{&*55{EU}j&5m1P-aR>3Kvc58zQ9iUo?OO(YLa< zqiW%vuax+xH6qvmJGovjPAgxYts8dO3V8vJ9WOW&pnab@Ca4m3LgQE zK;O#+4I;dH(}W^4s1?iZc%uvyL_*mN@cUI=)~@p}=6+H)pB>Rl^sR-+hbXt^{C zXQLurlDOIJ_V^zwOxy{?rmqCBJqzT570blD<=x?LE7AddKWDIhZE`#Y%r*rCKK$eZ zp?iD+Y1mh0OI0pA<%w7kpi~PoJOenm73sc82c2EzOzkFY}IOC-PPOJJmaX zs&UFkIy7maC5jn;t;Vu%N@l@{FJGA%0Dj^J4_SbOxi?LOAH?pqcJIPrNphD%I2G8C z$QmYA^WD*`HP8qGGA5fX7x4R>3$qdTHBN{aYd0>{W^<3^-d@x~ZwQ}fE;{P<{i19d z3Iq@O9v-Qb5X*oc8Fi_EFfj1;WCK4D!*aX=5fj<`8(WvKrRRaGn--G#Oa+8PUMDaT4 zqeo-1$udL1fL393kmadMT3t3k7~}SBw`nj13e~r1P+<0sXa*nemkd_$x&Po8plg)t zOLUZW3}7FDE{>RI*YzjrWUK3*9B!mgPxWQa3Iojwwz%|!o}A4^MzL+mfaTQ6@l!X} zQN09Up+xHleD&rj=oE{Hb6v`W*i&Ek6F^inyPlXsAptDj@$>VvP@+X>wS4?7*Q>ss>|Ec^n{SFZ9LCHSbsc zV|K1?EPf6S+&AG~>8`FIj;CDSK05M!7YEvQ81d* zd>8_7adG+CFP+(m4cNF?C=hzwhkWMrH(%zS5mfB&Bu(`r5n6)0K$BxQZ*r=wYSf+J1R^UDD;mQ4Up z2~qT(14Dz$)_~=I=)?|0Yqp!mtWQnr?d=VZY-GBXho~WF+a(h@?EoKmatWpVmly`U zgLEVK@Gta8+g`|Dj3{OvC>&D{J7Zae|FpKf4ZG8uy4)El1N0dHS+JD!WdJX70(>lw z_xBAcC{%CF{}BMK+Q*U*wCc$v7`+73U!P9hUhuF!JS8DNbQ~xk8(jm`F0C{$uDUeJ zRNo@J!>W`Y9J|2!H@8Me?d3}Ru^Z#u@a(yALmIF15H$vKw9#Vzg~ ztQ5|7d;amyJNs9_4MHfqx*GlRdwqR9b)zrh6&>9bwIQ|<%yhl#dsgJ4+NGGmG1f}x z{7EzTI1FyWzBCSFiDaqBH8ND0oY`Ryb6C6PN^U2}>l*N_`}O5%9%5PrKqU?BZx2G_ zwf+0Or9q>_N?rZ0S`@&_mr8shOU_2BUU|UhnoL-@JcckyP*ikc70+WG8Uz?{6ftR$ z2tj(w*JA<3_6@opJnXQZQcML`ePmG*xMj(&Wd`=#IT zWwwGIS{b^sd5QaDHmwj#3|2!^cR()W(O`L;BTZ2#2V!&1w9f!T{5O(532H3^bKgnX zhw!LmahG6mj&xQkq-#OG{s4dcD6N%`>l-IBM}sxV3}mncEz%dn6;4UbPiIQl{W(f8 zZIuNqq-t1=(P=0tts=LCu}=Lbw8bE|i2|(Bv>+6Up+!Z*aIDMPN~qGeZ3` zh$}(@r!tC%R#Qk_9?o9&`Dc&%8lEfH(lR1PJ@)v|e*mGEJ3r1c-k%m^qU+PlmPU5 zi_cbQjmQ^UV(21~WACWu;Q+pdmdL{sp7omp=NM7sCM_@JWvNw4ufAf0Ue%YBpe0ydjsS?@(l(7+~E=ve%BACh{G{7uJR?^o9&77Puh52 z(Gx7I{9Aw0faq?T*Udio4;4gopk}{i)8!;3N~`(&7xe7pLP7NiY?O^9x?pySPFx`= zeg|^XbxWV?SX`M~kLba49f)2oCx46dceLrs#ykb{Ee=(Y2C~9Zt)aoEiaYT;Lc+~{ z&*Oy)IgYvXA6slxSYZ?jk%lM_7sbFfSM1sZSAe_v6U?fj6v#m0cqbY;6VFR>Zww;#q zWqqp#eE$hAXCO_5u8uI(4WbO63zj3~Rr|e8B+<%!YtdXoTc|W__pr3wQF&v5E%$GC zEN4C#eN#yHHB}jyhYP@oo{~=r{Y_;oUpEZO&7W$zIVDQ zz3A+GK=qd(SZ_8amas!OQ~eTd2r_o`$9@viX!AHZL|5XoU#MyO`e1I#`vcSscLZW7 z@5b|y{sTS~nL8*6uJT1T8A)sl+G3@cQjfjbqCG#~5XINNpSMN&wEuv0lNAuToF4WA zBV;+DXsKFV^%{YDN$`9}B`}eSlatf;dVi`jsQ(d;q1%)M@4NBe_YbGBMp(39 z%JJmm2{7b=tU>@yagIkEe}f{uhX48o|17+UnT;@bytYPf0=zX1Na63Ep@y&#JI+-S z&nfeNt~HZU5k<7u#pOkrbd(YBep)TvZm=evi!FOa^r)-jcVOZi%3J#Y{SG=yxU|S5 z)6uQL@96E*M4pTfi( z>|9ush4g2bAA+OEazQX|6cbZ7J-m-XQl}cS=|xxU-j9R7e-~c5%KALcr*P_i?%d{& z80VBjJnfJOiaqIjq?Hq(5JFnUhW;e zYWg3fwR&p*x_{0e;ap_}pj3-khRRtQuwtwy^8;Uwo6zL=n z&_u%s&txIhLGP;D|B3JWL2>P zlf_221v+Qk{UpHTL;XT2S)R?v(JRJ5pPj(L?pPjhKZtAfM&oWt7o)A zNqM$bg{Ez|BRS8#irbku;Fklgxo*Ty)EDZd>dCNu{JyTj;A_(x2ruI+Kngeg3rPP^ zKvYXUvSbU55=NkGrY!#eDOI=WiL%jxtfe^&KlN^5r!5)y zSF<|C{#eKIL=~7y<;nLIiP$40s_X&0gIs*@MWKAccScHE8QKp35*4q{OJJA=ra{>c zfxgnp(o?y!{riip17RmoqRgZk4Mh(BnNNONiIQaQEg!?n^D-6&xex|Plg)lTBFJ$t zBp$f})?7y{FS{KVQfHCGd}QIm*n8{oTrmg%Dv$e2hA4{ywHiUNOSx{X*?qXtZjP1q zT8m4F5(xz))E!*q$9GnOSal5Ex<5$zH?k8!_FRVz0q>{3ct=kA{|Z zfeG55aLz*|0u zaGXr@h4 zA3?u6Q_(++HEkOTM>Pt)kM}E1j_pY`0Ej*HPabsM{$VWeLspup)T=Lgfx9L?xu*Fv?ncng8 zk)Yt7(`6x#6zSe~g^~TWCaRP3xm??^^As9_u8F5#4<9~5<3){L2UmFkddc9atHq|y ztwskP=PrFXSNs1t#`V%QuhyzIWfxyT`$h0@dqhW1&qs|d$HH$z6BU=PCKrI9Ylw)W&czkeTg3QJ%wQN_t_+Gqo%CCvMg)?x1p1=SuZl zsQdO%((@GE4n3(Abu$O2T0xsy@rbumhUZ$Be}rnp481!`<{Hs!d!9I&x$^UHY$?+S z>SD9g8bB(ks+L3Ch3xAQDCExDl$Viz%_cgctr)Xh@lX@Z$Q*X|E;fwj6AraxC^|Hj z1N*z;3+zFW!>Ge60y9cdnGar7C#X*L&Nsb{~f~ zWR=BhDoLO`j$N^2BfKDZo9g&?eGG}=@X83^(UDrp_gh9_NAHvRajC`!CGorf;aS^U zagW%oaz%l^AgK>gcr(BB?~KWEgrWv}YFU?!`2Vf9%lQYYsHh-6-V0X2-n*&Cv=zmt zR{dpJ=`ZFGb4VEx2)f3U(cq!_b(9IAuXnztskQtrpY+wl=>DrVH?uT%!)mCcNVq$L!6g-0xy4R2JK-Q5{`I# zQ~xEA5$ImRBQkK`oy(BcM%5-x)6Z-@SuatpUczEB?{WcW8OE3AB5AUS0ok-f#e2A| zK{n3oX^vyUPUWXoh`e*q#Q+8aAQ1T{7c&ePW@7k}ujjCcZ8IL8Ln*B!;n|zNW z3u#Mn=cCy7eRcu+t-*m0A9@)r!RGhC_kNpx%2r5?tr#n9jm-MPnRTV3%)TWdp$d#@ z7^|}??5cEQ0j&+%5sqsyj{9AT@*JM{lp#rAT2!cW|Zno zx~W;Cp)<&_W&!~Zuc^!$>BYJMk#>gU!r{!<%o)p4r0H?_XFr5Qv+6aaUyTU_`2~Jfkm-%@yuMhsg zMMiL)iMC!4tGFjp6{p4Z+k!tf6N@Nk6p(WCy>N75ZeYCt>k|5!)&|TiwItbA*wpLz z+UlIN`wxDc5N`i-B)(y9XE|-io15ZPB3wz(euy4 zM=AonmQ$bT{d6pPc_HBrObA?l!{OYxee=!%enQJ!YGT{mGTqE19NF5GbZM~=%UKj6 zs0D$?CVChe0n0pmkv<>Vo%5SmS3u1B@^|n@KO5CsbZXX?*cFP6v?=}9@q9l{!S_Cr zxl3^AdJR72npbVKe-_I4>I?^S7uh#N7HIOoY!phn!zjWBp)~EZ^0M+p=k`eW!{fhO zKKC4tYr(fLpc!5$!83OP1|kDM%=RC1fBQ!>MiyqtcBvtAPxPlp&8?kau(N$CYksQD-YlPbh4vhd%Z z64NGbW_#Fl6DMic`pu(Q%Zx3eHZeuz6(qI=0V2o-HCJcQ$VB&EQkY#Ik|XYZ*Ws40 zvcP?JOxooV`XjoTAtgz5J;?%*1PLRwMtsQtFX9}!@hlk#yBJ)Htq?fnC!OFE=odUr z6u7NCS&i>5+JN!eT8Y=!WW7ye0(#)#368n!w1VN}?S&w~1P z+1Iq{P>SeDFu>^r9j3?mWt;NF-u}(h2F6;@*|%8HdTC#bUApgVsY>6%{rmXisru_C z%Y3xuON9`)YN2$UR+Btn3``#OD?Gj+D+j>#41)hf@f2T8;uKMY*DX*s&%zA}vm0rv zGYb7Hp-?4O&A)5x^F!*_Sdr-V`@0BN73^m5YD*l3dwn1mwI z+gQL;V2|m%-X`JHq+P&LvKXBA(Yx*pYf&H>5Hy&(PuB7pH=4g~Bao)X{`KVQ>6!8R z+LXSXn4H?yoH(jspF+&h4tikg{u8)K+l4E|=GzXu`QCI5oz7{zW)M-RzFy`@>FRB? zq|CYJ4P+lwbct3=_w-S(AmmK+*(i$Hw3X;UvhCXE{~ip)|dX+qill&JfM zx|ObMJAB`+oVn4xSHE}KE^hS==E1~C1G2=U00BkhMm7QUSbk;jk2-}0FYQ(Hej>>M zuHU)C(y~;njtvB)Z=MaGsqPKFCfX^TQaAd{I8G^Qp{i8d`gAvaPq3xD{mhknAQFNT znvd;07tPHoi^0uDZAF^w5hst2@aCEPI`SZ~SY{lFmt}_SZ-}xNeaGpwwZvo1U33o~ zyo^pI0V+!7?f^G3*PFx!uqh%z2+I$mAI4D&pw_S3${MT^C#-qr6dNJeXHD24tSH9# zgo$eXE*KxR=sqT1_Pi#N8nFK936t|kIHx*JVVB-`Bh_Cdz$0*H>K_@hEjkr)EH)~M nT_xa0rZ2p8v=`VMwgSqw1s!`SXB7Rn?en9IvUHWCN$~#w$6JiI literal 11241 zcmZ9SWmptn8^(8+g{8Y2mhNT&>F!blqy;Gjq`O;Ex{*d2l#V5(q@-KvP#Ttg_y6^s z>)P2*Gjq+E=iK-2xzCBw(Nf02q{IXO063~D3VO(Y_x_@WVieL9pA$J^KMDIuzn4el&fw@J9mO zK;MdV;C=KJ^qR6kcVL}SBw_i@rE}f%N)7F!egvM~&$3q{8Q1yg>8 zswb68L2J^EL%+vGwk2X`jcAnKb2qFEjHU+1puCAl53a@KuYaW+E#T|yc+e&C^&qSM zU7>*TZ|8lLcKnVn;LF@p0?tiGMf88PsY$=ly1Y1aZQV;)QK=PE5^=G4&B=!E;cEGmy_8d#FH86*=?ZoR})B#M$1 z4-i_JOoUqJLWMmoEX^q+G}EI+rY8S_>9XExqI_27NV5Vi1k4|O3KT-;W@q;aQ)R?a zqdiZ`8|lFk+kESt0@TjkA^Fx^QeR#Do;F3n11!z$xH#?`@JNMoDyho!nfu?re>mhA zvw)z$z(A^Vd`Xg`7ueQ!mI4W21zexOp`qrtOgg{^Ys9zTX#$)5*1t6+Qx#0gUzwQ& zigI(;utH_IKfkeFq?QYOjyeeFk0zY3n{Sc`MVvLGK$6&b#rM`;m+WT?7qv|fpMtZ;> zicUa*p%z2k*ze{14GNrlK!3ucK*T44nVFeO@#v&-kX2An zKtV!s)vlW)!!S`$_$unP9%zQ`b&|<9l#w>Pv=wGZ(qMb?B&lGM!w5eMA-SuY+cO3h z){)JW4~SMG6`h#7K(^G{k#9x@D0oNrZyT=d zN2A;GAL*xx8`AlCbM);2afQIU}eQ;vi#3D}lyoUU)ZJP3|5){V-2K*KiE z;H32@Ajf$MxeF?KVtPh~yo1AGfS1>eh)J^{-5B+u?m?6O>lmKO6FEzCI7Ra7ndmMZ z-Rv=Vl_eoDDXE;CTpv&^*w=RlZ|W!6Apy&Zz3I&bhRYU~B?dfeQX`2uKMXS~9B3H! z1W1;{CnXih`COPRrCQ*-w6sLJ&7#-3Zcc`$=Lp{HD--PjEp6fN(%IP&C#f)Eq_UGa&49Ag}E&BKDT*o$8kBUFB1+S}U? zBB!Y{!+dC17*)*3S&cG{IWNBo-85lv)2D0tPxtC(ML*#+#!kU;ATo@3O4nOpgWdC8 z$xQ(x+K<<~=sHH8l4REQ-Q&_ay$peu`vhPxcwBD$;Dx+~y1@~F=6veq)05TV<5eiY zWKqZhb=Y-_V%U)%(_+w0Lqp^AGqbw!jINBA&99eHbG+Q#dC=x{^B{CMXH{#DsgzAh zNY~KLA_EkP0zyIQDZ_1u(eitCT&Vp{Pv)t;l9rFA35E5=M%4Qqy$CARkB7#OP>5nfkLW)RM^el$~-0_c6X_tz$$%NuB{~) zS-!qWFSLYrb!F1?@^D(5H>9F7iC6J9zXf@Fdy}~WN50*Ns(?~`1HplGVinl_idavJ zxT6ye`2Zhf7P-Pa$pMW_RK+OV&}#Y(Hu<8~d>!OhUe1^F^Q9N@I$2*+u>%RwPt*xI z=WprDLuJH()z91QVFgY2tZ(9hJ+oU-^yfa(PG2diw4x!H%=LCm(vc8EEr3Tp;-!N- z!}R5)du!n1oz*~@#RMNtK#?%x#EB2TM2`ov%xsG%B}&gK7Y+SEd8G$P>a)Pdzb|3W zYOlnG2!fkBnK$HoykQ?xZIpJfk0}~UI%l_1t8vH8artCCnJlYrDugtd- z#lb2o;);ByZruQuUk>e&sW|#O3YCBP9pwRSnpxo6jXHqp`|93Z@Sm+MEIbb)0&Txep z|D}AL5g3c=DkUZ5h+jp?nY6WySC%t2HZ~b8d2LPxTFw8*1_Syl5t;^`o7=Wd1u(8Y z4&Z9Nsq%74!+_sH1k2@P${}ZXYS=97k<*39gVRJv1u77oBJK5WwSkdUjo%uC(9UCl z0jGF64*_2;-ZKo0;7(lCQ60=O%h)VlR`C{c@yVR@mqn|ZsPb@da0HB{b3Y2Pgu!m* zBQq)=6s2&I0U_7R48bflwcyQ%u+Z;SvY#_6ZjNGDSXsl$%gc%4(XwWRZzF`CVW3QW zUcavHaOpW_c$fCyxD-I{l4e9vfS*iyMpesi9TUZbiuAl_qXDNp4u!TpQ7g^S(NX_P z=7&liAZ>NF!|Hzz5NrCf{ji~4d_$>Ad2&_>T;9)l zdL0G0bxPTE_fjrql_?E6jmJujirkM63SJzjc2Q7JxVyVLg3x;&Z9}GbMf=t|4~9Go z@qSOXTZcL)*h1lMPrZ>>^Z^JR^S&=I<-$LRvn||cQvp!LU&w4Rm~Uia;>lp+vZo!a zvL-;G<>6)x)=5=Nz}4~J=EIL?aG(e$A3U7ud4BSvvC6P&%{M-Ko-S4{20O@6Z%SWj zK3Ay&a^bFyj8wp{+*O(P?5SpF1U5^;dywtzZ3`cvf9z9W{%QlM;K0h?NRNC&2w7Y( zM5ou*qA5U_Eb&8SUX7VIgGZrIg&jH|*$B$HPZEYS5ka-D6 zYpq~kit7tH01bdn7E$|?+8VoQ2@p1DIUa~YSSt&B_Y zD`L{pULE}hCSs-50%0rw7Wn6R&E$ncNBN7Wgl~eBr?1(o5Fg_=t6tFqRer>`1)t*5 zh<{V&sv^cL1=~IJ<4I?#Gks>0XO-F2GVvTRb4;c!=*D%mo!>`?SBY>ZCJ+h{$ELjf zx89W_;~Pjd(8vLqo-i^&mRH&hU!@8I6+}dM~9!ZYoP|xpepyk(Jl={)<~af{KP9k>2YFSnrrow zE1HhW_7%D9*lUr+1O#w!NW^r*p_LFlmnQU>C|4B|1!P4o}g;R zMl$(;0u}+rXWOvi;^K>iT2l$ihH6qEk69-ymAV2Quj~Zu-I=rh@bQ|1SEA_St>8og zu)tRF{su!SxV5$Qvu2J+ylxiYvi~PKupYPKWdR1gxtUAkwMU>wxB<7;Jj->Y>Om&Ju5df9VtQcw+J zNT4A*6HqPa?x6fE+J+;fL00Duw)WkFNx1CdLLj}?;65MeR5yv#9H6^ zlL>#r<<|2{?j{oOJ|oFHL7q!OLPAtO3*hABRF{OZOAZ3Ua=t^+Ma15Rq!|42kPC({ z98;Y7kQC2+d)bSQPePK`Vl^1s@`?q1ywtQqXonnql;K|514mjJ3go3d2OP(=7c{xE zu7;N;co1)a7{$=3dK2Rc4Zf$i=xFcrs|E8!8Lq8$QVybEY&uD#rx78Y#Lg4KXmRF6 z>w=g=HD;C_uww~xndUZ<4Jjky za&0=IR~W%s@*NKk4`V}efCUKU?M@xl)z!s!;!q7!&~SfQP-Op&IZb5BS~-R|=kY%m ziQ#OkR#&!@6-6CdPjg2syN}@EpFbnq_9pTavGut`t3BAkEsWH+OCrL;S7ZTs#7H7= ziTtdxpS*bp@YAh6DW1kY$z9U%z5P7zRkqDCXlrc^k0D~chDNfdzhaaLJNQ+;Wil#~ zoSeLIc+|+2O%Fe>OLQ3Izuy0c4jkZB@%K`Tzd)T~)d<;dDU+G;Lx>R^`YZdfxvt?g zhWh96eER(PGYEo5JAmrCvben5Ty}3Vn-ADlhrQ3dyS>#c2TyfGGziHCn;(lhN*YBv zg4d|G0kY08nc{W zxTYzqSy%k2xsoU(#>N-QX&7fAAt9ApKN5f3!fT?iDf-f6XMH~u-ie5a#2;jH!plLnArsTNiPS=@$pYP418 zoCMcp0zM@qq$59p6VS>NSp^?vV*syWz`Cq`@2`)SP}J4bE=Tcn@&JzCK+BTO++ioA z�W;AHxEid8TQAWm=J;npCpc>1C7fx<;nKFPJa?jQ$GawCXQfZ*Py|xU0(|*$`(3ZrIKSZPZ(oyzgqkor+jrUI@LMwbXn? zW3fs)^6S?x$d$BJ`r&ir;c$VPGWqLyW(^1Bz-*!uUw&M{J3aDp4xqu$6;>E9{o&-> zjN9a50A67Bca&B0J3}4N@K`FZ*_3*o`6mzzv6U#z_v9>PH$|U|{!4)dVmM78LIHE3 zR|*yXqt;=a=~K-sfUFDuhJT+6H-A*7H+g~Qy>Ya9?)sYX=4X8wwLV}ZZCS9IM;oLU zIIF!XFq)bi&!MwwDNi+AtlL$$b%l&q(i12Ce#ssU1=KhP$K z;sEzbyfvnB|75_e)lXC=Q5|;SfL88gCiW5h5Oa#B@rjAZCfmt7qBvuy0sFhT`R7*; zNMBzb8bZUE8l0ST7L%;p@Bea`Dp#G=p$xlix-1zDH#wWLbUE59AqAdy&#Vu*i>FI# zZ*6T^{z#zdp;uxz$eCAs=DY)%NWuM=8f}V^iQ<*j0=Wk|pWA{dOayMUwfO@ik!xG6 z<#9Nk*IWN_*#KrK*kk}6&Jre4oA9>(;HYOhMmPS11_oOPJs^RWUdiw3a4vo5nQAr- zkaim^@?qHmM(nkEl%JXD9oYlQ_>v~i%aU-Ys{XTvEgt^({xMB_QY+_XOnC8jV)PIL z{y{4*%?2A@7x!fS?w!@uo-jwya!z)3bFKPAk2u9_wevOG}0f*46@&?}QHw)~^M^bC-koslGo-1s`#7+fT z?{l5laTi{d)CL~^MY?QOLEV?52g-%#Hs33UKS>Pok&Ge|$o8H6~9r)NN;rQo2B$t(yg?4n@+YB&T5jIqg>?h8YkT;O7wR^}b z7q3j@N!XGS6AurFW7=f{+8YPW@HPV-7>47h5fAHhs`%8@CTDA28vzN1-@bi24qu#A zI6sn3v8pltE@!(KYM6+m&Lel?Bz}6()iax2^(Awi$f&3sd)R|BU_m zB^YjTVl!aziIjUh+%>SuBl6=Qx1*yYQ}9)Hz`fMuNN?BWz$=y-ZLpzAtuRci;Nao8 z;$D_;6*>F|hjUdKjMqqjaXG7+mlN-q6YKxUesK!&jf#pg=MHGTh|=I9Uy6iPmfG404(2om6E^bDYsNSj8e>N(;Et7#{ELM6_&VKo(?z$@Vs361JnnO!S(XnU3$S7sQF4UrXK(^S z;Ba_KX7;Y~1|JGnb`SI!X0=m0dN+8TOQySB7 zGDq}et)!$x!}=>Z^0`XEyjnxOH9_aYO_sq1Zj*Ifet!2O(ZUhyJzqq6>&OYyPMGJv zJHF;TZz^wTxtXu7=DH21FsX_lpeK})BE(^pr_@Jr5R`8{$Qz8 zos7%RNx8wc=@s8UzS`DVwe&E#>kr2R3d&vXzVSN zZildRF(Pd%KF91$i~X;mP^G|cwU24>$mFN*Lz`|eY@kVBPmf7R-o!EvjQF=Ijek`U z4Qxx8fj~dCIIVr=GV3HK>o`>9pEqDQFD3%L$RL9+A;L>xFRr@1&x$x}e}#4j*D3)` z@h#TP8(g?~AWV9s^RvO1mC>=Wp< zDX1waDMM@s+edNiQjq%Oe~iniKW{z$U(&%A@JHP97(rK= z>;?tCE5f{#iMfa9qMM*vz0}Auab(+43wd{h9@b)coj@p4rTc|MAqorQ$+?S=Ug&#p z8ZSD1K>gweq(=lXMzW=g>%JGBQ(V-yiOR%%?fv^PhR7}D4#hZ4PQs}S6XajM)!@Rw zk#pc5Qi7+8v$?w)SvrA&FAr8<8`wt74>1fezZB2R${H!1xeFPpP{TNL}UE7>kS&{f~yx6csAVDlo!&XN5N%M})E&5h38qv2a`H_63 z-m?Dz*?JMqdM=+s_|C$+zO%r4{^=1iy=xG#VepXNO>kOm)op*j)TkIu5liESdZ7P| zO3<=bfh8yg$C zi1Ye`gjIyqkQ`l!wx*s6ElFP=GZ7I{=a)bv28(6q=3?trzT|ZLWVQBl{f#{bO3?K* zIgIEUtY(zeIHl<1N{t_b?RR}F7Edl)jX&8X8{oi^p29k8M_4Q>F7hAZhWd#S z5Wci4HbdW=rNtcuNGuqqvUe!;YHbJ&TkT*Oe?lNv`z+GR>3WA`m@(>Dtm5t}7$f-X zLma>7v^Xwk{lh~JMyJ-dbyE#@yI<@>GvVLKQs8G6sd^BvE^Ps(7w>FFjgKE~TI=V` zgC0qoY;1%>BVUNEIe$$$o#6zrRgtINqYO02vd`LL+av2fw*NM7dEIgIqbvpDIR&!* zTYJ+)ffUTL+YrMp4hT|H!+hua>~rx`bVFXl{cyHYRQu-W+l)S2kc?DBfJ01(QT$1wj9rts++re z=jP>oczk^PMS&4lwwGaL17B*g6;y%1X!tEGEvHW+Y^mO~l#|;R;W22xD#g#Bv(qG^ zT7wae=ZH=w4}Wp5zMn78+s~@x@w1yO5JQf7){9fGEwQc#@1QO+?2@xQD<0KI(bX=b z8MqR{#Ke}iXzLawR7&-XBJz)wCadSCX(=Lmx;i^&+VTs!@Ba~FnKV``?oOd#JiRAoNJ`0#IBrBHc-hvzNQ z4FE_^c@cB++2JYhywoLhPTOo(4^X*{N{cSB|#2*)mrdq3&n9KXYz$x+cXsc&EJZPU(#6YzgGq})B$*f3R(Sf)h_BvF~lOdoV5V016>bqNGs z!>~xXT#&7Ddz7TEr>BVMMkax5|E6+f*yiNeSjvyB5+|{&wWj=WZKUd zCAs${tT$h$fFIDOKfP2RWhOYVzjG}HHoOkJ|J$+=;O{Ts;z=NGy`R}YAS)Lateps|JFLXkWeC;d~|iTv?Wn^5}NX& z#@n|*(HDG*Wjtl*s!VBF&(F_)ARoCY>n=q8!g;+@djvm#n~zT?G{Pel{k=kk3&j3jbx;IWzFsVqDro7}67XG`@>2nc6 zP0;;#{U(EmRSTaMtoG55rke8)k?fF2VIVGoTmxPxCnFP#+{01aVrpX2{W~~ulC*$- z?(V^?N4TyqUX_+RLhZH~xJPA}l3yu$sHX;|CW)jcSY2IpBD8-=Nkuglp;d6clgyti ztGkO2xnYkKYOr2fSZKLKt~YcOBjjOiVUxrK+){7z=l5>zxCScwrvVGXQ&1BN;FXnf$ z-etS@S;^0zr#;Md&xXT`>p;*+y+G{f%0mS-=)z9ME>J8l7`bIu^dw8d=zrP^Nohth z(#qHv=Pr`FN%r}E>4xwo=by{GWh>11A=uEOyb-@k4>HIDvYouHjuNMBE3A?&?#b5{V4A4P}u?2ex@`>!uDl%XR@iUm3 zrEXzQPf$opi`NUYtuoCVPvqPPcxP{aOX@{Oy?-F-Do_pyKr=lMU+M&{qPyLV!$aClS*WVJ)LM zKEi}rV2NL68&HZ|xq|A7h)GEB(vRBzMe6tSB2rUTkK?rqqyln=E6|VgVsN!tHZ_@{ zDnrO@Dj&Ji!g)90X=1;#tseu{6STAp<8(W+gn# zAv5AkSVIpl^DyAh&P_z67j23#0r;}+rnu{N?9#Anzrk->J zUNqkN>^yRB=0-c5GvmgF(jfpBoN6i))?*_KgDOL5RW-b=b>BTqp7yNNQx*!=H?fGz sy;M%Pn@dups5n{xxT1OEG(SVyYMTtD+IQ%YTX+GgidqV_au%Wg19ZoHsQ>@~ From ab277675a15afa7d546bfa0c7e92a61bf5484dc3 Mon Sep 17 00:00:00 2001 From: bachinger Date: Wed, 9 Aug 2017 14:35:38 -0700 Subject: [PATCH 0233/2472] set mediaSession flags properly and keep queue in sync when timeline changes ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164774783 --- .../mediasession/MediaSessionConnector.java | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java index 743b2e066f..419614347f 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java @@ -74,6 +74,10 @@ public final class MediaSessionConnector { } public static final String EXTRAS_PITCH = "EXO_PITCH"; + private static final int BASE_MEDIA_SESSION_FLAGS = MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS + | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS; + private static final int EDITOR_MEDIA_SESSION_FLAGS = BASE_MEDIA_SESSION_FLAGS + | MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS; /** * Interface to which playback preparation actions are delegated. @@ -308,7 +312,6 @@ public final class MediaSessionConnector { private Player player; private CustomActionProvider[] customActionProviders; - private int currentWindowIndex; private Map customActionMap; private ErrorMessageProvider errorMessageProvider; private PlaybackPreparer playbackPreparer; @@ -359,8 +362,7 @@ public final class MediaSessionConnector { this.handler = new Handler(Looper.myLooper() != null ? Looper.myLooper() : Looper.getMainLooper()); this.doMaintainMetadata = doMaintainMetadata; - mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS - | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS); + mediaSession.setFlags(BASE_MEDIA_SESSION_FLAGS); mediaController = mediaSession.getController(); mediaSessionCallback = new MediaSessionCallback(); exoPlayerEventListener = new ExoPlayerEventListener(); @@ -424,6 +426,8 @@ public final class MediaSessionConnector { */ public void setQueueEditor(QueueEditor queueEditor) { this.queueEditor = queueEditor; + mediaSession.setFlags(queueEditor == null ? BASE_MEDIA_SESSION_FLAGS + : EDITOR_MEDIA_SESSION_FLAGS); } private void updateMediaSessionPlaybackState() { @@ -574,11 +578,20 @@ public final class MediaSessionConnector { private class ExoPlayerEventListener implements Player.EventListener { + private int currentWindowIndex; + private int currentWindowCount; + @Override public void onTimelineChanged(Timeline timeline, Object manifest) { if (queueNavigator != null) { queueNavigator.onTimelineChanged(player); } + int windowCount = player.getCurrentTimeline().getWindowCount(); + if (currentWindowCount != windowCount) { + // active queue item and queue navigation actions may need to be updated + updateMediaSessionPlaybackState(); + } + currentWindowCount = windowCount; currentWindowIndex = player.getCurrentWindowIndex(); updateMediaSessionMetadata(); } From 2d0044fee4f99097cc4c53854b5f91322adaf282 Mon Sep 17 00:00:00 2001 From: eguven Date: Thu, 10 Aug 2017 02:25:55 -0700 Subject: [PATCH 0234/2472] Generic DownloadManager and DownloadService Downloader specific parameters and constructor is moved to DownloadAction class. DownloadAction objects need to be serialized so they can be passed in Intents and can be stored to filesystem (to be implemented). So DownloadAction.Serializer is added. Didn't use Serializable interface because of the concerns over incompabilities when the new versions of classes are used. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164831115 --- library/dash/build.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/library/dash/build.gradle b/library/dash/build.gradle index 2220e5b250..48e9b9b97e 100644 --- a/library/dash/build.gradle +++ b/library/dash/build.gradle @@ -35,7 +35,6 @@ android { dependencies { compile project(modulePrefix + 'library-core') compile 'com.android.support:support-annotations:' + supportLibraryVersion - compile 'com.android.support:support-core-utils:' + supportLibraryVersion androidTestCompile project(modulePrefix + 'testutils') androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion From 5ab0c620bf84f8d24b9a1894359c25a17c7ab479 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 10 Aug 2017 04:31:01 -0700 Subject: [PATCH 0235/2472] Clean up ClearKey UUIDs - There is a proper ClearKey UUID now. This change requires it to be used instead of the Common PSSH UUID when instantiating DRM components. - Internally, we'll map the ClearKey UUID onto the Common PSSH UUID where necessary to (a) access the ClearKey CDM on older devices, and (b) access drm init data stored under the Common PSSH UUID in the stream. Issue: #3138 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164839213 --- .../android/exoplayer2/demo/SampleChooserActivity.java | 2 +- .../src/main/java/com/google/android/exoplayer2/C.java | 9 ++++++++- .../android/exoplayer2/drm/DefaultDrmSessionManager.java | 6 ++++++ .../google/android/exoplayer2/drm/FrameworkMediaDrm.java | 8 +++++++- 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java index 87b8e92e83..382c783598 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java @@ -271,7 +271,7 @@ public class SampleChooserActivity extends Activity { return C.WIDEVINE_UUID; case "playready": return C.PLAYREADY_UUID; - case "cenc": + case "clearkey": return C.CLEARKEY_UUID; default: try { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/C.java b/library/core/src/main/java/com/google/android/exoplayer2/C.java index d7d0ed40aa..1cfcded1cb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/C.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/C.java @@ -604,12 +604,19 @@ public final class C { */ public static final UUID UUID_NIL = new UUID(0L, 0L); + /** + * UUID for the W3C + * Common PSSH + * box. + */ + public static final UUID COMMON_PSSH_UUID = new UUID(0x1077EFECC0B24D02L, 0xACE33C1E52E2FB4BL); + /** * UUID for the ClearKey DRM scheme. *

        * ClearKey is supported on Android devices running Android 5.0 (API Level 21) and up. */ - public static final UUID CLEARKEY_UUID = new UUID(0x1077EFECC0B24D02L, 0xACE33C1E52E2FB4BL); + public static final UUID CLEARKEY_UUID = new UUID(0xE2719D58A985B3C9L, 0x781AB030AF78D30EL); /** * UUID for the Widevine DRM scheme. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java index cafbe6e8f7..fcd8ac862b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java @@ -216,6 +216,8 @@ public class DefaultDrmSessionManager implements DrmSe public DefaultDrmSessionManager(UUID uuid, ExoMediaDrm mediaDrm, MediaDrmCallback callback, HashMap optionalKeyRequestParameters, Handler eventHandler, EventListener eventListener) { + Assertions.checkNotNull(uuid); + Assertions.checkArgument(!C.COMMON_PSSH_UUID.equals(uuid), "Use C.CLEARKEY_UUID instead"); this.uuid = uuid; this.mediaDrm = mediaDrm; this.callback = callback; @@ -346,6 +348,10 @@ public class DefaultDrmSessionManager implements DrmSe if (offlineLicenseKeySetId == null) { SchemeData schemeData = drmInitData.get(uuid); + if (schemeData == null && C.CLEARKEY_UUID.equals(uuid)) { + // If present, the Common PSSH box should be used for ClearKey. + schemeData = drmInitData.get(C.COMMON_PSSH_UUID); + } if (schemeData == null) { onError(new IllegalStateException("Media does not support uuid: " + uuid)); return this; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java index ed4494559a..5d0cb038d4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java @@ -57,7 +57,13 @@ public final class FrameworkMediaDrm implements ExoMediaDrm Date: Thu, 10 Aug 2017 04:42:27 -0700 Subject: [PATCH 0236/2472] Support multiple video/text/metadata outputs We've seen more than one issue filed where a developer has registered a video listener and been confused by the fact their SimpleExoPlayerView no longer works properly. There are also valid use cases for having multiple metadata/text outputs. Issue: #2933 Issue: #2800 Issue: #2286 Issue: #2240 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164839882 --- .../exoplayer2/demo/PlayerActivity.java | 2 +- .../android/exoplayer2/SimpleExoPlayer.java | 139 ++++++++++++++---- .../exoplayer2/ui/SimpleExoPlayerView.java | 12 +- 3 files changed, 113 insertions(+), 40 deletions(-) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index 9e53dff857..6416cd5aa2 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -294,9 +294,9 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi player = ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector); player.addListener(this); player.addListener(eventLogger); + player.addMetadataOutput(eventLogger); player.setAudioDebugListener(eventLogger); player.setVideoDebugListener(eventLogger); - player.setMetadataOutput(eventLogger); simpleExoPlayerView.setPlayer(player); player.setPlayWhenReady(shouldAutoPlay); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 16a44aa016..cc0791bf44 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -41,6 +41,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.video.VideoRendererEventListener; import java.util.List; +import java.util.concurrent.CopyOnWriteArraySet; /** * An {@link ExoPlayer} implementation that uses default {@link Renderer} components. Instances can @@ -87,6 +88,9 @@ public class SimpleExoPlayer implements ExoPlayer { private final ExoPlayer player; private final ComponentListener componentListener; + private final CopyOnWriteArraySet videoListeners; + private final CopyOnWriteArraySet textOutputs; + private final CopyOnWriteArraySet metadataOutputs; private final int videoRendererCount; private final int audioRendererCount; @@ -99,9 +103,6 @@ public class SimpleExoPlayer implements ExoPlayer { private int videoScalingMode; private SurfaceHolder surfaceHolder; private TextureView textureView; - private TextRenderer.Output textOutput; - private MetadataRenderer.Output metadataOutput; - private VideoListener videoListener; private AudioRendererEventListener audioDebugListener; private VideoRendererEventListener videoDebugListener; private DecoderCounters videoDecoderCounters; @@ -113,6 +114,9 @@ public class SimpleExoPlayer implements ExoPlayer { protected SimpleExoPlayer(RenderersFactory renderersFactory, TrackSelector trackSelector, LoadControl loadControl) { componentListener = new ComponentListener(); + videoListeners = new CopyOnWriteArraySet<>(); + textOutputs = new CopyOnWriteArraySet<>(); + metadataOutputs = new CopyOnWriteArraySet<>(); Looper eventLooper = Looper.myLooper() != null ? Looper.myLooper() : Looper.getMainLooper(); Handler eventHandler = new Handler(eventLooper); renderers = renderersFactory.createRenderers(eventHandler, componentListener, componentListener, @@ -440,63 +444,132 @@ public class SimpleExoPlayer implements ExoPlayer { } /** - * Sets a listener to receive video events. + * Adds a listener to receive video events. + * + * @param listener The listener to register. + */ + public void addVideoListener(VideoListener listener) { + videoListeners.add(listener); + } + + /** + * Removes a listener of video events. + * + * @param listener The listener to unregister. + */ + public void removeVideoListener(VideoListener listener) { + videoListeners.remove(listener); + } + + /** + * Sets a listener to receive video events, removing all existing listeners. * * @param listener The listener. + * @deprecated Use {@link #addVideoListener(VideoListener)}. */ + @Deprecated public void setVideoListener(VideoListener listener) { - videoListener = listener; + videoListeners.clear(); + if (listener != null) { + addVideoListener(listener); + } } /** - * Clears the listener receiving video events if it matches the one passed. Else does nothing. + * Equivalent to {@link #removeVideoListener(VideoListener)}. * * @param listener The listener to clear. + * @deprecated Use {@link #removeVideoListener(VideoListener)}. */ + @Deprecated public void clearVideoListener(VideoListener listener) { - if (videoListener == listener) { - videoListener = null; - } + removeVideoListener(listener); } /** - * Sets an output to receive text events. + * Registers an output to receive text events. + * + * @param listener The output to register. + */ + public void addTextOutput(TextRenderer.Output listener) { + textOutputs.add(listener); + } + + /** + * Removes a text output. + * + * @param listener The output to remove. + */ + public void removeTextOutput(TextRenderer.Output listener) { + textOutputs.remove(listener); + } + + /** + * Sets an output to receive text events, removing all existing outputs. * * @param output The output. + * @deprecated Use {@link #addTextOutput(TextRenderer.Output)}. */ + @Deprecated 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; + textOutputs.clear(); + if (output != null) { + addTextOutput(output); } } /** - * Sets a listener to receive metadata events. + * Equivalent to {@link #removeTextOutput(TextRenderer.Output)}. + * + * @param output The output to clear. + * @deprecated Use {@link #removeTextOutput(TextRenderer.Output)}. + */ + @Deprecated + public void clearTextOutput(TextRenderer.Output output) { + removeTextOutput(output); + } + + /** + * Registers an output to receive metadata events. + * + * @param listener The output to register. + */ + public void addMetadataOutput(MetadataRenderer.Output listener) { + metadataOutputs.add(listener); + } + + /** + * Removes a metadata output. + * + * @param listener The output to remove. + */ + public void removeMetadataOutput(MetadataRenderer.Output listener) { + metadataOutputs.remove(listener); + } + + /** + * Sets an output to receive metadata events, removing all existing outputs. * * @param output The output. + * @deprecated Use {@link #addMetadataOutput(MetadataRenderer.Output)}. */ + @Deprecated public void setMetadataOutput(MetadataRenderer.Output output) { - metadataOutput = output; + metadataOutputs.clear(); + if (output != null) { + addMetadataOutput(output); + } } /** - * Clears the output receiving metadata events if it matches the one passed. Else does nothing. + * Equivalent to {@link #removeMetadataOutput(MetadataRenderer.Output)}. * * @param output The output to clear. + * @deprecated Use {@link #removeMetadataOutput(MetadataRenderer.Output)}. */ + @Deprecated public void clearMetadataOutput(MetadataRenderer.Output output) { - if (metadataOutput == output) { - metadataOutput = null; - } + removeMetadataOutput(output); } /** @@ -816,7 +889,7 @@ public class SimpleExoPlayer implements ExoPlayer { @Override public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { - if (videoListener != null) { + for (VideoListener videoListener : videoListeners) { videoListener.onVideoSizeChanged(width, height, unappliedRotationDegrees, pixelWidthHeightRatio); } @@ -828,8 +901,10 @@ public class SimpleExoPlayer implements ExoPlayer { @Override public void onRenderedFirstFrame(Surface surface) { - if (videoListener != null && SimpleExoPlayer.this.surface == surface) { - videoListener.onRenderedFirstFrame(); + if (SimpleExoPlayer.this.surface == surface) { + for (VideoListener videoListener : videoListeners) { + videoListener.onRenderedFirstFrame(); + } } if (videoDebugListener != null) { videoDebugListener.onRenderedFirstFrame(surface); @@ -902,7 +977,7 @@ public class SimpleExoPlayer implements ExoPlayer { @Override public void onCues(List cues) { - if (textOutput != null) { + for (TextRenderer.Output textOutput : textOutputs) { textOutput.onCues(cues); } } @@ -911,7 +986,7 @@ public class SimpleExoPlayer implements ExoPlayer { @Override public void onMetadata(Metadata metadata) { - if (metadataOutput != null) { + for (MetadataRenderer.Output metadataOutput : metadataOutputs) { metadataOutput.onMetadata(metadata); } } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index 17923767d1..b3dc3c7264 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -379,9 +379,7 @@ 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. + * Set the {@link SimpleExoPlayer} to use. *

        * To transition a {@link SimpleExoPlayer} from targeting one view to another, it's recommended to * use {@link #switchTargetView(SimpleExoPlayer, SimpleExoPlayerView, SimpleExoPlayerView)} rather @@ -397,8 +395,8 @@ public final class SimpleExoPlayerView extends FrameLayout { } if (this.player != null) { this.player.removeListener(componentListener); - this.player.clearTextOutput(componentListener); - this.player.clearVideoListener(componentListener); + this.player.removeTextOutput(componentListener); + this.player.removeVideoListener(componentListener); if (surfaceView instanceof TextureView) { this.player.clearVideoTextureView((TextureView) surfaceView); } else if (surfaceView instanceof SurfaceView) { @@ -418,8 +416,8 @@ public final class SimpleExoPlayerView extends FrameLayout { } else if (surfaceView instanceof SurfaceView) { player.setVideoSurfaceView((SurfaceView) surfaceView); } - player.setVideoListener(componentListener); - player.setTextOutput(componentListener); + player.addVideoListener(componentListener); + player.addTextOutput(componentListener); player.addListener(componentListener); maybeShowController(false); updateForCurrentTrackSelections(); From 1b28d83f16e5a35e78082c79f8fed3c556122955 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 10 Aug 2017 04:45:40 -0700 Subject: [PATCH 0237/2472] Fix maskingX variables when timeline becomes empty ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164840037 --- .../com/google/android/exoplayer2/ExoPlayerImpl.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index c3a76cd962..f22c08f585 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -448,6 +448,12 @@ import java.util.concurrent.CopyOnWriteArraySet; case ExoPlayerImplInternal.MSG_SEEK_ACK: { if (--pendingSeekAcks == 0) { playbackInfo = (ExoPlayerImplInternal.PlaybackInfo) msg.obj; + if (timeline.isEmpty()) { + // Update the masking variables, which are used when the timeline is empty. + maskingPeriodIndex = 0; + maskingWindowIndex = 0; + maskingWindowPositionMs = 0; + } if (msg.arg1 != 0) { for (Player.EventListener listener : listeners) { listener.onPositionDiscontinuity(); @@ -472,6 +478,12 @@ import java.util.concurrent.CopyOnWriteArraySet; timeline = sourceInfo.timeline; manifest = sourceInfo.manifest; playbackInfo = sourceInfo.playbackInfo; + if (pendingSeekAcks == 0 && timeline.isEmpty()) { + // Update the masking variables, which are used when the timeline is empty. + maskingPeriodIndex = 0; + maskingWindowIndex = 0; + maskingWindowPositionMs = 0; + } for (Player.EventListener listener : listeners) { listener.onTimelineChanged(timeline, manifest); } From fc9a58529a1fddc2ef9ade587266846e47bfc535 Mon Sep 17 00:00:00 2001 From: ybai001 Date: Thu, 10 Aug 2017 14:32:52 +0800 Subject: [PATCH 0238/2472] Support 7.1 EAC3 in MP4, DASH and HLS formats --- .../android/exoplayer2/audio/Ac3Util.java | 42 ++++++++++++++++--- .../dash/manifest/DashManifestParser.java | 31 +++++++++++++- 2 files changed, 66 insertions(+), 7 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java index 4b64ffb030..bc46a4f10a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java @@ -38,6 +38,10 @@ public final class Ac3Util { * {@link MimeTypes#AUDIO_E_AC3}. */ public final String mimeType; + /** + * The type of the bitstream, only for AUDIO_E_AC3 + */ + public final int streamType; /** * The audio sampling rate in Hz. */ @@ -55,9 +59,10 @@ public final class Ac3Util { */ public final int sampleCount; - private Ac3SyncFrameInfo(String mimeType, int channelCount, int sampleRate, int frameSize, - int sampleCount) { + private Ac3SyncFrameInfo(String mimeType, int streamType, int channelCount, int sampleRate, + int frameSize, int sampleCount) { this.mimeType = mimeType; + this.streamType = streamType; this.channelCount = channelCount; this.sampleRate = sampleRate; this.frameSize = frameSize; @@ -101,6 +106,13 @@ public final class Ac3Util { private static final int[] SYNCFRAME_SIZE_WORDS_BY_HALF_FRMSIZECOD_44_1 = new int[] {69, 87, 104, 121, 139, 174, 208, 243, 278, 348, 417, 487, 557, 696, 835, 975, 1114, 1253, 1393}; + /** + * Stream type. See ETSI TS 102 366 E.1.3.1.1. + */ + public static final int STREAM_TYPE_UNDEFINED = -1; + public static final int STREAM_TYPE_TYPE0 = 0; + public static final int STREAM_TYPE_TYPE1 = 1; + /** * Returns the AC-3 format given {@code data} containing the AC3SpecificBox according to * ETSI TS 102 366 Annex F. The reading position of {@code data} will be modified. @@ -138,8 +150,8 @@ public final class Ac3Util { String language, DrmInitData drmInitData) { data.skipBytes(2); // data_rate, num_ind_sub - // Read only the first substream. - // TODO: Read later substreams? + // Read only the first independent substream. + // TODO: Read later independent substreams? int fscod = (data.readUnsignedByte() & 0xC0) >> 6; int sampleRate = SAMPLE_RATE_BY_FSCOD[fscod]; int nextByte = data.readUnsignedByte(); @@ -147,6 +159,19 @@ public final class Ac3Util { if ((nextByte & 0x01) != 0) { // lfeon channelCount++; } + + // Read only the first dependent substream. + // TODO: Read later dependent substreams? + nextByte = data.readUnsignedByte(); + int numDepSub = ((nextByte & 0x1E) >> 1); + if (numDepSub > 0) { + int lowByteChanLoc = data.readUnsignedByte(); + // Read Lrs/Rrs pair + // TODO: Read other channel configuration + if ((lowByteChanLoc & 0x02) != 0) { + channelCount += 2; + } + } return Format.createAudioSampleFormat(trackId, MimeTypes.AUDIO_E_AC3, null, Format.NO_VALUE, Format.NO_VALUE, channelCount, sampleRate, null, drmInitData, 0, language); } @@ -164,13 +189,16 @@ public final class Ac3Util { boolean isEac3 = data.readBits(5) == 16; data.setPosition(initialPosition); String mimeType; + int streamType; int sampleRate; int acmod; int frameSize; int sampleCount; if (isEac3) { mimeType = MimeTypes.AUDIO_E_AC3; - data.skipBits(16 + 2 + 3); // syncword, strmtype, substreamid + data.skipBits(16); // syncword + streamType = data.readBits(2); + data.skipBits(3); // substreamid frameSize = (data.readBits(11) + 1) * 2; int fscod = data.readBits(2); int audioBlocks; @@ -187,6 +215,7 @@ public final class Ac3Util { } else /* is AC-3 */ { mimeType = MimeTypes.AUDIO_AC3; data.skipBits(16 + 16); // syncword, crc1 + streamType = STREAM_TYPE_UNDEFINED; // AC-3 stream hasn't streamType int fscod = data.readBits(2); int frmsizecod = data.readBits(6); frameSize = getAc3SyncframeSize(fscod, frmsizecod); @@ -206,7 +235,8 @@ public final class Ac3Util { } boolean lfeon = data.readBit(); int channelCount = CHANNEL_COUNT_BY_ACMOD[acmod] + (lfeon ? 1 : 0); - return new Ac3SyncFrameInfo(mimeType, channelCount, sampleRate, frameSize, sampleCount); + return new Ac3SyncFrameInfo(mimeType, streamType, channelCount, sampleRate, + frameSize, sampleCount); } /** diff --git a/library/dash/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 index 53115a7a0e..a34d248006 100644 --- a/library/dash/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 @@ -690,7 +690,9 @@ public class DashManifestParser extends DefaultHandler throws XmlPullParserException, IOException { String schemeIdUri = parseString(xpp, "schemeIdUri", null); int audioChannels = "urn:mpeg:dash:23003:3:audio_channel_configuration:2011".equals(schemeIdUri) - ? parseInt(xpp, "value", Format.NO_VALUE) : Format.NO_VALUE; + ? parseInt(xpp, "value", Format.NO_VALUE) : + ("tag:dolby.com,2014:dash:audio_channel_configuration:2011".equals(schemeIdUri) + ? parseDolbyChannelConfiguration(xpp, "value", Format.NO_VALUE) : Format.NO_VALUE); do { xpp.next(); } while (!XmlPullParserUtil.isEndTag(xpp, "AudioChannelConfiguration")); @@ -901,6 +903,33 @@ public class DashManifestParser extends DefaultHandler return value == null ? defaultValue : value; } + protected static int parseDolbyChannelConfiguration(XmlPullParser xpp, String name, + int defaultValue) { + String value = Util.toLowerInvariant(xpp.getAttributeValue(null, name)); + if (value == null) { + return defaultValue; + } + int channels; + // TODO: Parse other channel configurations + switch (value) { + case "4000": + channels = 1; + break; + case "a000": + channels = 2; + break; + case "f801": + channels = 6; + break; + case "fa01": + channels = 8; + break; + default: + channels = defaultValue; + } + return channels; + } + private static final class RepresentationInfo { public final Format format; From 40f34956a836557ba7dac681984bb076658715f1 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 10 Aug 2017 09:34:25 -0700 Subject: [PATCH 0239/2472] Bump minimum and target API levels + support lib version ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164863447 --- constants.gradle | 12 ++++++------ demos/main/src/main/AndroidManifest.xml | 2 +- .../cronet/src/androidTest/AndroidManifest.xml | 2 +- .../flac/src/androidTest/AndroidManifest.xml | 2 +- .../opus/src/androidTest/AndroidManifest.xml | 2 +- extensions/vp9/src/androidTest/AndroidManifest.xml | 2 +- .../exoplayer2/ext/vp9/VpxVideoSurfaceView.java | 2 -- library/core/src/androidTest/AndroidManifest.xml | 2 +- library/dash/src/androidTest/AndroidManifest.xml | 2 +- library/hls/src/androidTest/AndroidManifest.xml | 2 +- .../src/androidTest/AndroidManifest.xml | 2 +- .../android/exoplayer2/ui/PlaybackControlView.java | 14 ++------------ playbacktests/src/androidTest/AndroidManifest.xml | 2 +- .../android/exoplayer2/testutil/HostActivity.java | 9 +-------- 14 files changed, 19 insertions(+), 38 deletions(-) diff --git a/constants.gradle b/constants.gradle index b7cc8b6906..576091f937 100644 --- a/constants.gradle +++ b/constants.gradle @@ -12,16 +12,16 @@ // See the License for the specific language governing permissions and // limitations under the License. project.ext { - // Important: ExoPlayer specifies a minSdkVersion of 9 because various + // Important: ExoPlayer specifies a minSdkVersion of 14 because various // 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 = 14 + compileSdkVersion = 26 + targetSdkVersion = 26 + buildToolsVersion = '26' testSupportLibraryVersion = '0.5' - supportLibraryVersion = '25.4.0' + supportLibraryVersion = '26.0.1' dexmakerVersion = '1.2' mockitoVersion = '1.9.5' releaseVersion = 'r2.5.1' diff --git a/demos/main/src/main/AndroidManifest.xml b/demos/main/src/main/AndroidManifest.xml index 1f66822dc7..4f90cef623 100644 --- a/demos/main/src/main/AndroidManifest.xml +++ b/demos/main/src/main/AndroidManifest.xml @@ -23,7 +23,7 @@ - + - + - + - + - + - + - + - + - + = 11) { - setViewAlphaV11(view, enabled ? 1f : 0.3f); - view.setVisibility(VISIBLE); - } else { - view.setVisibility(enabled ? VISIBLE : INVISIBLE); - } - } - - @TargetApi(11) - private void setViewAlphaV11(View view, float alpha) { - view.setAlpha(alpha); + view.setAlpha(enabled ? 1f : 0.3f); + view.setVisibility(VISIBLE); } private void previous() { diff --git a/playbacktests/src/androidTest/AndroidManifest.xml b/playbacktests/src/androidTest/AndroidManifest.xml index 053fe4e61c..1a660591d8 100644 --- a/playbacktests/src/androidTest/AndroidManifest.xml +++ b/playbacktests/src/androidTest/AndroidManifest.xml @@ -21,7 +21,7 @@ - + Date: Fri, 11 Aug 2017 06:27:59 -0700 Subject: [PATCH 0240/2472] Add possibility of forcing a specific license URL in HttpMediaDrmCallback Resubmit of https://github.com/google/ExoPlayer/pull/3136 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164971900 --- .../exoplayer2/drm/HttpMediaDrmCallback.java | 35 ++++++++-------- .../exoplayer2/drm/OfflineLicenseHelper.java | 40 +++++++++++++++---- 2 files changed, 50 insertions(+), 25 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java index f08d9b59b5..dfbf3dee07 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java @@ -24,7 +24,6 @@ import com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest; import com.google.android.exoplayer2.upstream.DataSourceInputStream; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.HttpDataSource; -import com.google.android.exoplayer2.upstream.HttpDataSource.Factory; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.io.IOException; @@ -39,33 +38,33 @@ import java.util.UUID; public final class HttpMediaDrmCallback implements MediaDrmCallback { private final HttpDataSource.Factory dataSourceFactory; - private final String defaultUrl; + private final String defaultLicenseUrl; + private final boolean forceDefaultLicenseUrl; private final Map keyRequestProperties; /** - * @param defaultUrl The default license URL. + * @param defaultLicenseUrl The default license URL. Used for key requests that do not specify + * their own license URL. * @param dataSourceFactory A factory from which to obtain {@link HttpDataSource} instances. */ - public HttpMediaDrmCallback(String defaultUrl, HttpDataSource.Factory dataSourceFactory) { - this(defaultUrl, dataSourceFactory, null); + public HttpMediaDrmCallback(String defaultLicenseUrl, HttpDataSource.Factory dataSourceFactory) { + this(defaultLicenseUrl, false, dataSourceFactory); } /** - * @deprecated Use {@link HttpMediaDrmCallback#HttpMediaDrmCallback(String, Factory)}. Request - * properties can be set by calling {@link #setKeyRequestProperty(String, String)}. - * @param defaultUrl The default license URL. + * @param defaultLicenseUrl The default license URL. Used for key requests that do not specify + * their own license URL, or for all key requests if {@code forceDefaultLicenseUrl} is + * set to true. + * @param forceDefaultLicenseUrl Whether to use {@code defaultLicenseUrl} for key requests that + * include their own license URL. * @param dataSourceFactory A factory from which to obtain {@link HttpDataSource} instances. - * @param keyRequestProperties Request properties to set when making key requests, or null. */ - @Deprecated - public HttpMediaDrmCallback(String defaultUrl, HttpDataSource.Factory dataSourceFactory, - Map keyRequestProperties) { + public HttpMediaDrmCallback(String defaultLicenseUrl, boolean forceDefaultLicenseUrl, + HttpDataSource.Factory dataSourceFactory) { this.dataSourceFactory = dataSourceFactory; - this.defaultUrl = defaultUrl; + this.defaultLicenseUrl = defaultLicenseUrl; + this.forceDefaultLicenseUrl = forceDefaultLicenseUrl; this.keyRequestProperties = new HashMap<>(); - if (keyRequestProperties != null) { - this.keyRequestProperties.putAll(keyRequestProperties); - } } /** @@ -112,8 +111,8 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback { @Override public byte[] executeKeyRequest(UUID uuid, KeyRequest request) throws Exception { String url = request.getDefaultUrl(); - if (TextUtils.isEmpty(url)) { - url = defaultUrl; + if (forceDefaultLicenseUrl || TextUtils.isEmpty(url)) { + url = defaultLicenseUrl; } Map requestProperties = new HashMap<>(); // Add standard request properties for supported schemes. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java index 2eb3463b3d..741ad1f06f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java @@ -43,23 +43,47 @@ public final class OfflineLicenseHelper { * 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 defaultLicenseUrl The default license URL. Used for key requests that do not specify + * their own license URL. * @param httpDataSourceFactory A factory from which to obtain {@link HttpDataSource} instances. * @return A new instance which uses Widevine CDM. * @throws UnsupportedDrmException If the Widevine DRM scheme is unsupported or cannot be * instantiated. */ public static OfflineLicenseHelper newWidevineInstance( - String licenseUrl, Factory httpDataSourceFactory) throws UnsupportedDrmException { - return newWidevineInstance( - new HttpMediaDrmCallback(licenseUrl, httpDataSourceFactory), null); + String defaultLicenseUrl, Factory httpDataSourceFactory) + throws UnsupportedDrmException { + return newWidevineInstance(defaultLicenseUrl, false, httpDataSourceFactory, null); } /** * 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 defaultLicenseUrl The default license URL. Used for key requests that do not specify + * their own license URL. + * @param forceDefaultLicenseUrl Whether to use {@code defaultLicenseUrl} for key requests that + * include their own license URL. + * @param httpDataSourceFactory A factory from which to obtain {@link HttpDataSource} instances. + * @return A new instance which uses Widevine CDM. + * @throws UnsupportedDrmException If the Widevine DRM scheme is unsupported or cannot be + * instantiated. + */ + public static OfflineLicenseHelper newWidevineInstance( + String defaultLicenseUrl, boolean forceDefaultLicenseUrl, Factory httpDataSourceFactory) + throws UnsupportedDrmException { + return newWidevineInstance(defaultLicenseUrl, forceDefaultLicenseUrl, httpDataSourceFactory, + null); + } + + /** + * Instantiates a new instance which uses Widevine CDM. Call {@link #release()} when the instance + * is no longer required. + * + * @param defaultLicenseUrl The default license URL. Used for key requests that do not specify + * their own license URL. + * @param forceDefaultLicenseUrl Whether to use {@code defaultLicenseUrl} for key requests that + * include their own license URL. * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument * to {@link MediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null. * @return A new instance which uses Widevine CDM. @@ -69,9 +93,11 @@ public final class OfflineLicenseHelper { * MediaDrmCallback, HashMap, Handler, EventListener) */ public static OfflineLicenseHelper newWidevineInstance( - MediaDrmCallback callback, HashMap optionalKeyRequestParameters) + String defaultLicenseUrl, boolean forceDefaultLicenseUrl, Factory httpDataSourceFactory, + HashMap optionalKeyRequestParameters) throws UnsupportedDrmException { - return new OfflineLicenseHelper<>(FrameworkMediaDrm.newInstance(C.WIDEVINE_UUID), callback, + return new OfflineLicenseHelper<>(FrameworkMediaDrm.newInstance(C.WIDEVINE_UUID), + new HttpMediaDrmCallback(defaultLicenseUrl, forceDefaultLicenseUrl, httpDataSourceFactory), optionalKeyRequestParameters); } From 730d2dd18ffa8902d84e08adf807c8bbb9f8afc6 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 11 Aug 2017 15:46:49 +0100 Subject: [PATCH 0241/2472] Formatting cleanup --- .../android/exoplayer2/audio/Ac3Util.java | 40 +++++++++++-------- .../dash/manifest/DashManifestParser.java | 39 +++++++++--------- 2 files changed, 44 insertions(+), 35 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java index bc46a4f10a..e1a70e2579 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java @@ -33,13 +33,31 @@ public final class Ac3Util { */ public static final class Ac3SyncFrameInfo { + /** + * Undefined AC3 stream type. + */ + public static final int STREAM_TYPE_UNDEFINED = -1; + /** + * Type 0 AC3 stream type. See ETSI TS 102 366 E.1.3.1.1. + */ + public static final int STREAM_TYPE_TYPE0 = 0; + /** + * Type 1 AC3 stream type. See ETSI TS 102 366 E.1.3.1.1. + */ + public static final int STREAM_TYPE_TYPE1 = 1; + /** + * Type 2 AC3 stream type. See ETSI TS 102 366 E.1.3.1.1. + */ + public static final int STREAM_TYPE_TYPE2 = 2; + /** * The sample mime type of the bitstream. One of {@link MimeTypes#AUDIO_AC3} and * {@link MimeTypes#AUDIO_E_AC3}. */ public final String mimeType; /** - * The type of the bitstream, only for AUDIO_E_AC3 + * The type of the stream if {@link #mimeType} is {@link MimeTypes#AUDIO_E_AC3}, or + * {@link #STREAM_TYPE_UNDEFINED} otherwise. */ public final int streamType; /** @@ -106,13 +124,6 @@ public final class Ac3Util { private static final int[] SYNCFRAME_SIZE_WORDS_BY_HALF_FRMSIZECOD_44_1 = new int[] {69, 87, 104, 121, 139, 174, 208, 243, 278, 348, 417, 487, 557, 696, 835, 975, 1114, 1253, 1393}; - /** - * Stream type. See ETSI TS 102 366 E.1.3.1.1. - */ - public static final int STREAM_TYPE_UNDEFINED = -1; - public static final int STREAM_TYPE_TYPE0 = 0; - public static final int STREAM_TYPE_TYPE1 = 1; - /** * Returns the AC-3 format given {@code data} containing the AC3SpecificBox according to * ETSI TS 102 366 Annex F. The reading position of {@code data} will be modified. @@ -150,8 +161,7 @@ public final class Ac3Util { String language, DrmInitData drmInitData) { data.skipBytes(2); // data_rate, num_ind_sub - // Read only the first independent substream. - // TODO: Read later independent substreams? + // Read the first independent substream. int fscod = (data.readUnsignedByte() & 0xC0) >> 6; int sampleRate = SAMPLE_RATE_BY_FSCOD[fscod]; int nextByte = data.readUnsignedByte(); @@ -160,8 +170,7 @@ public final class Ac3Util { channelCount++; } - // Read only the first dependent substream. - // TODO: Read later dependent substreams? + // Read the first dependent substream. nextByte = data.readUnsignedByte(); int numDepSub = ((nextByte & 0x1E) >> 1); if (numDepSub > 0) { @@ -189,7 +198,7 @@ public final class Ac3Util { boolean isEac3 = data.readBits(5) == 16; data.setPosition(initialPosition); String mimeType; - int streamType; + int streamType = Ac3SyncFrameInfo.STREAM_TYPE_UNDEFINED; int sampleRate; int acmod; int frameSize; @@ -215,7 +224,6 @@ public final class Ac3Util { } else /* is AC-3 */ { mimeType = MimeTypes.AUDIO_AC3; data.skipBits(16 + 16); // syncword, crc1 - streamType = STREAM_TYPE_UNDEFINED; // AC-3 stream hasn't streamType int fscod = data.readBits(2); int frmsizecod = data.readBits(6); frameSize = getAc3SyncframeSize(fscod, frmsizecod); @@ -235,8 +243,8 @@ public final class Ac3Util { } boolean lfeon = data.readBit(); int channelCount = CHANNEL_COUNT_BY_ACMOD[acmod] + (lfeon ? 1 : 0); - return new Ac3SyncFrameInfo(mimeType, streamType, channelCount, sampleRate, - frameSize, sampleCount); + return new Ac3SyncFrameInfo(mimeType, streamType, channelCount, sampleRate, frameSize, + sampleCount); } /** diff --git a/library/dash/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 index a34d248006..2e85f3a1ad 100644 --- a/library/dash/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 @@ -690,9 +690,9 @@ public class DashManifestParser extends DefaultHandler throws XmlPullParserException, IOException { String schemeIdUri = parseString(xpp, "schemeIdUri", null); int audioChannels = "urn:mpeg:dash:23003:3:audio_channel_configuration:2011".equals(schemeIdUri) - ? parseInt(xpp, "value", Format.NO_VALUE) : - ("tag:dolby.com,2014:dash:audio_channel_configuration:2011".equals(schemeIdUri) - ? parseDolbyChannelConfiguration(xpp, "value", Format.NO_VALUE) : Format.NO_VALUE); + ? parseInt(xpp, "value", Format.NO_VALUE) + : ("tag:dolby.com,2014:dash:audio_channel_configuration:2011".equals(schemeIdUri) + ? parseDolbyChannelConfiguration(xpp) : Format.NO_VALUE); do { xpp.next(); } while (!XmlPullParserUtil.isEndTag(xpp, "AudioChannelConfiguration")); @@ -903,31 +903,32 @@ public class DashManifestParser extends DefaultHandler return value == null ? defaultValue : value; } - protected static int parseDolbyChannelConfiguration(XmlPullParser xpp, String name, - int defaultValue) { - String value = Util.toLowerInvariant(xpp.getAttributeValue(null, name)); + /** + * Parses the number of channels from the value attribute of an AudioElementConfiguration with + * schemeIdUri "tag:dolby.com,2014:dash:audio_channel_configuration:2011", as defined by table E.5 + * in ETSI TS 102 366. + * + * @param xpp The parser from which to read. + * @return The parsed number of channels, or {@link Format#NO_VALUE} if the channel count could + * not be parsed. + */ + protected static int parseDolbyChannelConfiguration(XmlPullParser xpp) { + String value = Util.toLowerInvariant(xpp.getAttributeValue(null, "value")); if (value == null) { - return defaultValue; + return Format.NO_VALUE; } - int channels; - // TODO: Parse other channel configurations switch (value) { case "4000": - channels = 1; - break; + return 1; case "a000": - channels = 2; - break; + return 2; case "f801": - channels = 6; - break; + return 6; case "fa01": - channels = 8; - break; + return 8; default: - channels = defaultValue; + return Format.NO_VALUE; } - return channels; } private static final class RepresentationInfo { From 1829d71d29435ca1eb4240d4aed7cb1315e2b606 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 11 Aug 2017 15:47:03 +0100 Subject: [PATCH 0242/2472] Restrict usage of secure DummySurface for all Samsung devices. --- .../java/com/google/android/exoplayer2/video/DummySurface.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java index e32f23fed7..7a80294929 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java @@ -154,7 +154,7 @@ public final class DummySurface extends Surface { */ private static boolean deviceNeedsSecureDummySurfaceWorkaround(Context context) { return Util.SDK_INT == 24 - && (Util.MODEL.startsWith("SM-G950") || Util.MODEL.startsWith("SM-G955")) + && "samsung".equals(Util.MANUFACTURER) && !hasVrModeHighPerformanceSystemFeatureV24(context.getPackageManager()); } From 469780f7ea5ec59ccf8b72b085ed98249724b195 Mon Sep 17 00:00:00 2001 From: WeiChungChang Date: Thu, 17 Aug 2017 13:49:41 +0800 Subject: [PATCH 0243/2472] Support H262 video in MP4 --- .../google/android/exoplayer2/extractor/mp4/AtomParsers.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/library/core/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 index 0a5e0e8a6d..cc70804a29 100644 --- a/library/core/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 @@ -1027,6 +1027,10 @@ import java.util.List; case 0xAB: mimeType = MimeTypes.AUDIO_DTS_HD; return Pair.create(mimeType, null); + case 0x60: /* Visual 13818-2 Simple Profile */ + case 0x61: /* Visual 13818-2 Main Profile */ + mimeType = MimeTypes.VIDEO_MPEG2; + break; default: mimeType = null; break; From 571f98f546d7c846ed672ff5250e47b43177c3d9 Mon Sep 17 00:00:00 2001 From: Danny Brain Date: Thu, 17 Aug 2017 21:30:46 +1000 Subject: [PATCH 0244/2472] Fix possible subrip timing line NPE --- .../google/android/exoplayer2/text/subrip/SubripDecoder.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/core/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 index e76f0fd7e2..49ebe84d67 100644 --- a/library/core/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 @@ -69,8 +69,8 @@ public final class SubripDecoder extends SimpleSubtitleDecoder { // Read and parse the timing line. boolean haveEndTimecode = false; currentLine = subripData.readLine(); - Matcher matcher = SUBRIP_TIMING_LINE.matcher(currentLine); - if (matcher.matches()) { + Matcher matcher = currentLine == null ? null : SUBRIP_TIMING_LINE.matcher(currentLine); + if (matcher != null && matcher.matches()) { cueTimesUs.add(parseTimecode(matcher, 1)); if (!TextUtils.isEmpty(matcher.group(6))) { haveEndTimecode = true; From f764fe70b0306d7650cf83345f06ce3dd753470e Mon Sep 17 00:00:00 2001 From: Bei Yi Date: Thu, 17 Aug 2017 11:01:42 -0700 Subject: [PATCH 0245/2472] Support crop mode for AspectRatioFrameLayout --- .../exoplayer2/ui/AspectRatioFrameLayout.java | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java index b0df16b484..2f04b8800d 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java @@ -32,7 +32,7 @@ public final class AspectRatioFrameLayout extends FrameLayout { * Resize modes for {@link AspectRatioFrameLayout}. */ @Retention(RetentionPolicy.SOURCE) - @IntDef({RESIZE_MODE_FIT, RESIZE_MODE_FIXED_WIDTH, RESIZE_MODE_FIXED_HEIGHT, RESIZE_MODE_FILL}) + @IntDef({RESIZE_MODE_FIT, RESIZE_MODE_FIXED_WIDTH, RESIZE_MODE_FIXED_HEIGHT, RESIZE_MODE_FILL, RESIZE_MODE_CROP}) public @interface ResizeMode {} /** @@ -51,6 +51,10 @@ public final class AspectRatioFrameLayout extends FrameLayout { * The specified aspect ratio is ignored. */ public static final int RESIZE_MODE_FILL = 3; + /** + * The height or width is increased or decreased to crop and to obtain the desired aspect ratio. + */ + public static final int RESIZE_MODE_CROP = 4; /** * The {@link FrameLayout} will not resize itself if the fractional difference between its natural @@ -96,6 +100,15 @@ public final class AspectRatioFrameLayout extends FrameLayout { } } + /** + * Gets the resize mode. + * + * @return The resize mode. + */ + public int getResizeMode() { + return this.resizeMode; + } + /** * Sets the resize mode. * @@ -132,6 +145,13 @@ public final class AspectRatioFrameLayout extends FrameLayout { case RESIZE_MODE_FIXED_HEIGHT: width = (int) (height * videoAspectRatio); break; + case RESIZE_MODE_CROP: + if (videoAspectRatio > viewAspectRatio) { + width = (int) (height * videoAspectRatio); + } else { + height = (int) (width / videoAspectRatio); + } + break; default: if (aspectDeformation > 0) { height = (int) (width / videoAspectRatio); From 870c8ae4df9b3a0157318bcd4790906efc48430e Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 14 Aug 2017 03:15:43 -0700 Subject: [PATCH 0246/2472] Fix canAcquireSession for ClearKey ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=165159376 --- .../drm/DefaultDrmSessionManager.java | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java index fcd8ac862b..69403a90a6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java @@ -311,7 +311,7 @@ public class DefaultDrmSessionManager implements DrmSe @Override public boolean canAcquireSession(@NonNull DrmInitData drmInitData) { - SchemeData schemeData = drmInitData.get(uuid); + SchemeData schemeData = getSchemeData(drmInitData); if (schemeData == null) { // No data for this manager's scheme. return false; @@ -347,11 +347,7 @@ public class DefaultDrmSessionManager implements DrmSe postRequestHandler = new PostRequestHandler(requestHandlerThread.getLooper()); if (offlineLicenseKeySetId == null) { - SchemeData schemeData = drmInitData.get(uuid); - if (schemeData == null && C.CLEARKEY_UUID.equals(uuid)) { - // If present, the Common PSSH box should be used for ClearKey. - schemeData = drmInitData.get(C.COMMON_PSSH_UUID); - } + SchemeData schemeData = getSchemeData(drmInitData); if (schemeData == null) { onError(new IllegalStateException("Media does not support uuid: " + uuid)); return this; @@ -627,6 +623,21 @@ public class DefaultDrmSessionManager implements DrmSe } } + /** + * Extracts {@link SchemeData} suitable for this manager. + * + * @param drmInitData The {@link DrmInitData} from which to extract the {@link SchemeData}. + * @return The extracted {@link SchemeData}, or null if no suitable data is present. + */ + private SchemeData getSchemeData(DrmInitData drmInitData) { + SchemeData schemeData = drmInitData.get(uuid); + if (schemeData == null && C.CLEARKEY_UUID.equals(uuid)) { + // If present, the Common PSSH box should be used for ClearKey. + schemeData = drmInitData.get(C.COMMON_PSSH_UUID); + } + return schemeData; + } + @SuppressLint("HandlerLeak") private class MediaDrmHandler extends Handler { From fb83872365f8023f019567c65757addccbc0dbda Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 14 Aug 2017 05:57:59 -0700 Subject: [PATCH 0247/2472] Remove unused method ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=165168924 --- .../exoplayer2/source/dash/DashUtil.java | 43 ------------------- 1 file changed, 43 deletions(-) diff --git a/library/dash/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 index 2febeb8c81..2d093a5a68 100644 --- a/library/dash/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 @@ -25,7 +25,6 @@ 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; @@ -66,48 +65,6 @@ 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. From cf6534ea5b50855488f78011d5907ef84101acc8 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 14 Aug 2017 08:31:59 -0700 Subject: [PATCH 0248/2472] SegmentDownloader loadManifest cleanup. - HlsDownloader.loadManifest (previously called getHlsPlaylist) suppressed errors for the offline case, where-as the DashUtil equivalent method did not. This change makes them consistent and moves both other to use ParsingLoadable. - Enable GZIP for manifest loads in both cases. - Use Uri rather than String to represent Uris. Previously the strings were parsed into Uris quite deep in the code, which isn't ideal if the parsing fails; you'd probably prefer the error to occur early at the top level. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=165181398 --- .../exoplayer2/upstream/ParsingLoadable.java | 15 +++++++- .../exoplayer2/source/dash/DashUtil.java | 21 +++++------ .../gts/DashWidevineOfflineTest.java | 3 +- .../exoplayer2/testutil/CacheAsserts.java | 37 ++++++++++++------- .../exoplayer2/testutil/FakeDataSet.java | 37 +++++++++++++++---- 5 files changed, 77 insertions(+), 36 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ParsingLoadable.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ParsingLoadable.java index adf245d9aa..308340b8b2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ParsingLoadable.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ParsingLoadable.java @@ -72,8 +72,19 @@ public final class ParsingLoadable implements Loadable { * @param parser Parses the object from the response. */ public ParsingLoadable(DataSource dataSource, Uri uri, int type, Parser parser) { + this(dataSource, new DataSpec(uri, DataSpec.FLAG_ALLOW_GZIP), type, parser); + } + + /** + * @param dataSource A {@link DataSource} to use when loading the data. + * @param dataSpec The {@link DataSpec} from which the object should be loaded. + * @param type See {@link #type}. + * @param parser Parses the object from the response. + */ + public ParsingLoadable(DataSource dataSource, DataSpec dataSpec, int type, + Parser parser) { this.dataSource = dataSource; - this.dataSpec = new DataSpec(uri, DataSpec.FLAG_ALLOW_GZIP); + this.dataSpec = dataSpec; this.type = type; this.parser = parser; } @@ -108,7 +119,7 @@ public final class ParsingLoadable implements Loadable { } @Override - public final void load() throws IOException, InterruptedException { + public final void load() throws IOException { DataSourceInputStream inputStream = new DataSourceInputStream(dataSource, dataSpec); try { inputStream.open(); diff --git a/library/dash/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 index 2d093a5a68..83cb10b99c 100644 --- a/library/dash/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 @@ -31,9 +31,9 @@ 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; -import com.google.android.exoplayer2.upstream.DataSourceInputStream; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.HttpDataSource; +import com.google.android.exoplayer2.upstream.ParsingLoadable; import com.google.android.exoplayer2.util.MimeTypes; import java.io.IOException; import java.util.List; @@ -47,21 +47,18 @@ public final class DashUtil { * Loads a DASH manifest. * * @param dataSource The {@link HttpDataSource} from which the manifest should be read. - * @param manifestUri The URI of the manifest to be read. + * @param uri The {@link Uri} of the manifest to be read. * @return An instance of {@link DashManifest}. * @throws IOException Thrown when there is an error while loading. */ - public static DashManifest loadManifest(DataSource dataSource, String manifestUri) + public static DashManifest loadManifest(DataSource dataSource, Uri uri) throws IOException { - DataSourceInputStream inputStream = new DataSourceInputStream(dataSource, - new DataSpec(Uri.parse(manifestUri), DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH)); - try { - inputStream.open(); - DashManifestParser parser = new DashManifestParser(); - return parser.parse(dataSource.getUri(), inputStream); - } finally { - inputStream.close(); - } + DataSpec dataSpec = new DataSpec(uri, + DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH | DataSpec.FLAG_ALLOW_GZIP); + ParsingLoadable loadable = new ParsingLoadable<>(dataSource, dataSpec, + C.DATA_TYPE_MANIFEST, new DashManifestParser()); + loadable.load(); + return loadable.getResult(); } /** 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 c2b102d1ec..88215ae1eb 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 @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.playbacktests.gts; import android.media.MediaDrm.MediaDrmStateException; +import android.net.Uri; import android.test.ActivityInstrumentationTestCase2; import android.util.Pair; import com.google.android.exoplayer2.drm.DrmInitData; @@ -170,7 +171,7 @@ public final class DashWidevineOfflineTest extends ActivityInstrumentationTestCa private void downloadLicense() throws InterruptedException, DrmSessionException, IOException { DataSource dataSource = httpDataSourceFactory.createDataSource(); DashManifest dashManifest = DashUtil.loadManifest(dataSource, - DashTestData.WIDEVINE_H264_MANIFEST); + Uri.parse(DashTestData.WIDEVINE_H264_MANIFEST)); DrmInitData drmInitData = DashUtil.loadDrmInitData(dataSource, dashManifest.getPeriod(0)); offlineLicenseKeySetId = offlineLicenseHelper.downloadLicense(drmInitData); Assert.assertNotNull(offlineLicenseKeySetId); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java index c8ead5dcba..82fff0d4fe 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java @@ -39,11 +39,11 @@ public final class CacheAsserts { /** Asserts that the cache content is equal to the data in the {@code fakeDataSet}. */ public static void assertCachedData(Cache cache, FakeDataSet fakeDataSet) throws IOException { ArrayList allData = fakeDataSet.getAllData(); - String[] uriStrings = new String[allData.size()]; + Uri[] uris = new Uri[allData.size()]; for (int i = 0; i < allData.size(); i++) { - uriStrings[i] = allData.get(i).uri; + uris[i] = allData.get(i).uri; } - assertCachedData(cache, fakeDataSet, uriStrings); + assertCachedData(cache, fakeDataSet, uris); } /** @@ -51,30 +51,41 @@ public final class CacheAsserts { */ public static void assertCachedData(Cache cache, FakeDataSet fakeDataSet, String... uriStrings) throws IOException { + Uri[] uris = new Uri[uriStrings.length]; + for (int i = 0; i < uriStrings.length; i++) { + uris[i] = Uri.parse(uriStrings[i]); + } + assertCachedData(cache, fakeDataSet, uris); + } + + /** + * Asserts that the cache content is equal to the given subset of data in the {@code fakeDataSet}. + */ + public static void assertCachedData(Cache cache, FakeDataSet fakeDataSet, Uri... uris) + throws IOException { int totalLength = 0; - for (String uriString : uriStrings) { - byte[] data = fakeDataSet.getData(uriString).getData(); - assertDataCached(cache, uriString, data); + for (Uri uri : uris) { + byte[] data = fakeDataSet.getData(uri).getData(); + assertDataCached(cache, uri, data); totalLength += data.length; } assertEquals(totalLength, cache.getCacheSpace()); } /** Asserts that the cache contains the given subset of data in the {@code fakeDataSet}. */ - public static void assertDataCached(Cache cache, FakeDataSet fakeDataSet, String... uriStrings) + public static void assertDataCached(Cache cache, FakeDataSet fakeDataSet, Uri... uris) throws IOException { - for (String uriString : uriStrings) { - assertDataCached(cache, uriString, fakeDataSet.getData(uriString).getData()); + for (Uri uri : uris) { + assertDataCached(cache, uri, fakeDataSet.getData(uri).getData()); } } /** Asserts that the cache contains the given data for {@code uriString}. */ - public static void assertDataCached(Cache cache, String uriString, byte[] expected) - throws IOException { + public static void assertDataCached(Cache cache, Uri uri, byte[] expected) throws IOException { CacheDataSource dataSource = new CacheDataSource(cache, DummyDataSource.INSTANCE, 0); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); DataSourceInputStream inputStream = new DataSourceInputStream(dataSource, - new DataSpec(Uri.parse(uriString), DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH)); + new DataSpec(uri, DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH)); try { inputStream.open(); byte[] buffer = new byte[1024]; @@ -87,7 +98,7 @@ public final class CacheAsserts { } finally { inputStream.close(); } - MoreAsserts.assertEquals("Cached data doesn't match expected for '" + uriString + "',", + MoreAsserts.assertEquals("Cached data doesn't match expected for '" + uri + "',", expected, outputStream.toByteArray()); } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSet.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSet.java index fd85b02d78..e77e0714e7 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSet.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeDataSet.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.testutil; +import android.net.Uri; import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.upstream.DataSpec; @@ -28,11 +29,11 @@ import java.util.List; /** * Collection of {@link FakeData} to be served by a {@link FakeDataSource}. * - *

        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 + *

        Multiple fake data can be defined by {@link FakeDataSet#setData(Uri, byte[])} and {@link + * FakeDataSet#newData(Uri)} 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 + *

        {@link FakeDataSet#newData(Uri)} and {@link FakeDataSet#newDefaultData()} return a {@link * FakeData} instance which can be used to define specific results during * {@link FakeDataSource#read(byte[], int, int)} calls. * @@ -104,8 +105,8 @@ public class FakeDataSet { this(null, 0, null, action, previousSegment); } - private Segment(byte[] data, int length, IOException exception, Runnable action, - Segment previousSegment) { + private Segment(@Nullable byte[] data, int length, @Nullable IOException exception, + @Nullable Runnable action, Segment previousSegment) { this.exception = exception; this.action = action; this.data = data; @@ -125,12 +126,12 @@ public class FakeDataSet { } /** Uri of the data or null if this is the default FakeData. */ - public final String uri; + public final Uri uri; private final ArrayList segments; private final FakeDataSet dataSet; private boolean simulateUnknownLength; - private FakeData(FakeDataSet dataSet, String uri) { + private FakeData(FakeDataSet dataSet, Uri uri) { this.uri = uri; this.segments = new ArrayList<>(); this.dataSet = dataSet; @@ -219,7 +220,7 @@ public class FakeDataSet { } - private final HashMap dataMap; + private final HashMap dataMap; private FakeData defaultData; public FakeDataSet() { @@ -234,16 +235,31 @@ public class FakeDataSet { /** Sets random data with the given {@code length} for the given {@code uri}. */ public FakeDataSet setRandomData(String uri, int length) { + return setRandomData(Uri.parse(uri), length); + } + + /** Sets random data with the given {@code length} for the given {@code uri}. */ + public FakeDataSet setRandomData(Uri uri, int length) { return setData(uri, TestUtil.buildTestData(length)); } /** Sets the given {@code data} for the given {@code uri}. */ public FakeDataSet setData(String uri, byte[] data) { + return setData(Uri.parse(uri), data); + } + + /** Sets the given {@code data} for the given {@code uri}. */ + public FakeDataSet setData(Uri uri, byte[] data) { return newData(uri).appendReadData(data).endData(); } /** Returns a new {@link FakeData} with the given {@code uri}. */ public FakeData newData(String uri) { + return newData(Uri.parse(uri)); + } + + /** Returns a new {@link FakeData} with the given {@code uri}. */ + public FakeData newData(Uri uri) { FakeData data = new FakeData(this, uri); dataMap.put(uri, data); return data; @@ -251,6 +267,11 @@ public class FakeDataSet { /** Returns the data for the given {@code uri}, or {@code defaultData} if no data is set. */ public FakeData getData(String uri) { + return getData(Uri.parse(uri)); + } + + /** Returns the data for the given {@code uri}, or {@code defaultData} if no data is set. */ + public FakeData getData(Uri uri) { FakeData data = dataMap.get(uri); return data != null ? data : defaultData; } From c9393db8789edfacdaff47fd7254990d11c3ff0f Mon Sep 17 00:00:00 2001 From: susnata Date: Mon, 14 Aug 2017 08:53:03 -0700 Subject: [PATCH 0249/2472] Creating an leanback extension for ExoPlayer. Leanback added support for new transport control, which allows developers to plug in any media player. This extension provides the PlayerAdapter implementation for ExoPlayer. Demo: https://drive.google.com/open?id=0B1GHUu5ruGULZTJVV1pVNlBuVjQ ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=165183497 --- core_settings.gradle | 2 + extensions/leanback/README.md | 26 ++ extensions/leanback/build.gradle | 41 +++ .../leanback/src/main/AndroidManifest.xml | 17 + .../ext/leanback/LeanbackPlayerAdapter.java | 314 ++++++++++++++++++ 5 files changed, 400 insertions(+) create mode 100644 extensions/leanback/README.md create mode 100644 extensions/leanback/build.gradle create mode 100644 extensions/leanback/src/main/AndroidManifest.xml create mode 100644 extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java diff --git a/core_settings.gradle b/core_settings.gradle index 20e7b235a2..7a8320b1a1 100644 --- a/core_settings.gradle +++ b/core_settings.gradle @@ -33,6 +33,7 @@ include modulePrefix + 'extension-okhttp' include modulePrefix + 'extension-opus' include modulePrefix + 'extension-vp9' include modulePrefix + 'extension-rtmp' +include modulePrefix + 'extension-leanback' project(modulePrefix + 'library').projectDir = new File(rootDir, 'library/all') project(modulePrefix + 'library-core').projectDir = new File(rootDir, 'library/core') @@ -50,6 +51,7 @@ project(modulePrefix + 'extension-okhttp').projectDir = new File(rootDir, 'exten project(modulePrefix + 'extension-opus').projectDir = new File(rootDir, 'extensions/opus') project(modulePrefix + 'extension-vp9').projectDir = new File(rootDir, 'extensions/vp9') project(modulePrefix + 'extension-rtmp').projectDir = new File(rootDir, 'extensions/rtmp') +project(modulePrefix + 'extension-leanback').projectDir = new File(rootDir, 'extensions/leanback') if (gradle.ext.has('exoplayerIncludeCronetExtension') && gradle.ext.exoplayerIncludeCronetExtension) { diff --git a/extensions/leanback/README.md b/extensions/leanback/README.md new file mode 100644 index 0000000000..2326887724 --- /dev/null +++ b/extensions/leanback/README.md @@ -0,0 +1,26 @@ +# ExoPlayer Leanback Extension # + +## Description ## + +This [Leanback][] Extension provides a [PlayerAdapter][] implementation for +Exoplayer. + +[PlayerAdapter]: https://developer.android.com/reference/android/support/v17/leanback/media/PlayerAdapter.html +[Leanback]: https://developer.android.com/reference/android/support/v17/leanback/package-summary.html + +## Getting the extension ## + +The easiest way to use the extension is to add it as a gradle dependency: + +```gradle +compile 'com.google.android.exoplayer:extension-leanback:rX.X.X' +``` + +where `rX.X.X` is the version, which must match the version of the ExoPlayer +library being used. + +Alternatively, you can clone the ExoPlayer repository and depend on the module +locally. Instructions for doing this can be found in ExoPlayer's +[top level README][]. + +[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md diff --git a/extensions/leanback/build.gradle b/extensions/leanback/build.gradle new file mode 100644 index 0000000000..eaf58cc990 --- /dev/null +++ b/extensions/leanback/build.gradle @@ -0,0 +1,41 @@ +// 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 from: '../../constants.gradle' +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(modulePrefix + 'library-core') + compile('com.android.support:leanback-v17:' + supportLibraryVersion) +} + +ext { + javadocTitle = 'Leanback extension for Exoplayer library' +} +apply from: '../../javadoc_library.gradle' + +ext { + releaseArtifact = 'extension-leanback' + releaseDescription = 'Leanback extension for ExoPlayer.' +} +apply from: '../../publish.gradle' diff --git a/extensions/leanback/src/main/AndroidManifest.xml b/extensions/leanback/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..20cc9bf285 --- /dev/null +++ b/extensions/leanback/src/main/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + diff --git a/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java b/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java new file mode 100644 index 0000000000..58c8ee973d --- /dev/null +++ b/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java @@ -0,0 +1,314 @@ +/* + * 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.ext.leanback; + +import android.content.Context; +import android.os.Handler; +import android.support.v17.leanback.R; +import android.support.v17.leanback.media.PlaybackGlueHost; +import android.support.v17.leanback.media.PlayerAdapter; +import android.support.v17.leanback.media.SurfaceHolderGlueHost; +import android.util.Pair; +import android.view.SurfaceHolder; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.ExoPlayerLibraryInfo; +import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.SimpleExoPlayer; +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.ErrorMessageProvider; + +/** + * Leanback {@link PlayerAdapter} implementation for {@link SimpleExoPlayer}. + */ +public final class LeanbackPlayerAdapter extends PlayerAdapter { + + private final Context context; + private final SimpleExoPlayer player; + private final Handler handler; + private final Runnable updatePlayerRunnable = new Runnable() { + @Override + public void run() { + getCallback().onCurrentPositionChanged(LeanbackPlayerAdapter.this); + getCallback().onBufferedPositionChanged(LeanbackPlayerAdapter.this); + handler.postDelayed(this, updatePeriod); + } + }; + + private SurfaceHolderGlueHost surfaceHolderGlueHost; + private boolean initialized; + private boolean hasDisplay; + private boolean isBuffering; + private ErrorMessageProvider errorMessageProvider; + private final int updatePeriod; + private final ExoPlayerEventListenerImpl exoPlayerListener = new ExoPlayerEventListenerImpl(); + private final SimpleExoPlayer.VideoListener videoListener = new SimpleExoPlayer.VideoListener() { + @Override + public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, + float pixelWidthHeightRatio) { + getCallback().onVideoSizeChanged(LeanbackPlayerAdapter.this, width, height); + } + + @Override + public void onRenderedFirstFrame() { + } + }; + + static { + ExoPlayerLibraryInfo.registerModule("goog.exo.leanback"); + } + + /** + * Constructor. + * Users are responsible for managing {@link SimpleExoPlayer} lifecycle. You must + * stop/release the player once you're done playing the media. + * + * @param context The current context (activity). + * @param player Instance of your exoplayer that needs to be configured. + */ + public LeanbackPlayerAdapter(Context context, SimpleExoPlayer player, int updatePeriod) { + this.context = context; + this.player = player; + this.handler = new Handler(); + this.updatePeriod = updatePeriod; + } + + @Override + public void onAttachedToHost(PlaybackGlueHost host) { + if (host instanceof SurfaceHolderGlueHost) { + surfaceHolderGlueHost = ((SurfaceHolderGlueHost) host); + surfaceHolderGlueHost.setSurfaceHolderCallback(new VideoPlayerSurfaceHolderCallback()); + } + initializePlayer(); + } + + private void initializePlayer() { + notifyListeners(); + this.player.addListener(exoPlayerListener); + this.player.setVideoListener(videoListener); + } + + private void notifyListeners() { + boolean oldIsPrepared = isPrepared(); + int playbackState = player.getPlaybackState(); + boolean isInitialized = playbackState != ExoPlayer.STATE_IDLE; + isBuffering = playbackState == ExoPlayer.STATE_BUFFERING; + boolean hasEnded = playbackState == ExoPlayer.STATE_ENDED; + + initialized = isInitialized; + if (oldIsPrepared != isPrepared()) { + getCallback().onPreparedStateChanged(LeanbackPlayerAdapter.this); + } + + getCallback().onPlayStateChanged(this); + notifyBufferingState(); + + if (hasEnded) { + getCallback().onPlayCompleted(this); + } + } + + /** + * Sets the optional {@link ErrorMessageProvider}. + * + * @param errorMessageProvider The {@link ErrorMessageProvider}. + */ + public void setErrorMessageProvider( + ErrorMessageProvider errorMessageProvider) { + this.errorMessageProvider = errorMessageProvider; + } + + private void uninitializePlayer() { + if (initialized) { + initialized = false; + notifyBufferingState(); + if (hasDisplay) { + getCallback().onPlayStateChanged(LeanbackPlayerAdapter.this); + getCallback().onPreparedStateChanged(LeanbackPlayerAdapter.this); + } + + player.removeListener(exoPlayerListener); + player.clearVideoListener(videoListener); + } + } + + /** + * Notify the state of buffering. For example, an app may enable/disable a loading figure + * according to the state of buffering. + */ + private void notifyBufferingState() { + getCallback().onBufferingStateChanged(LeanbackPlayerAdapter.this, + isBuffering || !initialized); + } + + @Override + public void onDetachedFromHost() { + if (surfaceHolderGlueHost != null) { + surfaceHolderGlueHost.setSurfaceHolderCallback(null); + surfaceHolderGlueHost = null; + } + uninitializePlayer(); + hasDisplay = false; + } + + @Override + public void setProgressUpdatingEnabled(final boolean enabled) { + handler.removeCallbacks(updatePlayerRunnable); + if (!enabled) { + return; + } + handler.postDelayed(updatePlayerRunnable, updatePeriod); + } + + @Override + public boolean isPlaying() { + return initialized && player.getPlayWhenReady(); + } + + @Override + public long getDuration() { + long duration = player.getDuration(); + return duration != C.TIME_UNSET ? duration : -1; + } + + @Override + public long getCurrentPosition() { + return initialized ? player.getCurrentPosition() : -1; + } + + @Override + public void play() { + if (player.getPlaybackState() == ExoPlayer.STATE_ENDED) { + seekTo(0); + } + player.setPlayWhenReady(true); + getCallback().onPlayStateChanged(this); + } + + @Override + public void pause() { + player.setPlayWhenReady(false); + getCallback().onPlayStateChanged(this); + } + + @Override + public void seekTo(long newPosition) { + player.seekTo(newPosition); + } + + @Override + public long getBufferedPosition() { + return player.getBufferedPosition(); + } + + /** + * @return True if ExoPlayer is ready and got a SurfaceHolder if + * {@link PlaybackGlueHost} provides SurfaceHolder. + */ + @Override + public boolean isPrepared() { + return initialized && (surfaceHolderGlueHost == null || hasDisplay); + } + + /** + * @see SimpleExoPlayer#setVideoSurfaceHolder(SurfaceHolder) + */ + private void setDisplay(SurfaceHolder surfaceHolder) { + hasDisplay = surfaceHolder != null; + player.setVideoSurface(surfaceHolder.getSurface()); + getCallback().onPreparedStateChanged(this); + } + + /** + * Implements {@link SurfaceHolder.Callback} that can then be set on the + * {@link PlaybackGlueHost}. + */ + private final class VideoPlayerSurfaceHolderCallback implements SurfaceHolder.Callback { + @Override + public void surfaceCreated(SurfaceHolder surfaceHolder) { + setDisplay(surfaceHolder); + } + + @Override + public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) { + } + + @Override + public void surfaceDestroyed(SurfaceHolder surfaceHolder) { + setDisplay(null); + } + } + + private final class ExoPlayerEventListenerImpl implements ExoPlayer.EventListener { + + @Override + public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + LeanbackPlayerAdapter.this.notifyListeners(); + } + + @Override + public void onPlayerError(ExoPlaybackException exception) { + String errMsg = ""; + if (errorMessageProvider != null) { + Pair message = errorMessageProvider.getErrorMessage(exception); + if (message != null) { + getCallback().onError(LeanbackPlayerAdapter.this, + message.first, + message.second); + return; + } + } + getCallback().onError(LeanbackPlayerAdapter.this, + exception.type, + context.getString(R.string.lb_media_player_error, + exception.type, + exception.rendererIndex)); + } + + @Override + public void onLoadingChanged(boolean isLoading) { + } + + @Override + public void onTimelineChanged(Timeline timeline, Object manifest) { + getCallback().onDurationChanged(LeanbackPlayerAdapter.this); + getCallback().onCurrentPositionChanged(LeanbackPlayerAdapter.this); + getCallback().onBufferedPositionChanged(LeanbackPlayerAdapter.this); + } + + @Override + public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { + } + + @Override + public void onPositionDiscontinuity() { + getCallback().onCurrentPositionChanged(LeanbackPlayerAdapter.this); + getCallback().onBufferedPositionChanged(LeanbackPlayerAdapter.this); + } + + @Override + public void onPlaybackParametersChanged(PlaybackParameters params) { + } + + @Override + public void onRepeatModeChanged(int repeatMode) { + } + } +} + From d9cd4641f2bc3678cc232bf3358b0f40b04d9dee Mon Sep 17 00:00:00 2001 From: mdoucleff Date: Mon, 14 Aug 2017 10:35:37 -0700 Subject: [PATCH 0250/2472] Add flag to CachedContentIndex to disable encryption. This allows the encryption feature to be disabled gracefully: encrypted index files may be read, but plaintext will be written. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=165196508 --- .../upstream/cache/CachedContentIndex.java | 21 +++++++++++++++---- .../upstream/cache/SimpleCache.java | 16 +++++++++++++- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/library/core/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 index 10bc298579..e1c2c13865 100644 --- a/library/core/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 @@ -64,6 +64,7 @@ import javax.crypto.spec.SecretKeySpec; private final AtomicFile atomicFile; private final Cipher cipher; private final SecretKeySpec secretKeySpec; + private final boolean encrypt; private boolean changed; private ReusableBufferedOutputStream bufferedOutputStream; @@ -80,10 +81,21 @@ import javax.crypto.spec.SecretKeySpec; * Creates a CachedContentIndex which works on the index file in the given cacheDir. * * @param cacheDir Directory where the index file is kept. - * @param secretKey If not null, cache keys will be stored encrypted on filesystem using AES/CBC. - * The key must be 16 bytes long. + * @param secretKey 16 byte AES key for reading and writing the cache index. */ public CachedContentIndex(File cacheDir, byte[] secretKey) { + this(cacheDir, secretKey, secretKey != null); + } + + /** + * Creates a CachedContentIndex which works on the index file in the given cacheDir. + * + * @param cacheDir Directory where the index file is kept. + * @param secretKey 16 byte AES key for reading, and optionally writing, the cache index. + * @param encrypt When false, a plaintext index will be written. + */ + public CachedContentIndex(File cacheDir, byte[] secretKey, boolean encrypt) { + this.encrypt = encrypt; if (secretKey != null) { Assertions.checkArgument(secretKey.length == 16); try { @@ -288,10 +300,11 @@ import javax.crypto.spec.SecretKeySpec; output = new DataOutputStream(bufferedOutputStream); output.writeInt(VERSION); - int flags = cipher != null ? FLAG_ENCRYPTED_INDEX : 0; + boolean writeEncrypted = encrypt && cipher != null; + int flags = writeEncrypted ? FLAG_ENCRYPTED_INDEX : 0; output.writeInt(flags); - if (cipher != null) { + if (writeEncrypted) { byte[] initializationVector = new byte[16]; new Random().nextBytes(initializationVector); output.write(initializationVector); diff --git a/library/core/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 index 62bd2783b8..bb1ac83698 100644 --- a/library/core/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 @@ -60,10 +60,24 @@ public final class SimpleCache implements Cache { * The key must be 16 bytes long. */ public SimpleCache(File cacheDir, CacheEvictor evictor, byte[] secretKey) { + this(cacheDir, evictor, secretKey, secretKey != null); + } + + /** + * Constructs the cache. The cache will delete any unrecognized files from the directory. Hence + * the directory cannot be used to store other files. + * + * @param cacheDir A dedicated cache directory. + * @param evictor The evictor to be used. + * @param secretKey If not null, cache keys will be stored encrypted on filesystem using AES/CBC. + * The key must be 16 bytes long. + * @param encrypt When false, a plaintext index will be written. + */ + public SimpleCache(File cacheDir, CacheEvictor evictor, byte[] secretKey, boolean encrypt) { this.cacheDir = cacheDir; this.evictor = evictor; this.lockedSpans = new HashMap<>(); - this.index = new CachedContentIndex(cacheDir, secretKey); + this.index = new CachedContentIndex(cacheDir, secretKey, encrypt); this.listeners = new HashMap<>(); // Start cache initialization. final ConditionVariable conditionVariable = new ConditionVariable(); From 08d16c55d4713eb5865c05fff890f4a5770b0376 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 14 Aug 2017 13:20:23 -0700 Subject: [PATCH 0251/2472] Some Leanback extension + minVersion bump fixes ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=165219604 --- .../ByteArrayUploadDataProviderTest.java | 4 - extensions/leanback/README.md | 2 +- extensions/leanback/build.gradle | 2 +- .../ext/leanback/LeanbackPlayerAdapter.java | 92 +++++++------------ .../android/exoplayer2/ui/DefaultTimeBar.java | 1 - 5 files changed, 36 insertions(+), 65 deletions(-) diff --git a/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/ByteArrayUploadDataProviderTest.java b/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/ByteArrayUploadDataProviderTest.java index 203fd5e21c..a65bb0951b 100644 --- a/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/ByteArrayUploadDataProviderTest.java +++ b/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/ByteArrayUploadDataProviderTest.java @@ -21,11 +21,8 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.MockitoAnnotations.initMocks; -import android.annotation.TargetApi; -import android.os.Build.VERSION_CODES; import android.support.test.InstrumentationRegistry; import android.support.test.runner.AndroidJUnit4; - import java.io.IOException; import java.nio.ByteBuffer; import java.util.Arrays; @@ -67,7 +64,6 @@ public final class ByteArrayUploadDataProviderTest { assertArrayEquals(TEST_DATA, byteBuffer.array()); } - @TargetApi(VERSION_CODES.GINGERBREAD) @Test public void testReadPartialBuffer() throws IOException { byte[] firstHalf = Arrays.copyOfRange(TEST_DATA, 0, TEST_DATA.length / 2); diff --git a/extensions/leanback/README.md b/extensions/leanback/README.md index 2326887724..0fb3846f7e 100644 --- a/extensions/leanback/README.md +++ b/extensions/leanback/README.md @@ -3,7 +3,7 @@ ## Description ## This [Leanback][] Extension provides a [PlayerAdapter][] implementation for -Exoplayer. +ExoPlayer. [PlayerAdapter]: https://developer.android.com/reference/android/support/v17/leanback/media/PlayerAdapter.html [Leanback]: https://developer.android.com/reference/android/support/v17/leanback/package-summary.html diff --git a/extensions/leanback/build.gradle b/extensions/leanback/build.gradle index eaf58cc990..715e2e56d7 100644 --- a/extensions/leanback/build.gradle +++ b/extensions/leanback/build.gradle @@ -19,7 +19,7 @@ android { buildToolsVersion project.ext.buildToolsVersion defaultConfig { - minSdkVersion project.ext.minSdkVersion + minSdkVersion 17 targetSdkVersion project.ext.targetSdkVersion } } diff --git a/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java b/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java index 58c8ee973d..df77a737b8 100644 --- a/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java +++ b/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java @@ -22,12 +22,14 @@ import android.support.v17.leanback.media.PlaybackGlueHost; import android.support.v17.leanback.media.PlayerAdapter; import android.support.v17.leanback.media.SurfaceHolderGlueHost; import android.util.Pair; +import android.view.Surface; import android.view.SurfaceHolder; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.TrackGroupArray; @@ -39,6 +41,10 @@ import com.google.android.exoplayer2.util.ErrorMessageProvider; */ public final class LeanbackPlayerAdapter extends PlayerAdapter { + static { + ExoPlayerLibraryInfo.registerModule("goog.exo.leanback"); + } + private final Context context; private final SimpleExoPlayer player; private final Handler handler; @@ -53,7 +59,7 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { private SurfaceHolderGlueHost surfaceHolderGlueHost; private boolean initialized; - private boolean hasDisplay; + private boolean hasSurface; private boolean isBuffering; private ErrorMessageProvider errorMessageProvider; private final int updatePeriod; @@ -70,10 +76,6 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { } }; - static { - ExoPlayerLibraryInfo.registerModule("goog.exo.leanback"); - } - /** * Constructor. * Users are responsible for managing {@link SimpleExoPlayer} lifecycle. You must @@ -92,32 +94,28 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { @Override public void onAttachedToHost(PlaybackGlueHost host) { if (host instanceof SurfaceHolderGlueHost) { - surfaceHolderGlueHost = ((SurfaceHolderGlueHost) host); - surfaceHolderGlueHost.setSurfaceHolderCallback(new VideoPlayerSurfaceHolderCallback()); + surfaceHolderGlueHost = ((SurfaceHolderGlueHost) host); + surfaceHolderGlueHost.setSurfaceHolderCallback(new VideoPlayerSurfaceHolderCallback()); } - initializePlayer(); - } - - private void initializePlayer() { notifyListeners(); - this.player.addListener(exoPlayerListener); - this.player.setVideoListener(videoListener); + player.addListener(exoPlayerListener); + player.addVideoListener(videoListener); } private void notifyListeners() { boolean oldIsPrepared = isPrepared(); int playbackState = player.getPlaybackState(); - boolean isInitialized = playbackState != ExoPlayer.STATE_IDLE; - isBuffering = playbackState == ExoPlayer.STATE_BUFFERING; - boolean hasEnded = playbackState == ExoPlayer.STATE_ENDED; + boolean isInitialized = playbackState != Player.STATE_IDLE; + isBuffering = playbackState == Player.STATE_BUFFERING; + boolean hasEnded = playbackState == Player.STATE_ENDED; initialized = isInitialized; if (oldIsPrepared != isPrepared()) { - getCallback().onPreparedStateChanged(LeanbackPlayerAdapter.this); + getCallback().onPreparedStateChanged(this); } getCallback().onPlayStateChanged(this); - notifyBufferingState(); + getCallback().onBufferingStateChanged(this, isBuffering || !initialized); if (hasEnded) { getCallback().onPlayCompleted(this); @@ -134,37 +132,20 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { this.errorMessageProvider = errorMessageProvider; } - private void uninitializePlayer() { - if (initialized) { - initialized = false; - notifyBufferingState(); - if (hasDisplay) { - getCallback().onPlayStateChanged(LeanbackPlayerAdapter.this); - getCallback().onPreparedStateChanged(LeanbackPlayerAdapter.this); - } - - player.removeListener(exoPlayerListener); - player.clearVideoListener(videoListener); - } - } - - /** - * Notify the state of buffering. For example, an app may enable/disable a loading figure - * according to the state of buffering. - */ - private void notifyBufferingState() { - getCallback().onBufferingStateChanged(LeanbackPlayerAdapter.this, - isBuffering || !initialized); - } - @Override public void onDetachedFromHost() { + player.removeListener(exoPlayerListener); + player.removeVideoListener(videoListener); if (surfaceHolderGlueHost != null) { surfaceHolderGlueHost.setSurfaceHolderCallback(null); surfaceHolderGlueHost = null; } - uninitializePlayer(); - hasDisplay = false; + initialized = false; + hasSurface = false; + Callback callback = getCallback(); + callback.onBufferingStateChanged(this, false); + callback.onPlayStateChanged(this); + callback.onPreparedStateChanged(this); } @Override @@ -194,7 +175,7 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { @Override public void play() { - if (player.getPlaybackState() == ExoPlayer.STATE_ENDED) { + if (player.getPlaybackState() == Player.STATE_ENDED) { seekTo(0); } player.setPlayWhenReady(true); @@ -208,8 +189,8 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { } @Override - public void seekTo(long newPosition) { - player.seekTo(newPosition); + public void seekTo(long positionMs) { + player.seekTo(positionMs); } @Override @@ -223,26 +204,22 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { */ @Override public boolean isPrepared() { - return initialized && (surfaceHolderGlueHost == null || hasDisplay); + return initialized && (surfaceHolderGlueHost == null || hasSurface); } - /** - * @see SimpleExoPlayer#setVideoSurfaceHolder(SurfaceHolder) - */ - private void setDisplay(SurfaceHolder surfaceHolder) { - hasDisplay = surfaceHolder != null; - player.setVideoSurface(surfaceHolder.getSurface()); + private void setVideoSurface(Surface surface) { + hasSurface = surface != null; + player.setVideoSurface(surface); getCallback().onPreparedStateChanged(this); } /** - * Implements {@link SurfaceHolder.Callback} that can then be set on the - * {@link PlaybackGlueHost}. + * Implements {@link SurfaceHolder.Callback} that can then be set on the {@link PlaybackGlueHost}. */ private final class VideoPlayerSurfaceHolderCallback implements SurfaceHolder.Callback { @Override public void surfaceCreated(SurfaceHolder surfaceHolder) { - setDisplay(surfaceHolder); + setVideoSurface(surfaceHolder.getSurface()); } @Override @@ -251,7 +228,7 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { @Override public void surfaceDestroyed(SurfaceHolder surfaceHolder) { - setDisplay(null); + setVideoSurface(null); } } @@ -264,7 +241,6 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { @Override public void onPlayerError(ExoPlaybackException exception) { - String errMsg = ""; if (errorMessageProvider != null) { Pair message = errorMessageProvider.getErrorMessage(exception); if (message != null) { 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 index 523c7fd73d..8fe8dbfa5d 100644 --- 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 @@ -378,7 +378,6 @@ public class DefaultTimeBar extends View implements TimeBar { super.onSizeChanged(width, height, oldWidth, oldHeight); } - @TargetApi(14) @Override public void onInitializeAccessibilityEvent(AccessibilityEvent event) { super.onInitializeAccessibilityEvent(event); From 00547c578345676bc26fdfe1d84339adfd7c067e Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 15 Aug 2017 04:06:29 -0700 Subject: [PATCH 0252/2472] Disable secure dummy surface on all Samsung N devices ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=165291627 --- .../google/android/exoplayer2/video/DummySurface.java | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java index 7a80294929..a45616c6ed 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java @@ -42,7 +42,6 @@ import static android.opengl.GLES20.glGenTextures; import android.annotation.TargetApi; import android.content.Context; -import android.content.pm.PackageManager; import android.graphics.SurfaceTexture; import android.graphics.SurfaceTexture.OnFrameAvailableListener; import android.opengl.EGL14; @@ -152,15 +151,9 @@ public final class DummySurface extends Surface { * * @param context Any {@link Context}. */ + @SuppressWarnings("unused") // Context may be needed in the future for better targeting. private static boolean deviceNeedsSecureDummySurfaceWorkaround(Context context) { - return Util.SDK_INT == 24 - && "samsung".equals(Util.MANUFACTURER) - && !hasVrModeHighPerformanceSystemFeatureV24(context.getPackageManager()); - } - - @TargetApi(24) - private static boolean hasVrModeHighPerformanceSystemFeatureV24(PackageManager packageManager) { - return packageManager.hasSystemFeature(PackageManager.FEATURE_VR_MODE_HIGH_PERFORMANCE); + return Util.SDK_INT == 24 && "samsung".equals(Util.MANUFACTURER); } private static class DummySurfaceThread extends HandlerThread implements OnFrameAvailableListener, From 757bcf7c6346a4c8757b1d3a462bc5a84a21123c Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 15 Aug 2017 04:14:24 -0700 Subject: [PATCH 0253/2472] Update instructions to include Google Maven repository ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=165291982 --- README.md | 11 +++++++---- extensions/gvr/README.md | 12 +----------- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index d7bc23f700..f4dd9b69ec 100644 --- a/README.md +++ b/README.md @@ -20,18 +20,21 @@ and extend, and can be updated through Play Store application updates. ## Using ExoPlayer ## -ExoPlayer modules can be obtained via jCenter. It's also possible to clone the +ExoPlayer modules can be obtained via JCenter. It's also possible to clone the repository and depend on the modules locally. -### Via jCenter ### +### Via JCenter ### The easiest way to get started using ExoPlayer is to add it as a gradle -dependency. You need to make sure you have the jcenter repository included in -the `build.gradle` file in the root of your project: +dependency. You need to make sure you have the JCenter and Google Maven +repositories included in the `build.gradle` file in the root of your project: ```gradle repositories { jcenter() + maven { + url "https://maven.google.com" + } } ``` diff --git a/extensions/gvr/README.md b/extensions/gvr/README.md index 7e072d070c..4e08ee6387 100644 --- a/extensions/gvr/README.md +++ b/extensions/gvr/README.md @@ -11,17 +11,7 @@ of surround sound and ambisonic soundfields. ## Getting the extension ## -The easiest way to use the extension is to add it as a gradle dependency. You -need to make sure you have the jcenter repository included in the `build.gradle` -file in the root of your project: - -```gradle -repositories { - jcenter() -} -``` - -Next, include the following in your module's `build.gradle` file: +The easiest way to use the extension is to add it as a gradle dependency: ```gradle compile 'com.google.android.exoplayer:extension-gvr:rX.X.X' From f8e47553e8c34c2105b0865f33918f300d37a823 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 15 Aug 2017 04:45:22 -0700 Subject: [PATCH 0254/2472] Destroy GL context when releasing dummy surface ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=165293386 --- .../exoplayer2/video/DummySurface.java | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java index a45616c6ed..a1820ed7a1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java @@ -34,6 +34,7 @@ import static android.opengl.EGL14.EGL_WINDOW_BIT; import static android.opengl.EGL14.eglChooseConfig; import static android.opengl.EGL14.eglCreateContext; import static android.opengl.EGL14.eglCreatePbufferSurface; +import static android.opengl.EGL14.eglDestroyContext; import static android.opengl.EGL14.eglGetDisplay; import static android.opengl.EGL14.eglInitialize; import static android.opengl.EGL14.eglMakeCurrent; @@ -164,6 +165,8 @@ public final class DummySurface extends Surface { private static final int MSG_RELEASE = 3; private final int[] textureIdHolder; + private EGLContext context; + private EGLDisplay display; private Handler handler; private SurfaceTexture surfaceTexture; @@ -248,7 +251,7 @@ public final class DummySurface extends Surface { } private void initInternal(boolean secure) { - EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY); + display = eglGetDisplay(EGL_DEFAULT_DISPLAY); Assertions.checkState(display != null, "eglGetDisplay failed"); int[] version = new int[2]; @@ -285,8 +288,8 @@ public final class DummySurface extends Surface { EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE}; } - EGLContext context = eglCreateContext(display, config, android.opengl.EGL14.EGL_NO_CONTEXT, - glAttributes, 0); + context = eglCreateContext(display, config, android.opengl.EGL14.EGL_NO_CONTEXT, glAttributes, + 0); Assertions.checkState(context != null, "eglCreateContext failed"); int[] pbufferAttributes; @@ -316,11 +319,18 @@ public final class DummySurface extends Surface { private void releaseInternal() { try { - surfaceTexture.release(); + if (surfaceTexture != null) { + surfaceTexture.release(); + glDeleteTextures(1, textureIdHolder, 0); + } } finally { + if (context != null) { + eglDestroyContext(display, context); + } + display = null; + context = null; surface = null; surfaceTexture = null; - glDeleteTextures(1, textureIdHolder, 0); } } From c94bce17b4d8c39046d81c7ec4aa16a9a8f43b75 Mon Sep 17 00:00:00 2001 From: olly Date: Sun, 16 Jul 2017 05:14:19 +0100 Subject: [PATCH 0255/2472] Make data sources private in SegmentDownloader Also fix SegmentDownloader.remove to not return early ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=165295432 --- .../com/google/android/exoplayer2/source/dash/DashUtil.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/library/dash/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 index 83cb10b99c..59fbfb18fe 100644 --- a/library/dash/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 @@ -111,7 +111,8 @@ public final class DashUtil { * * @param dataSource The source from which the data should be loaded. * @param representation The representation which initialization chunk belongs to. - * @return {@link ChunkIndex} of the given representation. + * @return The {@link ChunkIndex} of the given representation, or null if no initialization or + * index data exists. * @throws IOException Thrown when there is an error while loading. * @throws InterruptedException Thrown if the thread was interrupted. */ From 4b706d948471c9d2e2794d4e780f3de9ac207796 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 15 Aug 2017 05:23:15 -0700 Subject: [PATCH 0256/2472] Add SsManifest.copy and TrackKey for SmoothStreaming downloads ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=165295985 --- .../manifest/SsManifestTest.java | 128 ++++++++++++++++++ .../smoothstreaming/manifest/SsManifest.java | 86 ++++++++++-- 2 files changed, 205 insertions(+), 9 deletions(-) create mode 100644 library/smoothstreaming/src/androidTest/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestTest.java diff --git a/library/smoothstreaming/src/androidTest/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestTest.java b/library/smoothstreaming/src/androidTest/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestTest.java new file mode 100644 index 0000000000..0a221b6932 --- /dev/null +++ b/library/smoothstreaming/src/androidTest/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestTest.java @@ -0,0 +1,128 @@ +/* + * 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.smoothstreaming.manifest; + +import android.test.MoreAsserts; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest.ProtectionElement; +import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest.StreamElement; +import com.google.android.exoplayer2.util.MimeTypes; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Random; +import junit.framework.TestCase; + +/** + * Unit tests for {@link SsManifest}. + */ +public class SsManifestTest extends TestCase { + + private static ProtectionElement DUMMY_PROTECTION_ELEMENT = + new ProtectionElement(C.WIDEVINE_UUID, new byte[] {0, 1, 2}); + + public void testCopy() throws Exception { + Format[][] formats = newFormats(2, 3); + SsManifest sourceManifest = newSsManifest( + newStreamElement("1",formats[0]), + newStreamElement("2", formats[1])); + + List keys = Arrays.asList( + new TrackKey(0, 0), + new TrackKey(0, 2), + new TrackKey(1, 0)); + // Keys don't need to be in any particular order + Collections.shuffle(keys, new Random(0)); + + SsManifest copyManifest = sourceManifest.copy(keys); + + SsManifest expectedManifest = newSsManifest( + newStreamElement("1", formats[0][0], formats[0][2]), + newStreamElement("2", formats[1][0])); + assertManifestEquals(expectedManifest, copyManifest); + } + + public void testCopyRemoveStreamElement() throws Exception { + Format[][] formats = newFormats(2, 3); + SsManifest sourceManifest = newSsManifest( + newStreamElement("1", formats[0]), + newStreamElement("2", formats[1])); + + List keys = Arrays.asList( + new TrackKey(1, 0)); + // Keys don't need to be in any particular order + Collections.shuffle(keys, new Random(0)); + + SsManifest copyManifest = sourceManifest.copy(keys); + + SsManifest expectedManifest = newSsManifest( + newStreamElement("2", formats[1][0])); + assertManifestEquals(expectedManifest, copyManifest); + } + + private static void assertManifestEquals(SsManifest expected, SsManifest actual) { + assertEquals(expected.durationUs, actual.durationUs); + assertEquals(expected.dvrWindowLengthUs, actual.dvrWindowLengthUs); + assertEquals(expected.isLive, actual.isLive); + assertEquals(expected.lookAheadCount, actual.lookAheadCount); + assertEquals(expected.majorVersion, actual.majorVersion); + assertEquals(expected.minorVersion, actual.minorVersion); + assertEquals(expected.protectionElement.uuid, actual.protectionElement.uuid); + assertEquals(expected.protectionElement, actual.protectionElement); + for (int i = 0; i < expected.streamElements.length; i++) { + StreamElement expectedStreamElement = expected.streamElements[i]; + StreamElement actualStreamElement = actual.streamElements[i]; + assertEquals(expectedStreamElement.chunkCount, actualStreamElement.chunkCount); + assertEquals(expectedStreamElement.displayHeight, actualStreamElement.displayHeight); + assertEquals(expectedStreamElement.displayWidth, actualStreamElement.displayWidth); + assertEquals(expectedStreamElement.language, actualStreamElement.language); + assertEquals(expectedStreamElement.maxHeight, actualStreamElement.maxHeight); + assertEquals(expectedStreamElement.maxWidth, actualStreamElement.maxWidth); + assertEquals(expectedStreamElement.name, actualStreamElement.name); + assertEquals(expectedStreamElement.subType, actualStreamElement.subType); + assertEquals(expectedStreamElement.timescale, actualStreamElement.timescale); + assertEquals(expectedStreamElement.type, actualStreamElement.type); + MoreAsserts.assertEquals(expectedStreamElement.formats, actualStreamElement.formats); + } + } + + private static Format[][] newFormats(int streamElementCount, int trackCounts) { + Format[][] formats = new Format[streamElementCount][]; + for (int i = 0; i < streamElementCount; i++) { + formats[i] = new Format[trackCounts]; + for (int j = 0; j < trackCounts; j++) { + formats[i][j] = newFormat(i + "." + j); + } + } + return formats; + } + + private static SsManifest newSsManifest(StreamElement... streamElements) { + return new SsManifest(1, 2, 1000, 5000, 0, 0, false, DUMMY_PROTECTION_ELEMENT, streamElements); + } + + private static StreamElement newStreamElement(String name, Format... formats) { + return new StreamElement("baseUri", "chunkTemplate", C.TRACK_TYPE_VIDEO, "subType", + 1000, name, 1024, 768, 1024, 768, null, formats, Collections.emptyList(), 0); + } + + private static Format newFormat(String id) { + return Format.createContainerFormat(id, MimeTypes.VIDEO_MP4, MimeTypes.VIDEO_H264, null, + Format.NO_VALUE, 0, null); + } + +} diff --git a/library/smoothstreaming/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 index 1bb877eb59..fbc3726a0e 100644 --- a/library/smoothstreaming/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 @@ -21,6 +21,9 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.UriUtil; import com.google.android.exoplayer2.util.Util; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedList; import java.util.List; import java.util.UUID; @@ -96,16 +99,60 @@ public class SsManifest { public SsManifest(int majorVersion, int minorVersion, long timescale, long duration, long dvrWindowLength, int lookAheadCount, boolean isLive, ProtectionElement protectionElement, StreamElement[] streamElements) { + this(majorVersion, minorVersion, + duration == 0 ? C.TIME_UNSET + : Util.scaleLargeTimestamp(duration, C.MICROS_PER_SECOND, timescale), + dvrWindowLength == 0 ? C.TIME_UNSET + : Util.scaleLargeTimestamp(dvrWindowLength, C.MICROS_PER_SECOND, timescale), + lookAheadCount, isLive, protectionElement, streamElements); + } + + private SsManifest(int majorVersion, int minorVersion, long durationUs, long dvrWindowLengthUs, + int lookAheadCount, boolean isLive, ProtectionElement protectionElement, + StreamElement[] streamElements) { this.majorVersion = majorVersion; this.minorVersion = minorVersion; + this.durationUs = durationUs; + this.dvrWindowLengthUs = dvrWindowLengthUs; this.lookAheadCount = lookAheadCount; this.isLive = isLive; this.protectionElement = protectionElement; this.streamElements = streamElements; - dvrWindowLengthUs = dvrWindowLength == 0 ? C.TIME_UNSET - : Util.scaleLargeTimestamp(dvrWindowLength, C.MICROS_PER_SECOND, timescale); - durationUs = duration == 0 ? C.TIME_UNSET - : Util.scaleLargeTimestamp(duration, C.MICROS_PER_SECOND, timescale); + } + + /** + * Creates a copy of this manifest which includes only the tracks identified by the given keys. + * + * @param trackKeys List of keys for the tracks to be included in the copy. + * @return A copy of this manifest with the selected tracks. + * @throws IndexOutOfBoundsException If a key has an invalid index. + */ + public final SsManifest copy(List trackKeys) { + LinkedList sortedKeys = new LinkedList<>(trackKeys); + Collections.sort(sortedKeys); + + StreamElement currentStreamElement = null; + List copiedStreamElements = new ArrayList<>(); + List copiedFormats = new ArrayList<>(); + for (int i = 0; i < sortedKeys.size(); i++) { + TrackKey key = sortedKeys.get(i); + StreamElement streamElement = streamElements[key.streamElementIndex]; + if (streamElement != currentStreamElement && currentStreamElement != null) { + // We're advancing to a new stream element. Add the current one. + copiedStreamElements.add(currentStreamElement.copy(copiedFormats.toArray(new Format[0]))); + copiedFormats.clear(); + } + currentStreamElement = streamElement; + copiedFormats.add(streamElement.formats[key.trackIndex]); + } + if (currentStreamElement != null) { + // Add the last stream element. + copiedStreamElements.add(currentStreamElement.copy(copiedFormats.toArray(new Format[0]))); + } + + StreamElement[] copiedStreamElementsArray = copiedStreamElements.toArray(new StreamElement[0]); + return new SsManifest(majorVersion, minorVersion, durationUs, dvrWindowLengthUs, lookAheadCount, + isLive, protectionElement, copiedStreamElementsArray); } /** @@ -156,6 +203,16 @@ public class SsManifest { long timescale, String name, int maxWidth, int maxHeight, int displayWidth, int displayHeight, String language, Format[] formats, List chunkStartTimes, long lastChunkDuration) { + this (baseUri, chunkTemplate, type, subType, timescale, name, maxWidth, maxHeight, + displayWidth, displayHeight, language, formats, chunkStartTimes, + Util.scaleLargeTimestamps(chunkStartTimes, C.MICROS_PER_SECOND, timescale), + Util.scaleLargeTimestamp(lastChunkDuration, C.MICROS_PER_SECOND, timescale)); + } + + private StreamElement(String baseUri, String chunkTemplate, int type, String subType, + long timescale, String name, int maxWidth, int maxHeight, int displayWidth, + int displayHeight, String language, Format[] formats, List chunkStartTimes, + long[] chunkStartTimesUs, long lastChunkDurationUs) { this.baseUri = baseUri; this.chunkTemplate = chunkTemplate; this.type = type; @@ -168,12 +225,23 @@ public class SsManifest { this.displayHeight = displayHeight; this.language = language; this.formats = formats; - this.chunkCount = chunkStartTimes.size(); this.chunkStartTimes = chunkStartTimes; - lastChunkDurationUs = - Util.scaleLargeTimestamp(lastChunkDuration, C.MICROS_PER_SECOND, timescale); - chunkStartTimesUs = - Util.scaleLargeTimestamps(chunkStartTimes, C.MICROS_PER_SECOND, timescale); + this.chunkStartTimesUs = chunkStartTimesUs; + this.lastChunkDurationUs = lastChunkDurationUs; + chunkCount = chunkStartTimes.size(); + } + + /** + * Creates a copy of this stream element with the formats replaced with those specified. + * + * @param formats The formats to be included in the copy. + * @return A copy of this stream element with the formats replaced. + * @throws IndexOutOfBoundsException If a key has an invalid index. + */ + public StreamElement copy(Format[] formats) { + return new StreamElement(baseUri, chunkTemplate, type, subType, timescale, name, maxWidth, + maxHeight, displayWidth, displayHeight, language, formats, chunkStartTimes, + chunkStartTimesUs, lastChunkDurationUs); } /** From b9f5cc7256041f869dfef259f6311cfed4d9c49d Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 15 Aug 2017 05:26:13 -0700 Subject: [PATCH 0257/2472] Add missing TrackKey class ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=165296184 --- .../smoothstreaming/manifest/TrackKey.java | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/TrackKey.java diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/TrackKey.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/TrackKey.java new file mode 100644 index 0000000000..ed52e6fa12 --- /dev/null +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/TrackKey.java @@ -0,0 +1,76 @@ +/* + * 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.smoothstreaming.manifest; + +import android.os.Parcel; +import android.os.Parcelable; +import android.support.annotation.NonNull; + +/** + * Uniquely identifies a track in a {@link SsManifest}. + */ +public final class TrackKey implements Parcelable, Comparable { + + public final int streamElementIndex; + public final int trackIndex; + + public TrackKey(int streamElementIndex, int trackIndex) { + this.streamElementIndex = streamElementIndex; + this.trackIndex = trackIndex; + } + + @Override + public String toString() { + return streamElementIndex + "." + trackIndex; + } + + // Parcelable implementation. + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(streamElementIndex); + dest.writeInt(trackIndex); + } + + public static final Creator CREATOR = new Creator() { + @Override + public TrackKey createFromParcel(Parcel in) { + return new TrackKey(in.readInt(), in.readInt()); + } + + @Override + public TrackKey[] newArray(int size) { + return new TrackKey[size]; + } + }; + + // Comparable implementation. + + @Override + public int compareTo(@NonNull TrackKey o) { + int result = streamElementIndex - o.streamElementIndex; + if (result == 0) { + result = trackIndex - o.trackIndex; + } + return result; + } + +} From f72cd2b01484698e8c6ffe6ea47d6dd71d6b1722 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 15 Aug 2017 07:36:28 -0700 Subject: [PATCH 0258/2472] Add SmoothStreaming downloader and download action ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=165305815 --- .../offline/SsDownloadAction.java | 85 ++++++++++++++ .../smoothstreaming/offline/SsDownloader.java | 111 ++++++++++++++++++ 2 files changed, 196 insertions(+) create mode 100644 library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadAction.java create mode 100644 library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadAction.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadAction.java new file mode 100644 index 0000000000..29b6ad1516 --- /dev/null +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadAction.java @@ -0,0 +1,85 @@ +/* + * 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.smoothstreaming.offline; + +import android.net.Uri; +import com.google.android.exoplayer2.offline.DownloadAction; +import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; +import com.google.android.exoplayer2.offline.SegmentDownloadAction; +import com.google.android.exoplayer2.source.smoothstreaming.manifest.TrackKey; +import com.google.android.exoplayer2.util.ClosedSource; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +/** An action to download or remove downloaded SmoothStreaming streams. */ +@ClosedSource(reason = "Not ready yet") +public final class SsDownloadAction extends SegmentDownloadAction { + + public static final Serializer SERIALIZER = new SegmentDownloadActionSerializer() { + + private static final String TYPE = "SsDownloadAction"; + + @Override + public String getType() { + return TYPE; + } + + @Override + protected void writeKey(DataOutputStream output, TrackKey key) throws IOException { + output.writeInt(key.streamElementIndex); + output.writeInt(key.trackIndex); + } + + @Override + protected TrackKey readKey(DataInputStream input) throws IOException { + return new TrackKey(input.readInt(), input.readInt()); + } + + @Override + protected TrackKey[] createKeyArray(int keyCount) { + return new TrackKey[keyCount]; + } + + @Override + protected DownloadAction createDownloadAction(Uri manifestUri, boolean removeAction, + TrackKey[] keys) { + return new SsDownloadAction(manifestUri, removeAction, keys); + } + + }; + + /** @see SegmentDownloadAction#SegmentDownloadAction(Uri, boolean, Object[]) */ + public SsDownloadAction(Uri manifestUri, boolean removeAction, TrackKey... keys) { + super(manifestUri, removeAction, keys); + } + + @Override + public SsDownloader createDownloader(DownloaderConstructorHelper constructorHelper) + throws IOException { + SsDownloader downloader = new SsDownloader(manifestUri, constructorHelper); + if (!isRemoveAction()) { + downloader.selectRepresentations(keys); + } + return downloader; + } + + @Override + public Serializer getSerializer() { + return SERIALIZER; + } + +} diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java new file mode 100644 index 0000000000..fe9c21d855 --- /dev/null +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java @@ -0,0 +1,111 @@ +/* + * 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.smoothstreaming.offline; + +import android.net.Uri; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; +import com.google.android.exoplayer2.offline.SegmentDownloader; +import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest; +import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest.StreamElement; +import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifestParser; +import com.google.android.exoplayer2.source.smoothstreaming.manifest.TrackKey; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.upstream.ParsingLoadable; +import com.google.android.exoplayer2.util.ClosedSource; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * Helper class to download SmoothStreaming streams. + * + *

        Except {@link #getTotalSegments()}, {@link #getDownloadedSegments()} and {@link + * #getDownloadedBytes()}, this class isn't thread safe. + * + *

        Example usage: + * + *

        + * {@code
        + * SimpleCache cache = new SimpleCache(downloadFolder, new NoOpCacheEvictor());
        + * DefaultHttpDataSourceFactory factory = new DefaultHttpDataSourceFactory("ExoPlayer", null);
        + * DownloaderConstructorHelper constructorHelper =
        + *     new DownloaderConstructorHelper(cache, factory);
        + * SsDownloader ssDownloader = new SsDownloader(manifestUrl, constructorHelper);
        + * // Select the first track of the first stream element
        + * ssDownloader.selectRepresentations(new TrackKey[] {new TrackKey(0, 0)});
        + * ssDownloader.download(new ProgressListener() {
        + *   @Override
        + *   public void onDownloadProgress(Downloader downloader, float downloadPercentage,
        + *       long downloadedBytes) {
        + *     // Invoked periodically during the download.
        + *   }
        + * });
        + * // Access downloaded data using CacheDataSource
        + * CacheDataSource cacheDataSource =
        + *     new CacheDataSource(cache, factory.createDataSource(), CacheDataSource.FLAG_BLOCK_ON_CACHE);}
        + * 
        + */ +@ClosedSource(reason = "Not ready yet") +public final class SsDownloader extends SegmentDownloader { + + /** + * @see SegmentDownloader#SegmentDownloader(Uri, DownloaderConstructorHelper) + */ + public SsDownloader(Uri manifestUri, DownloaderConstructorHelper constructorHelper) { + super(manifestUri, constructorHelper); + } + + @Override + public SsManifest getManifest(DataSource dataSource, Uri uri) throws IOException { + DataSpec dataSpec = new DataSpec(uri, + DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH | DataSpec.FLAG_ALLOW_GZIP); + ParsingLoadable loadable = new ParsingLoadable<>(dataSource, dataSpec, + C.DATA_TYPE_MANIFEST, new SsManifestParser()); + loadable.load(); + return loadable.getResult(); + } + + @Override + protected List getAllSegments(DataSource dataSource, SsManifest manifest, + boolean allowIndexLoadErrors) throws InterruptedException, IOException { + ArrayList segments = new ArrayList<>(); + for (int i = 0; i < manifest.streamElements.length; i++) { + StreamElement streamElement = manifest.streamElements[i]; + for (int j = 0; j < streamElement.formats.length; j++) { + segments.addAll(getSegments(dataSource, manifest, new TrackKey[] {new TrackKey(i, j)}, + allowIndexLoadErrors)); + } + } + return segments; + } + + @Override + protected List getSegments(DataSource dataSource, SsManifest manifest, + TrackKey[] keys, boolean allowIndexLoadErrors) throws InterruptedException, IOException { + ArrayList segments = new ArrayList<>(); + for (TrackKey key : keys) { + StreamElement streamElement = manifest.streamElements[key.streamElementIndex]; + for (int i = 0; i < streamElement.chunkCount; i++) { + segments.add(new Segment(streamElement.getStartTimeUs(i), + new DataSpec(streamElement.buildRequestUri(key.trackIndex, i)))); + } + } + return segments; + } + +} From e0b69b8115290b884f1f2f56eae1571449cfc620 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 15 Aug 2017 09:04:00 -0700 Subject: [PATCH 0259/2472] Add Format.copyWithRotationDegrees ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=165314250 --- .../com/google/android/exoplayer2/Format.java | 68 +++++++++++-------- 1 file changed, 38 insertions(+), 30 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Format.java b/library/core/src/main/java/com/google/android/exoplayer2/Format.java index 4e387ac7ce..c6be2e2eba 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Format.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Format.java @@ -428,28 +428,28 @@ 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, colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay, - encoderPadding, selectionFlags, language, accessibilityChannel, subsampleOffsetUs, - initializationData, drmInitData, metadata); + return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width, + height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, 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, colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay, - encoderPadding, selectionFlags, language, accessibilityChannel, subsampleOffsetUs, - initializationData, drmInitData, metadata); + return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width, + height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, 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, colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay, - encoderPadding, selectionFlags, language, accessibilityChannel, subsampleOffsetUs, - initializationData, drmInitData, metadata); + return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width, + height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, + colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, + selectionFlags, language, accessibilityChannel, subsampleOffsetUs, initializationData, + drmInitData, metadata); } @SuppressWarnings("ReferenceEquality") @@ -474,27 +474,35 @@ public final class Format implements Parcelable { } public Format copyWithGaplessInfo(int encoderDelay, int encoderPadding) { - return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, - width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, - stereoMode, colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay, - encoderPadding, selectionFlags, language, accessibilityChannel, subsampleOffsetUs, - initializationData, drmInitData, metadata); + return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width, + height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, 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, colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay, - encoderPadding, selectionFlags, language, accessibilityChannel, subsampleOffsetUs, - initializationData, drmInitData, metadata); + return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width, + height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, 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, colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay, - encoderPadding, selectionFlags, language, accessibilityChannel, subsampleOffsetUs, - initializationData, drmInitData, metadata); + return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width, + height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, + colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, + selectionFlags, language, accessibilityChannel, subsampleOffsetUs, initializationData, + drmInitData, metadata); + } + + public Format copyWithRotationDegrees(int rotationDegrees) { + return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width, + height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, + colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, + selectionFlags, language, accessibilityChannel, subsampleOffsetUs, initializationData, + drmInitData, metadata); } /** From bbc3b182bbd3bdfeeeb758c67d030b7e73e52615 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 16 Aug 2017 04:47:51 -0700 Subject: [PATCH 0260/2472] Restore the interrupted flag after blocking operations If the main thread was interrupted during ExoPlayerImplInternal.blockingSendMessage/release, the interrupted flag was immediately set but then wait() was called on the next iteration. wait() would immediately throw InterruptedException, causing the main thread to spin until the blocking operation completed. Instead of resetting the flag immediately, reset it after the blocking operation completes. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=165426493 --- .../android/exoplayer2/ExoPlayerImplInternal.java | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index a789dbc1b2..b8274126b5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -263,13 +263,18 @@ import java.io.IOException; } int messageNumber = customMessagesSent++; handler.obtainMessage(MSG_CUSTOM, messages).sendToTarget(); + boolean wasInterrupted = false; while (customMessagesProcessed <= messageNumber) { try { wait(); } catch (InterruptedException e) { - Thread.currentThread().interrupt(); + wasInterrupted = true; } } + if (wasInterrupted) { + // Restore the interrupted status. + Thread.currentThread().interrupt(); + } } public synchronized void release() { @@ -277,13 +282,18 @@ import java.io.IOException; return; } handler.sendEmptyMessage(MSG_RELEASE); + boolean wasInterrupted = false; while (!released) { try { wait(); } catch (InterruptedException e) { - Thread.currentThread().interrupt(); + wasInterrupted = true; } } + if (wasInterrupted) { + // Restore the interrupted status. + Thread.currentThread().interrupt(); + } internalPlaybackThread.quit(); } From 8e45bd27df2b37be2b3ac9cfda3774698e497a0a Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 16 Aug 2017 07:29:11 -0700 Subject: [PATCH 0261/2472] Add nullable annotations to Renderer constructors ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=165437929 --- .../exoplayer2/DefaultRenderersFactory.java | 29 ++++++++++--------- .../android/exoplayer2/ExoPlayerFactory.java | 8 +++-- .../audio/AudioRendererEventListener.java | 8 +++-- .../android/exoplayer2/audio/AudioTrack.java | 5 ++-- .../audio/MediaCodecAudioRenderer.java | 21 +++++++------- .../mediacodec/MediaCodecRenderer.java | 6 ++-- .../video/MediaCodecVideoRenderer.java | 12 ++++---- .../video/VideoRendererEventListener.java | 8 +++-- 8 files changed, 54 insertions(+), 43 deletions(-) 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 index 3e7cb8a68b..27852f0c15 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java @@ -19,6 +19,7 @@ import android.content.Context; import android.os.Handler; import android.os.Looper; import android.support.annotation.IntDef; +import android.support.annotation.Nullable; import android.util.Log; import com.google.android.exoplayer2.audio.AudioCapabilities; import com.google.android.exoplayer2.audio.AudioProcessor; @@ -79,7 +80,7 @@ public class DefaultRenderersFactory implements RenderersFactory { protected static final int MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY = 50; private final Context context; - private final DrmSessionManager drmSessionManager; + @Nullable private final DrmSessionManager drmSessionManager; private final @ExtensionRendererMode int extensionRendererMode; private final long allowedVideoJoiningTimeMs; @@ -96,29 +97,28 @@ public class DefaultRenderersFactory implements RenderersFactory { * playbacks are not required. */ public DefaultRenderersFactory(Context context, - DrmSessionManager drmSessionManager) { + @Nullable 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.. + * 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, + @Nullable DrmSessionManager drmSessionManager, @ExtensionRendererMode int extensionRendererMode) { - this(context, drmSessionManager, extensionRendererMode, - DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS); + 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.. + * 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. @@ -126,7 +126,7 @@ public class DefaultRenderersFactory implements RenderersFactory { * to seamlessly join an ongoing playback. */ public DefaultRenderersFactory(Context context, - DrmSessionManager drmSessionManager, + @Nullable DrmSessionManager drmSessionManager, @ExtensionRendererMode int extensionRendererMode, long allowedVideoJoiningTimeMs) { this.context = context; this.drmSessionManager = drmSessionManager; @@ -137,8 +137,8 @@ public class DefaultRenderersFactory implements RenderersFactory { @Override public Renderer[] createRenderers(Handler eventHandler, VideoRendererEventListener videoRendererEventListener, - AudioRendererEventListener audioRendererEventListener, - TextRenderer.Output textRendererOutput, MetadataRenderer.Output metadataRendererOutput) { + AudioRendererEventListener audioRendererEventListener, TextRenderer.Output textRendererOutput, + MetadataRenderer.Output metadataRendererOutput) { ArrayList renderersList = new ArrayList<>(); buildVideoRenderers(context, drmSessionManager, allowedVideoJoiningTimeMs, eventHandler, videoRendererEventListener, extensionRendererMode, renderersList); @@ -166,9 +166,10 @@ public class DefaultRenderersFactory implements RenderersFactory { * @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) { + @Nullable 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)); @@ -211,7 +212,7 @@ public class DefaultRenderersFactory implements RenderersFactory { * @param out An array to which the built renderers should be appended. */ protected void buildAudioRenderers(Context context, - DrmSessionManager drmSessionManager, + @Nullable DrmSessionManager drmSessionManager, AudioProcessor[] audioProcessors, Handler eventHandler, AudioRendererEventListener eventListener, @ExtensionRendererMode int extensionRendererMode, ArrayList out) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java index 97a310c3da..b647e541bc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2; import android.content.Context; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.trackselection.TrackSelector; @@ -54,7 +55,8 @@ public final class ExoPlayerFactory { */ @Deprecated public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector, - LoadControl loadControl, DrmSessionManager drmSessionManager) { + LoadControl loadControl, + @Nullable DrmSessionManager drmSessionManager) { RenderersFactory renderersFactory = new DefaultRenderersFactory(context, drmSessionManager); return newSimpleInstance(renderersFactory, trackSelector, loadControl); } @@ -74,7 +76,7 @@ public final class ExoPlayerFactory { */ @Deprecated public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector, - LoadControl loadControl, DrmSessionManager drmSessionManager, + LoadControl loadControl, @Nullable DrmSessionManager drmSessionManager, @DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode) { RenderersFactory renderersFactory = new DefaultRenderersFactory(context, drmSessionManager, extensionRendererMode); @@ -98,7 +100,7 @@ public final class ExoPlayerFactory { */ @Deprecated public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector, - LoadControl loadControl, DrmSessionManager drmSessionManager, + LoadControl loadControl, @Nullable DrmSessionManager drmSessionManager, @DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode, long allowedVideoJoiningTimeMs) { RenderersFactory renderersFactory = new DefaultRenderersFactory(context, drmSessionManager, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioRendererEventListener.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioRendererEventListener.java index 612018917b..5f9f599f01 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioRendererEventListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioRendererEventListener.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.audio; import android.os.Handler; import android.os.SystemClock; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Renderer; @@ -84,15 +85,16 @@ public interface AudioRendererEventListener { */ final class EventDispatcher { - private final Handler handler; - private final AudioRendererEventListener listener; + @Nullable private final Handler handler; + @Nullable private final AudioRendererEventListener listener; /** * @param handler A handler for dispatching events, or null if creating a dummy instance. * @param listener The listener to which events should be dispatched, or null if creating a * dummy instance. */ - public EventDispatcher(Handler handler, AudioRendererEventListener listener) { + public EventDispatcher(@Nullable Handler handler, + @Nullable AudioRendererEventListener listener) { this.handler = listener != null ? Assertions.checkNotNull(handler) : null; this.listener = listener; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java index 79cb26bf39..d7ebd69fbf 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java @@ -22,6 +22,7 @@ import android.media.AudioManager; import android.media.AudioTimestamp; import android.os.ConditionVariable; import android.os.SystemClock; +import android.support.annotation.Nullable; import android.util.Log; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.PlaybackParameters; @@ -277,7 +278,7 @@ public final class AudioTrack { */ public static boolean failOnSpuriousAudioTimestamp = false; - private final AudioCapabilities audioCapabilities; + @Nullable private final AudioCapabilities audioCapabilities; private final ChannelMappingAudioProcessor channelMappingAudioProcessor; private final SonicAudioProcessor sonicAudioProcessor; private final AudioProcessor[] availableAudioProcessors; @@ -355,7 +356,7 @@ public final class AudioTrack { * output. May be empty. * @param listener Listener for audio track events. */ - public AudioTrack(AudioCapabilities audioCapabilities, AudioProcessor[] audioProcessors, + public AudioTrack(@Nullable AudioCapabilities audioCapabilities, AudioProcessor[] audioProcessors, Listener listener) { this.audioCapabilities = audioCapabilities; this.listener = listener; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index 4d97c292ac..e146238dcc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java @@ -21,6 +21,7 @@ import android.media.MediaCrypto; import android.media.MediaFormat; import android.media.audiofx.Virtualizer; import android.os.Handler; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; @@ -72,7 +73,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media * has obtained the keys necessary to decrypt encrypted regions of the media. */ public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector, - DrmSessionManager drmSessionManager, + @Nullable DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys) { this(mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, null, null); } @@ -83,8 +84,8 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media * null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. */ - public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector, Handler eventHandler, - AudioRendererEventListener eventListener) { + public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector, + @Nullable Handler eventHandler, @Nullable AudioRendererEventListener eventListener) { this(mediaCodecSelector, null, true, eventHandler, eventListener); } @@ -102,9 +103,9 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media * @param eventListener A listener of events. May be null if delivery of events is not required. */ public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector, - DrmSessionManager drmSessionManager, - boolean playClearSamplesWithoutKeys, Handler eventHandler, - AudioRendererEventListener eventListener) { + @Nullable DrmSessionManager drmSessionManager, + boolean playClearSamplesWithoutKeys, @Nullable Handler eventHandler, + @Nullable AudioRendererEventListener eventListener) { this(mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, eventHandler, eventListener, null); } @@ -127,10 +128,10 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media * output. */ public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector, - DrmSessionManager drmSessionManager, - boolean playClearSamplesWithoutKeys, Handler eventHandler, - AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities, - AudioProcessor... audioProcessors) { + @Nullable DrmSessionManager drmSessionManager, + boolean playClearSamplesWithoutKeys, @Nullable Handler eventHandler, + @Nullable AudioRendererEventListener eventListener, + @Nullable AudioCapabilities audioCapabilities, AudioProcessor... audioProcessors) { super(C.TRACK_TYPE_AUDIO, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys); audioTrack = new AudioTrack(audioCapabilities, audioProcessors, new AudioTrackListener()); eventDispatcher = new EventDispatcher(eventHandler, eventListener); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java index 7c0549de25..31c6a824ef 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java @@ -169,7 +169,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { private static final int ADAPTATION_WORKAROUND_SLICE_WIDTH_HEIGHT = 32; private final MediaCodecSelector mediaCodecSelector; - private final DrmSessionManager drmSessionManager; + @Nullable private final DrmSessionManager drmSessionManager; private final boolean playClearSamplesWithoutKeys; private final DecoderInputBuffer buffer; private final DecoderInputBuffer flagsOnlyBuffer; @@ -223,7 +223,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { * has obtained the keys necessary to decrypt encrypted regions of the media. */ public MediaCodecRenderer(int trackType, MediaCodecSelector mediaCodecSelector, - DrmSessionManager drmSessionManager, + @Nullable DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys) { super(trackType); Assertions.checkState(Util.SDK_INT >= 16); @@ -1090,7 +1090,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { * @param drmInitData {@link DrmInitData} of the format to check for support. * @return Whether the encryption scheme is supported, or true if {@code drmInitData} is null. */ - private static boolean isDrmSchemeSupported(DrmSessionManager drmSessionManager, + private static boolean isDrmSchemeSupported(@Nullable DrmSessionManager drmSessionManager, @Nullable DrmInitData drmInitData) { if (drmInitData == null) { // Content is unencrypted. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index 9a2927cc3f..8fe3476351 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -26,6 +26,7 @@ import android.media.MediaFormat; import android.os.Handler; import android.os.SystemClock; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.util.Log; import android.view.Surface; import com.google.android.exoplayer2.C; @@ -137,8 +138,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { * invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}. */ public MediaCodecVideoRenderer(Context context, MediaCodecSelector mediaCodecSelector, - long allowedJoiningTimeMs, Handler eventHandler, VideoRendererEventListener eventListener, - int maxDroppedFrameCountToNotify) { + long allowedJoiningTimeMs, @Nullable Handler eventHandler, + @Nullable VideoRendererEventListener eventListener, int maxDroppedFrameCountToNotify) { this(context, mediaCodecSelector, allowedJoiningTimeMs, null, false, eventHandler, eventListener, maxDroppedFrameCountToNotify); } @@ -162,9 +163,10 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { * invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}. */ public MediaCodecVideoRenderer(Context context, MediaCodecSelector mediaCodecSelector, - long allowedJoiningTimeMs, DrmSessionManager drmSessionManager, - boolean playClearSamplesWithoutKeys, Handler eventHandler, - VideoRendererEventListener eventListener, int maxDroppedFramesToNotify) { + long allowedJoiningTimeMs, + @Nullable DrmSessionManager drmSessionManager, + boolean playClearSamplesWithoutKeys, @Nullable Handler eventHandler, + @Nullable VideoRendererEventListener eventListener, int maxDroppedFramesToNotify) { super(C.TRACK_TYPE_VIDEO, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys); this.allowedJoiningTimeMs = allowedJoiningTimeMs; this.maxDroppedFramesToNotify = maxDroppedFramesToNotify; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoRendererEventListener.java b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoRendererEventListener.java index 53d6a76b8d..d6ea0ebae2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoRendererEventListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoRendererEventListener.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.video; import android.os.Handler; import android.os.SystemClock; +import android.support.annotation.Nullable; import android.view.Surface; import android.view.TextureView; import com.google.android.exoplayer2.Format; @@ -109,15 +110,16 @@ public interface VideoRendererEventListener { */ final class EventDispatcher { - private final Handler handler; - private final VideoRendererEventListener listener; + @Nullable private final Handler handler; + @Nullable private final VideoRendererEventListener listener; /** * @param handler A handler for dispatching events, or null if creating a dummy instance. * @param listener The listener to which events should be dispatched, or null if creating a * dummy instance. */ - public EventDispatcher(Handler handler, VideoRendererEventListener listener) { + public EventDispatcher(@Nullable Handler handler, + @Nullable VideoRendererEventListener listener) { this.handler = listener != null ? Assertions.checkNotNull(handler) : null; this.listener = listener; } From 978019a1a383eb4c87a3a628597a8c4df35ec433 Mon Sep 17 00:00:00 2001 From: eguven Date: Wed, 16 Aug 2017 08:45:10 -0700 Subject: [PATCH 0262/2472] Add ProgressiveDownloadAction ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=165447436 --- .../ProgressiveDownloadActionTest.java | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/offline/ProgressiveDownloadActionTest.java diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/offline/ProgressiveDownloadActionTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/offline/ProgressiveDownloadActionTest.java new file mode 100644 index 0000000000..b172b7692a --- /dev/null +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/offline/ProgressiveDownloadActionTest.java @@ -0,0 +1,126 @@ +/* + * 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.offline; + +import android.test.InstrumentationTestCase; +import com.google.android.exoplayer2.offline.DownloadAction.Serializer; +import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.upstream.DummyDataSource; +import com.google.android.exoplayer2.upstream.cache.Cache; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import org.mockito.Mockito; + +/** + * Unit tests for {@link ProgressiveDownloadAction}. + */ +public class ProgressiveDownloadActionTest extends InstrumentationTestCase { + + public void testDownloadActionIsNotRemoveAction() throws Exception { + ProgressiveDownloadAction action = new ProgressiveDownloadAction("uri", null, false); + assertFalse(action.isRemoveAction()); + } + + public void testRemoveActionIsRemoveAction() throws Exception { + ProgressiveDownloadAction action2 = new ProgressiveDownloadAction("uri", null, true); + assertTrue(action2.isRemoveAction()); + } + + public void testCreateDownloader() throws Exception { + TestUtil.setUpMockito(this); + ProgressiveDownloadAction action = new ProgressiveDownloadAction("uri", null, false); + DownloaderConstructorHelper constructorHelper = new DownloaderConstructorHelper( + Mockito.mock(Cache.class), DummyDataSource.FACTORY); + assertNotNull(action.createDownloader(constructorHelper)); + } + + public void testSameUriCacheKeyDifferentAction_IsSameMedia() throws Exception { + ProgressiveDownloadAction action1 = new ProgressiveDownloadAction("uri", null, true); + ProgressiveDownloadAction action2 = new ProgressiveDownloadAction("uri", null, false); + assertTrue(action1.isSameMedia(action2)); + } + + public void testNullCacheKeyDifferentUriAction_IsNotSameMedia() throws Exception { + ProgressiveDownloadAction action3 = new ProgressiveDownloadAction("uri2", null, true); + ProgressiveDownloadAction action4 = new ProgressiveDownloadAction("uri", null, false); + assertFalse(action3.isSameMedia(action4)); + } + + public void testSameCacheKeyDifferentUriAction_IsSameMedia() throws Exception { + ProgressiveDownloadAction action5 = new ProgressiveDownloadAction("uri2", "key", true); + ProgressiveDownloadAction action6 = new ProgressiveDownloadAction("uri", "key", false); + assertTrue(action5.isSameMedia(action6)); + } + + public void testSameUriDifferentCacheKeyAction_IsNotSameMedia() throws Exception { + ProgressiveDownloadAction action7 = new ProgressiveDownloadAction("uri", "key", true); + ProgressiveDownloadAction action8 = new ProgressiveDownloadAction("uri", "key2", false); + assertFalse(action7.isSameMedia(action8)); + } + + public void testEquals() throws Exception { + ProgressiveDownloadAction action1 = new ProgressiveDownloadAction("uri", null, true); + assertTrue(action1.equals(action1)); + + ProgressiveDownloadAction action2 = new ProgressiveDownloadAction("uri", null, true); + ProgressiveDownloadAction action3 = new ProgressiveDownloadAction("uri", null, true); + assertTrue(action2.equals(action3)); + + ProgressiveDownloadAction action4 = new ProgressiveDownloadAction("uri", null, true); + ProgressiveDownloadAction action5 = new ProgressiveDownloadAction("uri", null, false); + assertFalse(action4.equals(action5)); + + ProgressiveDownloadAction action6 = new ProgressiveDownloadAction("uri", null, true); + ProgressiveDownloadAction action7 = new ProgressiveDownloadAction("uri", "key", true); + assertFalse(action6.equals(action7)); + + ProgressiveDownloadAction action8 = new ProgressiveDownloadAction("uri", "key2", true); + ProgressiveDownloadAction action9 = new ProgressiveDownloadAction("uri", "key", true); + assertFalse(action8.equals(action9)); + + ProgressiveDownloadAction action10 = new ProgressiveDownloadAction("uri", null, true); + ProgressiveDownloadAction action11 = new ProgressiveDownloadAction("uri2", null, true); + assertFalse(action10.equals(action11)); + } + + public void testSerializerGetType() throws Exception { + ProgressiveDownloadAction action = new ProgressiveDownloadAction("uri", null, false); + Serializer serializer = action.getSerializer(); + assertNotNull(serializer.getType()); + } + + public void testSerializerWriteRead() throws Exception { + doTestSerializationRoundtrip(new ProgressiveDownloadAction("uri1", null, false)); + doTestSerializationRoundtrip(new ProgressiveDownloadAction("uri2", "key", true)); + } + + private void doTestSerializationRoundtrip(ProgressiveDownloadAction action1) throws IOException { + Serializer serializer = action1.getSerializer(); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + DataOutputStream output = new DataOutputStream(out); + serializer.writeToStream(output, action1); + + ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); + DataInputStream input = new DataInputStream(in); + DownloadAction action2 = serializer.readFromStream(input); + + assertEquals(action1, action2); + } + +} From c2d2d967e93a1156e3c4c5e218d5193e2ec99967 Mon Sep 17 00:00:00 2001 From: zhihuichen Date: Wed, 16 Aug 2017 14:38:57 -0700 Subject: [PATCH 0263/2472] Refactor DrmSession part into a separate class to prepare for multi session scenario. NO_SQ=flaky ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=165497666 --- .../exoplayer2/drm/DefaultDrmSession.java | 546 ++++++++++++++++++ .../drm/DefaultDrmSessionManager.java | 453 +-------------- 2 files changed, 561 insertions(+), 438 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java new file mode 100644 index 0000000000..cfb2cf9d8a --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java @@ -0,0 +1,546 @@ +/* + * 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.drm; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.media.DeniedByServerException; +import android.media.MediaDrm; +import android.media.NotProvisionedException; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; +import android.util.Log; +import android.util.Pair; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; +import com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest; +import com.google.android.exoplayer2.drm.ExoMediaDrm.OnEventListener; +import com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest; +import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil; +import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.Util; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * A {@link DrmSession} that supports playbacks using {@link MediaDrm}. + */ +@TargetApi(18) +/* package */ class DefaultDrmSession implements DrmSession { + private static final String TAG = "DefaultDrmSession"; + + private static final String CENC_SCHEME_MIME_TYPE = "cenc"; + + private static final int MSG_PROVISION = 0; + private static final int MSG_KEYS = 1; + + private static final int MAX_LICENSE_DURATION_TO_RENEW = 60; + + private final Handler eventHandler; + private final DefaultDrmSessionManager.EventListener eventListener; + private final ExoMediaDrm mediaDrm; + private final HashMap optionalKeyRequestParameters; + /* package */ final MediaDrmCallback callback; + /* package */ final UUID uuid; + /* package */ MediaDrmHandler mediaDrmHandler; + /* package */ PostResponseHandler postResponseHandler; + private HandlerThread requestHandlerThread; + private Handler postRequestHandler; + + @DefaultDrmSessionManager.Mode + private final int mode; + private int openCount; + private boolean provisioningInProgress; + @DrmSession.State + private int state; + private T mediaCrypto; + private DrmSessionException lastException; + private final byte[] schemeInitData; + private final String schemeMimeType; + private byte[] sessionId; + private byte[] offlineLicenseKeySetId; + + /** + * Instantiates a new DRM session. + * + * @param uuid The UUID of the drm scheme. + * @param mediaDrm The media DRM. + * @param initData The DRM init data. + * @param mode The DRM mode. + * @param offlineLicenseKeySetId The offlineLicense KeySetId. + * @param optionalKeyRequestParameters The optional key request parameters. + * @param callback The media DRM callback. + * @param playbackLooper The playback looper. + * @param eventHandler The handler to post listener events. + * @param eventListener The DRM session manager event listener. + */ + public DefaultDrmSession(UUID uuid, ExoMediaDrm mediaDrm, DrmInitData initData, + @DefaultDrmSessionManager.Mode int mode, byte[] offlineLicenseKeySetId, + HashMap optionalKeyRequestParameters, MediaDrmCallback callback, + Looper playbackLooper, Handler eventHandler, + DefaultDrmSessionManager.EventListener eventListener) { + this.uuid = uuid; + this.mediaDrm = mediaDrm; + this.mode = mode; + this.offlineLicenseKeySetId = offlineLicenseKeySetId; + this.optionalKeyRequestParameters = optionalKeyRequestParameters; + this.callback = callback; + + this.eventHandler = eventHandler; + this.eventListener = eventListener; + state = STATE_OPENING; + + mediaDrmHandler = new MediaDrmHandler(playbackLooper); + mediaDrm.setOnEventListener(new MediaDrmEventListener()); + postResponseHandler = new PostResponseHandler(playbackLooper); + requestHandlerThread = new HandlerThread("DrmRequestHandler"); + requestHandlerThread.start(); + postRequestHandler = new PostRequestHandler(requestHandlerThread.getLooper()); + + // Parse init data. + byte[] schemeInitData = null; + String schemeMimeType = null; + if (offlineLicenseKeySetId == null) { + SchemeData data = getSchemeData(initData, uuid); + if (data == null) { + onError(new IllegalStateException("Media does not support uuid: " + uuid)); + } else { + schemeInitData = data.data; + schemeMimeType = data.mimeType; + if (Util.SDK_INT < 21) { + // Prior to L the Widevine CDM required data to be extracted from the PSSH atom. + byte[] psshData = PsshAtomUtil.parseSchemeSpecificData(schemeInitData, uuid); + if (psshData == null) { + // Extraction failed. schemeData isn't a Widevine PSSH atom, so leave it unchanged. + } else { + schemeInitData = psshData; + } + } + if (Util.SDK_INT < 26 && C.CLEARKEY_UUID.equals(uuid) + && (MimeTypes.VIDEO_MP4.equals(schemeMimeType) + || MimeTypes.AUDIO_MP4.equals(schemeMimeType))) { + // Prior to API level 26 the ClearKey CDM only accepted "cenc" as the scheme for MP4. + schemeMimeType = CENC_SCHEME_MIME_TYPE; + } + } + } + this.schemeInitData = schemeInitData; + this.schemeMimeType = schemeMimeType; + } + + // Life cycle. + + public void acquire() { + if (++openCount == 1) { + if (state == STATE_ERROR) { + return; + } + if (openInternal(true)) { + doLicense(); + } + } + } + + /** + * @return True if the session is closed and cleaned up, false otherwise. + */ + public boolean release() { + if (--openCount == 0) { + state = STATE_RELEASED; + provisioningInProgress = false; + mediaDrmHandler.removeCallbacksAndMessages(null); + mediaDrmHandler = null; + postResponseHandler.removeCallbacksAndMessages(null); + postRequestHandler.removeCallbacksAndMessages(null); + postRequestHandler = null; + requestHandlerThread.quit(); + requestHandlerThread = null; + mediaCrypto = null; + lastException = null; + if (sessionId != null) { + mediaDrm.closeSession(sessionId); + sessionId = null; + } + return true; + } + return false; + } + + // DrmSession Implementation. + + @Override + @DrmSession.State + public final int getState() { + return state; + } + + @Override + public final DrmSessionException getError() { + return state == STATE_ERROR ? lastException : null; + } + + @Override + public final T getMediaCrypto() { + return mediaCrypto; + } + + @Override + public Map queryKeyStatus() { + return sessionId == null ? null : mediaDrm.queryKeyStatus(sessionId); + } + + @Override + public byte[] getOfflineLicenseKeySetId() { + return offlineLicenseKeySetId; + } + + // Internal methods. + + /** + * Try to open a session, do provisioning if necessary. + * @param allowProvisioning if provisioning is allowed, set this to false when calling from + * processing provision response. + * @return true on success, false otherwise. + */ + private boolean openInternal(boolean allowProvisioning) { + if (isOpen()) { + // Already opened + return true; + } + + try { + sessionId = mediaDrm.openSession(); + mediaCrypto = mediaDrm.createMediaCrypto(uuid, sessionId); + state = STATE_OPENED; + return true; + } catch (NotProvisionedException e) { + if (allowProvisioning) { + postProvisionRequest(); + } else { + onError(e); + } + } catch (Exception e) { + // MediaCryptoException + // ResourceBusyException only available on 19+ + onError(e); + } + + return false; + } + + private void postProvisionRequest() { + if (provisioningInProgress) { + return; + } + provisioningInProgress = true; + ProvisionRequest request = mediaDrm.getProvisionRequest(); + postRequestHandler.obtainMessage(MSG_PROVISION, request).sendToTarget(); + } + + private void onProvisionResponse(Object response) { + provisioningInProgress = false; + if (state != STATE_OPENING && !isOpen()) { + // This event is stale. + return; + } + + if (response instanceof Exception) { + onError((Exception) response); + return; + } + + try { + mediaDrm.provideProvisionResponse((byte[]) response); + if (openInternal(false)) { + doLicense(); + } + } catch (DeniedByServerException e) { + onError(e); + } + } + + private void doLicense() { + switch (mode) { + case DefaultDrmSessionManager.MODE_PLAYBACK: + case DefaultDrmSessionManager.MODE_QUERY: + if (offlineLicenseKeySetId == null) { + postKeyRequest(MediaDrm.KEY_TYPE_STREAMING); + } else { + if (restoreKeys()) { + long licenseDurationRemainingSec = getLicenseDurationRemainingSec(); + if (mode == DefaultDrmSessionManager.MODE_PLAYBACK + && licenseDurationRemainingSec <= MAX_LICENSE_DURATION_TO_RENEW) { + Log.d(TAG, "Offline license has expired or will expire soon. " + + "Remaining seconds: " + licenseDurationRemainingSec); + postKeyRequest(MediaDrm.KEY_TYPE_OFFLINE); + } else if (licenseDurationRemainingSec <= 0) { + onError(new KeysExpiredException()); + } else { + state = STATE_OPENED_WITH_KEYS; + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onDrmKeysRestored(); + } + }); + } + } + } + } + break; + case DefaultDrmSessionManager.MODE_DOWNLOAD: + if (offlineLicenseKeySetId == null) { + postKeyRequest(MediaDrm.KEY_TYPE_OFFLINE); + } else { + // Renew + if (restoreKeys()) { + postKeyRequest(MediaDrm.KEY_TYPE_OFFLINE); + } + } + break; + case DefaultDrmSessionManager.MODE_RELEASE: + // It's not necessary to restore the key (and open a session to do that) before releasing it + // but this serves as a good sanity/fast-failure check. + if (restoreKeys()) { + postKeyRequest(MediaDrm.KEY_TYPE_RELEASE); + } + break; + } + } + + private boolean restoreKeys() { + try { + mediaDrm.restoreKeys(sessionId, offlineLicenseKeySetId); + return true; + } catch (Exception e) { + Log.e(TAG, "Error trying to restore Widevine keys.", e); + onError(e); + } + return false; + } + + private long getLicenseDurationRemainingSec() { + if (!C.WIDEVINE_UUID.equals(uuid)) { + return Long.MAX_VALUE; + } + Pair pair = WidevineUtil.getLicenseDurationRemainingSec(this); + return Math.min(pair.first, pair.second); + } + + private void postKeyRequest(int type) { + byte[] scope = type == MediaDrm.KEY_TYPE_RELEASE ? offlineLicenseKeySetId : sessionId; + try { + KeyRequest request = mediaDrm.getKeyRequest(scope, schemeInitData, schemeMimeType, type, + optionalKeyRequestParameters); + postRequestHandler.obtainMessage(MSG_KEYS, request).sendToTarget(); + } catch (Exception e) { + onKeysError(e); + } + } + + private void onKeyResponse(Object response) { + if (!isOpen()) { + // This event is stale. + return; + } + + if (response instanceof Exception) { + onKeysError((Exception) response); + return; + } + + try { + if (mode == DefaultDrmSessionManager.MODE_RELEASE) { + mediaDrm.provideKeyResponse(offlineLicenseKeySetId, (byte[]) response); + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onDrmKeysRemoved(); + } + }); + } + } else { + byte[] keySetId = mediaDrm.provideKeyResponse(sessionId, (byte[]) response); + if ((mode == DefaultDrmSessionManager.MODE_DOWNLOAD + || (mode == DefaultDrmSessionManager.MODE_PLAYBACK && offlineLicenseKeySetId != null)) + && keySetId != null && keySetId.length != 0) { + offlineLicenseKeySetId = keySetId; + } + state = STATE_OPENED_WITH_KEYS; + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onDrmKeysLoaded(); + } + }); + } + } + } catch (Exception e) { + onKeysError(e); + } + } + + private void onKeysExpired() { + if (state == STATE_OPENED_WITH_KEYS) { + state = STATE_OPENED; + onError(new KeysExpiredException()); + } + } + + private void onKeysError(Exception e) { + if (e instanceof NotProvisionedException) { + postProvisionRequest(); + } else { + onError(e); + } + } + + private void onError(final Exception e) { + lastException = new DrmSessionException(e); + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onDrmSessionManagerError(e); + } + }); + } + if (state != STATE_OPENED_WITH_KEYS) { + state = STATE_ERROR; + } + } + + private boolean isOpen() { + return state == STATE_OPENED || state == STATE_OPENED_WITH_KEYS; + } + + @SuppressLint("HandlerLeak") + private class MediaDrmHandler extends Handler { + + public MediaDrmHandler(Looper looper) { + super(looper); + } + + @SuppressWarnings("deprecation") + @Override + public void handleMessage(Message msg) { + if (!isOpen()) { + return; + } + switch (msg.what) { + case MediaDrm.EVENT_KEY_REQUIRED: + doLicense(); + break; + case MediaDrm.EVENT_KEY_EXPIRED: + // When an already expired key is loaded MediaDrm sends this event immediately. Ignore + // this event if the state isn't STATE_OPENED_WITH_KEYS yet which means we're still + // waiting for key response. + onKeysExpired(); + break; + case MediaDrm.EVENT_PROVISION_REQUIRED: + state = STATE_OPENED; + postProvisionRequest(); + break; + } + } + + } + + private class MediaDrmEventListener implements OnEventListener { + + @Override + public void onEvent(ExoMediaDrm md, byte[] sessionId, int event, int extra, + byte[] data) { + if (mode == DefaultDrmSessionManager.MODE_PLAYBACK) { + mediaDrmHandler.sendEmptyMessage(event); + } + } + + } + + @SuppressLint("HandlerLeak") + private class PostResponseHandler extends Handler { + + public PostResponseHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_PROVISION: + onProvisionResponse(msg.obj); + break; + case MSG_KEYS: + onKeyResponse(msg.obj); + break; + } + } + + } + + @SuppressLint("HandlerLeak") + private class PostRequestHandler extends Handler { + + public PostRequestHandler(Looper backgroundLooper) { + super(backgroundLooper); + } + + @Override + public void handleMessage(Message msg) { + Object response; + try { + switch (msg.what) { + case MSG_PROVISION: + response = callback.executeProvisionRequest(uuid, (ProvisionRequest) msg.obj); + break; + case MSG_KEYS: + response = callback.executeKeyRequest(uuid, (KeyRequest) msg.obj); + break; + default: + throw new RuntimeException(); + } + } catch (Exception e) { + response = e; + } + postResponseHandler.obtainMessage(msg.what, response).sendToTarget(); + } + + } + + /** + * Extracts {@link SchemeData} suitable for the given DRM scheme {@link UUID}. + * + * @param drmInitData The {@link DrmInitData} from which to extract the {@link SchemeData}. + * @param uuid The UUID of the scheme. + * @return The extracted {@link SchemeData}, or null if no suitable data is present. + */ + public static SchemeData getSchemeData(DrmInitData drmInitData, UUID uuid) { + SchemeData schemeData = drmInitData.get(uuid); + if (schemeData == null && C.CLEARKEY_UUID.equals(uuid)) { + // If present, the Common PSSH box should be used for ClearKey. + schemeData = drmInitData.get(C.COMMON_PSSH_UUID); + } + return schemeData; + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java index 69403a90a6..25a73a67c1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java @@ -15,41 +15,27 @@ */ package com.google.android.exoplayer2.drm; -import android.annotation.SuppressLint; import android.annotation.TargetApi; -import android.media.DeniedByServerException; import android.media.MediaDrm; -import android.media.NotProvisionedException; import android.os.Handler; -import android.os.HandlerThread; import android.os.Looper; -import android.os.Message; import android.support.annotation.IntDef; import android.support.annotation.NonNull; import android.text.TextUtils; -import android.util.Log; -import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; -import com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest; -import com.google.android.exoplayer2.drm.ExoMediaDrm.OnEventListener; -import com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest; -import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil; import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.HashMap; -import java.util.Map; import java.util.UUID; /** * A {@link DrmSessionManager} that supports playbacks using {@link MediaDrm}. */ @TargetApi(18) -public class DefaultDrmSessionManager implements DrmSessionManager, - DrmSession { +public class DefaultDrmSessionManager implements DrmSessionManager { /** * Listener of {@link DefaultDrmSessionManager} events. @@ -95,8 +81,7 @@ public class DefaultDrmSessionManager implements DrmSe */ public static final int MODE_PLAYBACK = 0; /** - * Restores an offline license to allow its status to be queried. If the offline license is - * expired sets state to {@link #STATE_ERROR}. + * Restores an offline license to allow its status to be queried. */ public static final int MODE_QUERY = 1; /** Downloads an offline license or renews an existing one. */ @@ -104,40 +89,18 @@ public class DefaultDrmSessionManager implements DrmSe /** Releases an existing offline license. */ public static final int MODE_RELEASE = 3; - private static final String TAG = "OfflineDrmSessionMngr"; - private static final String CENC_SCHEME_MIME_TYPE = "cenc"; - - private static final int MSG_PROVISION = 0; - private static final int MSG_KEYS = 1; - - private static final int MAX_LICENSE_DURATION_TO_RENEW = 60; - private final Handler eventHandler; private final EventListener eventListener; private final ExoMediaDrm mediaDrm; private final HashMap optionalKeyRequestParameters; - /* package */ final MediaDrmCallback callback; - /* package */ final UUID uuid; - - /* package */ MediaDrmHandler mediaDrmHandler; - /* package */ PostResponseHandler postResponseHandler; + private final MediaDrmCallback callback; + private final UUID uuid; private Looper playbackLooper; - private HandlerThread requestHandlerThread; - private Handler postRequestHandler; - private int mode; - private int openCount; - private boolean provisioningInProgress; - @DrmSession.State - private int state; - private T mediaCrypto; - private DrmSessionException lastException; - private byte[] schemeInitData; - private String schemeMimeType; - private byte[] sessionId; private byte[] offlineLicenseKeySetId; + private DefaultDrmSession session; /** * Instantiates a new instance using the Widevine scheme. @@ -224,7 +187,6 @@ public class DefaultDrmSessionManager implements DrmSe this.optionalKeyRequestParameters = optionalKeyRequestParameters; this.eventHandler = eventHandler; this.eventListener = eventListener; - mediaDrm.setOnEventListener(new MediaDrmEventListener()); mode = MODE_PLAYBACK; } @@ -299,7 +261,7 @@ public class DefaultDrmSessionManager implements DrmSe * @param offlineLicenseKeySetId The key set id of the license to be used with the given mode. */ public void setMode(@Mode int mode, byte[] offlineLicenseKeySetId) { - Assertions.checkState(openCount == 0); + Assertions.checkState(session == null); if (mode == MODE_QUERY || mode == MODE_RELEASE) { Assertions.checkNotNull(offlineLicenseKeySetId); } @@ -311,7 +273,7 @@ public class DefaultDrmSessionManager implements DrmSe @Override public boolean canAcquireSession(@NonNull DrmInitData drmInitData) { - SchemeData schemeData = getSchemeData(drmInitData); + SchemeData schemeData = DefaultDrmSession.getSchemeData(drmInitData, uuid); if (schemeData == null) { // No data for this manager's scheme. return false; @@ -332,407 +294,22 @@ public class DefaultDrmSessionManager implements DrmSe @Override public DrmSession acquireSession(Looper playbackLooper, DrmInitData drmInitData) { Assertions.checkState(this.playbackLooper == null || this.playbackLooper == playbackLooper); - if (++openCount != 1) { - return this; - } - - if (this.playbackLooper == null) { + if (session == null) { this.playbackLooper = playbackLooper; - mediaDrmHandler = new MediaDrmHandler(playbackLooper); - postResponseHandler = new PostResponseHandler(playbackLooper); + session = new DefaultDrmSession(uuid, mediaDrm, drmInitData, mode, offlineLicenseKeySetId, + optionalKeyRequestParameters, callback, playbackLooper, eventHandler, eventListener); } - requestHandlerThread = new HandlerThread("DrmRequestHandler"); - requestHandlerThread.start(); - postRequestHandler = new PostRequestHandler(requestHandlerThread.getLooper()); - - if (offlineLicenseKeySetId == null) { - SchemeData schemeData = getSchemeData(drmInitData); - if (schemeData == null) { - onError(new IllegalStateException("Media does not support uuid: " + uuid)); - return this; - } - schemeInitData = schemeData.data; - schemeMimeType = schemeData.mimeType; - if (Util.SDK_INT < 21) { - // Prior to L the Widevine CDM required data to be extracted from the PSSH atom. - byte[] psshData = PsshAtomUtil.parseSchemeSpecificData(schemeInitData, C.WIDEVINE_UUID); - if (psshData == null) { - // Extraction failed. schemeData isn't a Widevine PSSH atom, so leave it unchanged. - } else { - schemeInitData = psshData; - } - } - if (Util.SDK_INT < 26 && C.CLEARKEY_UUID.equals(uuid) - && (MimeTypes.VIDEO_MP4.equals(schemeMimeType) - || MimeTypes.AUDIO_MP4.equals(schemeMimeType))) { - // Prior to API level 26 the ClearKey CDM only accepted "cenc" as the scheme for MP4. - schemeMimeType = CENC_SCHEME_MIME_TYPE; - } - } - state = STATE_OPENING; - openInternal(true); - return this; + session.acquire(); + return session; } @Override public void releaseSession(DrmSession session) { - if (--openCount != 0) { - return; + Assertions.checkState(session == this.session); + if (this.session.release()) { + this.session = null; } - state = STATE_RELEASED; - provisioningInProgress = false; - mediaDrmHandler.removeCallbacksAndMessages(null); - postResponseHandler.removeCallbacksAndMessages(null); - postRequestHandler.removeCallbacksAndMessages(null); - postRequestHandler = null; - requestHandlerThread.quit(); - requestHandlerThread = null; - schemeInitData = null; - schemeMimeType = null; - mediaCrypto = null; - lastException = null; - if (sessionId != null) { - mediaDrm.closeSession(sessionId); - sessionId = null; - } - } - - // DrmSession implementation. - - @Override - @DrmSession.State - public final int getState() { - return state; - } - - @Override - public final DrmSessionException getError() { - return state == STATE_ERROR ? lastException : null; - } - - @Override - public final T getMediaCrypto() { - return mediaCrypto; - } - - @Override - public Map queryKeyStatus() { - return sessionId == null ? null : mediaDrm.queryKeyStatus(sessionId); - } - - @Override - public byte[] getOfflineLicenseKeySetId() { - return offlineLicenseKeySetId; - } - - // Internal methods. - - private void openInternal(boolean allowProvisioning) { - try { - sessionId = mediaDrm.openSession(); - mediaCrypto = mediaDrm.createMediaCrypto(uuid, sessionId); - state = STATE_OPENED; - doLicense(); - } catch (NotProvisionedException e) { - if (allowProvisioning) { - postProvisionRequest(); - } else { - onError(e); - } - } catch (Exception e) { - onError(e); - } - } - - private void postProvisionRequest() { - if (provisioningInProgress) { - return; - } - provisioningInProgress = true; - ProvisionRequest request = mediaDrm.getProvisionRequest(); - postRequestHandler.obtainMessage(MSG_PROVISION, request).sendToTarget(); - } - - private void onProvisionResponse(Object response) { - provisioningInProgress = false; - if (state != STATE_OPENING && state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS) { - // This event is stale. - return; - } - - if (response instanceof Exception) { - onError((Exception) response); - return; - } - - try { - mediaDrm.provideProvisionResponse((byte[]) response); - if (state == STATE_OPENING) { - openInternal(false); - } else { - doLicense(); - } - } catch (DeniedByServerException e) { - onError(e); - } - } - - private void doLicense() { - switch (mode) { - case MODE_PLAYBACK: - case MODE_QUERY: - if (offlineLicenseKeySetId == null) { - postKeyRequest(sessionId, MediaDrm.KEY_TYPE_STREAMING); - } else { - if (restoreKeys()) { - long licenseDurationRemainingSec = getLicenseDurationRemainingSec(); - if (mode == MODE_PLAYBACK - && licenseDurationRemainingSec <= MAX_LICENSE_DURATION_TO_RENEW) { - Log.d(TAG, "Offline license has expired or will expire soon. " - + "Remaining seconds: " + licenseDurationRemainingSec); - postKeyRequest(sessionId, MediaDrm.KEY_TYPE_OFFLINE); - } else if (licenseDurationRemainingSec <= 0) { - onError(new KeysExpiredException()); - } else { - state = STATE_OPENED_WITH_KEYS; - if (eventHandler != null && eventListener != null) { - eventHandler.post(new Runnable() { - @Override - public void run() { - eventListener.onDrmKeysRestored(); - } - }); - } - } - } - } - break; - case MODE_DOWNLOAD: - if (offlineLicenseKeySetId == null) { - postKeyRequest(sessionId, MediaDrm.KEY_TYPE_OFFLINE); - } else { - // Renew - if (restoreKeys()) { - postKeyRequest(sessionId, MediaDrm.KEY_TYPE_OFFLINE); - } - } - break; - case MODE_RELEASE: - // It's not necessary to restore the key (and open a session to do that) before releasing it - // but this serves as a good sanity/fast-failure check. - if (restoreKeys()) { - postKeyRequest(offlineLicenseKeySetId, MediaDrm.KEY_TYPE_RELEASE); - } - break; - } - } - - private boolean restoreKeys() { - try { - mediaDrm.restoreKeys(sessionId, offlineLicenseKeySetId); - return true; - } catch (Exception e) { - Log.e(TAG, "Error trying to restore Widevine keys.", e); - onError(e); - } - return false; - } - - private long getLicenseDurationRemainingSec() { - if (!C.WIDEVINE_UUID.equals(uuid)) { - return Long.MAX_VALUE; - } - Pair pair = WidevineUtil.getLicenseDurationRemainingSec(this); - return Math.min(pair.first, pair.second); - } - - private void postKeyRequest(byte[] scope, int keyType) { - try { - KeyRequest keyRequest = mediaDrm.getKeyRequest(scope, schemeInitData, schemeMimeType, keyType, - optionalKeyRequestParameters); - postRequestHandler.obtainMessage(MSG_KEYS, keyRequest).sendToTarget(); - } catch (Exception e) { - onKeysError(e); - } - } - - private void onKeyResponse(Object response) { - if (state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS) { - // This event is stale. - return; - } - - if (response instanceof Exception) { - onKeysError((Exception) response); - return; - } - - try { - if (mode == MODE_RELEASE) { - mediaDrm.provideKeyResponse(offlineLicenseKeySetId, (byte[]) response); - if (eventHandler != null && eventListener != null) { - eventHandler.post(new Runnable() { - @Override - public void run() { - eventListener.onDrmKeysRemoved(); - } - }); - } - } else { - byte[] keySetId = mediaDrm.provideKeyResponse(sessionId, (byte[]) response); - if ((mode == MODE_DOWNLOAD || (mode == MODE_PLAYBACK && offlineLicenseKeySetId != null)) - && keySetId != null && keySetId.length != 0) { - offlineLicenseKeySetId = keySetId; - } - state = STATE_OPENED_WITH_KEYS; - if (eventHandler != null && eventListener != null) { - eventHandler.post(new Runnable() { - @Override - public void run() { - eventListener.onDrmKeysLoaded(); - } - }); - } - } - } catch (Exception e) { - onKeysError(e); - } - } - - private void onKeysError(Exception e) { - if (e instanceof NotProvisionedException) { - postProvisionRequest(); - } else { - onError(e); - } - } - - private void onError(final Exception e) { - lastException = new DrmSessionException(e); - if (eventHandler != null && eventListener != null) { - eventHandler.post(new Runnable() { - @Override - public void run() { - eventListener.onDrmSessionManagerError(e); - } - }); - } - if (state != STATE_OPENED_WITH_KEYS) { - state = STATE_ERROR; - } - } - - /** - * Extracts {@link SchemeData} suitable for this manager. - * - * @param drmInitData The {@link DrmInitData} from which to extract the {@link SchemeData}. - * @return The extracted {@link SchemeData}, or null if no suitable data is present. - */ - private SchemeData getSchemeData(DrmInitData drmInitData) { - SchemeData schemeData = drmInitData.get(uuid); - if (schemeData == null && C.CLEARKEY_UUID.equals(uuid)) { - // If present, the Common PSSH box should be used for ClearKey. - schemeData = drmInitData.get(C.COMMON_PSSH_UUID); - } - return schemeData; - } - - @SuppressLint("HandlerLeak") - private class MediaDrmHandler extends Handler { - - public MediaDrmHandler(Looper looper) { - super(looper); - } - - @SuppressWarnings("deprecation") - @Override - public void handleMessage(Message msg) { - if (openCount == 0 || (state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS)) { - return; - } - switch (msg.what) { - case MediaDrm.EVENT_KEY_REQUIRED: - doLicense(); - break; - case MediaDrm.EVENT_KEY_EXPIRED: - // When an already expired key is loaded MediaDrm sends this event immediately. Ignore - // this event if the state isn't STATE_OPENED_WITH_KEYS yet which means we're still - // waiting for key response. - if (state == STATE_OPENED_WITH_KEYS) { - state = STATE_OPENED; - onError(new KeysExpiredException()); - } - break; - case MediaDrm.EVENT_PROVISION_REQUIRED: - state = STATE_OPENED; - postProvisionRequest(); - break; - } - } - - } - - private class MediaDrmEventListener implements OnEventListener { - - @Override - public void onEvent(ExoMediaDrm md, byte[] sessionId, int event, int extra, - byte[] data) { - if (mode == MODE_PLAYBACK) { - mediaDrmHandler.sendEmptyMessage(event); - } - } - - } - - @SuppressLint("HandlerLeak") - private class PostResponseHandler extends Handler { - - public PostResponseHandler(Looper looper) { - super(looper); - } - - @Override - public void handleMessage(Message msg) { - switch (msg.what) { - case MSG_PROVISION: - onProvisionResponse(msg.obj); - break; - case MSG_KEYS: - onKeyResponse(msg.obj); - break; - } - } - - } - - @SuppressLint("HandlerLeak") - private class PostRequestHandler extends Handler { - - public PostRequestHandler(Looper backgroundLooper) { - super(backgroundLooper); - } - - @Override - public void handleMessage(Message msg) { - Object response; - try { - switch (msg.what) { - case MSG_PROVISION: - response = callback.executeProvisionRequest(uuid, (ProvisionRequest) msg.obj); - break; - case MSG_KEYS: - response = callback.executeKeyRequest(uuid, (KeyRequest) msg.obj); - break; - default: - throw new RuntimeException(); - } - } catch (Exception e) { - response = e; - } - postResponseHandler.obtainMessage(msg.what, response).sendToTarget(); - } - } } From a24f2b75b0e3c60813ef1d5e13728a8f0af3cb77 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 17 Aug 2017 04:04:01 -0700 Subject: [PATCH 0264/2472] Host GTS content ourselves + prevent spurious test passes ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=165561808 --- .../playbacktests/gts/CommonEncryptionDrmTest.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/CommonEncryptionDrmTest.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/CommonEncryptionDrmTest.java index 3f84b9ea85..c6dd72debd 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/CommonEncryptionDrmTest.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/CommonEncryptionDrmTest.java @@ -29,18 +29,18 @@ public final class CommonEncryptionDrmTest extends ActivityInstrumentationTestCa private static final String TAG = "CencDrmTest"; private static final String URL_cenc = - "https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd"; + "https://storage.googleapis.com/exoplayer-test-media-1/gts/tears-cenc.mpd"; private static final String URL_cbc1 = - "https://storage.googleapis.com/wvmedia/cbc1/h264/tears/tears_aes_cbc1.mpd"; + "https://storage.googleapis.com/exoplayer-test-media-1/gts/tears-aes-cbc1.mpd"; private static final String URL_cbcs = - "https://storage.googleapis.com/wvmedia/cbcs/h264/tears/tears_aes_cbcs.mpd"; + "https://storage.googleapis.com/exoplayer-test-media-1/gts/tears-aes-cbcs.mpd"; private static final String ID_AUDIO = "0"; private static final String[] IDS_VIDEO = new String[] {"1", "2"}; // Seeks help reproduce playback issues in certain devices. private static final ActionSchedule ACTION_SCHEDULE_WITH_SEEKS = new ActionSchedule.Builder(TAG) .delay(30000).seek(300000).delay(10000).seek(270000).delay(10000).seek(200000).delay(10000) - .stop().build(); + .seek(732000).build(); private DashTestRunner testRunner; From 1cfd246cd32f901148c36d7671c6aebc93fa5361 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 17 Aug 2017 06:40:44 -0700 Subject: [PATCH 0265/2472] Update extension README with usage instructions Issue: #3162 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=165572088 --- extensions/ffmpeg/README.md | 49 ++++++++++++++++++++++++++++++------ extensions/flac/README.md | 50 +++++++++++++++++++++++++++++++------ extensions/opus/README.md | 41 ++++++++++++++++++++++++------ extensions/vp9/README.md | 48 +++++++++++++++++++++++++++++------ 4 files changed, 160 insertions(+), 28 deletions(-) diff --git a/extensions/ffmpeg/README.md b/extensions/ffmpeg/README.md index b4514effbc..fbc919c36d 100644 --- a/extensions/ffmpeg/README.md +++ b/extensions/ffmpeg/README.md @@ -2,17 +2,18 @@ ## Description ## -The FFmpeg extension is a [Renderer][] implementation that uses FFmpeg to decode -audio. - -[Renderer]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/Renderer.html +The FFmpeg extension provides `FfmpegAudioRenderer`, which uses FFmpeg for +decoding and can render audio encoded in a variety of formats. ## Build instructions ## To use this extension you need to clone the ExoPlayer repository and depend on its modules locally. Instructions for doing this can be found in ExoPlayer's -[top level README][]. In addition, it's necessary to build the extension's -native components as follows: +[top level README][]. The extension is not provided via JCenter (see [#2781][] +for more information). + +In addition, it's necessary to build the extension's native components as +follows: * Set the following environment variables: @@ -34,7 +35,11 @@ NDK_PATH="" HOST_PLATFORM="linux-x86_64" ``` -* Fetch and build FFmpeg. For example, to fetch and build for armeabi-v7a, +* Fetch and build FFmpeg. The configuration flags determine which formats will + be supported. See the [Supported formats][] page for more details of the + available flags. + +For example, to fetch and build for armeabi-v7a, arm64-v8a and x86 on Linux x86_64: ``` @@ -103,5 +108,35 @@ cd "${FFMPEG_EXT_PATH}"/jni && \ ${NDK_PATH}/ndk-build APP_ABI="armeabi-v7a arm64-v8a x86" -j4 ``` +## Using the extension ## + +Once you've followed the instructions above to check out, build and depend on +the extension, the next step is to tell ExoPlayer to use `FfmpegAudioRenderer`. +How you do this depends on which player API you're using: + +* If you're passing a `DefaultRenderersFactory` to + `ExoPlayerFactory.newSimpleInstance`, you can enable using the extension by + setting the `extensionRendererMode` parameter of the `DefaultRenderersFactory` + constructor to `EXTENSION_RENDERER_MODE_ON`. This will use + `FfmpegAudioRenderer` for playback if `MediaCodecAudioRenderer` doesn't + support the input format. Pass `EXTENSION_RENDERER_MODE_PREFER` to give + `FfmpegAudioRenderer` priority over `MediaCodecAudioRenderer`. +* If you've subclassed `DefaultRenderersFactory`, add an `FfmpegAudioRenderer` + to the output list in `buildAudioRenderers`. ExoPlayer will use the first + `Renderer` in the list that supports the input media format. +* If you've implemented your own `RenderersFactory`, return an + `FfmpegAudioRenderer` instance from `createRenderers`. ExoPlayer will use the + first `Renderer` in the returned array that supports the input media format. +* If you're using `ExoPlayerFactory.newInstance`, pass an `FfmpegAudioRenderer` + in the array of `Renderer`s. ExoPlayer will use the first `Renderer` in the + list that supports the input media format. + +Note: These instructions assume you're using `DefaultTrackSelector`. If you have +a custom track selector the choice of `Renderer` is up to your implementation, +so you need to make sure you are passing an `FfmpegAudioRenderer` to the player, +then implement your own logic to use the renderer for a given track. + [top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md [Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html +[#2781]: https://github.com/google/ExoPlayer/issues/2781 +[Supported formats]: https://google.github.io/ExoPlayer/supported-formats.html#ffmpeg-extension diff --git a/extensions/flac/README.md b/extensions/flac/README.md index 9db2e5727d..505482f7ed 100644 --- a/extensions/flac/README.md +++ b/extensions/flac/README.md @@ -2,18 +2,17 @@ ## Description ## -The Flac extension is a [Renderer][] implementation that helps you bundle -libFLAC (the Flac decoding library) into your app and use it along with -ExoPlayer to play Flac audio on Android devices. - -[Renderer]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/Renderer.html +The Flac extension provides `FlacExtractor` and `LibflacAudioRenderer`, which +use libFLAC (the Flac decoding library) to extract and decode FLAC audio. ## Build instructions ## To use this extension you need to clone the ExoPlayer repository and depend on its modules locally. Instructions for doing this can be found in ExoPlayer's -[top level README][]. In addition, it's necessary to build the extension's -native components as follows: +[top level README][]. + +In addition, it's necessary to build the extension's native components as +follows: * Set the following environment variables: @@ -46,3 +45,40 @@ ${NDK_PATH}/ndk-build APP_ABI=all -j4 [top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md [Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html + +## Using the extension ## + +Once you've followed the instructions above to check out, build and depend on +the extension, the next step is to tell ExoPlayer to use the extractor and/or +renderer. + +### Using `FlacExtractor` ### + +`FlacExtractor` is used via `ExtractorMediaSource`. If you're using +`DefaultExtractorsFactory`, `FlacExtractor` will automatically be used to read +`.flac` files. If you're not using `DefaultExtractorsFactory`, return a +`FlacExtractor` from your `ExtractorsFactory.createExtractors` implementation. + +### Using `LibflacAudioRenderer` ### + +* If you're passing a `DefaultRenderersFactory` to + `ExoPlayerFactory.newSimpleInstance`, you can enable using the extension by + setting the `extensionRendererMode` parameter of the `DefaultRenderersFactory` + constructor to `EXTENSION_RENDERER_MODE_ON`. This will use + `LibflacAudioRenderer` for playback if `MediaCodecAudioRenderer` doesn't + support the input format. Pass `EXTENSION_RENDERER_MODE_PREFER` to give + `LibflacAudioRenderer` priority over `MediaCodecAudioRenderer`. +* If you've subclassed `DefaultRenderersFactory`, add a `LibflacAudioRenderer` + to the output list in `buildAudioRenderers`. ExoPlayer will use the first + `Renderer` in the list that supports the input media format. +* If you've implemented your own `RenderersFactory`, return a + `LibflacAudioRenderer` instance from `createRenderers`. ExoPlayer will use the + first `Renderer` in the returned array that supports the input media format. +* If you're using `ExoPlayerFactory.newInstance`, pass a `LibflacAudioRenderer` + in the array of `Renderer`s. ExoPlayer will use the first `Renderer` in the + list that supports the input media format. + +Note: These instructions assume you're using `DefaultTrackSelector`. If you have +a custom track selector the choice of `Renderer` is up to your implementation, +so you need to make sure you are passing an `LibflacAudioRenderer` to the +player, then implement your own logic to use the renderer for a given track. diff --git a/extensions/opus/README.md b/extensions/opus/README.md index e5f5bcb168..cc21c77cf9 100644 --- a/extensions/opus/README.md +++ b/extensions/opus/README.md @@ -2,18 +2,17 @@ ## Description ## -The Opus extension is a [Renderer][] implementation that helps you bundle -libopus (the Opus decoding library) into your app and use it along with -ExoPlayer to play Opus audio on Android devices. - -[Renderer]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/Renderer.html +The Opus extension provides `LibopusAudioRenderer`, which uses +libopus (the Opus decoding library) to decode Opus audio. ## Build instructions ## To use this extension you need to clone the ExoPlayer repository and depend on its modules locally. Instructions for doing this can be found in ExoPlayer's -[top level README][]. In addition, it's necessary to build the extension's -native components as follows: +[top level README][]. + +In addition, it's necessary to build the extension's native components as +follows: * Set the following environment variables: @@ -59,3 +58,31 @@ ${NDK_PATH}/ndk-build APP_ABI=all -j4 * Clean and re-build the project. * If you want to use your own version of libopus, place it in `${OPUS_EXT_PATH}/jni/libopus`. + +## Using the extension ## + +Once you've followed the instructions above to check out, build and depend on +the extension, the next step is to tell ExoPlayer to use `LibopusAudioRenderer`. +How you do this depends on which player API you're using: + +* If you're passing a `DefaultRenderersFactory` to + `ExoPlayerFactory.newSimpleInstance`, you can enable using the extension by + setting the `extensionRendererMode` parameter of the `DefaultRenderersFactory` + constructor to `EXTENSION_RENDERER_MODE_ON`. This will use + `LibopusAudioRenderer` for playback if `MediaCodecAudioRenderer` doesn't + support the input format. Pass `EXTENSION_RENDERER_MODE_PREFER` to give + `LibopusAudioRenderer` priority over `MediaCodecAudioRenderer`. +* If you've subclassed `DefaultRenderersFactory`, add a `LibopusAudioRenderer` + to the output list in `buildAudioRenderers`. ExoPlayer will use the first + `Renderer` in the list that supports the input media format. +* If you've implemented your own `RenderersFactory`, return a + `LibopusAudioRenderer` instance from `createRenderers`. ExoPlayer will use the + first `Renderer` in the returned array that supports the input media format. +* If you're using `ExoPlayerFactory.newInstance`, pass a `LibopusAudioRenderer` + in the array of `Renderer`s. ExoPlayer will use the first `Renderer` in the + list that supports the input media format. + +Note: These instructions assume you're using `DefaultTrackSelector`. If you have +a custom track selector the choice of `Renderer` is up to your implementation, +so you need to make sure you are passing an `LibopusAudioRenderer` to the +player, then implement your own logic to use the renderer for a given track. diff --git a/extensions/vp9/README.md b/extensions/vp9/README.md index 87c5c8d54f..d28aa70db0 100644 --- a/extensions/vp9/README.md +++ b/extensions/vp9/README.md @@ -2,18 +2,17 @@ ## Description ## -The VP9 extension is a [Renderer][] implementation that helps you bundle libvpx -(the VP9 decoding library) into your app and use it along with ExoPlayer to play -VP9 video on Android devices. - -[Renderer]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/Renderer.html +The VP9 extension provides `LibvpxVideoRenderer`, which uses +libvpx (the VPx decoding library) to decode VP9 video. ## Build instructions ## To use this extension you need to clone the ExoPlayer repository and depend on its modules locally. Instructions for doing this can be found in ExoPlayer's -[top level README][]. In addition, it's necessary to build the extension's -native components as follows: +[top level README][]. + +In addition, it's necessary to build the extension's native components as +follows: * Set the following environment variables: @@ -76,3 +75,38 @@ ${NDK_PATH}/ndk-build APP_ABI=all -j4 `${VP9_EXT_PATH}/jni/libvpx` or `${VP9_EXT_PATH}/jni/libyuv` respectively. But please note that `generate_libvpx_android_configs.sh` and the makefiles need to be modified to work with arbitrary versions of libvpx and libyuv. + +## Using the extension ## + +Once you've followed the instructions above to check out, build and depend on +the extension, the next step is to tell ExoPlayer to use `LibvpxVideoRenderer`. +How you do this depends on which player API you're using: + +* If you're passing a `DefaultRenderersFactory` to + `ExoPlayerFactory.newSimpleInstance`, you can enable using the extension by + setting the `extensionRendererMode` parameter of the `DefaultRenderersFactory` + constructor to `EXTENSION_RENDERER_MODE_ON`. This will use + `LibvpxVideoRenderer` for playback if `MediaCodecVideoRenderer` doesn't + support decoding the input VP9 stream. Pass `EXTENSION_RENDERER_MODE_PREFER` + to give `LibvpxVideoRenderer` priority over `MediaCodecVideoRenderer`. +* If you've subclassed `DefaultRenderersFactory`, add a `LibvpxVideoRenderer` + to the output list in `buildVideoRenderers`. ExoPlayer will use the first + `Renderer` in the list that supports the input media format. +* If you've implemented your own `RenderersFactory`, return a + `LibvpxVideoRenderer` instance from `createRenderers`. ExoPlayer will use the + first `Renderer` in the returned array that supports the input media format. +* If you're using `ExoPlayerFactory.newInstance`, pass a `LibvpxVideoRenderer` + in the array of `Renderer`s. ExoPlayer will use the first `Renderer` in the + list that supports the input media format. + +Note: These instructions assume you're using `DefaultTrackSelector`. If you have +a custom track selector the choice of `Renderer` is up to your implementation, +so you need to make sure you are passing an `LibvpxVideoRenderer` to the +player, then implement your own logic to use the renderer for a given track. + +`LibvpxVideoRenderer` can optionally output to a `VpxVideoSurfaceView` when not +being used via `SimpleExoPlayer`, in which case color space conversion will be +performed using a GL shader. To enable this mode, send the renderer a message of +type `LibvpxVideoRenderer.MSG_SET_OUTPUT_BUFFER_RENDERER` with the +`VpxVideoSurfaceView` as its object, instead of sending `MSG_SET_SURFACE` with a +`Surface`. From 04d76fa8fc3f997580c38ef1965dd8e6a58a7f29 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 17 Aug 2017 07:41:10 -0700 Subject: [PATCH 0266/2472] Allow easier ExoPlayer/Cast integration This CL adds the fundamental pieces for ExoPlayer/Cast integration and includes a demo app to showcase this functionality. However, media queues should be supported in the first release of this extension. Issue:#2283 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=165576892 --- constants.gradle | 1 + core_settings.gradle | 2 + demos/cast/README.md | 4 + demos/cast/build.gradle | 51 ++ demos/cast/src/main/AndroidManifest.xml | 43 ++ .../exoplayer2/castdemo/CastDemoUtil.java | 92 +++ .../exoplayer2/castdemo/MainActivity.java | 152 ++++ .../exoplayer2/castdemo/PlayerManager.java | 196 ++++++ .../src/main/res/layout/main_activity.xml | 41 ++ demos/cast/src/main/res/menu/menu.xml | 25 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 4315 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2734 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 6159 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 10578 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 11743 bytes demos/cast/src/main/res/values/strings.xml | 25 + demos/cast/src/main/res/values/styles.xml | 22 + extensions/cast/README.md | 33 + extensions/cast/build.gradle | 45 ++ extensions/cast/src/main/AndroidManifest.xml | 16 + .../exoplayer2/ext/cast/CastPlayer.java | 649 ++++++++++++++++++ .../exoplayer2/ext/cast/CastUtils.java | 94 +++ .../ext/cast/DefaultCastOptionsProvider.java | 42 ++ extensions/ima/build.gradle | 2 +- library/all/src/main/AndroidManifest.xml | 1 - 25 files changed, 1534 insertions(+), 2 deletions(-) create mode 100644 demos/cast/README.md create mode 100644 demos/cast/build.gradle create mode 100644 demos/cast/src/main/AndroidManifest.xml create mode 100644 demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/CastDemoUtil.java create mode 100644 demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java create mode 100644 demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java create mode 100644 demos/cast/src/main/res/layout/main_activity.xml create mode 100644 demos/cast/src/main/res/menu/menu.xml create mode 100644 demos/cast/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 demos/cast/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 demos/cast/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 demos/cast/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 demos/cast/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 demos/cast/src/main/res/values/strings.xml create mode 100644 demos/cast/src/main/res/values/styles.xml create mode 100644 extensions/cast/README.md create mode 100644 extensions/cast/build.gradle create mode 100644 extensions/cast/src/main/AndroidManifest.xml create mode 100644 extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java create mode 100644 extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastUtils.java create mode 100644 extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastOptionsProvider.java diff --git a/constants.gradle b/constants.gradle index 576091f937..4107faab4c 100644 --- a/constants.gradle +++ b/constants.gradle @@ -22,6 +22,7 @@ project.ext { buildToolsVersion = '26' testSupportLibraryVersion = '0.5' supportLibraryVersion = '26.0.1' + playServicesLibraryVersion = '11.0.2' dexmakerVersion = '1.2' mockitoVersion = '1.9.5' releaseVersion = 'r2.5.1' diff --git a/core_settings.gradle b/core_settings.gradle index 7a8320b1a1..20a7c87bde 100644 --- a/core_settings.gradle +++ b/core_settings.gradle @@ -28,6 +28,7 @@ include modulePrefix + 'extension-ffmpeg' include modulePrefix + 'extension-flac' include modulePrefix + 'extension-gvr' include modulePrefix + 'extension-ima' +include modulePrefix + 'extension-cast' include modulePrefix + 'extension-mediasession' include modulePrefix + 'extension-okhttp' include modulePrefix + 'extension-opus' @@ -46,6 +47,7 @@ project(modulePrefix + 'extension-ffmpeg').projectDir = new File(rootDir, 'exten project(modulePrefix + 'extension-flac').projectDir = new File(rootDir, 'extensions/flac') project(modulePrefix + 'extension-gvr').projectDir = new File(rootDir, 'extensions/gvr') project(modulePrefix + 'extension-ima').projectDir = new File(rootDir, 'extensions/ima') +project(modulePrefix + 'extension-cast').projectDir = new File(rootDir, 'extensions/cast') project(modulePrefix + 'extension-mediasession').projectDir = new File(rootDir, 'extensions/mediasession') project(modulePrefix + 'extension-okhttp').projectDir = new File(rootDir, 'extensions/okhttp') project(modulePrefix + 'extension-opus').projectDir = new File(rootDir, 'extensions/opus') diff --git a/demos/cast/README.md b/demos/cast/README.md new file mode 100644 index 0000000000..2c68a5277a --- /dev/null +++ b/demos/cast/README.md @@ -0,0 +1,4 @@ +# Cast demo application # + +This folder contains a demo application that showcases ExoPlayer integration +with Google Cast. diff --git a/demos/cast/build.gradle b/demos/cast/build.gradle new file mode 100644 index 0000000000..a9fa27ad58 --- /dev/null +++ b/demos/cast/build.gradle @@ -0,0 +1,51 @@ +// 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 from: '../../constants.gradle' +apply plugin: 'com.android.application' + +android { + compileSdkVersion project.ext.compileSdkVersion + buildToolsVersion project.ext.buildToolsVersion + + defaultConfig { + minSdkVersion 16 + targetSdkVersion project.ext.targetSdkVersion + } + + buildTypes { + release { + shrinkResources true + minifyEnabled true + proguardFiles getDefaultProguardFile('proguard-android.txt') + } + debug { + jniDebuggable = true + } + } + + lintOptions { + // The demo app does not have translations. + disable 'MissingTranslation' + } + +} + +dependencies { + compile project(modulePrefix + 'library-core') + compile project(modulePrefix + 'library-dash') + compile project(modulePrefix + 'library-hls') + compile project(modulePrefix + 'library-smoothstreaming') + compile project(modulePrefix + 'library-ui') + compile project(modulePrefix + 'extension-cast') +} diff --git a/demos/cast/src/main/AndroidManifest.xml b/demos/cast/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..eeb28438bd --- /dev/null +++ b/demos/cast/src/main/AndroidManifest.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/CastDemoUtil.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/CastDemoUtil.java new file mode 100644 index 0000000000..f819e54e50 --- /dev/null +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/CastDemoUtil.java @@ -0,0 +1,92 @@ +/* + * 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.castdemo; + +import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.gms.cast.MediaInfo; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Utility methods and constants for the Cast demo application. + */ +/* package */ final class CastDemoUtil { + + public static final String MIME_TYPE_DASH = "application/dash+xml"; + public static final String MIME_TYPE_HLS = "application/vnd.apple.mpegurl"; + public static final String MIME_TYPE_SS = "application/vnd.ms-sstr+xml"; + public static final String MIME_TYPE_VIDEO_MP4 = MimeTypes.VIDEO_MP4; + + /** + * The list of samples available in the cast demo app. + */ + public static final List SAMPLES; + + /** + * Represents a media sample. + */ + public static final class Sample { + + /** + * The uri from which the media sample is obtained. + */ + public final String uri; + /** + * A descriptive name for the sample. + */ + public final String name; + /** + * The mime type of the media sample, as required by {@link MediaInfo#setContentType}. + */ + public final String type; + + /** + * @param uri See {@link #uri}. + * @param name See {@link #name}. + * @param type See {@link #type}. + */ + public Sample(String uri, String name, String type) { + this.uri = uri; + this.name = name; + this.type = type; + } + + @Override + public String toString() { + return name; + } + + } + + static { + // App samples. + ArrayList samples = new ArrayList<>(); + samples.add(new Sample("https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd", + "DASH (clear,MP4,H264)", MIME_TYPE_DASH)); + samples.add(new Sample("https://commondatastorage.googleapis.com/gtv-videos-bucket/CastVideos/" + + "hls/TearsOfSteel.m3u8", "Tears of Steel (HLS)", MIME_TYPE_HLS)); + samples.add(new Sample("https://html5demos.com/assets/dizzy.mp4", "Dizzy (MP4)", + MIME_TYPE_VIDEO_MP4)); + + + SAMPLES = Collections.unmodifiableList(samples); + + } + + private CastDemoUtil() {} + +} diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java new file mode 100644 index 0000000000..e1367858aa --- /dev/null +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java @@ -0,0 +1,152 @@ +/* + * 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.castdemo; + +import android.graphics.Color; +import android.os.Bundle; +import android.support.v7.app.AppCompatActivity; +import android.view.KeyEvent; +import android.view.Menu; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.ListView; +import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.ext.cast.CastPlayer; +import com.google.android.exoplayer2.ui.PlaybackControlView; +import com.google.android.exoplayer2.ui.SimpleExoPlayerView; +import com.google.android.exoplayer2.util.Util; +import com.google.android.gms.cast.framework.CastButtonFactory; + +/** + * An activity that plays video using {@link SimpleExoPlayer} and {@link CastPlayer}. + */ +public class MainActivity extends AppCompatActivity { + + private SimpleExoPlayerView simpleExoPlayerView; + private PlaybackControlView castControlView; + private PlayerManager playerManager; + + // Activity lifecycle methods. + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.main_activity); + + simpleExoPlayerView = (SimpleExoPlayerView) findViewById(R.id.player_view); + simpleExoPlayerView.requestFocus(); + + castControlView = (PlaybackControlView) findViewById(R.id.cast_control_view); + + ListView sampleList = (ListView) findViewById(R.id.sample_list); + sampleList.setAdapter(new SampleListAdapter()); + sampleList.setOnItemClickListener(new SampleClickListener()); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + getMenuInflater().inflate(R.menu.menu, menu); + CastButtonFactory.setUpMediaRouteButton(getApplicationContext(), menu, + R.id.media_route_menu_item); + return true; + } + + @Override + public void onStart() { + super.onStart(); + if (Util.SDK_INT > 23) { + setupPlayerManager(); + } + } + + @Override + public void onResume() { + super.onResume(); + if ((Util.SDK_INT <= 23)) { + setupPlayerManager(); + } + } + + @Override + public void onPause() { + super.onPause(); + if (Util.SDK_INT <= 23) { + releasePlayerManager(); + } + } + + @Override + public void onStop() { + super.onStop(); + if (Util.SDK_INT > 23) { + releasePlayerManager(); + } + } + + // Activity input. + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + // If the event was not handled then see if the player view can handle it. + return super.dispatchKeyEvent(event) || playerManager.dispatchKeyEvent(event); + } + + // Internal methods. + + private void setupPlayerManager() { + playerManager = new PlayerManager(simpleExoPlayerView, castControlView, + getApplicationContext()); + } + + private void releasePlayerManager() { + playerManager.release(); + playerManager = null; + } + + // User controls. + + private final class SampleListAdapter extends ArrayAdapter { + + public SampleListAdapter() { + super(getApplicationContext(), android.R.layout.simple_list_item_1, CastDemoUtil.SAMPLES); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + View view = super.getView(position, convertView, parent); + view.setBackgroundColor(Color.WHITE); + return view; + } + + } + + private class SampleClickListener implements AdapterView.OnItemClickListener { + + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + if (parent.getSelectedItemPosition() != position) { + CastDemoUtil.Sample currentSample = CastDemoUtil.SAMPLES.get(position); + playerManager.setCurrentSample(currentSample, 0, true); + } + } + + } + +} diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java new file mode 100644 index 0000000000..741df7eff1 --- /dev/null +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java @@ -0,0 +1,196 @@ +/* + * 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.castdemo; + +import android.content.Context; +import android.net.Uri; +import android.view.KeyEvent; +import android.view.View; +import com.google.android.exoplayer2.DefaultRenderersFactory; +import com.google.android.exoplayer2.ExoPlayerFactory; +import com.google.android.exoplayer2.RenderersFactory; +import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.ext.cast.CastPlayer; +import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; +import com.google.android.exoplayer2.source.ExtractorMediaSource; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.dash.DashMediaSource; +import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource; +import com.google.android.exoplayer2.source.hls.HlsMediaSource; +import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource; +import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; +import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; +import com.google.android.exoplayer2.ui.PlaybackControlView; +import com.google.android.exoplayer2.ui.SimpleExoPlayerView; +import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; +import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; +import com.google.android.gms.cast.framework.CastContext; + +/** + * Manages players for the ExoPlayer/Cast integration app. + */ +/* package */ final class PlayerManager implements CastPlayer.SessionAvailabilityListener { + + private static final int PLAYBACK_REMOTE = 1; + private static final int PLAYBACK_LOCAL = 2; + + private static final String USER_AGENT = "ExoCastDemoPlayer"; + private static final DefaultBandwidthMeter BANDWIDTH_METER = new DefaultBandwidthMeter(); + private static final DefaultHttpDataSourceFactory DATA_SOURCE_FACTORY = + new DefaultHttpDataSourceFactory(USER_AGENT, BANDWIDTH_METER); + + private final SimpleExoPlayerView exoPlayerView; + private final PlaybackControlView castControlView; + private final CastContext castContext; + private final SimpleExoPlayer exoPlayer; + private final CastPlayer castPlayer; + + private int playbackLocation; + private CastDemoUtil.Sample currentSample; + + /** + * @param exoPlayerView The {@link SimpleExoPlayerView} for local playback. + * @param castControlView The {@link PlaybackControlView} to control remote playback. + * @param context A {@link Context}. + */ + public PlayerManager(SimpleExoPlayerView exoPlayerView, PlaybackControlView castControlView, + Context context) { + this.exoPlayerView = exoPlayerView; + this.castControlView = castControlView; + castContext = CastContext.getSharedInstance(context); + + DefaultTrackSelector trackSelector = new DefaultTrackSelector(BANDWIDTH_METER); + RenderersFactory renderersFactory = new DefaultRenderersFactory(context, null); + exoPlayer = ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector); + exoPlayerView.setPlayer(exoPlayer); + + castPlayer = new CastPlayer(castContext); + castPlayer.setSessionAvailabilityListener(this); + castControlView.setPlayer(castPlayer); + + setPlaybackLocation(castPlayer.isCastSessionAvailable() ? PLAYBACK_REMOTE : PLAYBACK_LOCAL); + } + + /** + * Starts playback of the given sample at the given position. + * + * @param currentSample The {@link CastDemoUtil} to play. + * @param positionMs The position at which playback should start. + * @param playWhenReady Whether the player should proceed when ready to do so. + */ + public void setCurrentSample(CastDemoUtil.Sample currentSample, long positionMs, + boolean playWhenReady) { + this.currentSample = currentSample; + if (playbackLocation == PLAYBACK_REMOTE) { + castPlayer.load(currentSample.name, currentSample.uri, currentSample.type, positionMs, + playWhenReady); + } else /* playbackLocation == PLAYBACK_LOCAL */ { + exoPlayer.setPlayWhenReady(playWhenReady); + exoPlayer.seekTo(positionMs); + exoPlayer.prepare(buildMediaSource(currentSample), true, true); + } + } + + /** + * Dispatches a given {@link KeyEvent} to whichever view corresponds according to the current + * playback location. + * + * @param event The {@link KeyEvent}. + * @return Whether the event was handled by the target view. + */ + public boolean dispatchKeyEvent(KeyEvent event) { + if (playbackLocation == PLAYBACK_REMOTE) { + return castControlView.dispatchKeyEvent(event); + } else /* playbackLocation == PLAYBACK_REMOTE */ { + return exoPlayerView.dispatchKeyEvent(event); + } + } + + /** + * Releases the manager and the players that it holds. + */ + public void release() { + castPlayer.setSessionAvailabilityListener(null); + castPlayer.release(); + exoPlayerView.setPlayer(null); + exoPlayer.release(); + } + + // CastPlayer.SessionAvailabilityListener implementation. + + @Override + public void onCastSessionAvailable() { + setPlaybackLocation(PLAYBACK_REMOTE); + } + + @Override + public void onCastSessionUnavailable() { + setPlaybackLocation(PLAYBACK_LOCAL); + } + + // Internal methods. + + private static MediaSource buildMediaSource(CastDemoUtil.Sample sample) { + Uri uri = Uri.parse(sample.uri); + switch (sample.type) { + case CastDemoUtil.MIME_TYPE_SS: + return new SsMediaSource(uri, DATA_SOURCE_FACTORY, + new DefaultSsChunkSource.Factory(DATA_SOURCE_FACTORY), null, null); + case CastDemoUtil.MIME_TYPE_DASH: + return new DashMediaSource(uri, DATA_SOURCE_FACTORY, + new DefaultDashChunkSource.Factory(DATA_SOURCE_FACTORY), null, null); + case CastDemoUtil.MIME_TYPE_HLS: + return new HlsMediaSource(uri, DATA_SOURCE_FACTORY, null, null); + case CastDemoUtil.MIME_TYPE_VIDEO_MP4: + return new ExtractorMediaSource(uri, DATA_SOURCE_FACTORY, new DefaultExtractorsFactory(), + null, null); + default: { + throw new IllegalStateException("Unsupported type: " + sample.type); + } + } + } + + private void setPlaybackLocation(int playbackLocation) { + if (this.playbackLocation == playbackLocation) { + return; + } + + // View management. + if (playbackLocation == PLAYBACK_LOCAL) { + exoPlayerView.setVisibility(View.VISIBLE); + castControlView.hide(); + } else { + exoPlayerView.setVisibility(View.GONE); + castControlView.show(); + } + + long playbackPositionMs = 0; + boolean playWhenReady = true; + if (exoPlayer != null) { + playbackPositionMs = exoPlayer.getCurrentPosition(); + playWhenReady = exoPlayer.getPlayWhenReady(); + } else if (this.playbackLocation == PLAYBACK_REMOTE) { + playbackPositionMs = castPlayer.getCurrentPosition(); + playWhenReady = castPlayer.getPlayWhenReady(); + } + + this.playbackLocation = playbackLocation; + if (currentSample != null) { + setCurrentSample(currentSample, playbackPositionMs, playWhenReady); + } + } + +} diff --git a/demos/cast/src/main/res/layout/main_activity.xml b/demos/cast/src/main/res/layout/main_activity.xml new file mode 100644 index 0000000000..7e39320e3b --- /dev/null +++ b/demos/cast/src/main/res/layout/main_activity.xml @@ -0,0 +1,41 @@ + + + + + + + diff --git a/demos/cast/src/main/res/menu/menu.xml b/demos/cast/src/main/res/menu/menu.xml new file mode 100644 index 0000000000..075ad34ec4 --- /dev/null +++ b/demos/cast/src/main/res/menu/menu.xml @@ -0,0 +1,25 @@ + + + + + + + diff --git a/demos/cast/src/main/res/mipmap-hdpi/ic_launcher.png b/demos/cast/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..52e8dc93d9895cf4e991eb391eb3f608d89b4743 GIT binary patch literal 4315 zcmV<15G3!3P)~6bRYXAG+QmeMh z<9=$p6+cwf=e|ixvhG*soC~t_&w2dk_|E|#l$=g3AmQX@a)-d%4)=a{y7yeEy*xG6 z+KGgCKc%)Hesl-`{_5@Yi)1jFM%FerHyk1eSy>IOe`%e4JA@*uy0*RSWtpYCQ}Hd^afC+ zE2p`cz)Q0sf_CJ%u|3<3ty${l{sK3OYh^fH`Vqy*ud>-}eRl5L8A_yMtZ82wdd zvpKP6&z_x#6Gq+(q!%Yt@M2kG79Y9snvH zk6MrNtC@DWs;C2uCKCnfMH1^#L0w(lQ$+H`y9^0wmmr4c4HUC4ptNeLfP3?Ma4&xe z?iCRNwqNFFB;@Ubpp0vMZnsG&Ct%${~{l+%4e^KjtK|cPww>~v{@h@okK^F$kwIks>8y+96ikh(0f%+y@ zl8CCEEaE#zwOK!k+-|q4s;X+>lP`7eMA|2aH@S;)^7ZAk6cjB)=yx8ZQ9^>#?E#>E{;}1GXUALd(j+UkX1W1* zP}itNktp0|n-a*-kJ{SW|J|@*eV4QQB80*_5y=;=(Gn3oFHw$}H!@0)zNpm+kPl9V zQlH8DDZbMJF=DX@s|8y8TNLAw3mtg$ODjH2aR_N}kwZml6qVKM3epO;B`+^GLXz@A z7)1if7q!uXii(P^ym$!(!B9afBOtHxrIoZ3qSp6;oHwd7C#v0=l8Fk1bMf?$*&A)tO*GRT1hM0d^P{lk*+eY9 zOq1h3i8zllh`XblaKUT=#1UZVtwj^+%D!;&k7l-5b@mW=#QVj-_q&lhQz|7k4N zKboXQT6i)FpN~aDOe9Qiy#d?&d628Cg(OW>&~UXcXJ%%;OC(=>{~^-rzhqgSM?o-3 zkZV~4cE=|$Qv3@N-|dV}K+}RKRD2VI?GcGcyE+-#HA}CepcmrM5EqU5m?+f8M#DU4 z037SqL1}7&2MAh_DAwHeSTuU{=+lU#;ccjJW*N?tGl^k>oEed#!+r&6q*KWHN0PAY zCQXc>6+y9Hq9akC7>~v=U&3|hAUtkY(4y+=>vPtuS#u4MG_;05?Dss+6B~^bBsln5 zysjYPQ={|+`HE`j-4ji}{tS0vVNlyDRvx=|?|zv`8s3HqH+{tY$=i0@RgZM%qq_VFac1Zne`CeCqA_KU;P z8$^xqFK6}jNyvXTR&H`O=LqGrg^Cd(QNLpjxzI|6Q z#IlVNB*ZwI&L;O9!ObpaI$%&aWWO zrpso`m~j`81d`TRwrtt8e1nl4tf1}rCM3-|iFlGU{TM#{Yz2OJ{xhUp)$4CYQ#y-u z=98zWNb}P?co1BN5BZg&397EH&YLu8(hWosNLqu1`f^Sb_ZuZBVde=4;u%dnemjH_ zedl1_O>g*>w3w{CE(u~JtBdg&;)4;xVczuio4%`m-9D6+l^q^7Y}l1V5=dHO`0(N9 zaVfHv77Z075Yg1@=lz)a&>*Ip|0vSdUEA+UgqR$jYIFn%lBONS=srIoCA??Aqng-1N+qH# zq!sBcHMA9eLL4lge+uW`z3@1lZC+|{x-%;zB%~{m1d`U^(Yd&|csNgNHZ;Z9?)ULh zEV%A}#HbeJVpXTk^y>@zs+CYImUeA@u*6{V=FOi77g8%n=-$11qiC_wNI{7+k7HuO z6y1@Gh;xL*z_&ZJI`jH#u+IJiaz#bEcNs-#F`LcSIdkU33m2^k3Yj`}>aDbxWu&0w zsYmeRbD!!8T6j$#{Px&T%(>+)ZT_UYVb;(+8BJeLfh#i;9!1d?qa%m~I&Z{?5qAg| ztqOYZ!3WRdT4a%tg2Yh&O~hDDkl)Dp=9Mw%^~(XgH+CJqjvR}H*Y;5h`sFPIi8>P> z3(KgHaPHUv#o^FNr@-sa;lqd1ZoKiv3;YS{L{8&LeHBG<8(D)!d^87(yLI;~NEGiO zgBEd0kO^_Vh`NEpQ}OHL!+Bi~EDcZcZrVq|^uhbEXQV?hne;cs+7LY)!R55Hv_Zmw z>Z0`{?1vwIxSuRZ1?_YAAWrH6E%u_YYf||c#6uy*m@L z@B08nNw32;eJb1~CGD#-I)b^wLYuFDfdUW%NAgwXe4`b;e=A z^{-*^mE9pCN-RUB#7xAi-|j@bc8w2lq}QA*%uG81%kgY@WViN*4Ti!M1h~|ibMCq4 zUf^4x{ia!JYHB=-waF+!0xfmkAH4>{`_9F%e)I4y>rL{sBY%<;H{%5FaRQ&NEX1xt zGwiYl%?Lt_{vMF|`|rO`)=Mw#2z&V9hr5-Olq@q+kgq7#8kLrjHbbS~l3Xl1UJH}W z4KxQULBBZf-@kw5?YG|^uBDgW5kk5yTC}Jq3zyj#LHdZOd1F4)X?G<`O%C<-anN$~ zqo<~(#=_|7!#HKK?%dBsAN8Zh|A_YAoL%2^ z^ytx4x7)2Rh`)G$u?0PtGOef6alAS=3%~6sM^U|<*BGBpfxbayYGvGU#~n9|KB|3f z-w{gA>f5*PQw+yLoMG$!olR|%3qv#V5j*`jeCteiPW;BLEykhJM!01qh;#~m)Z&}u zBd@*o+B2eWdh?z@Q|*EY6DB0{FVYKiC6U9FFl=c)MDZ1f_J-^RG&yA;*cosTh-c@k z<)TreMs;VOE(|=i^i!=1x%8J`etEaDva&)q$4MO{=|D*%4pJ8DjQ(GJbpYJ&s^s?a z`|QhQq)Qu9OMh+@8XDT|_uqf-SHUBYbmI?!ex2!n$PseB{r1}d+#KvC`l2&xwYwgj z-nnzqAxLzyJ=1Bj@h-c(Jb0McJAB2U4T_yUUQ+I-vbOyQdlTSYB$%*7)jtI>> z{^7)kXX1LYnIrHKuF`w)nY)Q+H%N|tB$3N|_wM~Xhs$)%DIEW3g7}Wzv1!w$jHINb z7sRs~A36Fp>HnIw0?ve@FlwuE|<%X_#yW`7z~EUzs!+mTGFu9jthPn$-0ZhVu{Sc9EK0O-R=it zGPa^9K5~&$xQ)7vnl+5F$#8dDtyX_57K`G)(;XJ|60!t!T>Iqlk6Rm=fEjzcK3@WdKaF#o7U#JdDfC%*?x9-!L%k-lrv_#VD{ ziJrh+sGLf?Ob_2ms4S?LsC+NJCvock(?B@!Fg>gw1mU3?Dd1-i;r~+ubmLq2>BjDM4V6tA8agFxPWeyg zDpJb79S+B-gxDX_bOf6S$^hMn#ZUUj0(DuLppCGBU2Ph+?NYJ6M#aWj6+2rsI9&mw zAyrkK+}YWADj^u3@S}q=Af-HpXFjla30c1k+S+%BcOiGC!L67<9u8?j)x21{Bx;&9 zHiD8m4Gtx+38&NfcU@iGv4mhq+hK=HA*0b4M#A3?m^LiZS_gIMFsK>N5YIx*iiWoS zBgp3UaBGTgB%t`oD;4~etzh#G6^iOct3_f1MWVD`gG&t*HK|yw)^{gPnjB69R|9k* zuQttrM6fv7UI;ZO9%^Q!j@L*|BDC$lK^m)k=X(4MjLKJ#l;S|dFLtEnDI5tjn+3os+AI=IGV*JN6Y+mKu%ksoJ00VC=(I_AJOR>ARkhaE z)>nj>9!>}yZ|8#w<}5mSGl7r{kZ4y%By6*z7#{O6UwylR2!1=}F8meR#+ zmnhz&;hCT9NZaZ{7ZvdaGEmd#lvI| zTnCI(acR5uGz=$%jy~We>S!tDVm>hx1Im&}Y)VeV{PU6zV9S66xY8q`Wk

        KMFM; z#bU+%31}Gn0-W>aKz6jl18yEYW%QZv7Fr?G=FXja3?Uey4{SEu^>jjGCN5Kji!gwKd^&;Z!Sj)7WN$9o6U&FhFnQ&ZDO)-3e4Zr!?L z$ysj51~R=l-t=1x(yul6M}b&$Q8IF_qT;^VfP!lbXpHbhV1Htx!A49ky@;+w3n7ih zZllrB(UG5$l5!j&1b3B{m1k<2Rv4-Qt^qF8`k!KvdA+}ZjLQrtdoUg?QL$)^jzx21 zEIL?lAoR3~kA>x<51_1B4e4|SjE2c%Dp|O2;n{=`+||_7TtWtR25Z3GUI>j0bdHPW z)h1vKETkGJzAr&fm(bv}G94X69vt5Zu+))?Pyf8iO(bpsS$- zWpDRK_D#He`ip1MsbK4=V0&UfPb=eqeo(6_{nCk6%%;4&yz2=exT~nBxHeb=fCm+O zI`QQj(=qXu*Rb$>1LJ2vUvYY6U|@f!)z#1q=n-H8Sy@@v6GCveY15|5sEC6~A$k>w<>X!`1w5f0_ieCzu&F0-RXU@EW5P~~0 zagL%WMWGnzKeZeQlgp7fsSK}vk%{ToKYwr}D2)s#q20HMikAzyF@lOX_7Fv!5C_W_ zpF!EQ32v9muZYP2uR^7N{q@&*gb>_)^UXIW(hip&ssR?yCf@jX31(e2jB0@m_!o25 z6$a=_X~VODmr=(2`m5;5%7nC9x?M&a8ylAm88YM~LJ01t9d0xlM}-`LN#z*$pUoI| z&pTLnUXs6fHbl(7An-<}um8rOLtvjW8Cp|QP#alQRn<7se1zD)(T@D0q!x;Sc$)ne zzstw$D~9;b{)K%p<>r?;GHfp3tAw$y0oGrCfvchdZYjGJu0yHrUcGwttAt>LKFH0@ zz0>7#HHBIOBYs}NMSMX1^LWO!BkVi|_bsm8qJHE`7X<$2I_b#-Z!DX+6u z!oSgKbR~sodng7NeL3wvax40e+NvKT;icg7ep^_7WCIXNG(M$t_RA2DLY zWwg$&?1=$AU0$7EjU~nHFga8{U;nAtE2^ujS0yGUUatQR=&l_(W5$eli^bAP;SOk^ zB!B@Wof0%}w-LxW4nYjT|{Lk+q2+T3DYxeNJAva^=@fr&IC2p&LzR zC(UvVVkeX$j_5n396uK9;mAOf?p~yezj(XduF$K_H>B&7U}j%$eeSs9jtjSL-8zx> zhJc&f#@!Ztle-(=5-VzqaJaPpHeUsh2}EJowY0me8O*pe!-jQ}0t?bXUARF3HUc03 z*aCr@004llf^HB347eQyq)`Xp!kh&FGhzS7AIxC4Yx?+jdi~%3_1^#9`@`;FE;RGB zfNRHbI@xUYjBlP_A*SnkdODq2^!lwK_rwocW8E#^*QHz{w9Q4P=C{3X0G)GLA&xXoxx>E=Fui>S+dje4+TAq!DkQdDd zfH>+)GiV`gq7!tTUQ-g~+O}OxrJ|T}<-6DN+Y|ENh0<4Fit`|VXo{nFiXjPtT513Q zI?@`dwf=Xc<$2j4{rm0H}!r01!nZXcuLWEroA(Y&IF=E*W!Fi{65GKV1PVr}uu(cpp~8 zds^&w-KnPT(wcZj8278e4zvLh$pATUUbO3i^1uIfbHGFM4pZT%{RRlCLicKUJ znD~^f@G4E=sS|>5g7}(iqAH+W_a6?2?-Ujmjwa(zSf>uu#q$7I`~h5!=K$Dl2d>K# zVSYIT^Z7cWuZNw_BnW?2gw_`7l!fY`MmAT)`%DQ!I9pS3m4!Lo6$Ka!Z3*n^B8%v=Rj+-Mh_%DTJOOIO=_l5{0H**x@Lt5K)!Y- zkv|lGgN~m0y)6^Mp*|AfTYvdCw){*-ZwLo^K#Y$dYszC=x-B7?@N@A3)*N?X-<3in zW=VcuS_rO;N=E|=VlEIp%?D1i**xOt(W7?}45c8Kp*1Q@01`Y9055`g?E7NMArx*4 zAflPXK}yb6hlnE2@_{)yIoU`sl!m-|Rv`kA(%h1t^_YBFfY?oZmow3~z|WEpJDNf` zKMi7T6upPY`9YHaAN_6xi|~0dd?(s4V~3OPbXf#W!kh;v5+p>W2@pijJ|kJtg&Q|+ zG$t5iRG0v{xw-e#ADJgtJlX`(Sy)D~2n3ZDbSg%`#0UraEV099 zf(@UCTEX}#%!=Ww?cja@#dgI0f8>S~1;?mo$gy}91%eM{#@_C_hTdxz95X+=%C+8kx(eO zaV1$Ov7#GPx{4k~0gYs=@$qr34F*Fsg5mQ}+wBYBbUN#k?VKb)UjY>ILu3Gilc5kZ z&%!OZWFb_|g4mMS+ZP{l;>WR)0N;+XV(kf+d>$ud?wDBs$`zjH0#Ou^ot=H={Q2`< z1j84S0NRsqLPA0fdeh&Dd@eb45I|*q*x3TyONjY#a+Ou?EGRWcgw-jQLb+?bi<0o+h6nM&Z_z87*zj)%20wq3B0>0D7(K- zrqX6pj!@f=squ0W1H-6ZAR371C(R1+L=K)Lg*HSQ6kOMMI(B`R8@H zTt#tlag#fQ?S0^jP|LjpgA#aGE~w3JOx#BipqBtL>GrkWuq-1i_kn$#w=Bpl@@#!B{be(VKQj~;|685oZ>3nX|oQ2vx zb7qz~Zdt_Hj~{EFaVNo`1l}oP#E22qsqP;pd%rFM5W#tid~l^#01kX_#P)Yh*zukT z2Y)i+TE76~FZH2wROE+UEg@c+hazhdlF9#%#|St<36*gnQ7Gw#lgRfw%&{YIvJE{K z*(m`_0@z({YQ_YP7#iUHTL4jIBIc;cEwH=KU5W|~Y2U9V!Jq`*DIz^R{dsz`<0Jrd z5rBwIo#cnzpP8_>PB7LyPpBnD+uxIAL zAr!$uL^@n0BtjsPvhqcw=ZSoTAj%|5=gAN?4*vuaf+nJ;BO|xfAsCdvJ4MiPnvux2 z(wkMf0BkK;Gkvk+eG`+7wY8NX39;)FBXZ{XDB6+yv+UWvIQ?rNwly&#=0_6>Mh=7X z=n+cPb`$|+`UDx3v7T(PSZrs`oM}!lC_(Xu)WO{&2*O-`13d(AZuG{9ra?0KHw8z; znbv`DZ7!Rz3zkXt75R>d&%?%g!HD@b5IKI{$Z6gTmdTUgx<+Gfq4LJw3WGYt#KbHf zKYsju1jB#9yD~^gN%5kKujnBF6PG>L7ZI;9Y3c-seA~pi0>ZXm7@VXL_lH0BeraN> zDtVuc^+|y5NPqxeWc&FrklVg3Y)ckHNKIuCbPDn$h16Vy|Mw`a_|pVK zX~-{R@H*xjbWxIgu^u@fe~FKB1*9G71z^bep;HIgR<3}Mkx^m!LAIf+tgMtxn>M{i zFqB5g3mG)Y3#5yzx(Fa+%P+XpJ&+P;RR~ZvKjaVS567O}5G|Gpvn5)ru~T)~j9@4Y z-j#s1>Xa}B^5{ob0VoPAD|~R&&qz&}CbB6Hlm!bAt4aU`TM9O!U{n}f=gz_Ha^1EN zcDvnu>eQ*>#BmLR@-}SPuzP9xIg12k2@#Eg>1agiFMbLntBeK~qSddk&Yp?F`1s<_QKY#axP1BYf)*`W+)psb;9Ut; ztXT0V&9XP?Er6ViEi1iw+~83z*zuAHmp=<+qLJn22Qwkl*AFQ_`yt(16Zy*dp<5T& z*RO+Mmd4(m3c<^jQBhHmBS(&WoM4c_I~m*qJWWa+W&!j}!Y;}Inc==T78K-BIlAXn z6B2*)mo27p!DVZpKk|nRhU4G?h&G$YB2XoCV$`Tn&k+nVN?y=bZO@)P^+Mr zNc1X&N^GSHz>Didymgf~ZVU>*&JU&Or&{u5oP0k>U*Z*P$uecK>PTQ$p$T)stXZ?( z;9^Hffa;W`-x5VJxv~UslK{>Q-bftn$Mb%+8%?$U1{$(Oe-prWf0c@m6Ve>6^XF7o zNST?LX$u!F{GeO`XcGD!36NeXGZ6sUr?%mEGZQv7k`t{qY^J$U(obYROhi?JdItNJ z&8h{UW-N2@;>DkoSBlo4SwpJee?I!i2ZW6@U_i7d2uRD^mcD02VapT5~AG`0q`>GQR41@gQ(MKO;Rqi358cG+4dJ4egx0-SU;a?76QZ+(( z937)|98FD@W%@J}MnzS~Dw)k@b8p|i{deN{Ugnx$U>M{V#q)`~&YnHnoczk7rvS!z zm+)7MX+&ZbFUat~%@~OM-+zPi#0j{a4h?prTmjKSmwnl?Wo?*a#aWOnz?wB{KB90* z)H4aUnREyX-{`{DQ*}#{bj<7B6ZUNp5c4YbNeLDpDJdx>G&J;!n+d?%9X)#Vs85M_ zKV77c0F-=t57~fK4S)4$QJ%%(WKBoy0?0n!t_^I97ePo#VbUvQQU21UOGo|v{k@oD zg7S9fop;u(RjbzHG&Y@~rvL-{EXSJq%{{ui7r)jCQ-Ay&i#T#>%VQ1C9!tq--V~Ne z6X3dfrBa`kqg6)i-Me?rGtWHp~SvqJ2 z)S0eYQ3UY4rn|60N>F}pepNG@OF2!ntGJ4Wv~$FF?=(o ze>(!J8?@ka$_gO+eD|)fZ`#ORtTvz5fGFC!cJ2BBb9*!Ymj$@9Ns}heQqowYR~2NM zdl6%d6I3VR(2(g|Ih7Hz`QN88sOM75`Jgw}&{$(rJ(D!9;D>Wtw}N%<926!dXkT2{ z02=LJ|Ni|eyu7@gXKrsLfT1RBba-g@?%i!^FlW`P4`@eGF#DsvH|>I1pnpCegw~Uf z@HJ%{y5fl7-~Z7v982Hmh!~$1u#6oI*M$pk7Zz%?8}0T(i!ipCGiP>YPHRe=p)^Wf z$S7`;@9WmB+v`-jo}f3;trPAzY8Q_pu%=#fxx%3%!$=d;7MS+kNVFfjPbR-y1kAsJ zrVB1(#)2a-A2U|3k#7rD8)(RvBMD4kf@(-|gu9k@`MnkrsH2q5x1 zthkOvC(@9d>nJ8cN75B={rdH|?%lh8!(2+7s@&vih(;98pF4MMI^_blE&?ccI^m}0 zF!3RauVUifBwR_eLJ-BOCSLw(QV~68HS?i|9;&6L`cdqP36D_w{4Le7(N!S8Er~ag zIK#Kb;!sR3Y%T%Nfq1o@@3m{!`0oFKIh45caFdfUZM6;`K0KbvDWU2GV4o+@vB%md zY`BmGv(=^Ie}d|J_mgdlt5&U=OwDa=^~qP+hFG~rJ9Oyq5q)pq6e(L}0?0n!cKJ2T zJ(`MyY`gwGuks<>)2C0L3knMQl(~~QBrY{>)oHtzj?dDAyWR8h@(T19Kq9{RqAM7) zD*@+|EpQ2<{&u4h^aV!?ea)*karPW@r*!vg$^|dI^wP^GPo7*tgQcR|5Wr)*F;YE0 zcykQ)-pGN)sjts#_%dG6)~#DtG-%M^746=yY(;kyo}%XL>)6=X^MW8~5@6dEGZCuJ zZkYV8t8ZZS*$iYBI5qgZ{?pfs#SY{jv=#$-LEYnKm72+ zji|mILz&+0QHT=q?1lIvGM-4jq)2=-^7+sm@i>*34`-qNKCcyYLptc0s`13l3u+Rt z=7cO;(JQaK@;WUrEFjMlJpCuC*V3)z>9Jta{YeN0+fx4xG1zf68~G#>wA+pDzMqqm zBdlM)esQBljb4|nM_uC8ZbkQVRrJLdUwm-n$dL`SZs7Jb5>oS=;2M6)z9dZ9pM*^p zvyetVE7EQ^|KsKwVDt=vXmPe}+qSK*M$FnVYCAleQEdjbZM#uzy_ZaF-H%D8J@34k z-Ps-Nsw&s>8tt5`l0ejn=mxIm|B?;{GUI;Gw{PDp^tx>PZ6iR*WV)H>;4fb*(Gcao zLCSt}QBhG2{l4f#2mGH&_5%!Pf&bmn>6MF{HJjj$#0c8WR6gRN*IPj4<>fr_cu*H# z`2B7%f}U(Ro@Hfa+1s~oUxAJJg|-_2HIkQW*REZapP!#g?4kp`Zd3l3eMGRnH3dOi zV={t}CiK8Rkt3>BF)*>c{3-JLEz!}@Da1_dU-tQ^$_ShV9svOXF>P&a^YL2X9ri+E zxoU;W0Tve4@7C7VE<|lNR@{!2s+IqxJF*13;Vc|Fefsp_hzgJJl%VMbAO>9BI*L~W zMj*EzMvTO&8n^TFK+PQN0`^BXuowYbJGv|GYuaj{tV}H+1AHH6{0?-ql@OC)GyihK zRVO3pfK9MG9q_-mhlfWDdWZ<454<4r);=Hak0L>eraxQZXT2#NDP9- zP_StP^%5Ux26p5OLUkx~=+L1G>Izi=d4b0{EOUZasp6U0WjcW}p?$o1(4avIWDTRt zBD3>sMA+SGNGm`~Vt(A3er(e_0jXHrsI*j>VHl zh~y8{)YL3QR9|jlViHanMOLd%W<9WIE^+4QN^R5z;M@pOS64SQK0ZEk>eQ)Y@lsbO z-r7HX^XARR>?YvK5M})fg@2goF!URoI}y0nlfd|*tod3EcEPzQYMkjMr73$ z*|mate&Uu)#1K4D_<#d(bd5%2SvYOlwDDbCU3F*z*omH+YcR+o_~n;u@1{|xM9yAO z?3er@VB*j6clf(d_@lFD&tBUJLZQ5lW8~%K6>?0DO<M8<9=i&6l%Y`wU?OwghAFPczAdkQXbi$B1KUuuwnKi`19$fw?>N=eqh8I}>`g{eCfNtOW_Gt_@pT--A!IZ>FJaevkT@go zvZ9gTf&`J55|}xaL6#?5J6uV6?Z;@4)Efi8c|qc8Y)H-7sCjxPW_z-lU;4X)S!QNtW@ct)W?tLFcf)2J@0giMW?@&g@_xo;?6lfmd!S`` zWmUwB7B=bYCsd^*$M<$5M{*=bawJD`5RtK&q)c`u<3$_5!`MZz1>$k;FDalzVHCIU zfazd1myaH0e8c+i2K?m@HKF}}p$!i_*nG*R#8iHL-x1C5Q-WhAs54IT!|K^+(*86t#l2L$07wn2=HK{UZHJEsndoK{LRQj^WRxkT&JP3vbK{j4#l?AW;XlUq zKG2hht`mvah_M@i9BsU_g^UWKqc6AgmaPC>Rch-|X-2|64&p!4^4@2xkVEGBE@s;(^V2aKYP1zGEh0 zqRawe@nlq*lvQm~Uad*Rjf%=z6xDYr>gdXtEt*xyrp8bfZ&>pfTi-4T0k!@C$Nu|w-x(76c6~619hzKGo zf`|^t%F3FusUi{q{Tj@PtAO~#OG`^VT5A?hvAr=knFZv8U8xd|)bH1BI#C2Il675O zT?Z~-zI>BJB)QMe!K}Cm2>$?M<;s;FrPR)@JC7_awN#L-@DKp}*ow$|@lT??2v41G zmS(S_>UKrz8cfQDP%^5-fkvR9&ZPD}O-i{VBHkkJEJg&_EiEn05zfZUfqn#L#6>`S zq8S+(21K+qu4A>U;$e~ayGggKB(?5YQrWkG+ewu?Pr7wASphFZL=(b^c&mZZ7IBCg z3Ru+Kqv@L_1=r!6Xn1Jj*XueJg-mxuq_w7@p`o=%s<%r`O&zs5a|mb-m^SVLvMs~- z3(h~)-Go__<^6_K?mc8KI)=;zhexycSWsL!W~L{w3ql`THcVQsh*f!Yq`N~teNb+DtOwo z+syjT5c>L9^m=sV2P}q76p$^fEjbYmc0cK^Lb6nd!&(dlqzLzg_PNhTdxO31_0fJ0 z_~@i({dE7AL0Z>nA~Zz{DFQ-a&AE@$iCB>AVy@&vT5CdosI0uae0LzfUAzP&;z4p| zAawl(nn~k37>iyHDlQho79e45*{P(~zbyHY;**V;fSjb5Dtz zkrLJ-Ti%KGXph4yKRi7z0V8>Iuzj|v_p-Cg2J ze#BxRz=<#Sa=HdIEhr4rWp4)PpoiFjqQlya9|qtq(cz3MFP_17R%P6&wk-ET875qp z=H%r38Kk_OT4Vs;#$G`A`S}Ktu<1&v>xjs24_)yQkPH0xY*J-!knXSJrlotOy=>e1 zG`;wr5FPedw21acID2wokk<}HIT25XcqEytFDG4>N>-p}v-?LQtwdyp;k7`Y zW{0Ch#M{I1e08}=5r+$LX>XtMzvQk)5~B*!FhQh-n|_ z^Upsws;a8yXsutcPgTTIK9!2Xk7$#oD!A-su_z58IfTVw?e&!*DG?}wD-LmM8wUC-9ap?nTp8f70n#EzvRROiMv>0E1{j~=NI}FF{A`)9qWC7X3 z8c&O?xsr6lBF>K-VQus!t+_R3ba;~KiyPkG;6mK7zVP(`we?0P(=A?wK9Fr+wY)F+ zOth11i3<^2$eWj+f9(0!KRNaAM|ys^*L}X}AZ0A%!PtEE*=HV1wEaLS^}e%gPj~^@ z9cl6L|6exJp6y&V;t-Gx2|Zy)1@-Ql5FPm7hLZmHr~RDs5K(kBX735{5H*98%=H02Xr|Let zN~~3~n{>~)R)s%&w}W?Pw~n2+JKN9W12Q)lAM6MSmcV~30@74X&YU?l5GQ?y|ExD+ zvsCE>O4x@-AZCXvMQw4Vhz-C-(?7@VbmjMf6j~&|W)QN?F>8xZvII)ba&Vgd+mycp zY5wN}*W%0x#6q-x2GYRuAXy0{_Q{MdUc3s}y#IoCoxzXOcUyj63ka6Le=K6nnl&28 zBAsDi{sOiV5U@%Q2r61mrAd>b73?%d_OtNM*uoOaKK=^8+=1pZ?YLxv2|1P2FK!T&*b z-p4+QCQ$4yS9&(ylTQ3>pdE#MX=~o3v9qWzop#gH^QoQ`x7!N0#_`eF0mQPA$j~qx za3YC#8jt{(9yz@?PwTmOxc*EWtvDVF#L;r3_s|ka>p>u%+3&m+e@8-ul#oY4py=2H z^8fSC2N2@d8Zcmh1`sU4;s;i)T&bNfVS*jx8vS_!QnWrwB#`K??)34dJ!O4qOKu5i zq{xHM0Q)NX(!-&iAYRJdWca#<^k7IcdNFni#b3Eh#@HBfEf(-zotQ+Gnn;S|+at;S zj+_}8nD0|V_UVxYS}_)tiz78JR7A;mv>i2j;XHlkRF z%0)tZ;~0{rf`bMP(g4E0;a}gpapQ)jTD59g%v}G%sk>M>R3d@c61@2dx4P1bBj0bjcJgPwvs-H8#>y(>j;-9ibkU#n{S z2lPUL7|ZggTD7VU>P0OeBnSSJEN0D`rCGIVl`~j5Su7kXl|Y#ANrG!X27^Hd;SpD!gTr*fGGY}^=%GW0d~n$vJ$h6F z2uXqeB#DtDM`~-=u5FJBeNY#?GD*cJUk#~UrHv>_0D?QR=|1!F?PbrocLK!|1;=KZZV(eu4EvP2zF~6AB`AB zv8PX&;v*u(5D2<2f@tsv;@~{(+qc&NLQ+WbMHb7KD_4$Jv(&^7K2QUJZuJl?knLeD zfut5GxOr2Gm_3u?Zr(tDEnW%Mh+7-T|)^kUPxpkp9QQF3P)ACor8meX6n?b zT0oFONa{sYp0)k^_jiU`ZKAaIrQ|GZUj>yC$UN=$@uJY)Jt^kk0SMUwnM9v6oqu=! z{P~%U8Z}CTu7vJ`TN$NFmC_=R&H+ETP1OA;oj@G$qG_sL0!iH(3Y6jF$5HIrvy>PU z^I3?5k##~DXWxt&GxAohTv-bUQvAOcx^?TOVfTeEmO=o1UYfouxf1<)E-KNLIp9?< zfut5Gq;*S*m_L`|A3Wf;$R`c7Q4b>M->u548a_N^o0>4M*Kyf$B!S^ zLm>6svinj}FU*w-eWqSm9`zGQv_|~M@ln*K4ICeR%Cz6;@9+N_F>!rxLff}*UkxCb zLy*2OY0@NZu3Wk7p?z+`Q?P0x5QB?;cY1l#u412_Ks%8IKK{|058O8J_M))CgDB?E zACwptYI73t*&~pS#t3v<2nro~_wKC&1ak<|7bZ`hY}cYi3l}s{2XXUMT?7L5nL6aB z-Sx7PV>=3r`$5W66%BN$hMun0!DCH5o}*S&+?RG2@}+ZC^z_Ef!*&D`>qku+Q}~4O z6nFIs+9HULh-@kEFrGMZVvMV+YwBUchS>pvIRxno0|ySY8#881I>=XNTMnQVDk$68 zkgLbC5QiJAex(d_qpml-Z0JdE-90GK!<_;>O^^Kim8&N`|Bc_XF?l3ue=y>s$fb)Z z{^3KLv_-%l0uSiixpPaQ56S=oV`tF~El(8Doe}bnhDLt$X ztcnklgAY!z2L#EO{idkzqq28#zvik&#R!*_JTeym|8^ z2$Vys)#?!5XaGTkVE%;L_8Bu~w1bE~4^lWWg;y?t_L&g2){XA=^`e8-Br*#taH4`9 zt_kGvQaE{}ZVj@Lh#Au<{_$hk=I5~n=4#KLJqu;YlEt1gv1GHbe*OAs^W@264>4JB z5a_nj2^786l^&1uq~ky8O?@yKh{NRDQ`kTc>wAghQ6(T^gQWHmTOzA_Vh( zxbpe(<#TA=y0sP%LW+?C5#69`@T*9#Ub_yz=WLa|ww zY$Y0QOouGY329qxeDRG^2$To{@sQx1J|;Qo)_gWB1{UCApqKgy#6V*<>i4x)69}t; z0~Rk{T%WlCFQo$n5rX*>bl^EnbH>UuZes*0g+R}?ETW@7qE%4HAi67?VOJqvr0pVs z)Nh3%X3mg8I1$GVN(w z9s`19+Y;!{FNHur#OzsC6A1mVphb%o{e&4o=Vr~C*#UwG!TgC{ueZY_hV${`$LoMV zig96cPre}ECD*9Ou-!E4*U_}8NOPOIutyEPAQ!f2qZ|YZK#8XTxZ&dF=4J;7A_Vg% z9v&Wcg$fmN_Ve?r!JT-e5NO`X=j5~E7J09{P0i+Cpn<*C(~{bKX-k2|Y>{LSqOaBQ z`7(m&mdz>t&TXp+gk9jT(4F|MaN)u(zyA8G9UzDh%%9Y+Utfo=t1}i!RYDs77-_ts z2qcn-A6v~jL!;X)qE(f;n1X5eZ`f2M8hr^Cz`x)zYC7?Yv^eic%GW1HBGiP?jxU(G~h!D)5z(ncL?{I>stRUo!=ao(% z(IPcobcK2j*-7*352a0oo5|wVj+8NQ6IDGV5`|}NT2a)RRcNffX7b1}rApVXUAqch zba9!Ez+uJ)bM*u;DVEzObud;J$ z*|KFW2#GeTg+NktoZv^xtM`VMS)7SI(lyk8;&mgF)VKg4`<@Yy+_)J z&ctpkm(PRCi8%r!dub2{IsNwAZ`rWoW3DJDQyYN{%WlxbMl)oKiS`#W(8cOL^znK)VZ@ z1odpKUfYsH;s9dX)>vS*h~l3Y`fkUL9TQOA$^1bCDqp_50|M!^P@%QN zk3^`7KpY3nyFe2f&7#c(n|_w5jE4<;Y**K59A_3 zx8*cm{ey*KTHduYMQz-`an2`S0|NuXDg1Q^4QpyN8W-jUAecjtzJL$vQsF{#wFs6g zY!^3kD4jr!7GI@V4aa^tqs?eTL@bz3ad+-;oby>`<6%JPq@m%F@fH%!t*sy_?S@?d*QrEO-S_FmMg%3 zBx1eGT%_+g3l=Qs!rZV(AY59Ar#aeAhXPVN^u&VH;KI1Ux~Whz^{#h8?<%xkUy44k zpW_@QEaT*{<}idZ-NA+Q?>V6bEM6LzD+ff~>DRAcUmq=v`(jH&$pjkHW+8Xy)kh$4 z?w$7n`THxP?k4uYuqX2g+FA{rot@J&Czu-_M4${AGT3L!mMtCnAH`tC4yb`Z<6F+7 zE&1gk(1v2IXomX)n$Ub6EvwmEl#R$oAaMkzDcTRSu$k=EO>RFZX>%(N6cR?7k2Yw~ zpbT>%efsqFA50)@q|)I_sa;%LvSVY}1pHvU(g`%O{ZiC^npjO9Cd>F{bE)y-E5sG; zRx?l2*j5WnvJw6ah-3|cOmU8$!bT1^?M94>QsVAtUaF9=dGqGkus%7M3(ScRB@ka) z~h!fv7u7vJn(3;nqHE(Kx^|i^vtru^;-e0@nIk zo`=VV-b?6-R)Xcpz+CuP0%>sBBl1p*ls+FuTc_iP6BSG#xSQ)VuK8TZI2u{uzM9bR zjw@KGmRz3=mgyET!G?5QO-ngG$}h%8N#E)K8(2;4hZ{GPyP;l^z6S&Zyo38%g!DZ> za{ycr0ZV+OgGoozPBHB(p z&ZMO^OM zB39oY60BQ05@J?p}nFBB$BAhn1eLU zV`B2{4G?E-*$y;WKaDy}Jdz^fFuZ|Q>+aHg%&y&f70I`cR26BY%~NzA?)T`?qamL; zpS|e5QhI4w2a9w*6H>WCSpB;gB5ScXrdR@r?xo4X%hVO_g=<7JT*uM;ABF%!X`1IG z8rFFwbwaw1CMPuT3sZuEL|lOjkjZbxfISiE1$V_?n@F(!w98E zgEbenG&DtFk*yGY4FvLjZ*`ily+bpP{Yf`oL{MyeqT1b+bR#|QK@1o^e0VEPm|%U< zi@~#81k$l04b032(S2E{)YfBONGvRVu>=z1oDut<(8-4(6d9Xfa#t!Rj0B=^eruH~ zRm$@j z6weX+wBWPlGsam$08Ia!P?bD~vPgwhLOAM&hP9QI2 zWi1frOg{XKu02Ezx9z=As`h3>g?!~6@}j%j(|D> zu1wVgG9WDjX#4EiCL!PP<_&aT=a&RKBk1wDKlMG1+byZS0%gwCDN+kA?h zKP_;M1%a%5rZ%-c5-arA_%q_6+(`S1Z{NOs95f`+Y!;PFAa9`Qn%gwd?-~8?Nhrm{ zOS8zY#&yMD=iwBbT}eJeK1)7R)7gHSO9wcExtZ`^AU{^))c~V+^G#Z9;mQ`Dc|m$B zEsif*7JYX~Zh^16+ z7z4S=k-gj`+KF0BqgA(4Tt((M=l6@R5$Blp1%8d>0gSw=<=C-fH=8$a?g^_?kk1Zh zXgO`SWk3z2;~)%aU~#O&?~ZurmE7^r{Wnq6cEdgL{>UJiWfpT!Jf~YPBUol32FPC} z&^h4ye!UqzdUPM0R|z0DpPh{ZXUi=Pz!?@O6SA`WC@_|X9AL`JmoK9dWSr;bSY_GS zm)r#>?+_Iz~B21ydWw8USV(Dx^=(dvjKAAtmMzSth7Q}zzN_2BNwHLVl4V6 zl*x~xQvFU&60tkG_a>4K+ztX^UeVeMuQ{`#2XCV!?T7!!1216Qy?gfw+;RC=oJlc0 z8$KgI6w1qQeXNa#fQ$&x@*$`$fuThAbGT$B5x^u##A6%+V+Oy@ul;9r{z0C12qpfH zy>o!EEQ!MKddIffwmlBkt+j0U9#P&H zXQ5H{Ecc{O?^z62SETyTs=>D~7S7Lt^GkT~cSCv&KZ#D1Fj2(JLwn{PB#+Fq35-+X z9=I1_PgaeTc_LteY_i(323c|z!SqQVIiEn;j-40%oG_scK8{Hn&r#@Sd~ z#bPbyk(%o(c!hXH%Z&`Bja{9T46F_Ra952wZK>omIlN0HsbU@Ui1 z$JYY&?chIqo!s*yk<_!qC}5Q-b^*fy@iR|6v9r~}ojnFIJO$k-4@J=8{;IC7u69h* zOP+rE>5p(e3Z|eg>sMQkLm!z+GGWAzF24BUOWWJqJ8;HUi;WnGm6!$WR*O5QC+;YI z>UpUUB{n@a%itfgY~sX;^~BP)zMerg-4}t8mQ^N_aZ(ZV=bn4+%d4-x`i5P0*<~9r zXb=lA5gRcISc%!H5k%FOT4^*On4e>qQ_Nifv=aVZmY0{;viIJ5Z%+yCjn6#u%;$=A zJE`lwUz(YzZ|z>w=Iuz^HP>8o6NhGdy5qJ$d#SpPScpl$MvMk5Q&J}aY2>{=juSTx zv7Kiag#VY*cD9iek@T_=%7 ztE63Pf;f@W=-cJ_o=%;Re5|sep<86Dv1s9w{4$PDEF8T)R?Qh-uN+vd~-=$NJ zBQOk-MyBb7!T+X(+&8ZA{PWL0ch_Ba-LAgAevULiPq~aEZPF;M(wuf(t+M!}PQpl@ z%4wLf>PW?aU#7^XC2d`Gb@ePahN{amX#DU1iEQt@@x~k9bK+ABrKh{En@HM7{eNL5 zBgHIq^G*HVpRI1@)r7+TH~y^ev83K5#o|HNgi^eHRaMn2*BY*=GHH+&X-bngL!x7x zgpmYyWY7zySk%bL?YrK}bx3ORQexD)$tIg@bnw9k?|05Q=bU-VEw|iFqW=qAcOSE{ zKcR^Kt!I~X+tjtf#%l#x0;A+`rZbEG_j>ee3@7N^>rwwANv>B0MIZa=g%@7<7=7ji znD=f*IZBUHvH2`vg4YPAyaSVc z$W{0W*#e(A{Wm(xhgeDPP$=-4Uwhwu_dSMZ_;!3%ml0YxmY=zYKil6y`OFN~QseJ- z4Ph-MuFW;N*0AO=Y0B~H{mVEBBdr`Pl7Zpm>KhpwHfCz5f6_5o6gIDne|z0ID@eGi z;T^0-3hkN;3JTgvOG{_)X8v5-ovh91Vr)o4`o>QGz0Nx8bon{1pt=9IV-~0m1VIpm ztlM{YjqCj{m#*+)eSqiyFS4z$H7Z}lC{{U?OF4~2jpZz(+Uh~Qs3&^6r`MX^6C(=3 z*@KWEV&tYyOt)P1zcAvzjlU?0+O~Z3bidLx&8V(xz3h=T<#8NO?B!3*wqyoVA)9>U zs~E*92X{3+K=!e)(a=noIVM8ztJ>*U_VYWGfnaK+(Gl~sSUGkD|j9d02_Xyw- zp5PH4@VO)Ah8&mVtf-NB!GxLS1Ox>!BpbrYp)g9Q96Xl~{s@I!F|>kUPFzr4R1i60 ghqt%9Sk6%}a~ch-AGh)%mH+?%07*qoM6N<$g3o+kWdHyG literal 0 HcmV?d00001 diff --git a/demos/cast/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/demos/cast/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..d1eb9b78cf195a355c8354f38c22999778ceb52a GIT binary patch literal 11743 zcmX9^Wmp|Ox19qAhXTdj-6`&F#T|+lmjcE8;85Ht?(S0Dix+p-;x5JE@_skZ%w+!T z9o;J{D+yOtltw|sM+5)>MOH>a^*!(Y?}dkb|EjLq!T|tNk*tKMx;yA(2_!+OLO6KI z{i%(nO%F$eT=M`;gfclEdh^3-rXc7*1Td2P0Mwk3f-nIw+5jrCJRsorkAVCmbMy1I zKm0or&MWIS_)TvlB;twY|K9x0-^|B^CG)2&Iq;*T{y!^PIrY3ir<7B|r1W-YJuB_; z(-|gb$<3hG-ARoB%n}=T8$bqii%(2f>;tU+bbpMnT^Kj@O+YSxOkmYgyKzTexI$-& zCSjFLT)6SdN{kUkElr^LEq9P{ntPat(a1s+BH>Y+`%#!3LbzOO{Hc>`gYe5(@#F$e zq#`zaJz1mOFD?h7zDmY)iQR5vw6z1^B(rOEA5*2EnHn6UDt=RnEn-N6yTxIQc?DdaXpRL#6_{Or*Vg1Pu7t}LT_Y5fbXc0b5;YDIIN?H{q}r%j-|q(}R^06qQ!#Vl(yFEMs)_&7zm_g$CRNi*eH zOP~T{FT=At*bYL|K89ZJ1O4BMt;*(c%D{R_$roSZ9v%~dOH*c zBwEB|{LW}I*!ceE@ziE;brfl5sZek6%_pfG3&?imqsz&UUev}5xQ_|SC-7}0T{qv} zU8>ygSZ)z>bzp6Xl9iZB*R)6@?c|}~ctP~SG_oD{krtd54@$1v^W(-m&X@W~W7b3n zME+zZEQTng_La*^{{E70CqFVV#AXN79{KHhmhLDT59E!mTadd#+W|Pg2|`u87aWUg zzD}V=_HJ@}S>UZo)kzv_ZSOLD`Y$+e#Py_HA&pV0hG8K=P6hz=jj?*zizZaKwRSq(v zYnm@mfNw4=EG(BA%QlBk5{>a|bDSx>Ixa^?YpW+xp`~bdKKk(hKky7S^{XZHTcRjE z46QwNL)l0~g)YnYV@^y?1odTbS}P^rBh{(_uQeGui>^U6_l9}>X(gr=y!&l-PJ_-W zRF3^Ot29?0NQgu`Nvo!&4Jc_LC-;{^&H^);Le97Y3H$Q03vCOaf+xI!+`;X7)po;83uz4x3T>1{h$jwb`SxBhdItPj!ppt z2*1i98!9kQU-u^)3Kjo`g;+T4HNoEl?*_R|a*f~y;$QZ?Chm zlJ?@i7=tE08S(N6AV&TY3XjJ|XGfM)m{N*IfO^!p_2Q$ar;j#!~-lk(8+Jk>;MGn92!*GZhSx)3f!xH8uQe12=Yb@uN;@_)p(L#%IzW z8-WvZcCzra9!1Zj+C01eq&mKysCkg3rKfL@RbLFs2ZMV2Ot1iEHv{Nb<9XYde4^!5 zSqf-KBPn#<8R|WU+n=BJt6C?`+v0F%uk+#iuiv*;{u0kN{HQ6DnZ-m(#zi8_7gSV` zWwW-ivHx4ZlQ?Ce81(f6px?hQ1H1^Vn(KPhoUyRmn>OBH5B*}i_hvT-l&f!JN8BgE z@AlioLwe@LlgRD6KUyVTgGzmK+Sv1=AZ^fei&XzS$>oH%=p{>>%fWhdKf4L#u>}xOW&-=(qjS&ZJn3Z0q-TgW&R` ziqsMelRSEJ=teQuZfBJM-FX*si9WE|Z7f9~qs*5&XcDH2?tGZ_4jw}G5jYO?7h?al zZ_@huYg)d5wj*>m_;CPhxYT^`#WUQjNg4;3?`0q*u;`;{OofHGJS`|XqvDH`nr;40 zH4l~NF_6tgQ@7u=76xN%LjlAN$A^bYqJrVr1}}%^adljQ7AF#)KTitnS&-%1O{d4x)wr(=HVbN=o0In zFHox38v9me>0&o>wu&`~S1%hAwqJA5ly7O#z-xg6Z6YYb%p)%##9(V{Yu}w$z=JUv zB=av+$cpgpqC@a2EHK}W88%{Hg3c|cz>M(YZo-ppQ4x)Yklu=G9Y&rc-xMoMoWy0)*yitpwa>vPYLF3AD{P(FR7sRZ!!3fqWjceUqERC59e?50i8Ay) z6t#MAT3XsskwW?}3XhY)pGz2t@!D)4K;KU^HwJB6;f3kLSEBch@E|dDTPnFl?bO-o zDIGwY{APT=D3{J!0Wa&Mm>|n39x^gA5;OeOdk)cNmcNJav01MwAH?IVkqrhIiOXrd z6ZmHHuRNdixBE*6s8sue{QKW5Kdu~x;-5HDdMEeZX=u=^PfFCk)wHycF0k59iU~HJ%(y3CqJWxbC~T# zzYN`%;-J)fYQTu86EywHOZ&>CQ$~)#kkHUThSqsiW!7S<%i)bR9F^T>(AA_LSqy$aAJzk(C%-7OKX2u|uUo*w%?yXT=<) zAL_>6;4f+g`9=#IxEJ;WmGty@RMP=6bfVKi@z2r?Zm1@X<_|yg6+Mig&yXZuKK1ha z)A}SlcnzmEvu$q%vm%$tN>fZ~IeBd&s7tS%2xks()&Bypwgvr@d=;#Qi zl(v508}0}aPdq)2#9)DP(2DQ(Fu8oS(|&2emJ!SOyJ=!x2ezl=x>OO>Yl%U{9<3tO z)z^2CDaCW=BLai3g@uLuW%fBs@BQ)-xNC^0y)t3H9!hjGqZjx@fhzKLmqNQ=bA@bovWd8(3f;CyuK zbGm6>+yQq_-EJkj;!|gqBAdcC`b}J%s=i8h^0LVk}+D8+ziS`SFECJdE!D=+G#R0 zny%pwp*zMe?Ok&KZ8KL?Bo>y25Uv3&$Ndb|PzPgF!bL2|=++<^v_By%+M1!c0pndp zA8t09+#x|sRV~GVph>jx7A#ChM|X@!mVCd)QV9!Cb$+^=2*U@EqQ@Xy3=QY`+ntzy z7Pa0wf>43-^F4wsgHB{2g?d}|3T$&sY;1EPpPMOyIIScLS=k;5VHm7X;oI9=>7zc9 za*&aija&`JRme4#H{n5jM-6P`8oFzU`kZ-Jzn&Q`3g87k;Q{ z9HM{oLsCV2l$XbqKf6OYNZD0OP8~!#C&=rJJA2(v$tavny7fk24BR~Qz@3XbyL~Cc zm&hsr@w$Xg*6(TvvE#}2yh4Wx7la#ByM=WAv*!frSVqJ603@Y9kn6iQn!bOAETXTR zrT0#5ugg;~kU6h@GY5qyr0KtP@@kP~qP)Dk5heJT*0`A8?-SC~+tMG!m^5LHZf8H| ze*mo3;|^(C>0p8vipzA!GcOgukU78o(+g9x0iUHrfl@2{cmAil;e9;@Eshc>_Q7xV zAG?O%a4{BWNbW0OB0>%7e(2eBnh>$Ejr;=K3#n)ng|-SGc=~Z#ZikhL0G4~gKYK}% z%(^PBg)87idR$7sVgQ;)>O4Ykm~~w&-$rwtBQk#I3XxC5yAAjC*#$YCW;aBqwP+9h zRVs&4e!H1{CjsSm%jH{X{BT0m+|fb|Isv_;ZEK1i-PV7XP=?yTJvB~{w>mehZWX)H ze{^SOXXAq}h!@O{Q@FZ%dV$~thrpFCZvpP+wzhueDz;T}nT&Rxn`6mLeS5^8X`f7$ zs4d3TIkSe98|RnG+qCX9Z-m>iu#j)M%n{J)>luh>k1emkXy}6DKjY6<$5|ghHQ`k? z+1Z8%0We6`&}~D|oiuQe*EgDP*8_cqsT&P)K#+yk3;3Q~Si2dbYS1x4`&Zed-FLCk zCIz}?6!l8pJQGN*QGt&Q7hJ$VzU@jzKx?lcIAz%>!Pu-1b9T=0vtN;`V5`uD1?4-0 zX7M=oqAbFM_?vxjVVPC-yWfJ7$zjX@-pFpc<`Ezu>}=*ogp0~B{2KHzp>F$2H~dtr1DrA=jbp(O-dyiaOtasvl#6Hg&Uqaj6pZNf%@Rzhz@uK z)(Bln-5$+34reWa@8LLR$20jqUlBxr7pdVP@Eem!q3e~|V1K&;jPB-2lm72=9@hg$ zAC0qgk^kO?C;4yn1j13gUNv^38lvRBEg3H)ZrpUp0JgzEZ?;oK$Zh;4@Dvij?^=(! z8MRoId9dN-+UIJ=gu)gnJ5XuR(MrT>%ZUDJ4%P0sGr;)lw3otSxLl6wH@m7Cwi4*6 zi#Z#*gyG!69HYY3l;550Ii2svJK6W^pL4Z_(v_W&5)D?wjd2!w^P9L*MBrNUsV6*@ zrx%5=%$*z>U+>f1ISCYUJtukjvV=AoDH01gkxldc$UHn6>p1&ZEk+XIfl%PH0?v-o z&BZr;x&k%s%kv5rqqZ#0z<3ftxe2*RY4v~)Btj>M$mx<0m3%3)kaA{vdh61Qw-tQQ z#~9~4Bw=2;6gKX(#i^AleI^vmhlifDxx|m?#*R?%pMMW0(5$@=g$^g^Xs$ezkd(hE z+3g2-F&Q4MZzrsC?W8c^4b3ex(Dw|~ZOKLc^snf?FEs2fMLQlSWn*K*jT3-Hh8``0 z@6M08c-xHlesT6zPY(J817&m?CX6~jSVjLf{8ke+d9fUGz?M&Yxy%`nsF174N`82J zeB6gS4#NmNL+JM)aM6cDMws(=Bx!U^oOK}#{?9ZAXcF)`nQeCgZOis31c_eXpwr-od`)U$y$q2-2#!1oOXHBFNbha@a31S2GZ zCPnheVgPw>Ud3<82<#Uq#uy@guJU29vhLuL`>-++tlxJjDv?e5o#64NA3WWGT-9xd z9}nsqqw3%x7^G$oSAU5hX1*mBpOOGS3^0$&5hi?Wq6z}*;0}GIweX5Hb-M_phB+kD zP%tk9HAxm?nk*6mzE770D^n`}%a@MfvbY$uP=|`-zfV08c(yz_`P$Gjj9X6H6Yq$|IOhml~)wp#@f0DM%)93V6*Z?JG zBU8l^Yi}^qAq?1EUFyMDBQZL~Fr-^UU2tgo`1Z*#p@~RzX)IXQ_OUN(RcW=NZOVyz z5#7T^PbJ10>3~H@NIf5K4uwmpMc!Utgk?$3zzciaNEGB_Xd=HS`*AGwkF6Redd;SG zPM-YWj_VbH#lk|DfQR50k@JB_yct66fi>(hgziCUGp;_t8FCPsE5XZ<*|O;JvQdzv z?B-n{b6|~g!~*Z#|2L+(?F)sokn;{!?D zyuC3rd@NYW-Pze0o|(BO{@Tn_^M^r|y_8CXD;i2b z$7x;@p48;`vs1%XsO*&mlDZ??P3;l`tCw|29Dt%r|L1O``wKfL5Y-|c( zqHgALXD|k;4>9@8xDHFqHfSgN8V(MQ1zr5|mjr+wD^aHufYf-}?@KZ!f%)`;F`m^k zmUzgHIk6&ddA{C3+|623kqr2q&z0kt(_KG2Jt?8tsMy8S5z0en758G-JMBNEMOCp^|wGl-oViTifd@40?^av<*_jn>bXZk>9U zxi2-0EWn^07oEN%TvzEw$-4R;s2ODllfUe~sQX{JySq1-X!eH*bSJ}avi~JLWm;G% za|;x*-}v<6(s^wu*X~Hfw_&YGvEJs!;yPU*`-X(4e`h^TKy8F`5RFTk0v@9d>@A&hjbj&2&1Ha$whu`^dXsew*Pki01T96==-rY43 zzjEbVF>|wGxv7JLg9t>Bs+~U|j-#I=nDlsfJQd~czCB-AI2#6E-a&Kn=jW#dytYEf z7Q5L9E=P|%C1erlw=mA0V8q!E@U#GkaEbG1>YW#e4shT3ju-6G#8f`!wKv&se4MC1 zhjzt6wqOqh-gfh$4fVp@D%)_?1JKf_x@T+oJE1r-0Tqf6mAsgD?3tH?#6S+;CsMf6 z0!(*Rz}7^%QJE(Q#(m7$f(;Kgs!#*U$lY9Rrj`#RwDHnq&vMzwfVW*|OCEs4;W3`U z`vO^$io*Zn>83W z8ZpFzX;5utX-3&8VxJRf89bkdfiNsakY`A6(6Ew3SWO zwbfNApni39#6lNlNQO#AmK^aaQJd(Wc7%XPKG>lV-g4oM=VwvNNzvhsYrI!^_@NZA znD`ZkWxHiH78&fk!!Xs+-fnOyqV>sW{su8NP68L7-eNrC;bDQQg~S08#^D83anPd7 zcfar}OYXzj9kT7ho#j!GenbAaQN%}5&u4A9Rwro_otY~H)+$Lp=YvV!13x3|MNTFV zS3JCvsWCOyW_E<5>uJDODcqd~DaexeY9WTvb|SRl%X-JcTAs&?=2X@XpTFoLi-}6v z@%h&?8-TPo=Mj|Ae;5VhuWz(y3l%1iqnOE80>JW@md2;YWG=qamq#%$8duscN@f3Lb^fqyOyg&hmf)q=Jv+0w{G zvKzUrq)&USP<(MuSS@Um`5PzF2pWd0cuy-Q2PvW6gID}g`a?vyY$Nf(>$X^X*n8}{ zf~CsUOXEpWHA>&2?x5ebJk$!eI=0qvr9Q&=52Wv(R4^E3dbEMDdG^C5kRt&z7j*5+8pZjE)kU zCX>t4B@BD8+f3tVQ7|W|mdNJ&Mzj7YsyAjpL0J9^e$+Dy$Ve@VEi<0vOR<8E>vhEb zeE34n1i+V4&-5eNN2X}f<0DB7IDyF9 z{MuUd#Ske3$VH!JfsB|2%)hAUG%q>Cdvx(Tfr8I5d9=K|k~>D+ov*v^T(!Y!rl_*J zAYd$memBMwPrcLIV;4lli}xDbG$4rtYZ(u~hF{u4&rgr*21br%P9r}*_F2teC_D#s zQo!7IG{H`f6I9bNPjUI%8~LC;34ewAK)48<%J}7%t?})Zv>7KYYFC}vC~T3&=F2gh zBjN>|9EhS(4nS8ta~oc|1t%DzR37X~m9Hr7r;3iYG8HU|zvZAKjmLEi`p|VZZR{$I zGPf5@T1IKQ&W>hd8Z1kBr`+lop{Ihm=Cu6Ox%zp&U&OL~jAFN$uQ)oifkKm=LNRFNjzU1r)d*{^Pk!z1e)I7YGjT~rX+o%L)E#gLzlFtP3s ze{aNlaE2;Jk0O&AGoYCfv*qs>+b0J$KE@a=#5J{{5%y`;*+f@lQ zJlrN>e_yeljxf^um7hN^h!&u=Stie52e#c&{Ax zQ69^C^CTl4uci`B)1+l+Abop)?mm&&mZL0Rx5CWp#dF_HGKcT;eQr0Ql6Mjqmde>z zfh9b^U#HHLE>I7W{_$xt4ZG>)5PZ*m!egH8Ev2+xs?Sk~0Z~X@ymvsmLtgN^}&m(8QUL_d5 zYly5hBBiQ@BbO6NOLVOn=U9EAQoX$hwTh6=6)3BX(BoVR&m{y?|(gw zDolst|N8S9DM=oxSu@A-`@KH*_sE|~E$pFsF)%YLYE#-K12rra?S%678^?TaUW}-6 zGo!+8XREgmwk!oY(4W$U*YAoT$6^OxM??p))$DUixxEybFuQ1ACi=b0Yj}u;Ht2km zb#5*%WnCQ|@j$k8xTC25nZ-Xr5lW^O@?@1|3Hf+p{A(b^*{s2XLSA}-FbhQ%>|%CQ z{Dhs}hbCaQdS8pZAauVr)q#OO)lZ|9@Mb$hgFBW7Nb0+kfBl;EPvO#o4p1zA0yT8! z%~dOjGj)3;*x0a#*EKS6BCAHSB7*H8%vUuD0V^GPO6t4~3} zAgYTsMdP)9T;oUUp5_Watmrcf@ax?gbf)3aY+{j#6{;Yv+1o`mlIc-ZG5Q?6qb4R+ zL8x`V*~k-qseT2DkjvG`>QRLV$J&h)LySY%whkhW^-RSVTql!`1bf3aKS##jmmvj})P=9Qv~KoY@xoAv?6!|B2Xp-9_Kk zNbDsh#Emi7Y_z%V^De=#=v6Oig!dX)hY{5?##y~Q-oio9je^ytIJg( z62)%k>-z3tdsr!*A_160xW5`_e_Z!WzLT0;02@<25lU+J@bzW_?zL~77x)qnR&f^m zWjEnn_09y}pZEXwoWTD4@L;v%fNewCMdCvjpNhw|re<=KHcNiiSsJDQs_^rLLaUdp zKWe}qG9KYfQ0tgk9CkPV1*nmCwwLAt#T#tsNH-CQ2AsVD!3yZII!I8Nlx+$QmLQ)E z*wA)U`9+MX=Lnb6GyUl6l*YV;``11=pKU|T6>2;dy{h-X5=FD@K4e^YKsYiKidk0-4+rNYV?txCDbbUu!&z%uuEOF2leXPUv7`Nd{y~P9_tsDWx$|ngl z_x2c+CZ}MlcV1C!=rqP@^a@@Q0IlpmJDPiME5*o-gd)oFT>tsRau< zC9QyKG2~}8()3wbjGe7>%h~Dj{b6yoZy-u76&L59^Gv@x(f?+5S6Mk7J$%5z`xOxc z6v?#8R>=0^0*n>Txsx$xLi)Z?Ioyh|xNW#X<>u{p*M9FJ?KJ!Q8d6IBCzV!`I?H8V zC#Au1Dz6$k+?z{Ayu8!%p{15e^L@JyYL&eCZd=sH2o}+>wNfvx74&lTB?14^!In>j zM=A!>aXwfGF7q1LuN4Rp{C1n$H^Ag-O3Ym-PkD2@=u|t=M6}Zer=Ri99R_y0Mk2|| z$1rSrV9kw)u8|CHP1ao zG#-6f*}W1q>JV?~#f$B6k?)P?#O;9p^(UEWWf(tI{mYUOw0>|I3OqO)E7pa=N2Y>|zrP+rX$AEUrG+MYJ!(+FpUE0gUk(AH>x)RhN#SWUnxk1TJ&_AxqVK zJpAX0{MUHOxHS=f->}hIqx+QGde(AQ998g!mW#DL#xBDIC;jj55V*5vA$YxzFsQOH zQSwR-P9_2w#pkoO^Um}6$`x`&9FSw7(plk6FaB5rgP zwzUVeN%=qlcSdxu%%7_EB zvhkRWTEaP)Yigl(zb3V%N$(?T45PbH2I2NU{nVK?w|6-fiu^Xk)+)9CpQngPYTg*+ z^}E=i5>;*jb?m3uSPJQAN~ECo!Y-HI>D-6u`lMr*1BN;g2Sk49_8oC*5&DuSW1IgV z)<*c)!^zt@B6_N^?TDcNkZdBZP;SlEs8JNAQhu(x(3q6+vnm2OYWbMLvO%SD^&&$;i3r9j zE~yCRsFZYkz7LziZ_hJQ{a5Vz$M`$AMN`FLYvSo~nOSbm&?I}d&-a(LRr+nrIF+ma z$OI2YjNRg=sQ4tC4siAJow|+&x#9DiOW7oYduR1DRS_!iQGyt%+pqZ6xNp-BW@N=f zKde;+EY(5PjiJC&Li74`ThuM95ge{HS<(0H+IK$ep^k00GjHF^9(*7QX_Y7rj@$~L zPG5i6$wtYmDXE!*!-g+#l&ymaxH!Rk_d7Cb63u6=y#N)p3;Tv2WxdM14J+aK-F*|~ z(^n+Ej>t>rx;AM2nr_rZ{(ci%&BzQbVkqg4tWIh94i?)Rg>_+)fS(Wo4d zyr{87kHcBZzQOyc7Ku0Q^U7L)f3L^8{0x%c?#a!=>!mapC)VE*FGhXM%<@hpgJrR>Mi7kj zij5RS$NA6O%Qxn(?>79J7Qlg}CUNsl6){CDd@8x{`5!Buotyv3JruOp4 zBbiuWtz}UJV8<2Fm6G%5lm%66W=VOHvM(c8I`3uMU%r}+B=SzI_hkrqO9%2Mi4)}@ zIjhp{@OPpHWyA}RC8*49#+ktoLc@Wu7x&UI&`SB*{m%5=)?8T1%c7q+CuhEhvgz;_ zj>4i6w}UKpmEk3+dumPk5uqq)1-R{&!5PbDT$|@ST>fAKxK5y}Sts<5cuidJtW%e$ zlSaz$63unT1Kzz3hxM+IFW0h|o_inv1#|9y8yr|)%L1$!v&)^P^f|fO=?v8jpTs-i9Hcg`ZJk`5 z+;L|3FN$=3AAW8r@0j|jx#~GMLFn&r@~uwHSJs&D_b@V6Z*NVR=42XAl92>JVy!ixixBi*>Ej;F44!QQ^6m~XYc^X>DettUrhPGqcX+r)HaUt2q zbeZH$czaNd!izqG--Nb?e0DM69Hs|>jA4cd`()u*uCUIY1K*3xoIYupJeFJ3ger75 zJ|mtWuez{kb{@eVL8+S4lZB^1xM_wMe7K|TP8wpP8N}gLhnP1)Keesn5rg#=9&ZrM zi5>&bzoL0c)0XkZ?KW`JX;`$1ttE8!NTp4ZMT1J9qv-HuIbNv9XVIqLq38}-3e8h& zSBGJw0+(}WF(}neW|@;*1^L}}lA=gF8k?73wnVvJ29<9s>7sQe8|JWYodH(q-A1og zlakaZjbN1UH>1e(#~>_qPV`Z^=HyKS`k0y|E&RHg8`z j+FnLhXTPmsyCS8pY|#c3+N-}`+y-PN6(y>~i~{}-cZF#7 literal 0 HcmV?d00001 diff --git a/demos/cast/src/main/res/values/strings.xml b/demos/cast/src/main/res/values/strings.xml new file mode 100644 index 0000000000..503892da27 --- /dev/null +++ b/demos/cast/src/main/res/values/strings.xml @@ -0,0 +1,25 @@ + + + + + + ExoCast Demo + + ExoCast + + DRM scheme not supported by this device. + + diff --git a/demos/cast/src/main/res/values/styles.xml b/demos/cast/src/main/res/values/styles.xml new file mode 100644 index 0000000000..1484a68a68 --- /dev/null +++ b/demos/cast/src/main/res/values/styles.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/extensions/cast/README.md b/extensions/cast/README.md new file mode 100644 index 0000000000..73f7041729 --- /dev/null +++ b/extensions/cast/README.md @@ -0,0 +1,33 @@ +# ExoPlayer Cast extension # + +## Description ## + +The cast extension is a [Player][] implementation that controls playback on a +Cast receiver app. + +[Player]: https://google.github.io/ExoPlayer/doc/reference/index.html?com/google/android/exoplayer2/Player.html + +## Getting the extension ## + +The easiest way to use the extension is to add it as a gradle dependency: + +```gradle +compile 'com.google.android.exoplayer:extension-cast:rX.X.X' +``` + +where `rX.X.X` is the version, which must match the version of the ExoPlayer +library being used. + +Alternatively, you can clone the ExoPlayer repository and depend on the module +locally. Instructions for doing this can be found in ExoPlayer's +[top level README][]. + +[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md + +## Using the extension ## + +Create a `CastPlayer` and use it to integrate Cast into your app using +ExoPlayer's common Player interface. You can try the Cast Extension to see how a +[PlaybackControlView][] can be used to control playback in a remote receiver app. + +[PlaybackControlView]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/ui/PlaybackControlView.html diff --git a/extensions/cast/build.gradle b/extensions/cast/build.gradle new file mode 100644 index 0000000000..7d252332c9 --- /dev/null +++ b/extensions/cast/build.gradle @@ -0,0 +1,45 @@ +// 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 from: '../../constants.gradle' +apply plugin: 'com.android.library' + +android { + compileSdkVersion project.ext.compileSdkVersion + buildToolsVersion project.ext.buildToolsVersion + + defaultConfig { + minSdkVersion 14 + targetSdkVersion project.ext.targetSdkVersion + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } +} + +dependencies { + compile 'com.android.support:appcompat-v7:' + supportLibraryVersion + compile 'com.android.support:mediarouter-v7:' + supportLibraryVersion + compile 'com.google.android.gms:play-services-cast-framework:' + playServicesLibraryVersion + compile project(modulePrefix + 'library-core') + compile project(modulePrefix + 'library-ui') +} + +ext { + javadocTitle = 'Cast extension' +} +apply from: '../../javadoc_library.gradle' + +ext { + releaseArtifact = 'extension-cast' + releaseDescription = 'Cast extension for ExoPlayer.' +} +apply from: '../../publish.gradle' diff --git a/extensions/cast/src/main/AndroidManifest.xml b/extensions/cast/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..c12fc1289f --- /dev/null +++ b/extensions/cast/src/main/AndroidManifest.xml @@ -0,0 +1,16 @@ + + + diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java new file mode 100644 index 0000000000..ef84c04c04 --- /dev/null +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java @@ -0,0 +1,649 @@ +/* + * 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.ext.cast; + +import android.support.annotation.Nullable; +import android.util.Log; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.SinglePeriodTimeline; +import com.google.android.exoplayer2.source.TrackGroup; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.trackselection.FixedTrackSelection; +import com.google.android.exoplayer2.trackselection.TrackSelection; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.Util; +import com.google.android.gms.cast.CastStatusCodes; +import com.google.android.gms.cast.MediaInfo; +import com.google.android.gms.cast.MediaMetadata; +import com.google.android.gms.cast.MediaStatus; +import com.google.android.gms.cast.MediaTrack; +import com.google.android.gms.cast.framework.CastContext; +import com.google.android.gms.cast.framework.CastSession; +import com.google.android.gms.cast.framework.SessionManager; +import com.google.android.gms.cast.framework.SessionManagerListener; +import com.google.android.gms.cast.framework.media.RemoteMediaClient; +import com.google.android.gms.cast.framework.media.RemoteMediaClient.MediaChannelResult; +import com.google.android.gms.common.api.CommonStatusCodes; +import com.google.android.gms.common.api.ResultCallback; +import java.util.List; +import java.util.concurrent.CopyOnWriteArraySet; + +/** + * {@link Player} implementation that communicates with a Cast receiver app. + * + *

        Calls to the methods in this class depend on the availability of an underlying cast session. + * If no session is available, method calls have no effect. To keep track of the underyling session, + * {@link #isCastSessionAvailable()} can be queried and {@link SessionAvailabilityListener} can be + * implemented and attached to the player. + * + *

        Methods should be called on the application's main thread. + * + *

        Known issues: + *

          + *
        • Part of the Cast API is not exposed through this interface. For instance, volume settings + * and track selection.
        • + *
        • Repeat mode is not working. See [internal: b/64137174].
        • + *
        + */ +public final class CastPlayer implements Player { + + /** + * Listener of changes in the cast session availability. + */ + public interface SessionAvailabilityListener { + + /** + * Called when a cast session becomes available to the player. + */ + void onCastSessionAvailable(); + + /** + * Called when the cast session becomes unavailable. + */ + void onCastSessionUnavailable(); + + } + + private static final String TAG = "CastPlayer"; + + private static final int RENDERER_COUNT = 3; + private static final int RENDERER_INDEX_VIDEO = 0; + private static final int RENDERER_INDEX_AUDIO = 1; + private static final int RENDERER_INDEX_TEXT = 2; + private static final long PROGRESS_REPORT_PERIOD_MS = 1000; + private static final TrackGroupArray EMPTY_TRACK_GROUP_ARRAY = new TrackGroupArray(); + private static final TrackSelectionArray EMPTY_TRACK_SELECTION_ARRAY = + new TrackSelectionArray(null, null, null); + private static final long[] EMPTY_TRACK_ID_ARRAY = new long[0]; + + private final CastContext castContext; + private final Timeline.Window window; + + // Result callbacks. + private final StatusListener statusListener; + private final RepeatModeResultCallback repeatModeResultCallback; + private final SeekResultCallback seekResultCallback; + + // Listeners. + private final CopyOnWriteArraySet listeners; + private SessionAvailabilityListener sessionAvailabilityListener; + + // Internal state. + private RemoteMediaClient remoteMediaClient; + private Timeline currentTimeline; + private TrackGroupArray currentTrackGroups; + private TrackSelectionArray currentTrackSelection; + private long lastReportedPositionMs; + private long pendingSeekPositionMs; + + /** + * @param castContext The context from which the cast session is obtained. + */ + public CastPlayer(CastContext castContext) { + this.castContext = castContext; + window = new Timeline.Window(); + statusListener = new StatusListener(); + repeatModeResultCallback = new RepeatModeResultCallback(); + seekResultCallback = new SeekResultCallback(); + listeners = new CopyOnWriteArraySet<>(); + SessionManager sessionManager = castContext.getSessionManager(); + sessionManager.addSessionManagerListener(statusListener, CastSession.class); + CastSession session = sessionManager.getCurrentCastSession(); + remoteMediaClient = session != null ? session.getRemoteMediaClient() : null; + pendingSeekPositionMs = C.TIME_UNSET; + updateInternalState(); + } + + /** + * Loads media into the receiver app. + * + * @param title The title of the media sample. + * @param url The url from which the media is obtained. + * @param contentMimeType The mime type of the content to play. + * @param positionMs The position at which the playback should start in milliseconds. + * @param playWhenReady Whether the player should start playback as soon as it is ready to do so. + */ + public void load(String title, String url, String contentMimeType, long positionMs, + boolean playWhenReady) { + lastReportedPositionMs = 0; + if (remoteMediaClient != null) { + MediaMetadata movieMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE); + movieMetadata.putString(MediaMetadata.KEY_TITLE, title); + MediaInfo mediaInfo = new MediaInfo.Builder(url).setStreamType(MediaInfo.STREAM_TYPE_BUFFERED) + .setContentType(contentMimeType).setMetadata(movieMetadata).build(); + remoteMediaClient.load(mediaInfo, playWhenReady, positionMs); + } + } + + /** + * Returns whether a cast session is available for playback. + */ + public boolean isCastSessionAvailable() { + return remoteMediaClient != null; + } + + /** + * Sets a listener for updates on the cast session availability. + * + * @param listener The {@link SessionAvailabilityListener}. + */ + public void setSessionAvailabilityListener(SessionAvailabilityListener listener) { + sessionAvailabilityListener = listener; + } + + // Player implementation. + + @Override + public void addListener(EventListener listener) { + listeners.add(listener); + } + + @Override + public void removeListener(EventListener listener) { + listeners.remove(listener); + } + + @Override + public int getPlaybackState() { + if (remoteMediaClient == null) { + return STATE_IDLE; + } + int receiverAppStatus = remoteMediaClient.getPlayerState(); + switch (receiverAppStatus) { + case MediaStatus.PLAYER_STATE_BUFFERING: + return STATE_BUFFERING; + case MediaStatus.PLAYER_STATE_PLAYING: + case MediaStatus.PLAYER_STATE_PAUSED: + return STATE_READY; + case MediaStatus.PLAYER_STATE_IDLE: + case MediaStatus.PLAYER_STATE_UNKNOWN: + default: + return STATE_IDLE; + } + } + + @Override + public void setPlayWhenReady(boolean playWhenReady) { + if (remoteMediaClient == null) { + return; + } + if (playWhenReady) { + remoteMediaClient.play(); + } else { + remoteMediaClient.pause(); + } + } + + @Override + public boolean getPlayWhenReady() { + return remoteMediaClient != null && !remoteMediaClient.isPaused(); + } + + @Override + public void seekToDefaultPosition() { + seekTo(0); + } + + @Override + public void seekToDefaultPosition(int windowIndex) { + seekTo(windowIndex, 0); + } + + @Override + public void seekTo(long positionMs) { + seekTo(0, positionMs); + } + + @Override + public void seekTo(int windowIndex, long positionMs) { + if (remoteMediaClient != null) { + remoteMediaClient.seek(positionMs).setResultCallback(seekResultCallback); + pendingSeekPositionMs = positionMs; + for (EventListener listener : listeners) { + listener.onPositionDiscontinuity(); + } + } + } + + @Override + public void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters) { + // Unsupported by the RemoteMediaClient API. Do nothing. + } + + @Override + public PlaybackParameters getPlaybackParameters() { + return PlaybackParameters.DEFAULT; + } + + @Override + public void stop() { + if (remoteMediaClient != null) { + remoteMediaClient.stop(); + } + } + + @Override + public void release() { + castContext.getSessionManager().removeSessionManagerListener(statusListener, CastSession.class); + } + + @Override + public int getRendererCount() { + // We assume there are three renderers: video, audio, and text. + return RENDERER_COUNT; + } + + @Override + public int getRendererType(int index) { + switch (index) { + case RENDERER_INDEX_VIDEO: + return C.TRACK_TYPE_VIDEO; + case RENDERER_INDEX_AUDIO: + return C.TRACK_TYPE_AUDIO; + case RENDERER_INDEX_TEXT: + return C.TRACK_TYPE_TEXT; + default: + throw new IndexOutOfBoundsException(); + } + } + + @Override + public void setRepeatMode(@RepeatMode int repeatMode) { + if (remoteMediaClient != null) { + int castRepeatMode; + switch (repeatMode) { + case REPEAT_MODE_ONE: + castRepeatMode = MediaStatus.REPEAT_MODE_REPEAT_SINGLE; + break; + case REPEAT_MODE_ALL: + castRepeatMode = MediaStatus.REPEAT_MODE_REPEAT_ALL; + break; + case REPEAT_MODE_OFF: + castRepeatMode = MediaStatus.REPEAT_MODE_REPEAT_OFF; + break; + default: + throw new IllegalArgumentException(); + } + remoteMediaClient.queueSetRepeatMode(castRepeatMode, null) + .setResultCallback(repeatModeResultCallback); + } + } + + @Override + @RepeatMode public int getRepeatMode() { + if (remoteMediaClient == null) { + return REPEAT_MODE_OFF; + } + MediaStatus mediaStatus = getMediaStatus(); + if (mediaStatus == null) { + // No media session active, yet. + return REPEAT_MODE_OFF; + } + int castRepeatMode = mediaStatus.getQueueRepeatMode(); + switch (castRepeatMode) { + case MediaStatus.REPEAT_MODE_REPEAT_SINGLE: + return REPEAT_MODE_ONE; + case MediaStatus.REPEAT_MODE_REPEAT_ALL: + case MediaStatus.REPEAT_MODE_REPEAT_ALL_AND_SHUFFLE: + return REPEAT_MODE_ALL; + case MediaStatus.REPEAT_MODE_REPEAT_OFF: + return REPEAT_MODE_OFF; + default: + throw new IllegalStateException(); + } + } + + @Override + public TrackSelectionArray getCurrentTrackSelections() { + return currentTrackSelection; + } + + @Override + public TrackGroupArray getCurrentTrackGroups() { + return currentTrackGroups; + } + + @Override + public Timeline getCurrentTimeline() { + return currentTimeline; + } + + @Override + @Nullable public Object getCurrentManifest() { + return null; + } + + @Override + public int getCurrentPeriodIndex() { + return 0; + } + + @Override + public int getCurrentWindowIndex() { + return 0; + } + + @Override + public long getDuration() { + return currentTimeline.isEmpty() ? C.TIME_UNSET + : currentTimeline.getWindow(0, window).getDurationMs(); + } + + @Override + public long getCurrentPosition() { + return remoteMediaClient == null ? lastReportedPositionMs + : pendingSeekPositionMs != C.TIME_UNSET ? pendingSeekPositionMs + : remoteMediaClient.getApproximateStreamPosition(); + } + + @Override + public long getBufferedPosition() { + return getCurrentPosition(); + } + + @Override + public int getBufferedPercentage() { + long position = getBufferedPosition(); + long duration = getDuration(); + return position == C.TIME_UNSET || duration == C.TIME_UNSET ? 0 + : duration == 0 ? 100 + : Util.constrainValue((int) ((position * 100) / duration), 0, 100); + } + + @Override + public boolean isCurrentWindowDynamic() { + return !currentTimeline.isEmpty() + && currentTimeline.getWindow(getCurrentWindowIndex(), window).isDynamic; + } + + @Override + public boolean isCurrentWindowSeekable() { + return !currentTimeline.isEmpty() + && currentTimeline.getWindow(getCurrentWindowIndex(), window).isSeekable; + } + + @Override + public boolean isPlayingAd() { + return false; + } + + @Override + public int getCurrentAdGroupIndex() { + return C.INDEX_UNSET; + } + + @Override + public int getCurrentAdIndexInAdGroup() { + return C.INDEX_UNSET; + } + + @Override + public boolean isLoading() { + return false; + } + + @Override + public long getContentPosition() { + return getCurrentPosition(); + } + + // Internal methods. + + private void setRemoteMediaClient(@Nullable RemoteMediaClient remoteMediaClient) { + if (this.remoteMediaClient == remoteMediaClient) { + // Do nothing. + return; + } + if (this.remoteMediaClient != null) { + this.remoteMediaClient.removeListener(statusListener); + this.remoteMediaClient.removeProgressListener(statusListener); + } + this.remoteMediaClient = remoteMediaClient; + if (remoteMediaClient != null) { + if (sessionAvailabilityListener != null) { + sessionAvailabilityListener.onCastSessionAvailable(); + } + remoteMediaClient.addListener(statusListener); + remoteMediaClient.addProgressListener(statusListener, PROGRESS_REPORT_PERIOD_MS); + } else { + if (sessionAvailabilityListener != null) { + sessionAvailabilityListener.onCastSessionUnavailable(); + } + } + } + + private @Nullable MediaStatus getMediaStatus() { + return remoteMediaClient != null ? remoteMediaClient.getMediaStatus() : null; + } + + private @Nullable MediaInfo getMediaInfo() { + return remoteMediaClient != null ? remoteMediaClient.getMediaInfo() : null; + } + + private void updateInternalState() { + currentTimeline = Timeline.EMPTY; + currentTrackGroups = EMPTY_TRACK_GROUP_ARRAY; + currentTrackSelection = EMPTY_TRACK_SELECTION_ARRAY; + MediaInfo mediaInfo = getMediaInfo(); + if (mediaInfo == null) { + return; + } + long streamDurationMs = mediaInfo.getStreamDuration(); + boolean isSeekable = streamDurationMs != MediaInfo.UNKNOWN_DURATION; + currentTimeline = new SinglePeriodTimeline( + isSeekable ? C.msToUs(streamDurationMs) : C.TIME_UNSET, isSeekable); + + List tracks = mediaInfo.getMediaTracks(); + if (tracks == null) { + return; + } + + MediaStatus mediaStatus = getMediaStatus(); + long[] activeTrackIds = mediaStatus != null ? mediaStatus.getActiveTrackIds() : null; + if (activeTrackIds == null) { + activeTrackIds = EMPTY_TRACK_ID_ARRAY; + } + + TrackGroup[] trackGroups = new TrackGroup[tracks.size()]; + TrackSelection[] trackSelections = new TrackSelection[RENDERER_COUNT]; + for (int i = 0; i < tracks.size(); i++) { + MediaTrack mediaTrack = tracks.get(i); + trackGroups[i] = new TrackGroup(CastUtils.mediaTrackToFormat(mediaTrack)); + + long id = mediaTrack.getId(); + int trackType = MimeTypes.getTrackType(mediaTrack.getContentType()); + int rendererIndex = getRendererIndexForTrackType(trackType); + if (isTrackActive(id, activeTrackIds) && rendererIndex != C.INDEX_UNSET + && trackSelections[rendererIndex] == null) { + trackSelections[rendererIndex] = new FixedTrackSelection(trackGroups[i], 0); + } + } + currentTrackSelection = new TrackSelectionArray(trackSelections); + currentTrackGroups = new TrackGroupArray(trackGroups); + } + + private static boolean isTrackActive(long id, long[] activeTrackIds) { + for (long activeTrackId : activeTrackIds) { + if (activeTrackId == id) { + return true; + } + } + return false; + } + + private static int getRendererIndexForTrackType(int trackType) { + return trackType == C.TRACK_TYPE_VIDEO ? RENDERER_INDEX_VIDEO + : trackType == C.TRACK_TYPE_AUDIO ? RENDERER_INDEX_AUDIO + : trackType == C.TRACK_TYPE_TEXT ? RENDERER_INDEX_TEXT + : C.INDEX_UNSET; + } + + private final class StatusListener implements RemoteMediaClient.Listener, + SessionManagerListener, RemoteMediaClient.ProgressListener { + + // RemoteMediaClient.ProgressListener implementation. + + @Override + public void onProgressUpdated(long progressMs, long unusedDurationMs) { + lastReportedPositionMs = progressMs; + } + + // RemoteMediaClient.Listener implementation. + + @Override + public void onStatusUpdated() { + boolean playWhenReady = getPlayWhenReady(); + int playbackState = getPlaybackState(); + for (EventListener listener : listeners) { + listener.onPlayerStateChanged(playWhenReady, playbackState); + } + } + + @Override + public void onMetadataUpdated() { + updateInternalState(); + for (EventListener listener : listeners) { + listener.onTracksChanged(currentTrackGroups, currentTrackSelection); + listener.onTimelineChanged(currentTimeline, null); + } + } + + @Override + public void onQueueStatusUpdated() {} + + @Override + public void onPreloadStatusUpdated() {} + + @Override + public void onSendingRemoteMediaRequest() {} + + @Override + public void onAdBreakStatusUpdated() {} + + + // SessionManagerListener implementation. + + @Override + public void onSessionStarted(CastSession castSession, String s) { + setRemoteMediaClient(castSession.getRemoteMediaClient()); + } + + @Override + public void onSessionResumed(CastSession castSession, boolean b) { + setRemoteMediaClient(castSession.getRemoteMediaClient()); + } + + @Override + public void onSessionEnded(CastSession castSession, int i) { + setRemoteMediaClient(null); + } + + @Override + public void onSessionSuspended(CastSession castSession, int i) { + setRemoteMediaClient(null); + } + + @Override + public void onSessionResumeFailed(CastSession castSession, int statusCode) { + Log.e(TAG, "Session resume failed. Error code " + statusCode + ": " + + CastUtils.getLogString(statusCode)); + } + + @Override + public void onSessionStarting(CastSession castSession) { + // Do nothing. + } + + @Override + public void onSessionStartFailed(CastSession castSession, int statusCode) { + Log.e(TAG, "Session start failed. Error code " + statusCode + ": " + + CastUtils.getLogString(statusCode)); + } + + @Override + public void onSessionEnding(CastSession castSession) { + // Do nothing. + } + + @Override + public void onSessionResuming(CastSession castSession, String s) { + // Do nothing. + } + + } + + // Result callbacks hooks. + + private final class RepeatModeResultCallback implements ResultCallback { + + @Override + public void onResult(MediaChannelResult result) { + int statusCode = result.getStatus().getStatusCode(); + if (statusCode == CommonStatusCodes.SUCCESS) { + int repeatMode = getRepeatMode(); + for (EventListener listener : listeners) { + listener.onRepeatModeChanged(repeatMode); + } + } else { + Log.e(TAG, "Set repeat mode failed. Error code " + statusCode + ": " + + CastUtils.getLogString(statusCode)); + } + } + + } + + private final class SeekResultCallback implements ResultCallback { + + @Override + public void onResult(MediaChannelResult result) { + int statusCode = result.getStatus().getStatusCode(); + if (statusCode == CommonStatusCodes.SUCCESS) { + pendingSeekPositionMs = C.TIME_UNSET; + } else if (statusCode == CastStatusCodes.REPLACED) { + // A seek was executed before this one completed. Do nothing. + } else { + Log.e(TAG, "Seek failed. Error code " + statusCode + ": " + + CastUtils.getLogString(statusCode)); + } + } + + } + +} diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastUtils.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastUtils.java new file mode 100644 index 0000000000..de60437444 --- /dev/null +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastUtils.java @@ -0,0 +1,94 @@ +/* + * 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.ext.cast; + +import com.google.android.exoplayer2.Format; +import com.google.android.gms.cast.CastStatusCodes; +import com.google.android.gms.cast.MediaTrack; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * Utility methods for ExoPlayer/Cast integration. + */ +/* package */ final class CastUtils { + + private static final Map CAST_STATUS_CODE_TO_STRING; + + /** + * Returns a descriptive log string for the given {@code statusCode}, or "Unknown." if not one of + * {@link CastStatusCodes}. + * + * @param statusCode A Cast API status code. + * @return A descriptive log string for the given {@code statusCode}, or "Unknown." if not one of + * {@link CastStatusCodes}. + */ + public static String getLogString(int statusCode) { + String description = CAST_STATUS_CODE_TO_STRING.get(statusCode); + return description != null ? description : "Unknown."; + } + + /** + * Creates a {@link Format} instance containing all information contained in the given + * {@link MediaTrack} object. + * + * @param mediaTrack The {@link MediaTrack}. + * @return The equivalent {@link Format}. + */ + public static Format mediaTrackToFormat(MediaTrack mediaTrack) { + return Format.createContainerFormat(mediaTrack.getContentId(), mediaTrack.getContentType(), + null, null, Format.NO_VALUE, 0, mediaTrack.getLanguage()); + } + + static { + HashMap statusCodeToString = new HashMap<>(); + statusCodeToString.put(CastStatusCodes.APPLICATION_NOT_FOUND, + "A requested application could not be found."); + statusCodeToString.put(CastStatusCodes.APPLICATION_NOT_RUNNING, + "A requested application is not currently running."); + statusCodeToString.put(CastStatusCodes.AUTHENTICATION_FAILED, "Authentication failure."); + statusCodeToString.put(CastStatusCodes.CANCELED, "An in-progress request has been " + + "canceled, most likely because another action has preempted it."); + statusCodeToString.put(CastStatusCodes.ERROR_SERVICE_CREATION_FAILED, + "The Cast Remote Display service could not be created."); + statusCodeToString.put(CastStatusCodes.ERROR_SERVICE_DISCONNECTED, + "The Cast Remote Display service was disconnected."); + statusCodeToString.put(CastStatusCodes.FAILED, "The in-progress request failed."); + statusCodeToString.put(CastStatusCodes.INTERNAL_ERROR, "An internal error has occurred."); + statusCodeToString.put(CastStatusCodes.INTERRUPTED, + "A blocking call was interrupted while waiting and did not run to completion."); + statusCodeToString.put(CastStatusCodes.INVALID_REQUEST, "An invalid request was made."); + statusCodeToString.put(CastStatusCodes.MESSAGE_SEND_BUFFER_TOO_FULL, "A message could " + + "not be sent because there is not enough room in the send buffer at this time."); + statusCodeToString.put(CastStatusCodes.MESSAGE_TOO_LARGE, + "A message could not be sent because it is too large."); + statusCodeToString.put(CastStatusCodes.NETWORK_ERROR, "Network I/O error."); + statusCodeToString.put(CastStatusCodes.NOT_ALLOWED, + "The request was disallowed and could not be completed."); + statusCodeToString.put(CastStatusCodes.REPLACED, + "The request's progress is no longer being tracked because another request of the same type" + + " has been made before the first request completed."); + statusCodeToString.put(CastStatusCodes.SUCCESS, "Success."); + statusCodeToString.put(CastStatusCodes.TIMEOUT, "An operation has timed out."); + statusCodeToString.put(CastStatusCodes.UNKNOWN_ERROR, + "An unknown, unexpected error has occurred."); + CAST_STATUS_CODE_TO_STRING = Collections.unmodifiableMap(statusCodeToString); + } + + private CastUtils() {} + +} diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastOptionsProvider.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastOptionsProvider.java new file mode 100644 index 0000000000..06f0bec971 --- /dev/null +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/DefaultCastOptionsProvider.java @@ -0,0 +1,42 @@ +/* + * 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.ext.cast; + +import android.content.Context; +import com.google.android.gms.cast.CastMediaControlIntent; +import com.google.android.gms.cast.framework.CastOptions; +import com.google.android.gms.cast.framework.OptionsProvider; +import com.google.android.gms.cast.framework.SessionProvider; +import java.util.List; + +/** + * A convenience {@link OptionsProvider} to target the default cast receiver app. + */ +public final class DefaultCastOptionsProvider implements OptionsProvider { + + @Override + public CastOptions getCastOptions(Context context) { + return new CastOptions.Builder() + .setReceiverApplicationId(CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID) + .setStopReceiverApplicationWhenEndingSession(true).build(); + } + + @Override + public List getAdditionalSessionProviders(Context context) { + return null; + } + +} diff --git a/extensions/ima/build.gradle b/extensions/ima/build.gradle index a4ead9e01f..c084ec6bf8 100644 --- a/extensions/ima/build.gradle +++ b/extensions/ima/build.gradle @@ -22,7 +22,7 @@ dependencies { // |-- com.android.support:support-v4:25.2.0 compile 'com.android.support:support-v4:' + supportLibraryVersion compile 'com.google.ads.interactivemedia.v3:interactivemedia:3.7.4' - compile 'com.google.android.gms:play-services-ads:11.0.2' + compile 'com.google.android.gms:play-services-ads:' + playServicesLibraryVersion androidTestCompile project(modulePrefix + 'library') androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion diff --git a/library/all/src/main/AndroidManifest.xml b/library/all/src/main/AndroidManifest.xml index 1efda648e9..f31f55b40a 100644 --- a/library/all/src/main/AndroidManifest.xml +++ b/library/all/src/main/AndroidManifest.xml @@ -13,5 +13,4 @@ See the License for the specific language governing permissions and limitations under the License. --> - From 4d7f37f5a9b55010f3a2f5354a45d86d63c80d25 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 17 Aug 2017 07:48:49 -0700 Subject: [PATCH 0267/2472] Work around issue with Xiaomi JB devices Issue: #3171 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=165577562 --- .../android/exoplayer2/mediacodec/MediaCodecUtil.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java index d3f3dae344..1073e8d9c1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java @@ -288,9 +288,11 @@ public final class MediaCodecUtil { return false; } - // Work around https://github.com/google/ExoPlayer/issues/1528 + // Work around https://github.com/google/ExoPlayer/issues/1528 and + // https://github.com/google/ExoPlayer/issues/3171 if (Util.SDK_INT < 18 && "OMX.MTK.AUDIO.DECODER.AAC".equals(name) - && "a70".equals(Util.DEVICE)) { + && ("a70".equals(Util.DEVICE) + || ("Xiaomi".equals(Util.MANUFACTURER) && Util.DEVICE.startsWith("HM")))) { return false; } From 34960ad891645514917545926877ad8d009b9ed6 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 17 Aug 2017 07:59:29 -0700 Subject: [PATCH 0268/2472] Tweak and add READMEs + remove refs to V1 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=165578518 --- README.md | 43 ++++++++++++++++--------------- demos/README.md | 4 +++ demos/main/README.md | 4 +-- extensions/README.md | 5 ++++ extensions/cronet/README.md | 9 +++++-- extensions/ffmpeg/README.md | 9 +++++-- extensions/flac/README.md | 9 +++++-- extensions/gvr/README.md | 18 ++++++++----- extensions/ima/README.md | 9 +++++-- extensions/leanback/README.md | 11 +++++--- extensions/mediasession/README.md | 9 +++++-- extensions/okhttp/README.md | 9 +++++-- extensions/opus/README.md | 13 +++++++--- extensions/rtmp/README.md | 9 +++++-- extensions/vp9/README.md | 13 +++++++--- library/README.md | 7 +++++ library/all/README.md | 13 ++++++++++ library/core/README.md | 9 +++++++ library/dash/README.md | 12 +++++++++ library/hls/README.md | 11 ++++++++ library/smoothstreaming/README.md | 12 +++++++++ library/ui/README.md | 10 +++++++ 22 files changed, 194 insertions(+), 54 deletions(-) create mode 100644 demos/README.md create mode 100644 extensions/README.md create mode 100644 library/README.md create mode 100644 library/all/README.md create mode 100644 library/core/README.md create mode 100644 library/dash/README.md create mode 100644 library/hls/README.md create mode 100644 library/smoothstreaming/README.md create mode 100644 library/ui/README.md diff --git a/README.md b/README.md index f4dd9b69ec..c67fb09d73 100644 --- a/README.md +++ b/README.md @@ -9,21 +9,23 @@ and extend, and can be updated through Play Store application updates. ## Documentation ## -* The [developer guide][] provides a wealth of information to help you get - started. -* The [class reference][] documents the ExoPlayer library classes. +* The [developer guide][] provides a wealth of information. +* The [class reference][] documents ExoPlayer classes. * The [release notes][] document the major changes in each release. +* Follow our [developer blog][] to keep up to date with the latest ExoPlayer + developments! [developer guide]: https://google.github.io/ExoPlayer/guide.html [class reference]: https://google.github.io/ExoPlayer/doc/reference -[release notes]: https://github.com/google/ExoPlayer/blob/dev-v2/RELEASENOTES.md +[release notes]: https://github.com/google/ExoPlayer/blob/release-v2/RELEASENOTES.md +[developer blog]: https://medium.com/google-exoplayer ## Using ExoPlayer ## -ExoPlayer modules can be obtained via JCenter. It's also possible to clone the +ExoPlayer modules can be obtained from JCenter. It's also possible to clone the repository and depend on the modules locally. -### Via JCenter ### +### From JCenter ### The easiest way to get started using ExoPlayer is to add it as a gradle dependency. You need to make sure you have the JCenter and Google Maven @@ -39,7 +41,7 @@ repositories { ``` 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: +module. The following will add a dependency to the full library: ```gradle compile 'com.google.android.exoplayer:exoplayer:r2.X.X' @@ -56,8 +58,8 @@ 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 +The available library modules are listed below. Adding a dependency to the full +library is equivalent to adding dependencies on all of the library modules individually. * `exoplayer-core`: Core functionality (required). @@ -66,11 +68,16 @@ individually. * `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][]. +In addition to library modules, ExoPlayer has multiple extension modules that +depend on external libraries to provide additional functionality. Some +extensions are available from JCenter, whereas others must be built manaully. +Browse the [extensions directory] and their individual READMEs for details. +More information on the library and extension modules that are available from +JCenter can be found on [Bintray][]. + +[extensions directory][]: https://github.com/google/ExoPlayer/tree/release-v2/extensions/ [Bintray]: https://bintray.com/google/exoplayer -[Release notes]: https://github.com/google/ExoPlayer/blob/release-v2/RELEASENOTES.md ### Locally ### @@ -109,15 +116,9 @@ compile project(':exoplayer-library-ui) #### Project branches #### - * The project has `dev-vX` and `release-vX` branches, where `X` is the major - version number. - * Most development work happens on the `dev-vX` branch with the highest major - version number. Pull requests should normally be made to this branch. - * Bug fixes may be submitted to older `dev-vX` branches. When doing this, the - same (or an equivalent) fix should also be submitted to all subsequent - `dev-vX` branches. - * A `release-vX` branch holds the most recent stable release for major version - `X`. +* Development work happens on the `dev-v2` branch. Pull requests should + normally be made to this branch. +* The `release-v2` branch holds the most recent release. #### Using Android Studio #### diff --git a/demos/README.md b/demos/README.md new file mode 100644 index 0000000000..7e62249db1 --- /dev/null +++ b/demos/README.md @@ -0,0 +1,4 @@ +# ExoPlayer demos # + +This directory contains applications that demonstrate how to use ExoPlayer. +Browse the individual demos and their READMEs to learn more. diff --git a/demos/main/README.md b/demos/main/README.md index ca37392623..bdb04e5ba8 100644 --- a/demos/main/README.md +++ b/demos/main/README.md @@ -1,5 +1,5 @@ -# Demo application # +# ExoPlayer main demo # -This folder contains a demo application that uses ExoPlayer to play a number +This is the main ExoPlayer demo application. It uses ExoPlayer to play a number of test streams. It can be used as a starting point or reference project when developing other applications that make use of the ExoPlayer library. diff --git a/extensions/README.md b/extensions/README.md new file mode 100644 index 0000000000..bf0effb358 --- /dev/null +++ b/extensions/README.md @@ -0,0 +1,5 @@ +# ExoPlayer extensions # + +ExoPlayer extensions are modules that depend on external libraries to provide +additional functionality. Browse the individual extensions and their READMEs to +learn more. diff --git a/extensions/cronet/README.md b/extensions/cronet/README.md index 2287c4c19b..37c031f35f 100644 --- a/extensions/cronet/README.md +++ b/extensions/cronet/README.md @@ -1,7 +1,5 @@ # ExoPlayer Cronet extension # -## Description ## - The Cronet extension is an [HttpDataSource][] implementation using [Cronet][]. [HttpDataSource]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer/upstream/HttpDataSource.html @@ -56,3 +54,10 @@ new DefaultDataSourceFactory( new CronetDataSourceFactory(...) /* baseDataSourceFactory argument */); ``` respectively. + +## Links ## + +* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.cronet.*` + belong to this module. + +[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html diff --git a/extensions/ffmpeg/README.md b/extensions/ffmpeg/README.md index fbc919c36d..57b637d1e2 100644 --- a/extensions/ffmpeg/README.md +++ b/extensions/ffmpeg/README.md @@ -1,7 +1,5 @@ # ExoPlayer FFmpeg extension # -## Description ## - The FFmpeg extension provides `FfmpegAudioRenderer`, which uses FFmpeg for decoding and can render audio encoded in a variety of formats. @@ -140,3 +138,10 @@ then implement your own logic to use the renderer for a given track. [Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html [#2781]: https://github.com/google/ExoPlayer/issues/2781 [Supported formats]: https://google.github.io/ExoPlayer/supported-formats.html#ffmpeg-extension + +## Links ## + +* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.ffmpeg.*` + belong to this module. + +[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html diff --git a/extensions/flac/README.md b/extensions/flac/README.md index 505482f7ed..113b41a93d 100644 --- a/extensions/flac/README.md +++ b/extensions/flac/README.md @@ -1,7 +1,5 @@ # ExoPlayer Flac extension # -## Description ## - The Flac extension provides `FlacExtractor` and `LibflacAudioRenderer`, which use libFLAC (the Flac decoding library) to extract and decode FLAC audio. @@ -82,3 +80,10 @@ Note: These instructions assume you're using `DefaultTrackSelector`. If you have a custom track selector the choice of `Renderer` is up to your implementation, so you need to make sure you are passing an `LibflacAudioRenderer` to the player, then implement your own logic to use the renderer for a given track. + +## Links ## + +* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.flac.*` + belong to this module. + +[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html diff --git a/extensions/gvr/README.md b/extensions/gvr/README.md index 4e08ee6387..250cf58c2f 100644 --- a/extensions/gvr/README.md +++ b/extensions/gvr/README.md @@ -1,7 +1,5 @@ # ExoPlayer GVR extension # -## Description ## - The GVR extension wraps the [Google VR SDK for Android][]. It provides a GvrAudioProcessor, which uses [GvrAudioSurround][] to provide binaural rendering of surround sound and ambisonic soundfields. @@ -26,9 +24,17 @@ locally. Instructions for doing this can be found in ExoPlayer's ## Using the extension ## -* If using SimpleExoPlayer, override SimpleExoPlayer.buildAudioProcessors to - return a GvrAudioProcessor. -* If constructing renderers directly, pass a GvrAudioProcessor to - MediaCodecAudioRenderer's constructor. +* If using `DefaultRenderersFactory`, override + `DefaultRenderersFactory.buildAudioProcessors` to return a + `GvrAudioProcessor`. +* If constructing renderers directly, pass a `GvrAudioProcessor` to + `MediaCodecAudioRenderer`'s constructor. [top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md + +## Links ## + +* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.gvr.*` + belong to this module. + +[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html diff --git a/extensions/ima/README.md b/extensions/ima/README.md index f328bb44cb..4f63214f04 100644 --- a/extensions/ima/README.md +++ b/extensions/ima/README.md @@ -1,7 +1,5 @@ # ExoPlayer IMA extension # -## Description ## - The IMA extension is a [MediaSource][] implementation wrapping the [Interactive Media Ads SDK for Android][IMA]. You can use it to insert ads alongside content. @@ -55,3 +53,10 @@ playback. [top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md [sample ad tags]: https://developers.google.com/interactive-media-ads/docs/sdks/android/tags + +## Links ## + +* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.ima.*` + belong to this module. + +[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html diff --git a/extensions/leanback/README.md b/extensions/leanback/README.md index 0fb3846f7e..1fa71c9a8c 100644 --- a/extensions/leanback/README.md +++ b/extensions/leanback/README.md @@ -1,6 +1,4 @@ -# ExoPlayer Leanback Extension # - -## Description ## +# ExoPlayer Leanback extension # This [Leanback][] Extension provides a [PlayerAdapter][] implementation for ExoPlayer. @@ -24,3 +22,10 @@ locally. Instructions for doing this can be found in ExoPlayer's [top level README][]. [top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md + +## Links ## + +* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.leanback.*` + belong to this module. + +[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html diff --git a/extensions/mediasession/README.md b/extensions/mediasession/README.md index 60fec9fb60..3278e8dba5 100644 --- a/extensions/mediasession/README.md +++ b/extensions/mediasession/README.md @@ -1,7 +1,5 @@ # ExoPlayer MediaSession extension # -## Description ## - The MediaSession extension mediates between a Player (or ExoPlayer) instance and a [MediaSession][]. It automatically retrieves and implements playback actions and syncs the player state with the state of the media session. The @@ -25,3 +23,10 @@ locally. Instructions for doing this can be found in ExoPlayer's [top level README][]. [top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md + +## Links ## + +* [Javadoc][]: Classes matching + `com.google.android.exoplayer2.ext.mediasession.*` belong to this module. + +[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html diff --git a/extensions/okhttp/README.md b/extensions/okhttp/README.md index b10c4ba629..f84d0c35f2 100644 --- a/extensions/okhttp/README.md +++ b/extensions/okhttp/README.md @@ -1,7 +1,5 @@ # ExoPlayer OkHttp extension # -## Description ## - The OkHttp extension is an [HttpDataSource][] implementation using Square's [OkHttp][]. @@ -49,3 +47,10 @@ new DefaultDataSourceFactory( new OkHttpDataSourceFactory(...) /* baseDataSourceFactory argument */); ``` respectively. + +## Links ## + +* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.okhttp.*` + belong to this module. + +[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html diff --git a/extensions/opus/README.md b/extensions/opus/README.md index cc21c77cf9..d766e8c9c4 100644 --- a/extensions/opus/README.md +++ b/extensions/opus/README.md @@ -1,9 +1,7 @@ # ExoPlayer Opus extension # -## Description ## - -The Opus extension provides `LibopusAudioRenderer`, which uses -libopus (the Opus decoding library) to decode Opus audio. +The Opus extension provides `LibopusAudioRenderer`, which uses libopus (the Opus +decoding library) to decode Opus audio. ## Build instructions ## @@ -86,3 +84,10 @@ Note: These instructions assume you're using `DefaultTrackSelector`. If you have a custom track selector the choice of `Renderer` is up to your implementation, so you need to make sure you are passing an `LibopusAudioRenderer` to the player, then implement your own logic to use the renderer for a given track. + +## Links ## + +* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.opus.*` + belong to this module. + +[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html diff --git a/extensions/rtmp/README.md b/extensions/rtmp/README.md index 80074f119c..7e6bc0d641 100644 --- a/extensions/rtmp/README.md +++ b/extensions/rtmp/README.md @@ -1,7 +1,5 @@ # ExoPlayer RTMP extension # -## Description ## - The RTMP extension is a [DataSource][] implementation for playing [RTMP][] streams using [LibRtmp Client for Android][]. @@ -41,3 +39,10 @@ application code are required. Alternatively, if you know that your application doesn't need to handle any other protocols, you can update any `DataSource`s and `DataSource.Factory` instantiations in your application code to use `RtmpDataSource` and `RtmpDataSourceFactory` directly. + +## Links ## + +* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.rtmp.*` + belong to this module. + +[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html diff --git a/extensions/vp9/README.md b/extensions/vp9/README.md index d28aa70db0..7bce4a2a25 100644 --- a/extensions/vp9/README.md +++ b/extensions/vp9/README.md @@ -1,9 +1,7 @@ # ExoPlayer VP9 extension # -## Description ## - -The VP9 extension provides `LibvpxVideoRenderer`, which uses -libvpx (the VPx decoding library) to decode VP9 video. +The VP9 extension provides `LibvpxVideoRenderer`, which uses libvpx (the VPx +decoding library) to decode VP9 video. ## Build instructions ## @@ -110,3 +108,10 @@ performed using a GL shader. To enable this mode, send the renderer a message of type `LibvpxVideoRenderer.MSG_SET_OUTPUT_BUFFER_RENDERER` with the `VpxVideoSurfaceView` as its object, instead of sending `MSG_SET_SURFACE` with a `Surface`. + +## Links ## + +* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.vp9.*` + belong to this module. + +[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html diff --git a/library/README.md b/library/README.md new file mode 100644 index 0000000000..7aa07fc521 --- /dev/null +++ b/library/README.md @@ -0,0 +1,7 @@ +# ExoPlayer library # + +The ExoPlayer library is split into multiple modules. See ExoPlayer's +[top level README][] for more information about the available library modules +and how to use them. + +[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md diff --git a/library/all/README.md b/library/all/README.md new file mode 100644 index 0000000000..8746e3afc6 --- /dev/null +++ b/library/all/README.md @@ -0,0 +1,13 @@ +# ExoPlayer full library # + +An empty module that depends on all of the other library modules. Depending on +the full library is equivalent to depending on all of the other library modules +individually. See ExoPlayer's [top level README][] for more information. + +[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md + +## Links ## + +* [Javadoc][]: Note that this Javadoc is combined with that of other modules. + +[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html diff --git a/library/core/README.md b/library/core/README.md new file mode 100644 index 0000000000..f31ffed131 --- /dev/null +++ b/library/core/README.md @@ -0,0 +1,9 @@ +# ExoPlayer core library module # + +The core of the ExoPlayer library. + +## Links ## + +* [Javadoc][]: Note that this Javadoc is combined with that of other modules. + +[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html diff --git a/library/dash/README.md b/library/dash/README.md new file mode 100644 index 0000000000..394a38a332 --- /dev/null +++ b/library/dash/README.md @@ -0,0 +1,12 @@ +# ExoPlayer DASH library module # + +Provides support for Dynamic Adaptive Streaming over HTTP (DASH) content. To +play DASH content, instantiate a `DashMediaSource` and pass it to +`ExoPlayer.prepare`. + +## Links ## + +* [Javadoc][]: Classes matching `com.google.android.exoplayer2.source.dash.*` + belong to this module. + +[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html diff --git a/library/hls/README.md b/library/hls/README.md new file mode 100644 index 0000000000..6f7e9d08d9 --- /dev/null +++ b/library/hls/README.md @@ -0,0 +1,11 @@ +# ExoPlayer HLS library module # + +Provides support for HTTP Live Streaming (HLS) content. To play HLS content, +instantiate a `HlsMediaSource` and pass it to `ExoPlayer.prepare`. + +## Links ## + +* [Javadoc][]: Classes matching `com.google.android.exoplayer2.source.hls.*` + belong to this module. + +[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html diff --git a/library/smoothstreaming/README.md b/library/smoothstreaming/README.md new file mode 100644 index 0000000000..69265e8702 --- /dev/null +++ b/library/smoothstreaming/README.md @@ -0,0 +1,12 @@ +# ExoPlayer SmoothStreaming library module # + +Provides support for Smooth Streaming content. To play Smooth Streaming content, +instantiate a `SsMediaSource` and pass it to `ExoPlayer.prepare`. + +## Links ## + +* [Javadoc][]: Classes matching + `com.google.android.exoplayer2.source.smoothstreaming.*` belong to this + module. + +[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html diff --git a/library/ui/README.md b/library/ui/README.md new file mode 100644 index 0000000000..34e93e43af --- /dev/null +++ b/library/ui/README.md @@ -0,0 +1,10 @@ +# ExoPlayer UI library module # + +Provides UI components and resources for use with ExoPlayer. + +## Links ## + +* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ui.*` + belong to this module. + +[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html From e3f305b5c09de7b635125d22d6e14d80c9a4857b Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 17 Aug 2017 08:14:46 -0700 Subject: [PATCH 0269/2472] Improve FORMAT_UNSUPPORTED_DRM related documentation and logging ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=165580016 --- .../android/exoplayer2/demo/EventLogger.java | 2 ++ .../android/exoplayer2/RendererCapabilities.java | 6 +++--- .../trackselection/MappingTrackSelector.java | 14 ++++++++------ 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java index 30dfb5140a..2ea4b5b7cf 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java @@ -431,6 +431,8 @@ import java.util.Locale; return "YES"; case RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES: return "NO_EXCEEDS_CAPABILITIES"; + case RendererCapabilities.FORMAT_UNSUPPORTED_DRM: + return "NO_UNSUPPORTED_DRM"; case RendererCapabilities.FORMAT_UNSUPPORTED_SUBTYPE: return "NO_UNSUPPORTED_TYPE"; case RendererCapabilities.FORMAT_UNSUPPORTED_TYPE: diff --git a/library/core/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java b/library/core/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java index f841a1b8b5..3f1be20cfb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java @@ -24,7 +24,7 @@ public interface RendererCapabilities { /** * A mask to apply to the result of {@link #supportsFormat(Format)} to obtain one of - * {@link #FORMAT_HANDLED}, {@link #FORMAT_EXCEEDS_CAPABILITIES}, + * {@link #FORMAT_HANDLED}, {@link #FORMAT_EXCEEDS_CAPABILITIES}, {@link #FORMAT_UNSUPPORTED_DRM}, * {@link #FORMAT_UNSUPPORTED_SUBTYPE} and {@link #FORMAT_UNSUPPORTED_TYPE}. */ int FORMAT_SUPPORT_MASK = 0b111; @@ -117,8 +117,8 @@ public interface RendererCapabilities { * the bitwise OR of three properties: *
          *
        • The level of support for the format itself. One of {@link #FORMAT_HANDLED}, - * {@link #FORMAT_EXCEEDS_CAPABILITIES}, {@link #FORMAT_UNSUPPORTED_SUBTYPE} and - * {@link #FORMAT_UNSUPPORTED_TYPE}.
        • + * {@link #FORMAT_EXCEEDS_CAPABILITIES}, {@link #FORMAT_UNSUPPORTED_DRM}, + * {@link #FORMAT_UNSUPPORTED_SUBTYPE} and {@link #FORMAT_UNSUPPORTED_TYPE}. *
        • The level of support for adapting from the format to another format of the same mime type. * One of {@link #ADAPTIVE_SEAMLESS}, {@link #ADAPTIVE_NOT_SEAMLESS} and * {@link #ADAPTIVE_NOT_SUPPORTED}.
        • diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java index 45ac9eab6e..d518b5a6be 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java @@ -199,6 +199,7 @@ public abstract class MappingTrackSelector extends TrackSelector { * @param trackIndex The index of the track within the track group. * @return One of {@link RendererCapabilities#FORMAT_HANDLED}, * {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}, + * {@link RendererCapabilities#FORMAT_UNSUPPORTED_DRM}, * {@link RendererCapabilities#FORMAT_UNSUPPORTED_SUBTYPE} and * {@link RendererCapabilities#FORMAT_UNSUPPORTED_TYPE}. */ @@ -214,6 +215,7 @@ public abstract class MappingTrackSelector extends TrackSelector { * Tracks for which {@link #getTrackFormatSupport(int, int, int)} returns * {@link RendererCapabilities#FORMAT_HANDLED} are always considered. * Tracks for which {@link #getTrackFormatSupport(int, int, int)} returns + * {@link RendererCapabilities#FORMAT_UNSUPPORTED_DRM}, * {@link RendererCapabilities#FORMAT_UNSUPPORTED_TYPE} or * {@link RendererCapabilities#FORMAT_UNSUPPORTED_SUBTYPE} are never considered. * Tracks for which {@link #getTrackFormatSupport(int, int, int)} returns @@ -615,12 +617,12 @@ public abstract class MappingTrackSelector extends TrackSelector { /** * Finds the renderer to which the provided {@link TrackGroup} should be mapped. *

          - * A {@link TrackGroup} is mapped to the renderer that reports - * {@link RendererCapabilities#FORMAT_HANDLED} support for one or more of the tracks in the group, - * or {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES} if no such renderer exists, or - * {@link RendererCapabilities#FORMAT_UNSUPPORTED_SUBTYPE} if again no such renderer exists. In - * the case that two or more renderers report the same level of support, the renderer with the - * lowest index is associated. + * A {@link TrackGroup} is mapped to the renderer that reports the highest of (listed in + * decreasing order of support) {@link RendererCapabilities#FORMAT_HANDLED}, + * {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}, + * {@link RendererCapabilities#FORMAT_UNSUPPORTED_DRM} and + * {@link RendererCapabilities#FORMAT_UNSUPPORTED_SUBTYPE}. In the case that two or more renderers + * report the same level of support, the renderer with the lowest index is associated. *

          * If all renderers report {@link RendererCapabilities#FORMAT_UNSUPPORTED_TYPE} for all of the * tracks in the group, then {@code renderers.length} is returned to indicate that the group was From 106d88df41974ce3c2e5ed2cbd62de1c68e710ba Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 19 Jul 2017 11:26:49 +0100 Subject: [PATCH 0270/2472] Some no-op LeanbackPlayerAdapter cleanup - Have a single inner class for all listeners. This is inline with what we do elsewhere in the library. - Avoid repeated getCallback() calls within methods. - Seek to default position from ended state, rather than 0. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=165603927 --- .../ext/leanback/LeanbackPlayerAdapter.java | 150 +++++++++--------- 1 file changed, 75 insertions(+), 75 deletions(-) diff --git a/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java b/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java index df77a737b8..26fd4e9c8f 100644 --- a/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java +++ b/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java @@ -26,7 +26,6 @@ import android.view.Surface; import android.view.SurfaceHolder; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; -import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; @@ -37,7 +36,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.util.ErrorMessageProvider; /** - * Leanback {@link PlayerAdapter} implementation for {@link SimpleExoPlayer}. + * Leanback {@code PlayerAdapter} implementation for {@link SimpleExoPlayer}. */ public final class LeanbackPlayerAdapter extends PlayerAdapter { @@ -48,58 +47,49 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { private final Context context; private final SimpleExoPlayer player; private final Handler handler; - private final Runnable updatePlayerRunnable = new Runnable() { - @Override - public void run() { - getCallback().onCurrentPositionChanged(LeanbackPlayerAdapter.this); - getCallback().onBufferedPositionChanged(LeanbackPlayerAdapter.this); - handler.postDelayed(this, updatePeriod); - } - }; + private final ComponentListener componentListener; + private final Runnable updatePlayerRunnable; + private ErrorMessageProvider errorMessageProvider; private SurfaceHolderGlueHost surfaceHolderGlueHost; private boolean initialized; private boolean hasSurface; private boolean isBuffering; - private ErrorMessageProvider errorMessageProvider; - private final int updatePeriod; - private final ExoPlayerEventListenerImpl exoPlayerListener = new ExoPlayerEventListenerImpl(); - private final SimpleExoPlayer.VideoListener videoListener = new SimpleExoPlayer.VideoListener() { - @Override - public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, - float pixelWidthHeightRatio) { - getCallback().onVideoSizeChanged(LeanbackPlayerAdapter.this, width, height); - } - - @Override - public void onRenderedFirstFrame() { - } - }; /** - * Constructor. - * Users are responsible for managing {@link SimpleExoPlayer} lifecycle. You must - * stop/release the player once you're done playing the media. + * Builds an instance. Note that the {@code PlayerAdapter} does not manage the lifecycle of the + * {@link SimpleExoPlayer} instance. The caller remains responsible for releasing the player when + * it's no longer required. * * @param context The current context (activity). * @param player Instance of your exoplayer that needs to be configured. + * @param updatePeriodMs The delay between player control updates, in milliseconds. */ - public LeanbackPlayerAdapter(Context context, SimpleExoPlayer player, int updatePeriod) { + public LeanbackPlayerAdapter(Context context, SimpleExoPlayer player, final int updatePeriodMs) { this.context = context; this.player = player; - this.handler = new Handler(); - this.updatePeriod = updatePeriod; + handler = new Handler(); + componentListener = new ComponentListener(); + updatePlayerRunnable = new Runnable() { + @Override + public void run() { + Callback callback = getCallback(); + callback.onCurrentPositionChanged(LeanbackPlayerAdapter.this); + callback.onBufferedPositionChanged(LeanbackPlayerAdapter.this); + handler.postDelayed(this, updatePeriodMs); + } + }; } @Override public void onAttachedToHost(PlaybackGlueHost host) { if (host instanceof SurfaceHolderGlueHost) { surfaceHolderGlueHost = ((SurfaceHolderGlueHost) host); - surfaceHolderGlueHost.setSurfaceHolderCallback(new VideoPlayerSurfaceHolderCallback()); + surfaceHolderGlueHost.setSurfaceHolderCallback(componentListener); } notifyListeners(); - player.addListener(exoPlayerListener); - player.addVideoListener(videoListener); + player.addListener(componentListener); + player.addVideoListener(componentListener); } private void notifyListeners() { @@ -110,15 +100,14 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { boolean hasEnded = playbackState == Player.STATE_ENDED; initialized = isInitialized; + Callback callback = getCallback(); if (oldIsPrepared != isPrepared()) { - getCallback().onPreparedStateChanged(this); + callback.onPreparedStateChanged(this); } - - getCallback().onPlayStateChanged(this); - getCallback().onBufferingStateChanged(this, isBuffering || !initialized); - + callback.onPlayStateChanged(this); + callback.onBufferingStateChanged(this, isBuffering || !initialized); if (hasEnded) { - getCallback().onPlayCompleted(this); + callback.onPlayCompleted(this); } } @@ -134,8 +123,8 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { @Override public void onDetachedFromHost() { - player.removeListener(exoPlayerListener); - player.removeVideoListener(videoListener); + player.removeListener(componentListener); + player.removeVideoListener(componentListener); if (surfaceHolderGlueHost != null) { surfaceHolderGlueHost.setSurfaceHolderCallback(null); surfaceHolderGlueHost = null; @@ -151,10 +140,9 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { @Override public void setProgressUpdatingEnabled(final boolean enabled) { handler.removeCallbacks(updatePlayerRunnable); - if (!enabled) { - return; + if (enabled) { + handler.post(updatePlayerRunnable); } - handler.postDelayed(updatePlayerRunnable, updatePeriod); } @Override @@ -164,8 +152,8 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { @Override public long getDuration() { - long duration = player.getDuration(); - return duration != C.TIME_UNSET ? duration : -1; + long durationMs = player.getDuration(); + return durationMs != C.TIME_UNSET ? durationMs : -1; } @Override @@ -176,7 +164,7 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { @Override public void play() { if (player.getPlaybackState() == Player.STATE_ENDED) { - seekTo(0); + player.seekToDefaultPosition(); } player.setPlayWhenReady(true); getCallback().onPlayStateChanged(this); @@ -198,10 +186,6 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { return player.getBufferedPosition(); } - /** - * @return True if ExoPlayer is ready and got a SurfaceHolder if - * {@link PlaybackGlueHost} provides SurfaceHolder. - */ @Override public boolean isPrepared() { return initialized && (surfaceHolderGlueHost == null || hasSurface); @@ -213,10 +197,11 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { getCallback().onPreparedStateChanged(this); } - /** - * Implements {@link SurfaceHolder.Callback} that can then be set on the {@link PlaybackGlueHost}. - */ - private final class VideoPlayerSurfaceHolderCallback implements SurfaceHolder.Callback { + private final class ComponentListener implements Player.EventListener, + SimpleExoPlayer.VideoListener, SurfaceHolder.Callback { + + // SurfaceHolder.Callback implementation. + @Override public void surfaceCreated(SurfaceHolder surfaceHolder) { setVideoSurface(surfaceHolder.getSurface()); @@ -224,67 +209,82 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { @Override public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) { + // Do nothing. } @Override public void surfaceDestroyed(SurfaceHolder surfaceHolder) { setVideoSurface(null); } - } - private final class ExoPlayerEventListenerImpl implements ExoPlayer.EventListener { + // Player.EventListener implementation. @Override public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { - LeanbackPlayerAdapter.this.notifyListeners(); + notifyListeners(); } @Override public void onPlayerError(ExoPlaybackException exception) { + Callback callback = getCallback(); if (errorMessageProvider != null) { - Pair message = errorMessageProvider.getErrorMessage(exception); - if (message != null) { - getCallback().onError(LeanbackPlayerAdapter.this, - message.first, - message.second); - return; - } + Pair errorMessage = errorMessageProvider.getErrorMessage(exception); + callback.onError(LeanbackPlayerAdapter.this, errorMessage.first, errorMessage.second); + } else { + callback.onError(LeanbackPlayerAdapter.this, exception.type, context.getString( + R.string.lb_media_player_error, exception.type, exception.rendererIndex)); } - getCallback().onError(LeanbackPlayerAdapter.this, - exception.type, - context.getString(R.string.lb_media_player_error, - exception.type, - exception.rendererIndex)); } @Override public void onLoadingChanged(boolean isLoading) { + // Do nothing. } @Override public void onTimelineChanged(Timeline timeline, Object manifest) { - getCallback().onDurationChanged(LeanbackPlayerAdapter.this); - getCallback().onCurrentPositionChanged(LeanbackPlayerAdapter.this); - getCallback().onBufferedPositionChanged(LeanbackPlayerAdapter.this); + Callback callback = getCallback(); + callback.onDurationChanged(LeanbackPlayerAdapter.this); + callback.onCurrentPositionChanged(LeanbackPlayerAdapter.this); + callback.onBufferedPositionChanged(LeanbackPlayerAdapter.this); } @Override public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { + // Do nothing. } @Override public void onPositionDiscontinuity() { - getCallback().onCurrentPositionChanged(LeanbackPlayerAdapter.this); - getCallback().onBufferedPositionChanged(LeanbackPlayerAdapter.this); + Callback callback = getCallback(); + callback.onCurrentPositionChanged(LeanbackPlayerAdapter.this); + callback.onBufferedPositionChanged(LeanbackPlayerAdapter.this); } @Override public void onPlaybackParametersChanged(PlaybackParameters params) { + // Do nothing. } @Override public void onRepeatModeChanged(int repeatMode) { + // Do nothing. } + + // SimpleExoplayerView.Callback implementation. + + @Override + public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, + float pixelWidthHeightRatio) { + getCallback().onVideoSizeChanged(LeanbackPlayerAdapter.this, width, height); + } + + @Override + public void onRenderedFirstFrame() { + // Do nothing. + } + } + } From 4917929a3aa59b2a688994977fef8680464bff83 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 17 Aug 2017 14:19:41 -0700 Subject: [PATCH 0271/2472] Improve MediaSource/MediaPeriod documentation ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=165628229 --- .../android/exoplayer2/source/MediaPeriod.java | 5 ++++- .../android/exoplayer2/source/MediaSource.java | 14 +++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java index 7a43dd7562..514b96ae8d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java @@ -16,12 +16,15 @@ package com.google.android.exoplayer2.source; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.trackselection.TrackSelection; import java.io.IOException; /** - * A source of a single period of media. + * Loads media corresponding to a {@link Timeline.Period}, and allows that media to be read. All + * methods are called on the player's internal playback thread, as described in the + * {@link ExoPlayer} Javadoc. */ public interface MediaPeriod extends SequenceableLoader { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java index 790620a80c..11489cfbb8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java @@ -23,7 +23,19 @@ import com.google.android.exoplayer2.upstream.Allocator; import java.io.IOException; /** - * A source of media consisting of one or more {@link MediaPeriod}s. + * Defines and provides media to be played by an {@link ExoPlayer}. A MediaSource has two main + * responsibilities: + *

            + *
          • To provide the player with a {@link Timeline} defining the structure of its media, and to + * provide a new timeline whenever the structure of the media changes. The MediaSource provides + * these timelines by calling {@link Listener#onSourceInfoRefreshed} on the {@link Listener} + * passed to {@link #prepareSource(ExoPlayer, boolean, Listener)}.
          • + *
          • To provide {@link MediaPeriod} instances for the periods in its timeline. MediaPeriods are + * obtained by calling {@link #createPeriod(MediaPeriodId, Allocator)}, and provide a way for the + * player to load and read the media.
          • + *
          + * All methods are called on the player's internal playback thread, as described in the + * {@link ExoPlayer} Javadoc. */ public interface MediaSource { From 2470b39d9520618c59a8c857085120dd8da2ffee Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 17 Aug 2017 14:20:56 -0700 Subject: [PATCH 0272/2472] Split metadata and text outputs from their renderer classes There will be non-ExoPlayer players that can output text and metadata, so the outputs should be standalone. There may also be ExoPlayer instances that use non-standard text and metadata renderers, for which this change also makes sense. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=165628420 --- .../android/exoplayer2/demo/EventLogger.java | 11 ++--- .../exoplayer2/DefaultRenderersFactory.java | 15 +++--- .../android/exoplayer2/RenderersFactory.java | 8 ++-- .../android/exoplayer2/SimpleExoPlayer.java | 48 +++++++++---------- .../exoplayer2/metadata/MetadataOutput.java | 30 ++++++++++++ .../exoplayer2/metadata/MetadataRenderer.java | 20 +++----- .../android/exoplayer2/text/TextOutput.java | 32 +++++++++++++ .../android/exoplayer2/text/TextRenderer.java | 23 ++++----- .../exoplayer2/ui/SimpleExoPlayerView.java | 10 ++-- .../android/exoplayer2/ui/SubtitleView.java | 4 +- .../testutil/ExoPlayerTestRunner.java | 8 ++-- 11 files changed, 128 insertions(+), 81 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataOutput.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/text/TextOutput.java diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java index 2ea4b5b7cf..cbc3536ef7 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java @@ -29,7 +29,7 @@ import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; import com.google.android.exoplayer2.metadata.Metadata; -import com.google.android.exoplayer2.metadata.MetadataRenderer; +import com.google.android.exoplayer2.metadata.MetadataOutput; import com.google.android.exoplayer2.metadata.emsg.EventMessage; import com.google.android.exoplayer2.metadata.id3.ApicFrame; import com.google.android.exoplayer2.metadata.id3.CommentFrame; @@ -55,10 +55,9 @@ import java.util.Locale; /** * Logs player events using {@link Log}. */ -/* package */ final class EventLogger implements Player.EventListener, AudioRendererEventListener, - VideoRendererEventListener, AdaptiveMediaSourceEventListener, - ExtractorMediaSource.EventListener, DefaultDrmSessionManager.EventListener, - MetadataRenderer.Output { +/* package */ final class EventLogger implements Player.EventListener, MetadataOutput, + AudioRendererEventListener, VideoRendererEventListener, AdaptiveMediaSourceEventListener, + ExtractorMediaSource.EventListener, DefaultDrmSessionManager.EventListener { private static final String TAG = "EventLogger"; private static final int MAX_TIMELINE_ITEM_LINES = 3; @@ -205,7 +204,7 @@ import java.util.Locale; Log.d(TAG, "]"); } - // MetadataRenderer.Output + // MetadataOutput @Override public void onMetadata(Metadata metadata) { 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 index 27852f0c15..2272306117 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java @@ -28,7 +28,9 @@ import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.mediacodec.MediaCodecSelector; +import com.google.android.exoplayer2.metadata.MetadataOutput; import com.google.android.exoplayer2.metadata.MetadataRenderer; +import com.google.android.exoplayer2.text.TextOutput; import com.google.android.exoplayer2.text.TextRenderer; import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; @@ -137,8 +139,8 @@ public class DefaultRenderersFactory implements RenderersFactory { @Override public Renderer[] createRenderers(Handler eventHandler, VideoRendererEventListener videoRendererEventListener, - AudioRendererEventListener audioRendererEventListener, TextRenderer.Output textRendererOutput, - MetadataRenderer.Output metadataRendererOutput) { + AudioRendererEventListener audioRendererEventListener, + TextOutput textRendererOutput, MetadataOutput metadataRendererOutput) { ArrayList renderersList = new ArrayList<>(); buildVideoRenderers(context, drmSessionManager, allowedVideoJoiningTimeMs, eventHandler, videoRendererEventListener, extensionRendererMode, renderersList); @@ -283,8 +285,8 @@ public class DefaultRenderersFactory implements RenderersFactory { * @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, + protected void buildTextRenderers(Context context, TextOutput output, Looper outputLooper, + @ExtensionRendererMode int extensionRendererMode, ArrayList out) { out.add(new TextRenderer(output, outputLooper)); } @@ -299,9 +301,8 @@ public class DefaultRenderersFactory implements RenderersFactory { * @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) { + protected void buildMetadataRenderers(Context context, MetadataOutput output, Looper outputLooper, + @ExtensionRendererMode int extensionRendererMode, ArrayList out) { out.add(new MetadataRenderer(output, outputLooper)); } 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 index 728cfa387a..a08ba448a4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/RenderersFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/RenderersFactory.java @@ -17,8 +17,8 @@ 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.metadata.MetadataOutput; +import com.google.android.exoplayer2.text.TextOutput; import com.google.android.exoplayer2.video.VideoRendererEventListener; /** @@ -38,7 +38,7 @@ public interface RenderersFactory { */ Renderer[] createRenderers(Handler eventHandler, VideoRendererEventListener videoRendererEventListener, - AudioRendererEventListener audioRendererEventListener, - TextRenderer.Output textRendererOutput, MetadataRenderer.Output metadataRendererOutput); + AudioRendererEventListener audioRendererEventListener, TextOutput textRendererOutput, + MetadataOutput metadataRendererOutput); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index cc0791bf44..d71c81f08f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -31,11 +31,11 @@ import com.google.android.exoplayer2.audio.AudioAttributes; import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.metadata.Metadata; -import com.google.android.exoplayer2.metadata.MetadataRenderer; +import com.google.android.exoplayer2.metadata.MetadataOutput; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.text.Cue; -import com.google.android.exoplayer2.text.TextRenderer; +import com.google.android.exoplayer2.text.TextOutput; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.util.Util; @@ -89,8 +89,8 @@ public class SimpleExoPlayer implements ExoPlayer { private final ExoPlayer player; private final ComponentListener componentListener; private final CopyOnWriteArraySet videoListeners; - private final CopyOnWriteArraySet textOutputs; - private final CopyOnWriteArraySet metadataOutputs; + private final CopyOnWriteArraySet textOutputs; + private final CopyOnWriteArraySet metadataOutputs; private final int videoRendererCount; private final int audioRendererCount; @@ -491,7 +491,7 @@ public class SimpleExoPlayer implements ExoPlayer { * * @param listener The output to register. */ - public void addTextOutput(TextRenderer.Output listener) { + public void addTextOutput(TextOutput listener) { textOutputs.add(listener); } @@ -500,7 +500,7 @@ public class SimpleExoPlayer implements ExoPlayer { * * @param listener The output to remove. */ - public void removeTextOutput(TextRenderer.Output listener) { + public void removeTextOutput(TextOutput listener) { textOutputs.remove(listener); } @@ -508,10 +508,10 @@ public class SimpleExoPlayer implements ExoPlayer { * Sets an output to receive text events, removing all existing outputs. * * @param output The output. - * @deprecated Use {@link #addTextOutput(TextRenderer.Output)}. + * @deprecated Use {@link #addTextOutput(TextOutput)}. */ @Deprecated - public void setTextOutput(TextRenderer.Output output) { + public void setTextOutput(TextOutput output) { textOutputs.clear(); if (output != null) { addTextOutput(output); @@ -519,13 +519,13 @@ public class SimpleExoPlayer implements ExoPlayer { } /** - * Equivalent to {@link #removeTextOutput(TextRenderer.Output)}. + * Equivalent to {@link #removeTextOutput(TextOutput)}. * * @param output The output to clear. - * @deprecated Use {@link #removeTextOutput(TextRenderer.Output)}. + * @deprecated Use {@link #removeTextOutput(TextOutput)}. */ @Deprecated - public void clearTextOutput(TextRenderer.Output output) { + public void clearTextOutput(TextOutput output) { removeTextOutput(output); } @@ -534,7 +534,7 @@ public class SimpleExoPlayer implements ExoPlayer { * * @param listener The output to register. */ - public void addMetadataOutput(MetadataRenderer.Output listener) { + public void addMetadataOutput(MetadataOutput listener) { metadataOutputs.add(listener); } @@ -543,7 +543,7 @@ public class SimpleExoPlayer implements ExoPlayer { * * @param listener The output to remove. */ - public void removeMetadataOutput(MetadataRenderer.Output listener) { + public void removeMetadataOutput(MetadataOutput listener) { metadataOutputs.remove(listener); } @@ -551,10 +551,10 @@ public class SimpleExoPlayer implements ExoPlayer { * Sets an output to receive metadata events, removing all existing outputs. * * @param output The output. - * @deprecated Use {@link #addMetadataOutput(MetadataRenderer.Output)}. + * @deprecated Use {@link #addMetadataOutput(MetadataOutput)}. */ @Deprecated - public void setMetadataOutput(MetadataRenderer.Output output) { + public void setMetadataOutput(MetadataOutput output) { metadataOutputs.clear(); if (output != null) { addMetadataOutput(output); @@ -562,13 +562,13 @@ public class SimpleExoPlayer implements ExoPlayer { } /** - * Equivalent to {@link #removeMetadataOutput(MetadataRenderer.Output)}. + * Equivalent to {@link #removeMetadataOutput(MetadataOutput)}. * * @param output The output to clear. - * @deprecated Use {@link #removeMetadataOutput(MetadataRenderer.Output)}. + * @deprecated Use {@link #removeMetadataOutput(MetadataOutput)}. */ @Deprecated - public void clearMetadataOutput(MetadataRenderer.Output output) { + public void clearMetadataOutput(MetadataOutput output) { removeMetadataOutput(output); } @@ -849,8 +849,8 @@ public class SimpleExoPlayer implements ExoPlayer { } private final class ComponentListener implements VideoRendererEventListener, - AudioRendererEventListener, TextRenderer.Output, MetadataRenderer.Output, - SurfaceHolder.Callback, TextureView.SurfaceTextureListener { + AudioRendererEventListener, TextOutput, MetadataOutput, SurfaceHolder.Callback, + TextureView.SurfaceTextureListener { // VideoRendererEventListener implementation @@ -973,20 +973,20 @@ public class SimpleExoPlayer implements ExoPlayer { audioSessionId = C.AUDIO_SESSION_ID_UNSET; } - // TextRenderer.Output implementation + // TextOutput implementation @Override public void onCues(List cues) { - for (TextRenderer.Output textOutput : textOutputs) { + for (TextOutput textOutput : textOutputs) { textOutput.onCues(cues); } } - // MetadataRenderer.Output implementation + // MetadataOutput implementation @Override public void onMetadata(Metadata metadata) { - for (MetadataRenderer.Output metadataOutput : metadataOutputs) { + for (MetadataOutput metadataOutput : metadataOutputs) { metadataOutput.onMetadata(metadata); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataOutput.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataOutput.java new file mode 100644 index 0000000000..b635cbc4b2 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataOutput.java @@ -0,0 +1,30 @@ +/* + * 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.metadata; + +/** + * Receives metadata output. + */ +public interface MetadataOutput { + + /** + * Called when there is metadata associated with current playback time. + * + * @param metadata The metadata. + */ + void onMetadata(Metadata metadata); + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java index 7ff426e2df..f46dd467c8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java @@ -33,18 +33,10 @@ import java.util.Arrays; public final class MetadataRenderer extends BaseRenderer implements Callback { /** - * Receives output from a {@link MetadataRenderer}. + * @deprecated Use {@link MetadataOutput}. */ - public interface Output { - - /** - * Called each time there is a metadata associated with current playback time. - * - * @param metadata The metadata. - */ - void onMetadata(Metadata metadata); - - } + @Deprecated + public interface Output extends MetadataOutput {} private static final int MSG_INVOKE_RENDERER = 0; // TODO: Holding multiple pending metadata objects is temporary mitigation against @@ -53,7 +45,7 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { private static final int MAX_PENDING_METADATA_COUNT = 5; private final MetadataDecoderFactory decoderFactory; - private final Output output; + private final MetadataOutput output; private final Handler outputHandler; private final FormatHolder formatHolder; private final MetadataInputBuffer buffer; @@ -73,7 +65,7 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { * {@link android.app.Activity#getMainLooper()}. Null may be passed if the output should be * called directly on the player's internal rendering thread. */ - public MetadataRenderer(Output output, Looper outputLooper) { + public MetadataRenderer(MetadataOutput output, Looper outputLooper) { this(output, outputLooper, MetadataDecoderFactory.DEFAULT); } @@ -86,7 +78,7 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { * called directly on the player's internal rendering thread. * @param decoderFactory A factory from which to obtain {@link MetadataDecoder} instances. */ - public MetadataRenderer(Output output, Looper outputLooper, + public MetadataRenderer(MetadataOutput output, Looper outputLooper, MetadataDecoderFactory decoderFactory) { super(C.TRACK_TYPE_METADATA); this.output = Assertions.checkNotNull(output); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/TextOutput.java b/library/core/src/main/java/com/google/android/exoplayer2/text/TextOutput.java new file mode 100644 index 0000000000..5a08db94cb --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/TextOutput.java @@ -0,0 +1,32 @@ +/* + * 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; + +import java.util.List; + +/** + * Receives text output. + */ +public interface TextOutput { + + /** + * Called when there is a change in the {@link Cue}s. + * + * @param cues The {@link Cue}s. + */ + void onCues(List cues); + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java index 1820d43e75..8e1966305e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java @@ -37,23 +37,15 @@ import java.util.List; *

          * {@link Subtitle}s are decoded from sample data using {@link SubtitleDecoder} instances obtained * from a {@link SubtitleDecoderFactory}. The actual rendering of the subtitle {@link Cue}s is - * delegated to an {@link Output}. + * delegated to an {@link TextOutput}. */ public final class TextRenderer extends BaseRenderer implements Callback { /** - * Receives output from a {@link TextRenderer}. + * @deprecated Use {@link TextOutput}. */ - public interface Output { - - /** - * Called each time there is a change in the {@link Cue}s. - * - * @param cues The {@link Cue}s. - */ - void onCues(List cues); - - } + @Deprecated + public interface Output extends TextOutput {} @Retention(RetentionPolicy.SOURCE) @IntDef({REPLACEMENT_STATE_NONE, REPLACEMENT_STATE_SIGNAL_END_OF_STREAM, @@ -79,7 +71,7 @@ public final class TextRenderer extends BaseRenderer implements Callback { private static final int MSG_UPDATE_OUTPUT = 0; private final Handler outputHandler; - private final Output output; + private final TextOutput output; private final SubtitleDecoderFactory decoderFactory; private final FormatHolder formatHolder; @@ -101,7 +93,7 @@ public final class TextRenderer extends BaseRenderer implements Callback { * using {@link android.app.Activity#getMainLooper()}. Null may be passed if the output * should be called directly on the player's internal rendering thread. */ - public TextRenderer(Output output, Looper outputLooper) { + public TextRenderer(TextOutput output, Looper outputLooper) { this(output, outputLooper, SubtitleDecoderFactory.DEFAULT); } @@ -114,7 +106,8 @@ public final class TextRenderer extends BaseRenderer implements Callback { * should be called directly on the player's internal rendering thread. * @param decoderFactory A factory from which to obtain {@link SubtitleDecoder} instances. */ - public TextRenderer(Output output, Looper outputLooper, SubtitleDecoderFactory decoderFactory) { + public TextRenderer(TextOutput output, Looper outputLooper, + SubtitleDecoderFactory decoderFactory) { super(C.TRACK_TYPE_TEXT); this.output = Assertions.checkNotNull(output); this.outputHandler = outputLooper == null ? null : new Handler(outputLooper, this); diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index b3dc3c7264..0d10e0fcf9 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -43,7 +43,7 @@ import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.id3.ApicFrame; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.text.Cue; -import com.google.android.exoplayer2.text.TextRenderer; +import com.google.android.exoplayer2.text.TextOutput; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.ui.AspectRatioFrameLayout.ResizeMode; @@ -844,10 +844,10 @@ public final class SimpleExoPlayerView extends FrameLayout { aspectRatioFrame.setResizeMode(resizeMode); } - private final class ComponentListener implements SimpleExoPlayer.VideoListener, - TextRenderer.Output, Player.EventListener { + private final class ComponentListener implements TextOutput, SimpleExoPlayer.VideoListener, + Player.EventListener { - // TextRenderer.Output implementation + // TextOutput implementation @Override public void onCues(List cues) { @@ -856,7 +856,7 @@ public final class SimpleExoPlayerView extends FrameLayout { } } - // SimpleExoPlayer.VideoListener implementation + // SimpleExoPlayer.VideoInfoListener implementation @Override public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java index 3bcfcc3ef3..618f2fa336 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java @@ -25,7 +25,7 @@ import android.view.View; import android.view.accessibility.CaptioningManager; import com.google.android.exoplayer2.text.CaptionStyleCompat; import com.google.android.exoplayer2.text.Cue; -import com.google.android.exoplayer2.text.TextRenderer; +import com.google.android.exoplayer2.text.TextOutput; import com.google.android.exoplayer2.util.Util; import java.util.ArrayList; import java.util.List; @@ -33,7 +33,7 @@ import java.util.List; /** * A view for displaying subtitle {@link Cue}s. */ -public final class SubtitleView extends View implements TextRenderer.Output { +public final class SubtitleView extends View implements TextOutput { /** * The default fractional text size. diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java index 2bfef0b4ab..f354ad6a76 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java @@ -29,12 +29,12 @@ 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; -import com.google.android.exoplayer2.metadata.MetadataRenderer; +import com.google.android.exoplayer2.metadata.MetadataOutput; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner.Builder.PlayerFactory; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; -import com.google.android.exoplayer2.text.TextRenderer.Output; +import com.google.android.exoplayer2.text.TextOutput; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.MappingTrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; @@ -164,8 +164,8 @@ public final class ExoPlayerTestRunner implements Player.EventListener { @Override public Renderer[] createRenderers(Handler eventHandler, VideoRendererEventListener videoRendererEventListener, - AudioRendererEventListener audioRendererEventListener, Output textRendererOutput, - MetadataRenderer.Output metadataRendererOutput) { + AudioRendererEventListener audioRendererEventListener, TextOutput textRendererOutput, + MetadataOutput metadataRendererOutput) { return renderers; } }; From 7e586c3a8fcf7d1967ee2d34eb17bf83aec2106f Mon Sep 17 00:00:00 2001 From: Dulmandakh Date: Wed, 9 Aug 2017 15:42:58 +0900 Subject: [PATCH 0273/2472] expose setPropertyByteArray, setPropertyString export setPropertyByteArray, setPropertyString of DefaultDrmSessionManager for easy customization. --- .../exoplayer2/drm/OfflineLicenseHelper.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java index 741ad1f06f..38bb982f60 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java @@ -210,6 +210,22 @@ public final class OfflineLicenseHelper { } return licenseDurationRemainingSec; } + + public byte[] getPropertyByteArray(String key) { + return drmSessionManager.getPropertyByteArray(key); + } + + public void setPropertyByteArray(String key, byte[] value) { + drmSessionManager.setPropertyByteArray(key, value); + } + + public String getPropertyString(String key) { + return drmSessionManager.getPropertyString(key); + } + + public void setPropertyString(String key, String value) { + drmSessionManager.setPropertyString(key, value); + } private byte[] blockingKeyRequest(@Mode int licenseMode, byte[] offlineLicenseKeySetId, DrmInitData drmInitData) throws DrmSessionException { From 61078078df9fe9b7f2305f0ceb016a486cb02af2 Mon Sep 17 00:00:00 2001 From: ojw28 Date: Thu, 17 Aug 2017 23:11:44 +0100 Subject: [PATCH 0274/2472] Revert "expose setPropertyByteArray, setPropertyString" --- .../exoplayer2/drm/OfflineLicenseHelper.java | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java index b5927dcd95..040ca50c76 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java @@ -185,22 +185,6 @@ public final class OfflineLicenseHelper { } return licenseDurationRemainingSec; } - - public byte[] getPropertyByteArray(String key) { - return drmSessionManager.getPropertyByteArray(key); - } - - public void setPropertyByteArray(String key, byte[] value) { - drmSessionManager.setPropertyByteArray(key, value); - } - - public String getPropertyString(String key) { - return drmSessionManager.getPropertyString(key); - } - - public void setPropertyString(String key, String value) { - drmSessionManager.setPropertyString(key, value); - } private byte[] blockingKeyRequest(@Mode int licenseMode, byte[] offlineLicenseKeySetId, DrmInitData drmInitData) throws DrmSessionException { From 4fa307d6549a9a6126527f95aa72581ba241b2c5 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 17 Aug 2017 23:13:12 +0100 Subject: [PATCH 0275/2472] Minor style tweaks --- .../assets/subrip/typical_unexpected_end | 10 ++++ .../text/subrip/SubripDecoderTest.java | 12 +++++ .../exoplayer2/drm/OfflineLicenseHelper.java | 48 ++++++++++++------- .../exoplayer2/extractor/mp4/AtomParsers.java | 18 +++---- .../exoplayer2/text/subrip/SubripDecoder.java | 9 +++- 5 files changed, 69 insertions(+), 28 deletions(-) create mode 100644 library/core/src/androidTest/assets/subrip/typical_unexpected_end diff --git a/library/core/src/androidTest/assets/subrip/typical_unexpected_end b/library/core/src/androidTest/assets/subrip/typical_unexpected_end new file mode 100644 index 0000000000..8e2949b8db --- /dev/null +++ b/library/core/src/androidTest/assets/subrip/typical_unexpected_end @@ -0,0 +1,10 @@ +1 +00:00:00,000 --> 00:00:01,234 +This is the first subtitle. + +2 +00:00:02,345 --> 00:00:03,456 +This is the second subtitle. +Second subtitle with second line. + +3 \ No newline at end of file diff --git a/library/core/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 index 167499fcdc..744634edda 100644 --- a/library/core/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 @@ -31,6 +31,7 @@ public final class SubripDecoderTest extends InstrumentationTestCase { private static final String TYPICAL_MISSING_TIMECODE = "subrip/typical_missing_timecode"; private static final String TYPICAL_MISSING_SEQUENCE = "subrip/typical_missing_sequence"; private static final String TYPICAL_NEGATIVE_TIMESTAMPS = "subrip/typical_negative_timestamps"; + private static final String TYPICAL_UNEXPECTED_END = "subrip/typical_unexpected_end"; private static final String NO_END_TIMECODES_FILE = "subrip/no_end_timecodes"; public void testDecodeEmpty() throws IOException { @@ -107,6 +108,17 @@ public final class SubripDecoderTest extends InstrumentationTestCase { assertTypicalCue3(subtitle, 0); } + public void testDecodeTypicalUnexpectedEnd() throws IOException { + // Parsing should succeed, parsing the first and second cues only. + SubripDecoder decoder = new SubripDecoder(); + byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_UNEXPECTED_END); + SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false); + + assertEquals(4, subtitle.getEventTimeCount()); + assertTypicalCue1(subtitle, 0); + assertTypicalCue2(subtitle, 2); + } + public void testDecodeNoEndTimecodes() throws IOException { SubripDecoder decoder = new SubripDecoder(); byte[] bytes = TestUtil.getByteArray(getInstrumentation(), NO_END_TIMECODES_FILE); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java index 38bb982f60..cafe41ed09 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java @@ -141,9 +141,32 @@ public final class OfflineLicenseHelper { optionalKeyRequestParameters, new Handler(handlerThread.getLooper()), eventListener); } - /** Releases the helper. Should be called when the helper is no longer required. */ - public void release() { - handlerThread.quit(); + /** + * @see DefaultDrmSessionManager#getPropertyByteArray + */ + public synchronized byte[] getPropertyByteArray(String key) { + return drmSessionManager.getPropertyByteArray(key); + } + + /** + * @see DefaultDrmSessionManager#setPropertyByteArray + */ + public synchronized void setPropertyByteArray(String key, byte[] value) { + drmSessionManager.setPropertyByteArray(key, value); + } + + /** + * @see DefaultDrmSessionManager#getPropertyString + */ + public synchronized String getPropertyString(String key) { + return drmSessionManager.getPropertyString(key); + } + + /** + * @see DefaultDrmSessionManager#setPropertyString + */ + public synchronized void setPropertyString(String key, String value) { + drmSessionManager.setPropertyString(key, value); } /** @@ -210,21 +233,12 @@ public final class OfflineLicenseHelper { } return licenseDurationRemainingSec; } - - public byte[] getPropertyByteArray(String key) { - return drmSessionManager.getPropertyByteArray(key); - } - public void setPropertyByteArray(String key, byte[] value) { - drmSessionManager.setPropertyByteArray(key, value); - } - - public String getPropertyString(String key) { - return drmSessionManager.getPropertyString(key); - } - - public void setPropertyString(String key, String value) { - drmSessionManager.setPropertyString(key, value); + /** + * Releases the helper. Should be called when the helper is no longer required. + */ + public void release() { + handlerThread.quit(); } private byte[] blockingKeyRequest(@Mode int licenseMode, byte[] offlineLicenseKeySetId, diff --git a/library/core/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 index cc70804a29..450e0682e6 100644 --- a/library/core/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 @@ -995,9 +995,10 @@ import java.util.List; int objectTypeIndication = parent.readUnsignedByte(); String mimeType; switch (objectTypeIndication) { - case 0x6B: - mimeType = MimeTypes.AUDIO_MPEG; - return Pair.create(mimeType, null); + case 0x60: + case 0x61: + mimeType = MimeTypes.VIDEO_MPEG2; + break; case 0x20: mimeType = MimeTypes.VIDEO_MP4V; break; @@ -1007,6 +1008,9 @@ import java.util.List; case 0x23: mimeType = MimeTypes.VIDEO_H265; break; + case 0x6B: + mimeType = MimeTypes.AUDIO_MPEG; + return Pair.create(mimeType, null); case 0x40: case 0x66: case 0x67: @@ -1027,10 +1031,6 @@ import java.util.List; case 0xAB: mimeType = MimeTypes.AUDIO_DTS_HD; return Pair.create(mimeType, null); - case 0x60: /* Visual 13818-2 Simple Profile */ - case 0x61: /* Visual 13818-2 Main Profile */ - mimeType = MimeTypes.VIDEO_MPEG2; - break; default: mimeType = null; break; @@ -1038,8 +1038,8 @@ import java.util.List; parent.skipBytes(12); - // Start of the AudioSpecificConfig. - parent.skipBytes(1); // AudioSpecificConfig tag + // Start of the DecoderSpecificInfo. + parent.skipBytes(1); // DecoderSpecificInfo tag int initializationDataSize = parseExpandableClassSize(parent); byte[] initializationData = new byte[initializationDataSize]; parent.readBytes(initializationData, 0, initializationDataSize); diff --git a/library/core/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 index 49ebe84d67..6cce902e87 100644 --- a/library/core/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 @@ -69,8 +69,13 @@ public final class SubripDecoder extends SimpleSubtitleDecoder { // Read and parse the timing line. boolean haveEndTimecode = false; currentLine = subripData.readLine(); - Matcher matcher = currentLine == null ? null : SUBRIP_TIMING_LINE.matcher(currentLine); - if (matcher != null && matcher.matches()) { + if (currentLine == null) { + Log.w(TAG, "Unexpected end"); + break; + } + + Matcher matcher = SUBRIP_TIMING_LINE.matcher(currentLine); + if (matcher.matches()) { cueTimesUs.add(parseTimecode(matcher, 1)); if (!TextUtils.isEmpty(matcher.group(6))) { haveEndTimecode = true; From cc9a93d9d9f5369c6af158541a01ad32b2b30d4b Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 17 Aug 2017 23:18:58 +0100 Subject: [PATCH 0276/2472] Rm SS offline for now --- .../offline/SsDownloadAction.java | 85 -------------- .../smoothstreaming/offline/SsDownloader.java | 111 ------------------ 2 files changed, 196 deletions(-) delete mode 100644 library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadAction.java delete mode 100644 library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadAction.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadAction.java deleted file mode 100644 index 29b6ad1516..0000000000 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadAction.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * 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.smoothstreaming.offline; - -import android.net.Uri; -import com.google.android.exoplayer2.offline.DownloadAction; -import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; -import com.google.android.exoplayer2.offline.SegmentDownloadAction; -import com.google.android.exoplayer2.source.smoothstreaming.manifest.TrackKey; -import com.google.android.exoplayer2.util.ClosedSource; -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; - -/** An action to download or remove downloaded SmoothStreaming streams. */ -@ClosedSource(reason = "Not ready yet") -public final class SsDownloadAction extends SegmentDownloadAction { - - public static final Serializer SERIALIZER = new SegmentDownloadActionSerializer() { - - private static final String TYPE = "SsDownloadAction"; - - @Override - public String getType() { - return TYPE; - } - - @Override - protected void writeKey(DataOutputStream output, TrackKey key) throws IOException { - output.writeInt(key.streamElementIndex); - output.writeInt(key.trackIndex); - } - - @Override - protected TrackKey readKey(DataInputStream input) throws IOException { - return new TrackKey(input.readInt(), input.readInt()); - } - - @Override - protected TrackKey[] createKeyArray(int keyCount) { - return new TrackKey[keyCount]; - } - - @Override - protected DownloadAction createDownloadAction(Uri manifestUri, boolean removeAction, - TrackKey[] keys) { - return new SsDownloadAction(manifestUri, removeAction, keys); - } - - }; - - /** @see SegmentDownloadAction#SegmentDownloadAction(Uri, boolean, Object[]) */ - public SsDownloadAction(Uri manifestUri, boolean removeAction, TrackKey... keys) { - super(manifestUri, removeAction, keys); - } - - @Override - public SsDownloader createDownloader(DownloaderConstructorHelper constructorHelper) - throws IOException { - SsDownloader downloader = new SsDownloader(manifestUri, constructorHelper); - if (!isRemoveAction()) { - downloader.selectRepresentations(keys); - } - return downloader; - } - - @Override - public Serializer getSerializer() { - return SERIALIZER; - } - -} diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java deleted file mode 100644 index fe9c21d855..0000000000 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * 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.smoothstreaming.offline; - -import android.net.Uri; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; -import com.google.android.exoplayer2.offline.SegmentDownloader; -import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest; -import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest.StreamElement; -import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifestParser; -import com.google.android.exoplayer2.source.smoothstreaming.manifest.TrackKey; -import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.upstream.DataSpec; -import com.google.android.exoplayer2.upstream.ParsingLoadable; -import com.google.android.exoplayer2.util.ClosedSource; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -/** - * Helper class to download SmoothStreaming streams. - * - *

          Except {@link #getTotalSegments()}, {@link #getDownloadedSegments()} and {@link - * #getDownloadedBytes()}, this class isn't thread safe. - * - *

          Example usage: - * - *

          - * {@code
          - * SimpleCache cache = new SimpleCache(downloadFolder, new NoOpCacheEvictor());
          - * DefaultHttpDataSourceFactory factory = new DefaultHttpDataSourceFactory("ExoPlayer", null);
          - * DownloaderConstructorHelper constructorHelper =
          - *     new DownloaderConstructorHelper(cache, factory);
          - * SsDownloader ssDownloader = new SsDownloader(manifestUrl, constructorHelper);
          - * // Select the first track of the first stream element
          - * ssDownloader.selectRepresentations(new TrackKey[] {new TrackKey(0, 0)});
          - * ssDownloader.download(new ProgressListener() {
          - *   @Override
          - *   public void onDownloadProgress(Downloader downloader, float downloadPercentage,
          - *       long downloadedBytes) {
          - *     // Invoked periodically during the download.
          - *   }
          - * });
          - * // Access downloaded data using CacheDataSource
          - * CacheDataSource cacheDataSource =
          - *     new CacheDataSource(cache, factory.createDataSource(), CacheDataSource.FLAG_BLOCK_ON_CACHE);}
          - * 
          - */ -@ClosedSource(reason = "Not ready yet") -public final class SsDownloader extends SegmentDownloader { - - /** - * @see SegmentDownloader#SegmentDownloader(Uri, DownloaderConstructorHelper) - */ - public SsDownloader(Uri manifestUri, DownloaderConstructorHelper constructorHelper) { - super(manifestUri, constructorHelper); - } - - @Override - public SsManifest getManifest(DataSource dataSource, Uri uri) throws IOException { - DataSpec dataSpec = new DataSpec(uri, - DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH | DataSpec.FLAG_ALLOW_GZIP); - ParsingLoadable loadable = new ParsingLoadable<>(dataSource, dataSpec, - C.DATA_TYPE_MANIFEST, new SsManifestParser()); - loadable.load(); - return loadable.getResult(); - } - - @Override - protected List getAllSegments(DataSource dataSource, SsManifest manifest, - boolean allowIndexLoadErrors) throws InterruptedException, IOException { - ArrayList segments = new ArrayList<>(); - for (int i = 0; i < manifest.streamElements.length; i++) { - StreamElement streamElement = manifest.streamElements[i]; - for (int j = 0; j < streamElement.formats.length; j++) { - segments.addAll(getSegments(dataSource, manifest, new TrackKey[] {new TrackKey(i, j)}, - allowIndexLoadErrors)); - } - } - return segments; - } - - @Override - protected List getSegments(DataSource dataSource, SsManifest manifest, - TrackKey[] keys, boolean allowIndexLoadErrors) throws InterruptedException, IOException { - ArrayList segments = new ArrayList<>(); - for (TrackKey key : keys) { - StreamElement streamElement = manifest.streamElements[key.streamElementIndex]; - for (int i = 0; i < streamElement.chunkCount; i++) { - segments.add(new Segment(streamElement.getStartTimeUs(i), - new DataSpec(streamElement.buildRequestUri(key.trackIndex, i)))); - } - } - return segments; - } - -} From 1e4f89954825b74d4064b1bb13e35f6a3b9a6759 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Fri, 18 Aug 2017 06:30:21 -0700 Subject: [PATCH 0277/2472] Add support for the data URI scheme ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=165699328 --- .../upstream/DataSchemeDataSourceTest.java | 90 +++++++++++++++++++ .../java/com/google/android/exoplayer2/C.java | 4 + .../upstream/DataSchemeDataSource.java | 88 ++++++++++++++++++ .../upstream/DefaultDataSource.java | 13 ++- 4 files changed, 194 insertions(+), 1 deletion(-) create mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/DataSchemeDataSourceTest.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSchemeDataSource.java diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/DataSchemeDataSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/DataSchemeDataSourceTest.java new file mode 100644 index 0000000000..5ba9e18e7d --- /dev/null +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/DataSchemeDataSourceTest.java @@ -0,0 +1,90 @@ +/* + * 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 com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.testutil.TestUtil; +import java.io.IOException; +import junit.framework.TestCase; + +/** + * Unit tests for {@link DataSchemeDataSource}. + */ +public final class DataSchemeDataSourceTest extends TestCase { + + private DataSource schemeDataDataSource; + + @Override + public void setUp() { + schemeDataDataSource = new DataSchemeDataSource(); + } + + public void testBase64Data() throws IOException { + DataSpec dataSpec = buildDataSpec("data:text/plain;base64,eyJwcm92aWRlciI6IndpZGV2aW5lX3Rlc3QiL" + + "CJjb250ZW50X2lkIjoiTWpBeE5WOTBaV0Z5Y3c9PSIsImtleV9pZHMiOlsiMDAwMDAwMDAwMDAwMDAwMDAwMDAwM" + + "DAwMDAwMDAwMDAiXX0="); + TestUtil.assertDataSourceContent(schemeDataDataSource, dataSpec, + ("{\"provider\":\"widevine_test\",\"content_id\":\"MjAxNV90ZWFycw==\",\"key_ids\":" + + "[\"00000000000000000000000000000000\"]}").getBytes()); + } + + public void testAsciiData() throws IOException { + TestUtil.assertDataSourceContent(schemeDataDataSource, buildDataSpec("data:,A%20brief%20note"), + "A brief note".getBytes()); + } + + public void testPartialReads() throws IOException { + byte[] buffer = new byte[18]; + DataSpec dataSpec = buildDataSpec("data:,012345678901234567"); + assertEquals(18, schemeDataDataSource.open(dataSpec)); + assertEquals(9, schemeDataDataSource.read(buffer, 0, 9)); + assertEquals(0, schemeDataDataSource.read(buffer, 3, 0)); + assertEquals(9, schemeDataDataSource.read(buffer, 9, 15)); + assertEquals(0, schemeDataDataSource.read(buffer, 1, 0)); + assertEquals(C.RESULT_END_OF_INPUT, schemeDataDataSource.read(buffer, 1, 1)); + assertEquals("012345678901234567", new String(buffer, 0, 18)); + } + + public void testIncorrectScheme() { + try { + schemeDataDataSource.open(buildDataSpec("http://www.google.com")); + fail(); + } catch (IOException e) { + // Expected. + } + } + + public void testMalformedData() { + try { + schemeDataDataSource.open(buildDataSpec("data:text/plain;base64,,This%20is%20Content")); + fail(); + } catch (IOException e) { + // Expected. + } + try { + schemeDataDataSource.open(buildDataSpec("data:text/plain;base64,IncorrectPadding==")); + fail(); + } catch (IOException e) { + // Expected. + } + } + + private static DataSpec buildDataSpec(String uriString) { + return new DataSpec(Uri.parse(uriString)); + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/C.java b/library/core/src/main/java/com/google/android/exoplayer2/C.java index 1cfcded1cb..e25538a062 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/C.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/C.java @@ -73,6 +73,10 @@ public final class C { */ public static final long NANOS_PER_SECOND = 1000000000L; + /** + * The name of the ASCII charset. + */ + public static final String ASCII_NAME = "US-ASCII"; /** * The name of the UTF-8 charset. */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSchemeDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSchemeDataSource.java new file mode 100644 index 0000000000..c547625819 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSchemeDataSource.java @@ -0,0 +1,88 @@ +/* + * 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 android.util.Base64; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ParserException; +import java.io.IOException; +import java.net.URLDecoder; + +/** + * A {@link DataSource} for reading data URLs, as defined by RFC 2397. + */ +public final class DataSchemeDataSource implements DataSource { + + public static final String SCHEME_DATA = "data"; + + private DataSpec dataSpec; + private int bytesRead; + private byte[] data; + + @Override + public long open(DataSpec dataSpec) throws IOException { + this.dataSpec = dataSpec; + Uri uri = dataSpec.uri; + String scheme = uri.getScheme(); + if (!SCHEME_DATA.equals(scheme)) { + throw new ParserException("Unsupported scheme: " + scheme); + } + String[] uriParts = uri.getSchemeSpecificPart().split(","); + if (uriParts.length > 2) { + throw new ParserException("Unexpected URI format: " + uri); + } + String dataString = uriParts[1]; + if (uriParts[0].contains(";base64")) { + try { + data = Base64.decode(dataString, 0); + } catch (IllegalArgumentException e) { + throw new ParserException("Error while parsing Base64 encoded string: " + dataString, e); + } + } else { + // TODO: Add support for other charsets. + data = URLDecoder.decode(dataString, C.ASCII_NAME).getBytes(); + } + return data.length; + } + + @Override + public int read(byte[] buffer, int offset, int readLength) { + if (readLength == 0) { + return 0; + } + int remainingBytes = data.length - bytesRead; + if (remainingBytes == 0) { + return C.RESULT_END_OF_INPUT; + } + readLength = Math.min(readLength, remainingBytes); + System.arraycopy(data, bytesRead, buffer, offset, readLength); + bytesRead += readLength; + return readLength; + } + + @Override + public Uri getUri() { + return dataSpec != null ? dataSpec.uri : null; + } + + @Override + public void close() throws IOException { + dataSpec = null; + data = null; + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java index cbb8ba92a5..853b40f73f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java @@ -34,10 +34,11 @@ import java.lang.reflect.InvocationTargetException; *
        • content: For fetching data from a content URI (e.g. content://authority/path/123). *
        • rtmp: For fetching data over RTMP. Only supported if the project using ExoPlayer has an * explicit dependency on ExoPlayer's RTMP extension.
        • + *
        • data: For parsing data inlined in the URI as defined in RFC 2397.
        • *
        • http(s): For fetching data over HTTP and HTTPS (e.g. https://www.something.com/media.mp4), if * constructed using {@link #DefaultDataSource(Context, TransferListener, String, boolean)}, or * any other schemes supported by a base data source if constructed using - * {@link #DefaultDataSource(Context, TransferListener, DataSource)}. + * {@link #DefaultDataSource(Context, TransferListener, DataSource)}.
        • *
        */ public final class DefaultDataSource implements DataSource { @@ -58,6 +59,7 @@ public final class DefaultDataSource implements DataSource { private DataSource assetDataSource; private DataSource contentDataSource; private DataSource rtmpDataSource; + private DataSource dataSchemeDataSource; private DataSource dataSource; @@ -130,6 +132,8 @@ public final class DefaultDataSource implements DataSource { dataSource = getContentDataSource(); } else if (SCHEME_RTMP.equals(scheme)) { dataSource = getRtmpDataSource(); + } else if (DataSchemeDataSource.SCHEME_DATA.equals(scheme)) { + dataSource = getDataSchemeDataSource(); } else { dataSource = baseDataSource; } @@ -202,4 +206,11 @@ public final class DefaultDataSource implements DataSource { return rtmpDataSource; } + private DataSource getDataSchemeDataSource() { + if (dataSchemeDataSource == null) { + dataSchemeDataSource = new DataSchemeDataSource(); + } + return dataSchemeDataSource; + } + } From a1add8f9e69a824842b59cceb87947b67ef39469 Mon Sep 17 00:00:00 2001 From: eguven Date: Fri, 18 Aug 2017 06:38:40 -0700 Subject: [PATCH 0278/2472] Fix downcasting in DownloadAction.Serializers ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=165699826 --- .../offline/ProgressiveDownloadActionTest.java | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/offline/ProgressiveDownloadActionTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/offline/ProgressiveDownloadActionTest.java index b172b7692a..ec45ea01c7 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/offline/ProgressiveDownloadActionTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/offline/ProgressiveDownloadActionTest.java @@ -16,7 +16,6 @@ package com.google.android.exoplayer2.offline; import android.test.InstrumentationTestCase; -import com.google.android.exoplayer2.offline.DownloadAction.Serializer; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.upstream.DummyDataSource; import com.google.android.exoplayer2.upstream.cache.Cache; @@ -101,24 +100,22 @@ public class ProgressiveDownloadActionTest extends InstrumentationTestCase { public void testSerializerGetType() throws Exception { ProgressiveDownloadAction action = new ProgressiveDownloadAction("uri", null, false); - Serializer serializer = action.getSerializer(); - assertNotNull(serializer.getType()); + assertNotNull(action.getType()); } public void testSerializerWriteRead() throws Exception { - doTestSerializationRoundtrip(new ProgressiveDownloadAction("uri1", null, false)); - doTestSerializationRoundtrip(new ProgressiveDownloadAction("uri2", "key", true)); + doTestSerializationRoundTrip(new ProgressiveDownloadAction("uri1", null, false)); + doTestSerializationRoundTrip(new ProgressiveDownloadAction("uri2", "key", true)); } - private void doTestSerializationRoundtrip(ProgressiveDownloadAction action1) throws IOException { - Serializer serializer = action1.getSerializer(); + private void doTestSerializationRoundTrip(ProgressiveDownloadAction action1) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); DataOutputStream output = new DataOutputStream(out); - serializer.writeToStream(output, action1); + action1.writeToStream(output); ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); DataInputStream input = new DataInputStream(in); - DownloadAction action2 = serializer.readFromStream(input); + DownloadAction action2 = ProgressiveDownloadAction.DESERIALIZER.readFromStream(input); assertEquals(action1, action2); } From 8e67837afec166c1a965a9f5d01815e2503ae828 Mon Sep 17 00:00:00 2001 From: mansfieldmark Date: Fri, 18 Aug 2017 09:46:31 -0700 Subject: [PATCH 0279/2472] Adding conservative @Nullable annotations to Exoplayer v2 Cache + CacheSpan ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=165715928 --- .../android/exoplayer2/upstream/cache/Cache.java | 5 +++-- .../android/exoplayer2/upstream/cache/CacheSpan.java | 10 ++++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/library/core/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 index 86ff810142..0265ef83ea 100644 --- a/library/core/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 @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.upstream.cache; +import android.support.annotation.Nullable; import java.io.File; import java.io.IOException; import java.util.NavigableSet; @@ -104,7 +105,7 @@ public interface Cache { * @param key The key for which spans should be returned. * @return The spans for the key. May be null if there are no such spans. */ - NavigableSet getCachedSpans(String key); + @Nullable NavigableSet getCachedSpans(String key); /** * Returns all keys in the cache. @@ -151,7 +152,7 @@ public interface Cache { * @param position The position of the data being requested. * @return The {@link CacheSpan}. Or null if the cache entry is locked. */ - CacheSpan startReadWriteNonBlocking(String key, long position) throws CacheException; + @Nullable CacheSpan startReadWriteNonBlocking(String key, long position) throws CacheException; /** * Obtains a cache file into which data can be written. Must only be called when holding a diff --git a/library/core/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 index 97d55c5fe2..2082740bb4 100644 --- a/library/core/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 @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.upstream.cache; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import java.io.File; @@ -43,7 +44,7 @@ public class CacheSpan implements Comparable { /** * The file corresponding to this {@link CacheSpan}, or null if {@link #isCached} is false. */ - public final File file; + public final @Nullable File file; /** * The last access timestamp, or {@link C#TIME_UNSET} if {@link #isCached} is false. */ @@ -68,11 +69,12 @@ public class CacheSpan implements Comparable { * @param position The position of the {@link CacheSpan} in the original stream. * @param length The length of the {@link CacheSpan}, or {@link C#LENGTH_UNSET} if this is an * open-ended hole. - * @param lastAccessTimestamp The last access timestamp, or {@link C#TIME_UNSET} if - * {@link #isCached} is false. + * @param lastAccessTimestamp The last access timestamp, or {@link C#TIME_UNSET} if {@link + * #isCached} is false. * @param file The file corresponding to this {@link CacheSpan}, or null if it's a hole. */ - public CacheSpan(String key, long position, long length, long lastAccessTimestamp, File file) { + public CacheSpan( + String key, long position, long length, long lastAccessTimestamp, @Nullable File file) { this.key = key; this.position = position; this.length = length; From 9a9bb2192ca5b8115d8b1bb41fe1d92d585804ae Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Fri, 18 Aug 2017 13:57:53 -0700 Subject: [PATCH 0280/2472] Fix third_party settings.gradle for demo-cast ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=165748557 --- settings.gradle | 2 ++ 1 file changed, 2 insertions(+) diff --git a/settings.gradle b/settings.gradle index 766d46bbae..f67f091650 100644 --- a/settings.gradle +++ b/settings.gradle @@ -19,8 +19,10 @@ if (gradle.ext.has('exoplayerModulePrefix')) { } include modulePrefix + 'demo' +include modulePrefix + 'demo-cast' include modulePrefix + 'playbacktests' project(modulePrefix + 'demo').projectDir = new File(rootDir, 'demos/main') +project(modulePrefix + 'demo-cast').projectDir = new File(rootDir, 'demos/cast') project(modulePrefix + 'playbacktests').projectDir = new File(rootDir, 'playbacktests') apply from: 'core_settings.gradle' From cec0c52c8d336bd54277e342379b81e1cf05745c Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 21 Aug 2017 00:47:34 -0700 Subject: [PATCH 0281/2472] Allow the app to specify extra ad markers Issue: #3184 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=165895259 --- .../exoplayer2/ui/PlaybackControlView.java | 38 ++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java index d3ee9bbab0..a53e133562 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java @@ -21,6 +21,7 @@ import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.drawable.Drawable; import android.os.SystemClock; +import android.support.annotation.Nullable; import android.util.AttributeSet; import android.view.KeyEvent; import android.view.LayoutInflater; @@ -311,6 +312,8 @@ public class PlaybackControlView extends FrameLayout { private long hideAtMs; private long[] adGroupTimesMs; private boolean[] playedAdGroups; + private long[] extraAdGroupTimesMs; + private boolean[] extraPlayedAdGroups; private final Runnable updateProgressAction = new Runnable() { @Override @@ -363,6 +366,8 @@ public class PlaybackControlView extends FrameLayout { formatter = new Formatter(formatBuilder, Locale.getDefault()); adGroupTimesMs = new long[0]; playedAdGroups = new boolean[0]; + extraAdGroupTimesMs = new long[0]; + extraPlayedAdGroups = new boolean[0]; componentListener = new ComponentListener(); controlDispatcher = DEFAULT_CONTROL_DISPATCHER; @@ -461,6 +466,29 @@ public class PlaybackControlView extends FrameLayout { updateTimeBarMode(); } + /** + * Sets the millisecond positions of extra ad markers relative to the start of the window (or + * timeline, if in multi-window mode) and whether each extra ad has been played or not. The + * markers are shown in addition to any ad markers for ads in the player's timeline. + * + * @param extraAdGroupTimesMs The millisecond timestamps of the extra ad markers to show, or + * {@code null} to show no extra ad markers. + * @param extraPlayedAdGroups Whether each ad has been played, or {@code null} to show no extra ad + * markers. + */ + public void setExtraAdGroupMarkers(@Nullable long[] extraAdGroupTimesMs, + @Nullable boolean[] extraPlayedAdGroups) { + if (extraAdGroupTimesMs == null) { + this.extraAdGroupTimesMs = new long[0]; + this.extraPlayedAdGroups = new boolean[0]; + } else { + Assertions.checkArgument(extraAdGroupTimesMs.length == extraPlayedAdGroups.length); + this.extraAdGroupTimesMs = extraAdGroupTimesMs; + this.extraPlayedAdGroups = extraPlayedAdGroups; + } + updateProgress(); + } + /** * Sets the {@link VisibilityListener}. * @@ -767,7 +795,15 @@ public class PlaybackControlView extends FrameLayout { bufferedPosition += player.getBufferedPosition(); } if (timeBar != null) { - timeBar.setAdGroupTimesMs(adGroupTimesMs, playedAdGroups, adGroupCount); + int extraAdGroupCount = extraAdGroupTimesMs.length; + int totalAdGroupCount = adGroupCount + extraAdGroupCount; + if (totalAdGroupCount > adGroupTimesMs.length) { + adGroupTimesMs = Arrays.copyOf(adGroupTimesMs, totalAdGroupCount); + playedAdGroups = Arrays.copyOf(playedAdGroups, totalAdGroupCount); + } + System.arraycopy(extraAdGroupTimesMs, 0, adGroupTimesMs, adGroupCount, extraAdGroupCount); + System.arraycopy(extraPlayedAdGroups, 0, playedAdGroups, adGroupCount, extraAdGroupCount); + timeBar.setAdGroupTimesMs(adGroupTimesMs, playedAdGroups, totalAdGroupCount); } } if (durationView != null) { From cb45a47da18f3c7867abc2b61a8b3f15f151bd89 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 21 Aug 2017 03:49:38 -0700 Subject: [PATCH 0282/2472] Remove isFirstWindow/isLastWindow from Timeline. These methods are only used in one place, and offer duplicate functionality to checking getNext(Previous)WindowIndex == C.INDEX_UNSET. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=165910258 --- .../google/android/exoplayer2/Timeline.java | 24 ------------------- .../exoplayer2/ui/PlaybackControlView.java | 7 +++--- 2 files changed, 4 insertions(+), 27 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java index 414c0804ad..7d4c1995eb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java @@ -593,30 +593,6 @@ public abstract class Timeline { } } - /** - * Returns whether the given window is the last window of the timeline depending on the - * {@code repeatMode}. - * - * @param windowIndex A window index. - * @param repeatMode A repeat mode. - * @return Whether the window of the given index is the last window of the timeline. - */ - public final boolean isLastWindow(int windowIndex, @Player.RepeatMode int repeatMode) { - return getNextWindowIndex(windowIndex, repeatMode) == C.INDEX_UNSET; - } - - /** - * Returns whether the given window is the first window of the timeline depending on the - * {@code repeatMode}. - * - * @param windowIndex A window index. - * @param repeatMode A repeat mode. - * @return Whether the window of the given index is the first window of the timeline. - */ - public final boolean isFirstWindow(int windowIndex, @Player.RepeatMode int repeatMode) { - return getPreviousWindowIndex(windowIndex, repeatMode) == C.INDEX_UNSET; - } - /** * Populates a {@link Window} with data for the window at the specified index. Does not populate * {@link Window#id}. diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java index a53e133562..54212eefdd 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java @@ -674,9 +674,10 @@ public class PlaybackControlView extends FrameLayout { int windowIndex = player.getCurrentWindowIndex(); timeline.getWindow(windowIndex, window); isSeekable = window.isSeekable; - enablePrevious = !timeline.isFirstWindow(windowIndex, player.getRepeatMode()) - || isSeekable || !window.isDynamic; - enableNext = !timeline.isLastWindow(windowIndex, player.getRepeatMode()) || window.isDynamic; + enablePrevious = isSeekable || !window.isDynamic + || timeline.getPreviousWindowIndex(windowIndex, player.getRepeatMode()) != C.INDEX_UNSET; + enableNext = window.isDynamic + || timeline.getNextWindowIndex(windowIndex, player.getRepeatMode()) != C.INDEX_UNSET; if (player.isPlayingAd()) { // Always hide player controls during ads. hide(); From f1b3f3a177b144d86fcee3504927e9aac7b10596 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 21 Aug 2017 06:31:41 -0700 Subject: [PATCH 0283/2472] Fix broken link + minor doc tweak ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=165920927 --- extensions/cronet/README.md | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/extensions/cronet/README.md b/extensions/cronet/README.md index 37c031f35f..66da774978 100644 --- a/extensions/cronet/README.md +++ b/extensions/cronet/README.md @@ -2,7 +2,7 @@ The Cronet extension is an [HttpDataSource][] implementation using [Cronet][]. -[HttpDataSource]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer/upstream/HttpDataSource.html +[HttpDataSource]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/upstream/HttpDataSource.html [Cronet]: https://chromium.googlesource.com/chromium/src/+/master/components/cronet?autodive=0%2F%2F ## Build instructions ## @@ -20,12 +20,9 @@ and enable the extension: 1. Copy the content of the downloaded `libs` directory into the `jniLibs` directory of this extension -* In your `settings.gradle` file, add the following line before the line that - applies `core_settings.gradle`: - -```gradle -gradle.ext.exoplayerIncludeCronetExtension = true; -``` +* In your `settings.gradle` file, add + `gradle.ext.exoplayerIncludeCronetExtension = true` before the line that + applies `core_settings.gradle`. [top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md [here]: https://console.cloud.google.com/storage/browser/chromium-cronet/android From f1a97317d9fb5c02c8427c990db456aba017086a Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 21 Aug 2017 07:26:50 -0700 Subject: [PATCH 0284/2472] Handle size==0 in MP4 atoms Issue: #3191 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=165925148 --- .../mp4/sample_fragmented_zero_size_atom.mp4 | Bin 1903 -> 0 bytes .../mp4/FragmentedMp4ExtractorTest.java | 6 ------ .../exoplayer2/extractor/mp4/Atom.java | 9 +++++++-- .../extractor/mp4/FragmentedMp4Extractor.java | 14 ++++++++++++-- .../extractor/mp4/Mp4Extractor.java | 18 ++++++++++++++++-- .../exoplayer2/extractor/mp4/Sniffer.java | 9 ++++++++- 6 files changed, 43 insertions(+), 13 deletions(-) delete mode 100644 library/core/src/androidTest/assets/mp4/sample_fragmented_zero_size_atom.mp4 diff --git a/library/core/src/androidTest/assets/mp4/sample_fragmented_zero_size_atom.mp4 b/library/core/src/androidTest/assets/mp4/sample_fragmented_zero_size_atom.mp4 deleted file mode 100644 index 3d3c63786ef7f40a9b4307fd17fcdc47f006f350..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1903 zcmb_cO-NKx6h3!;RP4t|4V9wCw3TEk7PhDcl$s#0h$JG&mv1H~=lSHlH$z4XsZ9h$ zl#3QcK~O}tX<>`XBKmKUB`u0t^dl4!Ve7)abMLizjTtSv%=gaEcka3Oo^$UIQ8elG z_oZChA_@>opvlN~HClbmjYOfFmThN=C~alCO-LGZ-LDLy;3u|8$e&cF?VO=_za8@% zGxY#ml_}eFnTiYy3=~rP6c4QO)^m&=xOaIyaxW#hz3?uGR2*x*A(`3j7^qMSPCv>q zqTe!829&5}=ASpy!1=e|<2YK;ZKfTm;ge07iD{i>2W&fT^qT1e$B0@h)tiJ;p0#9B z|CVY^#Vt0S1jq1Tes0D|N45UZ_4cHppLRW0HbMK3aHF8}@aL3{Pz#O}hsxkFBSN`- z-%2hsar;|^NlT~RQPp0^p;xiC@W^fvAzF8W=3k`A!&RMv3c4pgJY9ANGa|0%$%g4% zu-z`LvnYpsz-P0Hm@Yf#6IcGyT)q_f%@y)E`Dei0iL#rrmWfS|6E9yB?@>0CZ+)ml^JI`z9^PL&fGvME0 zC8Xpz)&6&db~#30A7A}nqb`+pJyIx*GtuC%zF$S2!|g8)gIt1~TjX5?PgM81Oi^!n#C56SBX@PEtm==&dPrb;1e7UF58 z+A*qX7T}uIli2!0`%7VSAmc2~fWHip0Zsxqueb)>1+Zr+Z$mx?SUv)d1FwPezykn1 z5U8&>1H1 Date: Tue, 22 Aug 2017 03:49:50 -0700 Subject: [PATCH 0285/2472] Add set/getShuffleModeEnabled to Player interface. And implement a basic version of the methods in all implementations. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166041342 --- .../android/exoplayer2/ext/cast/CastPlayer.java | 11 +++++++++++ .../DynamicConcatenatingMediaSourceTest.java | 10 ++++++++++ .../google/android/exoplayer2/ExoPlayerImpl.java | 14 ++++++++++++++ .../java/com/google/android/exoplayer2/Player.java | 12 ++++++++++++ .../google/android/exoplayer2/SimpleExoPlayer.java | 10 ++++++++++ .../exoplayer2/testutil/FakeSimpleExoPlayer.java | 10 ++++++++++ 6 files changed, 67 insertions(+) diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java index ef84c04c04..50ae7ea5ba 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java @@ -330,6 +330,17 @@ public final class CastPlayer implements Player { } } + @Override + public void setShuffleModeEnabled(boolean shuffleModeEnabled) { + // TODO: Support shuffle mode. + } + + @Override + public boolean getShuffleModeEnabled() { + // TODO: Support shuffle mode. + return false; + } + @Override public TrackSelectionArray getCurrentTrackSelections() { return currentTrackSelection; diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java index f8636b9990..8d29a95d89 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java @@ -396,6 +396,16 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { throw new UnsupportedOperationException(); } + @Override + public void setShuffleModeEnabled(boolean shuffleModeEnabled) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean getShuffleModeEnabled() { + throw new UnsupportedOperationException(); + } + @Override public boolean isLoading() { throw new UnsupportedOperationException(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index f22c08f585..f112ee9473 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -53,6 +53,7 @@ import java.util.concurrent.CopyOnWriteArraySet; private boolean tracksSelected; private boolean playWhenReady; private @RepeatMode int repeatMode; + private boolean shuffleModeEnabled; private int playbackState; private int pendingSeekAcks; private int pendingPrepareAcks; @@ -87,6 +88,7 @@ import java.util.concurrent.CopyOnWriteArraySet; this.trackSelector = Assertions.checkNotNull(trackSelector); this.playWhenReady = false; this.repeatMode = Player.REPEAT_MODE_OFF; + this.shuffleModeEnabled = false; this.playbackState = Player.STATE_IDLE; this.listeners = new CopyOnWriteArraySet<>(); emptyTrackSelections = new TrackSelectionArray(new TrackSelection[renderers.length]); @@ -189,6 +191,18 @@ import java.util.concurrent.CopyOnWriteArraySet; return repeatMode; } + @Override + public void setShuffleModeEnabled(boolean shuffleModeEnabled) { + if (this.shuffleModeEnabled != shuffleModeEnabled) { + this.shuffleModeEnabled = shuffleModeEnabled; + } + } + + @Override + public boolean getShuffleModeEnabled() { + return shuffleModeEnabled; + } + @Override public boolean isLoading() { return isLoading; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Player.java b/library/core/src/main/java/com/google/android/exoplayer2/Player.java index d2480c5b3a..f11b107f13 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Player.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Player.java @@ -219,6 +219,18 @@ public interface Player { */ @RepeatMode int getRepeatMode(); + /** + * Sets whether shuffling of windows is enabled. + * + * @param shuffleModeEnabled Whether shuffling is enabled. + */ + void setShuffleModeEnabled(boolean shuffleModeEnabled); + + /** + * Returns whether shuffling of windows is enabled. + */ + boolean getShuffleModeEnabled(); + /** * Whether the player is currently loading the source. * diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index d71c81f08f..9fcc4d2128 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -642,6 +642,16 @@ public class SimpleExoPlayer implements ExoPlayer { player.setRepeatMode(repeatMode); } + @Override + public void setShuffleModeEnabled(boolean shuffleModeEnabled) { + player.setShuffleModeEnabled(shuffleModeEnabled); + } + + @Override + public boolean getShuffleModeEnabled() { + return player.getShuffleModeEnabled(); + } + @Override public boolean isLoading() { return player.isLoading(); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java index cf88d10bc8..7edaa6b13e 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java @@ -154,6 +154,16 @@ public class FakeSimpleExoPlayer extends SimpleExoPlayer { return Player.REPEAT_MODE_OFF; } + @Override + public void setShuffleModeEnabled(boolean shuffleModeEnabled) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean getShuffleModeEnabled() { + return false; + } + @Override public boolean isLoading() { return isLoading; From 52ec70dd80d79396ba7bcea16422bc0dc2111200 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 22 Aug 2017 05:46:00 -0700 Subject: [PATCH 0286/2472] Add support for KEYFORMAT and SAMPLE-AES (only parsing, not decryption) Also add sample streams that use METHOD=SAMPLE-AES. Note that some of the streams provide alternative EXT-X-KEY's. Support for alternative decryption methods will be added in a later CL. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166048858 --- .../exoplayer2/source/hls/HlsChunkSource.java | 3 ++- .../exoplayer2/source/hls/HlsMediaChunk.java | 27 +++++++++++-------- .../source/hls/playlist/HlsMediaPlaylist.java | 19 ++++++++++--- .../hls/playlist/HlsPlaylistParser.java | 16 ++++++++--- 4 files changed, 45 insertions(+), 20 deletions(-) diff --git a/library/hls/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 index bca62ed230..4fed33eee3 100644 --- a/library/hls/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 @@ -306,7 +306,8 @@ import java.util.List; out.chunk = new HlsMediaChunk(mediaDataSource, dataSpec, initDataSpec, selectedUrl, muxedCaptionFormats, trackSelection.getSelectionReason(), trackSelection.getSelectionData(), startTimeUs, startTimeUs + segment.durationUs, chunkMediaSequence, discontinuitySequence, - isTimestampMaster, timestampAdjuster, previous, encryptionKey, encryptionIv); + isTimestampMaster, timestampAdjuster, previous, segment.keyFormat, encryptionKey, + encryptionIv); } /** diff --git a/library/hls/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 index 29b7e4a6a8..e3e1bab48d 100644 --- a/library/hls/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 @@ -32,6 +32,7 @@ import com.google.android.exoplayer2.metadata.id3.Id3Decoder; import com.google.android.exoplayer2.metadata.id3.PrivFrame; import com.google.android.exoplayer2.source.chunk.MediaChunk; import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl; +import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.util.MimeTypes; @@ -116,16 +117,19 @@ import java.util.concurrent.atomic.AtomicInteger; * @param isMasterTimestampSource True if the chunk can initialize the timestamp adjuster. * @param timestampAdjuster Adjuster corresponding to the provided discontinuity sequence number. * @param previousChunk The {@link HlsMediaChunk} that preceded this one. May be null. - * @param encryptionKey For AES encryption chunks, the encryption key. - * @param encryptionIv For AES encryption chunks, the encryption initialization vector. + * @param keyFormat A string describing the format for {@code keyData}, or null if the chunk is + * not encrypted. + * @param keyData Data specifying how to obtain the keys to decrypt the chunk, or null if the + * chunk is not encrypted. + * @param encryptionIv The AES initialization vector, or null if the chunk is not encrypted. */ public HlsMediaChunk(DataSource dataSource, DataSpec dataSpec, DataSpec initDataSpec, HlsUrl hlsUrl, List muxedCaptionFormats, int trackSelectionReason, Object trackSelectionData, long startTimeUs, long endTimeUs, int chunkIndex, int discontinuitySequenceNumber, boolean isMasterTimestampSource, - TimestampAdjuster timestampAdjuster, HlsMediaChunk previousChunk, byte[] encryptionKey, - byte[] encryptionIv) { - super(buildDataSource(dataSource, encryptionKey, encryptionIv), dataSpec, hlsUrl.format, + TimestampAdjuster timestampAdjuster, HlsMediaChunk previousChunk, String keyFormat, + byte[] keyData, byte[] encryptionIv) { + super(buildDataSource(dataSource, keyFormat, keyData, encryptionIv), dataSpec, hlsUrl.format, trackSelectionReason, trackSelectionData, startTimeUs, endTimeUs, chunkIndex); this.discontinuitySequenceNumber = discontinuitySequenceNumber; this.initDataSpec = initDataSpec; @@ -327,15 +331,16 @@ import java.util.concurrent.atomic.AtomicInteger; // Internal factory methods. /** - * If the content is encrypted, returns an {@link Aes128DataSource} that wraps the original in - * order to decrypt the loaded data. Else returns the original. + * If the content is encrypted using the "identity" key format, returns an + * {@link Aes128DataSource} that wraps the original in order to decrypt the loaded data. Else + * returns the original. */ - private static DataSource buildDataSource(DataSource dataSource, byte[] encryptionKey, + private static DataSource buildDataSource(DataSource dataSource, String keyFormat, byte[] keyData, byte[] encryptionIv) { - if (encryptionKey == null || encryptionIv == null) { - return dataSource; + if (HlsMediaPlaylist.KEYFORMAT_IDENTITY.equals(keyFormat)) { + return new Aes128DataSource(dataSource, keyData, encryptionIv); } - return new Aes128DataSource(dataSource, encryptionKey, encryptionIv); + return dataSource; } private Extractor createExtractor() { diff --git a/library/hls/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 index db4f041be2..1b573f41c2 100644 --- a/library/hls/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 @@ -53,6 +53,10 @@ public final class HlsMediaPlaylist extends HlsPlaylist { * Whether the segment is encrypted, as defined by #EXT-X-KEY. */ public final boolean isEncrypted; + /** + * The key format as defined by #EXT-X-KEY, or null if the segment is not encrypted. + */ + public final String keyFormat; /** * The encryption key uri as defined by #EXT-X-KEY, or null if the segment is not encrypted. */ @@ -73,7 +77,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { public final long byterangeLength; public Segment(String uri, long byterangeOffset, long byterangeLength) { - this(uri, 0, -1, C.TIME_UNSET, false, null, null, byterangeOffset, byterangeLength); + this(uri, 0, -1, C.TIME_UNSET, false, null, null, null, byterangeOffset, byterangeLength); } /** @@ -82,19 +86,21 @@ public final class HlsMediaPlaylist extends HlsPlaylist { * @param relativeDiscontinuitySequence See {@link #relativeDiscontinuitySequence}. * @param relativeStartTimeUs See {@link #relativeStartTimeUs}. * @param isEncrypted See {@link #isEncrypted}. + * @param keyFormat See {@link #keyFormat}. * @param encryptionKeyUri See {@link #encryptionKeyUri}. * @param encryptionIV See {@link #encryptionIV}. * @param byterangeOffset See {@link #byterangeOffset}. * @param byterangeLength See {@link #byterangeLength}. */ public Segment(String url, long durationUs, int relativeDiscontinuitySequence, - long relativeStartTimeUs, boolean isEncrypted, String encryptionKeyUri, String encryptionIV, - long byterangeOffset, long byterangeLength) { + long relativeStartTimeUs, boolean isEncrypted, String keyFormat, String encryptionKeyUri, + String encryptionIV, long byterangeOffset, long byterangeLength) { this.url = url; this.durationUs = durationUs; this.relativeDiscontinuitySequence = relativeDiscontinuitySequence; this.relativeStartTimeUs = relativeStartTimeUs; this.isEncrypted = isEncrypted; + this.keyFormat = keyFormat; this.encryptionKeyUri = encryptionKeyUri; this.encryptionIV = encryptionIV; this.byterangeOffset = byterangeOffset; @@ -110,7 +116,12 @@ public final class HlsMediaPlaylist extends HlsPlaylist { } /** - * Type of the playlist as defined by #EXT-X-PLAYLIST-TYPE. + * The identity key format, as defined by #EXT-X-KEY. + */ + public static final String KEYFORMAT_IDENTITY = "identity"; + + /** + * Type of the playlist, as defined by #EXT-X-PLAYLIST-TYPE. */ @Retention(RetentionPolicy.SOURCE) @IntDef({PLAYLIST_TYPE_UNKNOWN, PLAYLIST_TYPE_VOD, PLAYLIST_TYPE_EVENT}) diff --git a/library/hls/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 index c5d3302eca..dc5fd96f35 100644 --- a/library/hls/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 @@ -69,7 +69,8 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser Date: Tue, 22 Aug 2017 07:27:27 -0700 Subject: [PATCH 0287/2472] Add listener callback for shuffle mode changes. The listener implementations do not do anything yet. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166056933 --- .../com/google/android/exoplayer2/demo/EventLogger.java | 5 +++++ .../com/google/android/exoplayer2/demo/PlayerActivity.java | 5 +++++ .../android/exoplayer2/ext/flac/FlacPlaybackTest.java | 5 +++++ .../google/android/exoplayer2/ext/ima/ImaAdsLoader.java | 5 +++++ .../exoplayer2/ext/leanback/LeanbackPlayerAdapter.java | 5 +++++ .../exoplayer2/ext/mediasession/MediaSessionConnector.java | 5 +++++ .../android/exoplayer2/ext/opus/OpusPlaybackTest.java | 5 +++++ .../google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java | 5 +++++ .../java/com/google/android/exoplayer2/ExoPlayerImpl.java | 4 ++++ .../main/java/com/google/android/exoplayer2/Player.java | 7 +++++++ .../google/android/exoplayer2/ui/DebugTextViewHelper.java | 5 +++++ .../google/android/exoplayer2/ui/PlaybackControlView.java | 5 +++++ .../google/android/exoplayer2/ui/SimpleExoPlayerView.java | 5 +++++ .../com/google/android/exoplayer2/testutil/Action.java | 5 +++++ .../google/android/exoplayer2/testutil/ExoHostedTest.java | 5 +++++ .../android/exoplayer2/testutil/ExoPlayerTestRunner.java | 5 +++++ 16 files changed, 81 insertions(+) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java index cbc3536ef7..533306e0a2 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java @@ -99,6 +99,11 @@ import java.util.Locale; Log.d(TAG, "repeatMode [" + getRepeatModeString(repeatMode) + "]"); } + @Override + public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { + Log.d(TAG, "shuffleModeEnabled [" + shuffleModeEnabled + "]"); + } + @Override public void onPositionDiscontinuity() { Log.d(TAG, "positionDiscontinuity"); diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index 6416cd5aa2..6d733c9f97 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -501,6 +501,11 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi // Do nothing. } + @Override + public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { + // Do nothing. + } + @Override public void onPositionDiscontinuity() { if (inErrorState) { 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 1fa30bed9d..1257b652eb 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 @@ -132,6 +132,11 @@ public class FlacPlaybackTest extends InstrumentationTestCase { // Do nothing. } + @Override + public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { + // Do nothing. + } + private void releasePlayerAndQuitLooper() { player.release(); Looper.myLooper().quit(); diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index 9bfe33e988..c27ef17b87 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -566,6 +566,11 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, // Do nothing. } + @Override + public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { + // Do nothing. + } + @Override public void onPlayerError(ExoPlaybackException error) { if (playingAd) { diff --git a/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java b/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java index 26fd4e9c8f..8a207bea8f 100644 --- a/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java +++ b/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java @@ -271,6 +271,11 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { // Do nothing. } + @Override + public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { + // Do nothing. + } + // SimpleExoplayerView.Callback implementation. @Override diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java index 419614347f..84c164f76c 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java @@ -619,6 +619,11 @@ public final class MediaSessionConnector { updateMediaSessionPlaybackState(); } + @Override + public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { + // TODO: Support shuffle mode in MediaSessionConnector. + } + @Override public void onPlayerError(ExoPlaybackException error) { playbackException = error; 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 4c576b2cc0..6eeebaef4b 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 @@ -132,6 +132,11 @@ public class OpusPlaybackTest extends InstrumentationTestCase { // Do nothing. } + @Override + public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { + // Do nothing. + } + private void releasePlayerAndQuitLooper() { player.release(); Looper.myLooper().quit(); 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 0bc945174e..50f4bf394d 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 @@ -164,6 +164,11 @@ public class VpxPlaybackTest extends InstrumentationTestCase { // Do nothing. } + @Override + public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { + // Do nothing. + } + private void releasePlayerAndQuitLooper() { player.release(); Looper.myLooper().quit(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index f112ee9473..7530b2b948 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -195,6 +195,9 @@ import java.util.concurrent.CopyOnWriteArraySet; public void setShuffleModeEnabled(boolean shuffleModeEnabled) { if (this.shuffleModeEnabled != shuffleModeEnabled) { this.shuffleModeEnabled = shuffleModeEnabled; + for (Player.EventListener listener : listeners) { + listener.onShuffleModeEnabledChanged(shuffleModeEnabled); + } } } @@ -527,3 +530,4 @@ import java.util.concurrent.CopyOnWriteArraySet; } } + diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Player.java b/library/core/src/main/java/com/google/android/exoplayer2/Player.java index f11b107f13..6eee930018 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Player.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Player.java @@ -94,6 +94,13 @@ public interface Player { */ void onRepeatModeChanged(@RepeatMode int repeatMode); + /** + * Called when the value of {@link #getShuffleModeEnabled()} changes. + * + * @param shuffleModeEnabled Whether shuffling of windows is enabled. + */ + void onShuffleModeEnabledChanged(boolean shuffleModeEnabled); + /** * Called when an error occurs. The playback state will transition to {@link #STATE_IDLE} * immediately after this method is called. The player instance can still be used, and diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java index 060780eda2..be04ce2fe0 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java @@ -93,6 +93,11 @@ public final class DebugTextViewHelper implements Runnable, Player.EventListener // Do nothing. } + @Override + public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { + // Do nothing. + } + @Override public void onPositionDiscontinuity() { updateAndPost(); diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java index 54212eefdd..3e6b5bf158 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java @@ -1079,6 +1079,11 @@ public class PlaybackControlView extends FrameLayout { updateNavigation(); } + @Override + public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { + // TODO: Update UI. + } + @Override public void onPositionDiscontinuity() { updateNavigation(); diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index 0d10e0fcf9..bdbdf34331 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -896,6 +896,11 @@ public final class SimpleExoPlayerView extends FrameLayout { // Do nothing. } + @Override + public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { + // Do nothing. + } + @Override public void onPlayerError(ExoPlaybackException e) { // Do nothing. diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java index bbb694d6d6..ab1f448afd 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java @@ -411,6 +411,11 @@ public abstract class Action { } + @Override + public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { + + } + @Override public void onPlayerError(ExoPlaybackException error) { diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java index 77e197515b..c039dd3283 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java @@ -235,6 +235,11 @@ public abstract class ExoHostedTest implements HostedTest, Player.EventListener, // Do nothing. } + @Override + public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { + // Do nothing. + } + @Override public final void onPlayerError(ExoPlaybackException error) { playerWasPrepared = true; diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java index f354ad6a76..2b5ea11d94 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java @@ -360,6 +360,11 @@ public final class ExoPlayerTestRunner implements Player.EventListener { // Do nothing. } + @Override + public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { + // Do nothing. + } + @Override public void onPlayerError(ExoPlaybackException error) { handleException(exception); From 6a8c99d037a411f2b45aab0c23d468a5d0e801eb Mon Sep 17 00:00:00 2001 From: tonihei Date: Sun, 16 Jul 2017 07:33:01 +0100 Subject: [PATCH 0288/2472] Support shuffle mode in MediaSessionConnector. Changes to the player's shuffle mode are forwarded to the media session. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166057425 --- .../ext/mediasession/MediaSessionConnector.java | 12 +++++++++++- .../ext/mediasession/TimelineQueueNavigator.java | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java index 84c164f76c..4dc1100c1e 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java @@ -621,7 +621,9 @@ public final class MediaSessionConnector { @Override public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { - // TODO: Support shuffle mode in MediaSessionConnector. + mediaSession.setShuffleMode(shuffleModeEnabled ? PlaybackStateCompat.SHUFFLE_MODE_ALL + : PlaybackStateCompat.SHUFFLE_MODE_NONE); + updateMediaSessionPlaybackState(); } @Override @@ -805,6 +807,14 @@ public final class MediaSessionConnector { } } + @Override + public void onSetShuffleMode(int shuffleMode) { + if (canDispatchToQueueNavigator(PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE_ENABLED)) { + queueNavigator.onSetShuffleModeEnabled(player, + shuffleMode != PlaybackStateCompat.SHUFFLE_MODE_NONE); + } + } + @Override public void onAddQueueItem(MediaDescriptionCompat description) { if (queueEditor != null) { diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java index 521b4cd6e3..435d994dcc 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java @@ -163,7 +163,7 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu @Override public void onSetShuffleModeEnabled(Player player, boolean enabled) { - // TODO: Implement this. + player.setShuffleModeEnabled(enabled); } private void publishFloatingQueueWindow(Player player) { From f0bd40a5b69b0740a100480768481651ba252a5d Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 22 Aug 2017 07:41:32 -0700 Subject: [PATCH 0289/2472] Use flavorDimensions for external demo app - This is soon becoming mandatory. - It also looks like future versions of com.android.tools.build are being distributed via Google's Maven repository. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166058299 --- build.gradle | 3 +++ demos/main/build.gradle | 10 ++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index dbc8a41eb0..d5cc64baa1 100644 --- a/build.gradle +++ b/build.gradle @@ -14,6 +14,9 @@ buildscript { repositories { jcenter() + maven { + url "https://maven.google.com" + } } dependencies { classpath 'com.android.tools.build:gradle:2.3.3' diff --git a/demos/main/build.gradle b/demos/main/build.gradle index 029a44326e..099741d167 100644 --- a/demos/main/build.gradle +++ b/demos/main/build.gradle @@ -39,9 +39,15 @@ android { disable 'MissingTranslation' } + flavorDimensions "extensions" + productFlavors { - noExtensions - withExtensions + noExtensions { + dimension "extensions" + } + withExtensions { + dimension "extensions" + } } } From 5214bfa7b1ec229667ac841ff27040fb7ff19fc6 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 22 Aug 2017 08:00:09 -0700 Subject: [PATCH 0290/2472] Update ExoPlayerImplInternal with shuffle mode changes. The shuffle mode is saved using a message on the playback thread. After setting the shuffle mode, the same media period holder verification as for repeat mode changes gets executed. Note: the shuffle mode is not used yet to change the playback order. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166060231 --- .../android/exoplayer2/ExoPlayerImpl.java | 4 ++-- .../exoplayer2/ExoPlayerImplInternal.java | 24 ++++++++++++++++++- .../exoplayer2/MediaPeriodInfoSequence.java | 12 ++++++++-- 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index 7530b2b948..0ce920a16f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -107,7 +107,7 @@ import java.util.concurrent.CopyOnWriteArraySet; }; playbackInfo = new ExoPlayerImplInternal.PlaybackInfo(0, 0); internalPlayer = new ExoPlayerImplInternal(renderers, trackSelector, loadControl, playWhenReady, - repeatMode, eventHandler, playbackInfo, this); + repeatMode, shuffleModeEnabled, eventHandler, playbackInfo, this); } @Override @@ -195,6 +195,7 @@ import java.util.concurrent.CopyOnWriteArraySet; public void setShuffleModeEnabled(boolean shuffleModeEnabled) { if (this.shuffleModeEnabled != shuffleModeEnabled) { this.shuffleModeEnabled = shuffleModeEnabled; + internalPlayer.setShuffleModeEnabled(shuffleModeEnabled); for (Player.EventListener listener : listeners) { listener.onShuffleModeEnabledChanged(shuffleModeEnabled); } @@ -530,4 +531,3 @@ import java.util.concurrent.CopyOnWriteArraySet; } } - diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index b8274126b5..67586cc07a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -128,6 +128,7 @@ import java.io.IOException; private static final int MSG_TRACK_SELECTION_INVALIDATED = 10; private static final int MSG_CUSTOM = 11; private static final int MSG_SET_REPEAT_MODE = 12; + private static final int MSG_SET_SHUFFLE_ENABLED = 13; private static final int PREPARING_SOURCE_INTERVAL_MS = 10; private static final int RENDERING_INTERVAL_MS = 10; @@ -173,6 +174,7 @@ import java.io.IOException; private boolean isLoading; private int state; private @Player.RepeatMode int repeatMode; + private boolean shuffleModeEnabled; private int customMessagesSent; private int customMessagesProcessed; private long elapsedRealtimeUs; @@ -189,12 +191,14 @@ import java.io.IOException; public ExoPlayerImplInternal(Renderer[] renderers, TrackSelector trackSelector, LoadControl loadControl, boolean playWhenReady, @Player.RepeatMode int repeatMode, - Handler eventHandler, PlaybackInfo playbackInfo, ExoPlayer player) { + boolean shuffleModeEnabled, Handler eventHandler, PlaybackInfo playbackInfo, + ExoPlayer player) { this.renderers = renderers; this.trackSelector = trackSelector; this.loadControl = loadControl; this.playWhenReady = playWhenReady; this.repeatMode = repeatMode; + this.shuffleModeEnabled = shuffleModeEnabled; this.eventHandler = eventHandler; this.state = Player.STATE_IDLE; this.playbackInfo = playbackInfo; @@ -234,6 +238,10 @@ import java.io.IOException; handler.obtainMessage(MSG_SET_REPEAT_MODE, repeatMode, 0).sendToTarget(); } + public void setShuffleModeEnabled(boolean shuffleModeEnabled) { + handler.obtainMessage(MSG_SET_SHUFFLE_ENABLED, shuffleModeEnabled ? 1 : 0, 0).sendToTarget(); + } + public void seekTo(Timeline timeline, int windowIndex, long positionUs) { handler.obtainMessage(MSG_SEEK_TO, new SeekPosition(timeline, windowIndex, positionUs)) .sendToTarget(); @@ -346,6 +354,10 @@ import java.io.IOException; setRepeatModeInternal(msg.arg1); return true; } + case MSG_SET_SHUFFLE_ENABLED: { + setShuffleModeEnabledInternal(msg.arg1 != 0); + return true; + } case MSG_DO_SOME_WORK: { doSomeWork(); return true; @@ -457,7 +469,17 @@ import java.io.IOException; throws ExoPlaybackException { this.repeatMode = repeatMode; mediaPeriodInfoSequence.setRepeatMode(repeatMode); + validateExistingPeriodHolders(); + } + private void setShuffleModeEnabledInternal(boolean shuffleModeEnabled) + throws ExoPlaybackException { + this.shuffleModeEnabled = shuffleModeEnabled; + mediaPeriodInfoSequence.setShuffleModeEnabled(shuffleModeEnabled); + validateExistingPeriodHolders(); + } + + private void validateExistingPeriodHolders() throws ExoPlaybackException { // Find the last existing period holder that matches the new period order. MediaPeriodHolder lastValidPeriodHolder = playingPeriodHolder != null ? playingPeriodHolder : loadingPeriodHolder; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfoSequence.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfoSequence.java index 0e9c65421c..9e8c2645c1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfoSequence.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfoSequence.java @@ -102,8 +102,8 @@ import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; private final Timeline.Window window; private Timeline timeline; - @RepeatMode - private int repeatMode; + private @RepeatMode int repeatMode; + private boolean shuffleModeEnabled; /** * Creates a new media period info sequence. @@ -129,6 +129,14 @@ import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; this.repeatMode = repeatMode; } + /** + * Sets whether shuffling is enabled. Call {@link #getUpdatedMediaPeriodInfo} to update period + * information taking into account the shuffle mode. + */ + public void setShuffleModeEnabled(boolean shuffleModeEnabled) { + this.shuffleModeEnabled = shuffleModeEnabled; + } + /** * Returns the first {@link MediaPeriodInfo} to play, based on the specified playback position. */ From 50c485652cb0b52f593b8a4035dcd50e050747e1 Mon Sep 17 00:00:00 2001 From: Bei Yi Date: Thu, 24 Aug 2017 14:31:33 -0700 Subject: [PATCH 0291/2472] Support aspect ratio fill mode for AspectRatioFrameLayout --- .../android/exoplayer2/ui/AspectRatioFrameLayout.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java index 2f04b8800d..9b93b3a867 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java @@ -32,7 +32,7 @@ public final class AspectRatioFrameLayout extends FrameLayout { * Resize modes for {@link AspectRatioFrameLayout}. */ @Retention(RetentionPolicy.SOURCE) - @IntDef({RESIZE_MODE_FIT, RESIZE_MODE_FIXED_WIDTH, RESIZE_MODE_FIXED_HEIGHT, RESIZE_MODE_FILL, RESIZE_MODE_CROP}) + @IntDef({RESIZE_MODE_FIT, RESIZE_MODE_FIXED_WIDTH, RESIZE_MODE_FIXED_HEIGHT, RESIZE_MODE_FILL, RESIZE_MODE_ASPECT_FILL}) public @interface ResizeMode {} /** @@ -52,9 +52,9 @@ public final class AspectRatioFrameLayout extends FrameLayout { */ public static final int RESIZE_MODE_FILL = 3; /** - * The height or width is increased or decreased to crop and to obtain the desired aspect ratio. + * Either height or width is increased to obtain the desired aspect ratio. */ - public static final int RESIZE_MODE_CROP = 4; + public static final int RESIZE_MODE_ASPECT_FILL = 4; /** * The {@link FrameLayout} will not resize itself if the fractional difference between its natural @@ -145,7 +145,7 @@ public final class AspectRatioFrameLayout extends FrameLayout { case RESIZE_MODE_FIXED_HEIGHT: width = (int) (height * videoAspectRatio); break; - case RESIZE_MODE_CROP: + case RESIZE_MODE_ASPECT_FILL: if (videoAspectRatio > viewAspectRatio) { width = (int) (height * videoAspectRatio); } else { From b0848216786d6ebfd35c898286ebdd22e4aa5234 Mon Sep 17 00:00:00 2001 From: Bei Yi Date: Thu, 24 Aug 2017 16:13:44 -0700 Subject: [PATCH 0292/2472] Support zoom mode for AspectRatioFrameLayout --- .../android/exoplayer2/ui/AspectRatioFrameLayout.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java index 9b93b3a867..3367a46374 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java @@ -32,7 +32,7 @@ public final class AspectRatioFrameLayout extends FrameLayout { * Resize modes for {@link AspectRatioFrameLayout}. */ @Retention(RetentionPolicy.SOURCE) - @IntDef({RESIZE_MODE_FIT, RESIZE_MODE_FIXED_WIDTH, RESIZE_MODE_FIXED_HEIGHT, RESIZE_MODE_FILL, RESIZE_MODE_ASPECT_FILL}) + @IntDef({RESIZE_MODE_FIT, RESIZE_MODE_FIXED_WIDTH, RESIZE_MODE_FIXED_HEIGHT, RESIZE_MODE_FILL, RESIZE_MODE_ZOOM}) public @interface ResizeMode {} /** @@ -54,7 +54,7 @@ public final class AspectRatioFrameLayout extends FrameLayout { /** * Either height or width is increased to obtain the desired aspect ratio. */ - public static final int RESIZE_MODE_ASPECT_FILL = 4; + public static final int RESIZE_MODE_ZOOM = 4; /** * The {@link FrameLayout} will not resize itself if the fractional difference between its natural @@ -145,7 +145,7 @@ public final class AspectRatioFrameLayout extends FrameLayout { case RESIZE_MODE_FIXED_HEIGHT: width = (int) (height * videoAspectRatio); break; - case RESIZE_MODE_ASPECT_FILL: + case RESIZE_MODE_ZOOM: if (videoAspectRatio > viewAspectRatio) { width = (int) (height * videoAspectRatio); } else { From 10f1bd73961b972ec451076ed539ab7f189b1e62 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 23 Aug 2017 01:08:08 -0700 Subject: [PATCH 0293/2472] Add shuffle mode parameter to Timeline interface methods. This parameter is used by methods such as getNextWindowIndex and getPreviousWindowIndex to determine the playback order. Additionally, there are method to query the first and last window index given the shuffle mode. None of the timeline implementations nor the ExoPlayer implementation supports shuffling so far. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166170229 --- .../mediasession/TimelineQueueNavigator.java | 4 +- .../exoplayer2/ExoPlayerImplInternal.java | 7 +- .../exoplayer2/MediaPeriodInfoSequence.java | 4 +- .../google/android/exoplayer2/Timeline.java | 63 +++- .../exoplayer2/offline/DownloadException.java | 30 ++ .../exoplayer2/offline/Downloader.java | 94 +++++ .../offline/DownloaderConstructorHelper.java | 107 ++++++ .../offline/ProgressiveDownloader.java | 100 ++++++ .../exoplayer2/offline/SegmentDownloader.java | 331 ++++++++++++++++++ .../source/AbstractConcatenatedTimeline.java | 12 +- .../source/ConcatenatingMediaSource.java | 10 +- .../exoplayer2/source/ForwardingTimeline.java | 20 +- .../exoplayer2/source/LoopingMediaSource.java | 12 +- .../hls/offline/HlsDownloadTestData.java | 80 +++++ .../source/hls/offline/HlsDownloaderTest.java | 210 +++++++++++ .../source/hls/offline/HlsDownloadAction.java | 83 +++++ .../source/hls/offline/HlsDownloader.java | 139 ++++++++ .../offline/SsDownloadAction.java | 86 +++++ .../smoothstreaming/offline/SsDownloader.java | 111 ++++++ .../exoplayer2/ui/PlaybackControlView.java | 11 +- .../playbacktests/gts/DashDownloadTest.java | 192 ++++++++++ .../exoplayer2/testutil/TimelineAsserts.java | 27 +- 22 files changed, 1679 insertions(+), 54 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadException.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/offline/Downloader.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/offline/DownloaderConstructorHelper.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java create mode 100644 library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadTestData.java create mode 100644 library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloaderTest.java create mode 100644 library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadAction.java create mode 100644 library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java create mode 100644 library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadAction.java create mode 100644 library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java create mode 100644 playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashDownloadTest.java diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java index 435d994dcc..bd3f3f2820 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java @@ -127,7 +127,7 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu return; } int previousWindowIndex = timeline.getPreviousWindowIndex(player.getCurrentWindowIndex(), - player.getRepeatMode()); + player.getRepeatMode(), false); if (player.getCurrentPosition() > MAX_POSITION_FOR_SEEK_TO_PREVIOUS || previousWindowIndex == C.INDEX_UNSET) { player.seekTo(0); @@ -155,7 +155,7 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu return; } int nextWindowIndex = timeline.getNextWindowIndex(player.getCurrentWindowIndex(), - player.getRepeatMode()); + player.getRepeatMode(), false); if (nextWindowIndex != C.INDEX_UNSET) { player.seekTo(nextWindowIndex, C.TIME_UNSET); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 67586cc07a..7035ed637e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -488,7 +488,7 @@ import java.io.IOException; } while (true) { int nextPeriodIndex = timeline.getNextPeriodIndex(lastValidPeriodHolder.info.id.periodIndex, - period, window, repeatMode); + period, window, repeatMode, false); while (lastValidPeriodHolder.next != null && !lastValidPeriodHolder.info.isLastInTimelinePeriod) { lastValidPeriodHolder = lastValidPeriodHolder.next; @@ -1122,7 +1122,7 @@ import java.io.IOException; while (periodHolder.next != null) { MediaPeriodHolder previousPeriodHolder = periodHolder; periodHolder = periodHolder.next; - periodIndex = timeline.getNextPeriodIndex(periodIndex, period, window, repeatMode); + periodIndex = timeline.getNextPeriodIndex(periodIndex, period, window, repeatMode, false); if (periodIndex != C.INDEX_UNSET && periodHolder.uid.equals(timeline.getPeriod(periodIndex, period, true).uid)) { // The holder is consistent with the new timeline. Update its index and continue. @@ -1204,7 +1204,8 @@ import java.io.IOException; int newPeriodIndex = C.INDEX_UNSET; int maxIterations = oldTimeline.getPeriodCount(); for (int i = 0; i < maxIterations && newPeriodIndex == C.INDEX_UNSET; i++) { - oldPeriodIndex = oldTimeline.getNextPeriodIndex(oldPeriodIndex, period, window, repeatMode); + oldPeriodIndex = oldTimeline.getNextPeriodIndex(oldPeriodIndex, period, window, repeatMode, + false); if (oldPeriodIndex == C.INDEX_UNSET) { // We've reached the end of the old timeline. break; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfoSequence.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfoSequence.java index 9e8c2645c1..d7821ed705 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfoSequence.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfoSequence.java @@ -162,7 +162,7 @@ import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; // timeline is updated, to avoid repeatedly checking the same timeline. if (currentMediaPeriodInfo.isLastInTimelinePeriod) { int nextPeriodIndex = timeline.getNextPeriodIndex(currentMediaPeriodInfo.id.periodIndex, - period, window, repeatMode); + period, window, repeatMode, false); if (nextPeriodIndex == C.INDEX_UNSET) { // We can't create a next period yet. return null; @@ -353,7 +353,7 @@ import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; private boolean isLastInTimeline(MediaPeriodId id, boolean isLastMediaPeriodInPeriod) { int windowIndex = timeline.getPeriod(id.periodIndex, period).windowIndex; return !timeline.getWindow(windowIndex, window).isDynamic - && timeline.isLastPeriod(id.periodIndex, period, window, repeatMode) + && timeline.isLastPeriod(id.periodIndex, period, window, repeatMode, false) && isLastMediaPeriodInPeriod; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java index 7d4c1995eb..8a1d7964ee 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java @@ -553,20 +553,24 @@ public abstract class Timeline { /** * Returns the index of the window after the window at index {@code windowIndex} depending on the - * {@code repeatMode}. + * {@code repeatMode} and whether shuffling is enabled. * * @param windowIndex Index of a window in the timeline. * @param repeatMode A repeat mode. + * @param shuffleModeEnabled Whether shuffling is enabled. * @return The index of the next window, or {@link C#INDEX_UNSET} if this is the last window. */ - public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) { + public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode, + boolean shuffleModeEnabled) { switch (repeatMode) { case Player.REPEAT_MODE_OFF: - return windowIndex == getWindowCount() - 1 ? C.INDEX_UNSET : windowIndex + 1; + return windowIndex == getLastWindowIndex(shuffleModeEnabled) ? C.INDEX_UNSET + : windowIndex + 1; case Player.REPEAT_MODE_ONE: return windowIndex; case Player.REPEAT_MODE_ALL: - return windowIndex == getWindowCount() - 1 ? 0 : windowIndex + 1; + return windowIndex == getLastWindowIndex(shuffleModeEnabled) + ? getFirstWindowIndex(shuffleModeEnabled) : windowIndex + 1; default: throw new IllegalStateException(); } @@ -574,25 +578,51 @@ public abstract class Timeline { /** * Returns the index of the window before the window at index {@code windowIndex} depending on the - * {@code repeatMode}. + * {@code repeatMode} and whether shuffling is enabled. * * @param windowIndex Index of a window in the timeline. * @param repeatMode A repeat mode. + * @param shuffleModeEnabled Whether shuffling is enabled. * @return The index of the previous window, or {@link C#INDEX_UNSET} if this is the first window. */ - public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) { + public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode, + boolean shuffleModeEnabled) { switch (repeatMode) { case Player.REPEAT_MODE_OFF: - return windowIndex == 0 ? C.INDEX_UNSET : windowIndex - 1; + return windowIndex == getFirstWindowIndex(shuffleModeEnabled) ? C.INDEX_UNSET + : windowIndex - 1; case Player.REPEAT_MODE_ONE: return windowIndex; case Player.REPEAT_MODE_ALL: - return windowIndex == 0 ? getWindowCount() - 1 : windowIndex - 1; + return windowIndex == getFirstWindowIndex(shuffleModeEnabled) + ? getLastWindowIndex(shuffleModeEnabled) : windowIndex - 1; default: throw new IllegalStateException(); } } + /** + * Returns the index of the last window in the playback order depending on whether shuffling is + * enabled. + * + * @param shuffleModeEnabled Whether shuffling is enabled. + * @return The index of the last window in the playback order. + */ + public int getLastWindowIndex(boolean shuffleModeEnabled) { + return getWindowCount() - 1; + } + + /** + * Returns the index of the first window in the playback order depending on whether shuffling is + * enabled. + * + * @param shuffleModeEnabled Whether shuffling is enabled. + * @return The index of the first window in the playback order. + */ + public int getFirstWindowIndex(boolean shuffleModeEnabled) { + return 0; + } + /** * Populates a {@link Window} with data for the window at the specified index. Does not populate * {@link Window#id}. @@ -614,7 +644,7 @@ public abstract class Timeline { * null. The caller should pass false for efficiency reasons unless the field is required. * @return The populated {@link Window}, for convenience. */ - public Window getWindow(int windowIndex, Window window, boolean setIds) { + public final Window getWindow(int windowIndex, Window window, boolean setIds) { return getWindow(windowIndex, window, setIds, 0); } @@ -639,19 +669,20 @@ public abstract class Timeline { /** * Returns the index of the period after the period at index {@code periodIndex} depending on the - * {@code repeatMode}. + * {@code repeatMode} and whether shuffling is enabled. * * @param periodIndex Index of a period in the timeline. * @param period A {@link Period} to be used internally. Must not be null. * @param window A {@link Window} to be used internally. Must not be null. * @param repeatMode A repeat mode. + * @param shuffleModeEnabled Whether shuffling is enabled. * @return The index of the next period, or {@link C#INDEX_UNSET} if this is the last period. */ public final int getNextPeriodIndex(int periodIndex, Period period, Window window, - @Player.RepeatMode int repeatMode) { + @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled) { int windowIndex = getPeriod(periodIndex, period).windowIndex; if (getWindow(windowIndex, window).lastPeriodIndex == periodIndex) { - int nextWindowIndex = getNextWindowIndex(windowIndex, repeatMode); + int nextWindowIndex = getNextWindowIndex(windowIndex, repeatMode, shuffleModeEnabled); if (nextWindowIndex == C.INDEX_UNSET) { return C.INDEX_UNSET; } @@ -662,17 +693,19 @@ public abstract class Timeline { /** * Returns whether the given period is the last period of the timeline depending on the - * {@code repeatMode}. + * {@code repeatMode} and whether shuffling is enabled. * * @param periodIndex A period index. * @param period A {@link Period} to be used internally. Must not be null. * @param window A {@link Window} to be used internally. Must not be null. * @param repeatMode A repeat mode. + * @param shuffleModeEnabled Whether shuffling is enabled. * @return Whether the period of the given index is the last period of the timeline. */ public final boolean isLastPeriod(int periodIndex, Period period, Window window, - @Player.RepeatMode int repeatMode) { - return getNextPeriodIndex(periodIndex, period, window, repeatMode) == C.INDEX_UNSET; + @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled) { + return getNextPeriodIndex(periodIndex, period, window, repeatMode, shuffleModeEnabled) + == C.INDEX_UNSET; } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadException.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadException.java new file mode 100644 index 0000000000..239195892c --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadException.java @@ -0,0 +1,30 @@ +/* + * 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.offline; + +import com.google.android.exoplayer2.util.ClosedSource; +import java.io.IOException; + +/** Thrown on an error during downloading. */ +@ClosedSource(reason = "Not ready yet") +public final class DownloadException extends IOException { + + /** @param message The message for the exception. */ + public DownloadException(String message) { + super(message); + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/Downloader.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/Downloader.java new file mode 100644 index 0000000000..a130bb4052 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/Downloader.java @@ -0,0 +1,94 @@ +/* + * 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.offline; + +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.util.ClosedSource; +import java.io.IOException; + +/** + * An interface for stream downloaders. + */ +@ClosedSource(reason = "Not ready yet") +public interface Downloader { + + /** + * Listener notified when download progresses. + *

        + * No guarantees are made about the thread or threads on which the listener is called, but it is + * guaranteed that listener methods will be called in a serial fashion (i.e. one at a time) and in + * the same order as events occurred. + */ + interface ProgressListener { + /** + * Called during the download. Calling intervals depend on the {@link Downloader} + * implementation. + * + * @param downloader The reporting instance. + * @param downloadPercentage The download percentage. This value can be an estimation. + * @param downloadedBytes Total number of downloaded bytes. + * @see #download(ProgressListener) + */ + void onDownloadProgress(Downloader downloader, float downloadPercentage, long downloadedBytes); + } + + /** + * Initializes the downloader. + * + * @throws DownloadException Thrown if the media cannot be downloaded. + * @throws InterruptedException If the thread has been interrupted. + * @throws IOException Thrown when there is an io error while reading from cache. + * @see #getDownloadedBytes() + * @see #getDownloadPercentage() + */ + void init() throws InterruptedException, IOException; + + /** + * Downloads the media. + * + * @param listener If not null, called during download. + * @throws DownloadException Thrown if the media cannot be downloaded. + * @throws InterruptedException If the thread has been interrupted. + * @throws IOException Thrown when there is an io error while downloading. + */ + void download(@Nullable ProgressListener listener) + throws InterruptedException, IOException; + + /** + * Removes all of the downloaded data of the media. + * + * @throws InterruptedException Thrown if the thread was interrupted. + */ + void remove() throws InterruptedException; + + /** + * Returns the total number of downloaded bytes, or {@link C#LENGTH_UNSET} if it hasn't been + * calculated yet. + * + * @see #init() + */ + long getDownloadedBytes(); + + /** + * Returns the download percentage, or {@link Float#NaN} if it can't be calculated yet. This + * value can be an estimation. + * + * @see #init() + */ + float getDownloadPercentage(); + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloaderConstructorHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloaderConstructorHelper.java new file mode 100644 index 0000000000..5f9a4d973a --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloaderConstructorHelper.java @@ -0,0 +1,107 @@ +/* + * 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.offline; + +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.upstream.DataSink; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DataSource.Factory; +import com.google.android.exoplayer2.upstream.DummyDataSource; +import com.google.android.exoplayer2.upstream.FileDataSource; +import com.google.android.exoplayer2.upstream.PriorityDataSource; +import com.google.android.exoplayer2.upstream.cache.Cache; +import com.google.android.exoplayer2.upstream.cache.CacheDataSink; +import com.google.android.exoplayer2.upstream.cache.CacheDataSource; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.ClosedSource; +import com.google.android.exoplayer2.util.PriorityTaskManager; + +/** A helper class that holds necessary parameters for {@link Downloader} construction. */ +@ClosedSource(reason = "Not ready yet") +public final class DownloaderConstructorHelper { + + private final Cache cache; + private final Factory upstreamDataSourceFactory; + private final Factory cacheReadDataSourceFactory; + private final DataSink.Factory cacheWriteDataSinkFactory; + private final PriorityTaskManager priorityTaskManager; + + /** + * @param cache Cache instance to be used to store downloaded data. + * @param upstreamDataSourceFactory A {@link Factory} for downloading data. + */ + public DownloaderConstructorHelper(Cache cache, Factory upstreamDataSourceFactory) { + this(cache, upstreamDataSourceFactory, null, null, null); + } + + /** + * @param cache Cache instance to be used to store downloaded data. + * @param upstreamDataSourceFactory A {@link Factory} for downloading data. + * @param cacheReadDataSourceFactory A {@link Factory} for reading data from the cache. + * If null, null is passed to {@link Downloader} constructor. + * @param cacheWriteDataSinkFactory A {@link DataSink.Factory} for writing data to the cache. If + * null, null is passed to {@link Downloader} constructor. + * @param priorityTaskManager If one is given then the download priority is set lower than + * loading. If null, null is passed to {@link Downloader} constructor. + */ + public DownloaderConstructorHelper(Cache cache, Factory upstreamDataSourceFactory, + @Nullable Factory cacheReadDataSourceFactory, + @Nullable DataSink.Factory cacheWriteDataSinkFactory, + @Nullable PriorityTaskManager priorityTaskManager) { + Assertions.checkNotNull(upstreamDataSourceFactory); + this.cache = cache; + this.upstreamDataSourceFactory = upstreamDataSourceFactory; + this.cacheReadDataSourceFactory = cacheReadDataSourceFactory; + this.cacheWriteDataSinkFactory = cacheWriteDataSinkFactory; + this.priorityTaskManager = priorityTaskManager; + } + + /** Returns the {@link Cache} instance. */ + public Cache getCache() { + return cache; + } + + /** Returns a {@link PriorityTaskManager} instance.*/ + public PriorityTaskManager getPriorityTaskManager() { + // Return a dummy PriorityTaskManager if none is provided. Create a new PriorityTaskManager + // each time so clients don't affect each other over the dummy PriorityTaskManager instance. + return priorityTaskManager != null ? priorityTaskManager : new PriorityTaskManager(); + } + + /** + * Returns a new {@link CacheDataSource} instance. If {@code offline} is true, it can only read + * data from the cache. + */ + public CacheDataSource buildCacheDataSource(boolean offline) { + DataSource cacheReadDataSource = cacheReadDataSourceFactory != null + ? cacheReadDataSourceFactory.createDataSource() : new FileDataSource(); + if (offline) { + return new CacheDataSource(cache, DummyDataSource.INSTANCE, + cacheReadDataSource, null, CacheDataSource.FLAG_BLOCK_ON_CACHE, null); + } else { + DataSink cacheWriteDataSink = cacheWriteDataSinkFactory != null + ? cacheWriteDataSinkFactory.createDataSink() + : new CacheDataSink(cache, CacheDataSource.DEFAULT_MAX_CACHE_FILE_SIZE); + DataSource upstream = upstreamDataSourceFactory.createDataSource(); + upstream = priorityTaskManager == null ? upstream + : new PriorityDataSource(upstream, priorityTaskManager, C.PRIORITY_DOWNLOAD); + return new CacheDataSource(cache, upstream, cacheReadDataSource, + cacheWriteDataSink, CacheDataSource.FLAG_BLOCK_ON_CACHE, null); + } + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java new file mode 100644 index 0000000000..c6bb3bc432 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java @@ -0,0 +1,100 @@ +/* + * 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.offline; + +import android.net.Uri; +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.upstream.cache.Cache; +import com.google.android.exoplayer2.upstream.cache.CacheDataSource; +import com.google.android.exoplayer2.upstream.cache.CacheUtil; +import com.google.android.exoplayer2.upstream.cache.CacheUtil.CachingCounters; +import com.google.android.exoplayer2.util.ClosedSource; +import com.google.android.exoplayer2.util.PriorityTaskManager; +import java.io.IOException; + +/** + * A downloader for progressive media streams. + */ +@ClosedSource(reason = "Not ready yet") +public final class ProgressiveDownloader implements Downloader { + + private static final int BUFFER_SIZE_BYTES = 128 * 1024; + + private final DataSpec dataSpec; + private final Cache cache; + private final CacheDataSource dataSource; + private final PriorityTaskManager priorityTaskManager; + private final CacheUtil.CachingCounters cachingCounters; + + /** + * @param uri Uri of the data to be downloaded. + * @param customCacheKey A custom key that uniquely identifies the original stream. Used for cache + * indexing. May be null. + * @param constructorHelper a {@link DownloaderConstructorHelper} instance. + */ + public ProgressiveDownloader( + String uri, String customCacheKey, DownloaderConstructorHelper constructorHelper) { + this.dataSpec = new DataSpec(Uri.parse(uri), 0, C.LENGTH_UNSET, customCacheKey, 0); + this.cache = constructorHelper.getCache(); + this.dataSource = constructorHelper.buildCacheDataSource(false); + this.priorityTaskManager = constructorHelper.getPriorityTaskManager(); + cachingCounters = new CachingCounters(); + } + + @Override + public void init() { + CacheUtil.getCached(dataSpec, cache, cachingCounters); + } + + @Override + public void download(@Nullable ProgressListener listener) throws InterruptedException, + IOException { + priorityTaskManager.add(C.PRIORITY_DOWNLOAD); + try { + byte[] buffer = new byte[BUFFER_SIZE_BYTES]; + CacheUtil.cache(dataSpec, cache, dataSource, buffer, priorityTaskManager, C.PRIORITY_DOWNLOAD, + cachingCounters, true); + // TODO: Work out how to call onDownloadProgress periodically during the download, or else + // get rid of ProgressListener and move to a model where the manager periodically polls + // Downloaders. + if (listener != null) { + listener.onDownloadProgress(this, 100, cachingCounters.contentLength); + } + } finally { + priorityTaskManager.remove(C.PRIORITY_DOWNLOAD); + } + } + + @Override + public void remove() { + CacheUtil.remove(cache, CacheUtil.getKey(dataSpec)); + } + + @Override + public long getDownloadedBytes() { + return cachingCounters.totalCachedBytes(); + } + + @Override + public float getDownloadPercentage() { + long contentLength = cachingCounters.contentLength; + return contentLength == C.LENGTH_UNSET ? Float.NaN + : ((cachingCounters.totalCachedBytes() * 100f) / contentLength); + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java new file mode 100644 index 0000000000..93e7c57470 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java @@ -0,0 +1,331 @@ +/* + * 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.offline; + +import android.net.Uri; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +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.upstream.cache.Cache; +import com.google.android.exoplayer2.upstream.cache.CacheDataSource; +import com.google.android.exoplayer2.upstream.cache.CacheUtil; +import com.google.android.exoplayer2.upstream.cache.CacheUtil.CachingCounters; +import com.google.android.exoplayer2.util.ClosedSource; +import com.google.android.exoplayer2.util.PriorityTaskManager; +import java.io.IOException; +import java.util.Collections; +import java.util.List; + +/** + * Base class for multi segment stream downloaders. + * + *

        All of the methods are blocking. Also they are not thread safe, except {@link + * #getTotalSegments()}, {@link #getDownloadedSegments()} and {@link #getDownloadedBytes()}. + * + * @param The type of the manifest object. + * @param The type of the representation key object. + */ +@ClosedSource(reason = "Not ready yet") +public abstract class SegmentDownloader implements Downloader { + + /** Smallest unit of content to be downloaded. */ + protected static class Segment implements Comparable { + /** The start time of the segment in microseconds. */ + public final long startTimeUs; + + /** The {@link DataSpec} of the segment. */ + public final DataSpec dataSpec; + + /** Constructs a Segment. */ + public Segment(long startTimeUs, DataSpec dataSpec) { + this.startTimeUs = startTimeUs; + this.dataSpec = dataSpec; + } + + @Override + public int compareTo(@NonNull Segment other) { + long startOffsetDiff = startTimeUs - other.startTimeUs; + return startOffsetDiff == 0 ? 0 : ((startOffsetDiff < 0) ? -1 : 1); + } + } + + private static final int BUFFER_SIZE_BYTES = 128 * 1024; + + private final Uri manifestUri; + private final PriorityTaskManager priorityTaskManager; + private final Cache cache; + private final CacheDataSource dataSource; + private final CacheDataSource offlineDataSource; + + private M manifest; + private K[] keys; + private volatile int totalSegments; + private volatile int downloadedSegments; + private volatile long downloadedBytes; + + /** + * @param manifestUri The {@link Uri} of the manifest to be downloaded. + * @param constructorHelper a {@link DownloaderConstructorHelper} instance. + */ + public SegmentDownloader(Uri manifestUri, DownloaderConstructorHelper constructorHelper) { + this.manifestUri = manifestUri; + this.cache = constructorHelper.getCache(); + this.dataSource = constructorHelper.buildCacheDataSource(false); + this.offlineDataSource = constructorHelper.buildCacheDataSource(true); + this.priorityTaskManager = constructorHelper.getPriorityTaskManager(); + resetCounters(); + } + + /** + * Returns the manifest. Downloads and parses it if necessary. + * + * @return The manifest. + * @throws IOException If an error occurs reading data. + */ + public final M getManifest() throws IOException { + return getManifestIfNeeded(false); + } + + /** + * Selects multiple representations pointed to by the keys for downloading, checking status. Any + * previous selection is cleared. If keys are null or empty, all representations are downloaded. + */ + public final void selectRepresentations(K[] keys) { + this.keys = keys != null ? keys.clone() : null; + resetCounters(); + } + + /** + * Initializes the total segments, downloaded segments and downloaded bytes counters for the + * selected representations. + * + * @throws IOException Thrown when there is an io error while reading from cache. + * @throws DownloadException Thrown if the media cannot be downloaded. + * @throws InterruptedException If the thread has been interrupted. + * @see #getTotalSegments() + * @see #getDownloadedSegments() + * @see #getDownloadedBytes() + */ + @Override + public final void init() throws InterruptedException, IOException { + try { + getManifestIfNeeded(true); + } catch (IOException e) { + // Either the manifest file isn't available offline or not parsable. + return; + } + try { + initStatus(true); + } catch (IOException | InterruptedException e) { + resetCounters(); + throw e; + } + } + + /** + * Downloads the content for the selected representations in sync or resumes a previously stopped + * download. + * + * @param listener If not null, called during download. + * @throws IOException Thrown when there is an io error while downloading. + * @throws DownloadException Thrown if the media cannot be downloaded. + * @throws InterruptedException If the thread has been interrupted. + */ + @Override + public final synchronized void download(@Nullable ProgressListener listener) + throws IOException, InterruptedException { + priorityTaskManager.add(C.PRIORITY_DOWNLOAD); + try { + getManifestIfNeeded(false); + List segments = initStatus(false); + notifyListener(listener); // Initial notification. + Collections.sort(segments); + byte[] buffer = new byte[BUFFER_SIZE_BYTES]; + CachingCounters cachingCounters = new CachingCounters(); + for (int i = 0; i < segments.size(); i++) { + CacheUtil.cache(segments.get(i).dataSpec, cache, dataSource, buffer, + priorityTaskManager, C.PRIORITY_DOWNLOAD, cachingCounters, true); + downloadedBytes += cachingCounters.newlyCachedBytes; + downloadedSegments++; + notifyListener(listener); + } + } finally { + priorityTaskManager.remove(C.PRIORITY_DOWNLOAD); + } + } + + /** + * Returns the total number of segments in the representations which are selected, or {@link + * C#LENGTH_UNSET} if it hasn't been calculated yet. + * + * @see #init() + */ + public final int getTotalSegments() { + return totalSegments; + } + + /** + * Returns the total number of downloaded segments in the representations which are selected, or + * {@link C#LENGTH_UNSET} if it hasn't been calculated yet. + * + * @see #init() + */ + public final int getDownloadedSegments() { + return downloadedSegments; + } + + /** + * Returns the total number of downloaded bytes in the representations which are selected, or + * {@link C#LENGTH_UNSET} if it hasn't been calculated yet. + * + * @see #init() + */ + @Override + public final long getDownloadedBytes() { + return downloadedBytes; + } + + @Override + public float getDownloadPercentage() { + // Take local snapshot of the volatile fields + int totalSegments = this.totalSegments; + int downloadedSegments = this.downloadedSegments; + if (totalSegments == C.LENGTH_UNSET || downloadedSegments == C.LENGTH_UNSET) { + return Float.NaN; + } + return totalSegments == 0 ? 100f : (downloadedSegments * 100f) / totalSegments; + } + + @Override + public final void remove() throws InterruptedException { + try { + getManifestIfNeeded(true); + } catch (IOException e) { + // Either the manifest file isn't available offline, or it's not parsable. Continue anyway to + // reset the counters and attempt to remove the manifest file. + } + resetCounters(); + if (manifest != null) { + List segments = null; + try { + segments = getAllSegments(offlineDataSource, manifest, true); + } catch (IOException e) { + // Ignore exceptions. We do our best with what's available offline. + } + if (segments != null) { + for (int i = 0; i < segments.size(); i++) { + remove(segments.get(i).dataSpec.uri); + } + } + manifest = null; + } + remove(manifestUri); + } + + /** + * Loads and parses the manifest. + * + * @param dataSource The {@link DataSource} through which to load. + * @param uri The manifest uri. + * @return The manifest. + * @throws IOException If an error occurs reading data. + */ + protected abstract M getManifest(DataSource dataSource, Uri uri) throws IOException; + + /** + * Returns a list of {@link Segment}s for given keys. + * + * @param dataSource The {@link DataSource} through which to load any required data. + * @param manifest The manifest containing the segments. + * @param keys The selected representation keys. + * @param allowIncompleteIndex Whether to continue in the case that a load error prevents all + * segments from being listed. If true then a partial segment list will be returned. If false + * an {@link IOException} will be thrown. + * @throws InterruptedException Thrown if the thread was interrupted. + * @throws IOException Thrown if {@code allowPartialIndex} is false and a load error occurs, or if + * the media is not in a form that allows for its segments to be listed. + * @return A list of {@link Segment}s for given keys. + */ + protected abstract List getSegments(DataSource dataSource, M manifest, K[] keys, + boolean allowIncompleteIndex) throws InterruptedException, IOException; + + /** + * Returns a list of all segments. + * + * @see #getSegments(DataSource, M, Object[], boolean)}. + */ + protected abstract List getAllSegments(DataSource dataSource, M manifest, + boolean allowPartialIndex) throws InterruptedException, IOException; + + private void resetCounters() { + totalSegments = C.LENGTH_UNSET; + downloadedSegments = C.LENGTH_UNSET; + downloadedBytes = C.LENGTH_UNSET; + } + + private void remove(Uri uri) { + CacheUtil.remove(cache, CacheUtil.generateKey(uri)); + } + + private void notifyListener(ProgressListener listener) { + if (listener != null) { + listener.onDownloadProgress(this, getDownloadPercentage(), downloadedBytes); + } + } + + /** + * Initializes totalSegments, downloadedSegments and downloadedBytes for selected representations. + * If not offline then downloads missing metadata. + * + * @return A list of not fully downloaded segments. + */ + private synchronized List initStatus(boolean offline) + throws IOException, InterruptedException { + DataSource dataSource = getDataSource(offline); + List segments = keys != null && keys.length > 0 + ? getSegments(dataSource, manifest, keys, offline) + : getAllSegments(dataSource, manifest, offline); + CachingCounters cachingCounters = new CachingCounters(); + totalSegments = segments.size(); + downloadedSegments = 0; + downloadedBytes = 0; + for (int i = segments.size() - 1; i >= 0; i--) { + Segment segment = segments.get(i); + CacheUtil.getCached(segment.dataSpec, cache, cachingCounters); + downloadedBytes += cachingCounters.alreadyCachedBytes; + if (cachingCounters.alreadyCachedBytes == cachingCounters.contentLength) { + // The segment is fully downloaded. + downloadedSegments++; + segments.remove(i); + } + } + return segments; + } + + private M getManifestIfNeeded(boolean offline) throws IOException { + if (manifest == null) { + manifest = getManifest(getDataSource(offline), manifestUri); + } + return manifest; + } + + private DataSource getDataSource(boolean offline) { + return offline ? offlineDataSource : dataSource; + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java b/library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java index 42ac938677..9c2be39576 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java @@ -32,12 +32,14 @@ import com.google.android.exoplayer2.Timeline; } @Override - public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) { + public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode, + boolean shuffleModeEnabled) { int childIndex = getChildIndexByWindowIndex(windowIndex); int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex); int nextWindowIndexInChild = getTimelineByChildIndex(childIndex).getNextWindowIndex( windowIndex - firstWindowIndexInChild, - repeatMode == Player.REPEAT_MODE_ALL ? Player.REPEAT_MODE_OFF : repeatMode); + repeatMode == Player.REPEAT_MODE_ALL ? Player.REPEAT_MODE_OFF : repeatMode, + shuffleModeEnabled); if (nextWindowIndexInChild != C.INDEX_UNSET) { return firstWindowIndexInChild + nextWindowIndexInChild; } else { @@ -53,12 +55,14 @@ import com.google.android.exoplayer2.Timeline; } @Override - public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) { + public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode, + boolean shuffleModeEnabled) { int childIndex = getChildIndexByWindowIndex(windowIndex); int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex); int previousWindowIndexInChild = getTimelineByChildIndex(childIndex).getPreviousWindowIndex( windowIndex - firstWindowIndexInChild, - repeatMode == Player.REPEAT_MODE_ALL ? Player.REPEAT_MODE_OFF : repeatMode); + repeatMode == Player.REPEAT_MODE_ALL ? Player.REPEAT_MODE_OFF : repeatMode, + shuffleModeEnabled); if (previousWindowIndexInChild != C.INDEX_UNSET) { return firstWindowIndexInChild + previousWindowIndexInChild; } else { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java index 5d2bbcc33e..0f6f1b345d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java @@ -198,19 +198,21 @@ public final class ConcatenatingMediaSource implements MediaSource { } @Override - public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) { + public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode, + boolean shuffleModeEnabled) { if (isRepeatOneAtomic && repeatMode == Player.REPEAT_MODE_ONE) { repeatMode = Player.REPEAT_MODE_ALL; } - return super.getNextWindowIndex(windowIndex, repeatMode); + return super.getNextWindowIndex(windowIndex, repeatMode, shuffleModeEnabled); } @Override - public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) { + public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode, + boolean shuffleModeEnabled) { if (isRepeatOneAtomic && repeatMode == Player.REPEAT_MODE_ONE) { repeatMode = Player.REPEAT_MODE_ALL; } - return super.getPreviousWindowIndex(windowIndex, repeatMode); + return super.getPreviousWindowIndex(windowIndex, repeatMode, shuffleModeEnabled); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ForwardingTimeline.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ForwardingTimeline.java index 4203abbf39..cfa5cec387 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ForwardingTimeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ForwardingTimeline.java @@ -35,13 +35,25 @@ public abstract class ForwardingTimeline extends Timeline { } @Override - public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) { - return timeline.getNextWindowIndex(windowIndex, repeatMode); + public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode, + boolean shuffleModeEnabled) { + return timeline.getNextWindowIndex(windowIndex, repeatMode, shuffleModeEnabled); } @Override - public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) { - return timeline.getPreviousWindowIndex(windowIndex, repeatMode); + public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode, + boolean shuffleModeEnabled) { + return timeline.getPreviousWindowIndex(windowIndex, repeatMode, shuffleModeEnabled); + } + + @Override + public int getLastWindowIndex(boolean shuffleModeEnabled) { + return timeline.getLastWindowIndex(shuffleModeEnabled); + } + + @Override + public int getFirstWindowIndex(boolean shuffleModeEnabled) { + return timeline.getFirstWindowIndex(shuffleModeEnabled); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java index 1795fe8045..1c9c181914 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java @@ -167,14 +167,18 @@ public final class LoopingMediaSource implements MediaSource { } @Override - public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) { - int childNextWindowIndex = timeline.getNextWindowIndex(windowIndex, repeatMode); + public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode, + boolean shuffleModeEnabled) { + int childNextWindowIndex = timeline.getNextWindowIndex(windowIndex, repeatMode, + shuffleModeEnabled); return childNextWindowIndex == C.INDEX_UNSET ? 0 : childNextWindowIndex; } @Override - public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode) { - int childPreviousWindowIndex = timeline.getPreviousWindowIndex(windowIndex, repeatMode); + public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode, + boolean shuffleModeEnabled) { + int childPreviousWindowIndex = timeline.getPreviousWindowIndex(windowIndex, repeatMode, + shuffleModeEnabled); return childPreviousWindowIndex == C.INDEX_UNSET ? getWindowCount() - 1 : childPreviousWindowIndex; } diff --git a/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadTestData.java b/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadTestData.java new file mode 100644 index 0000000000..133bf19dba --- /dev/null +++ b/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadTestData.java @@ -0,0 +1,80 @@ +/* + * 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.hls.offline; + +import com.google.android.exoplayer2.util.ClosedSource; + +/** + * Data for HLS downloading tests. + */ +@ClosedSource(reason = "Not ready yet") +/* package */ interface HlsDownloadTestData { + + String MASTER_PLAYLIST_URI = "test.m3u8"; + + String MEDIA_PLAYLIST_0_DIR = "gear0/"; + String MEDIA_PLAYLIST_0_URI = MEDIA_PLAYLIST_0_DIR + "prog_index.m3u8"; + String MEDIA_PLAYLIST_1_DIR = "gear1/"; + String MEDIA_PLAYLIST_1_URI = MEDIA_PLAYLIST_1_DIR + "prog_index.m3u8"; + String MEDIA_PLAYLIST_2_DIR = "gear2/"; + String MEDIA_PLAYLIST_2_URI = MEDIA_PLAYLIST_2_DIR + "prog_index.m3u8"; + String MEDIA_PLAYLIST_3_DIR = "gear3/"; + String MEDIA_PLAYLIST_3_URI = MEDIA_PLAYLIST_3_DIR + "prog_index.m3u8"; + + byte[] MASTER_PLAYLIST_DATA = + ("#EXTM3U\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=232370,CODECS=\"mp4a.40.2, avc1.4d4015\"\n" + + MEDIA_PLAYLIST_1_URI + "\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=649879,CODECS=\"mp4a.40.2, avc1.4d401e\"\n" + + MEDIA_PLAYLIST_2_URI + "\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=991714,CODECS=\"mp4a.40.2, avc1.4d401e\"\n" + + MEDIA_PLAYLIST_3_URI + "\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=41457,CODECS=\"mp4a.40.2\"\n" + + MEDIA_PLAYLIST_0_URI).getBytes(); + + byte[] MEDIA_PLAYLIST_DATA = + ("#EXTM3U\n" + + "#EXT-X-TARGETDURATION:10\n" + + "#EXT-X-VERSION:3\n" + + "#EXT-X-MEDIA-SEQUENCE:0\n" + + "#EXT-X-PLAYLIST-TYPE:VOD\n" + + "#EXTINF:9.97667,\n" + + "fileSequence0.ts\n" + + "#EXTINF:9.97667,\n" + + "fileSequence1.ts\n" + + "#EXTINF:9.97667,\n" + + "fileSequence2.ts\n" + + "#EXT-X-ENDLIST").getBytes(); + + String ENC_MEDIA_PLAYLIST_URI = "enc_index.m3u8"; + + byte[] ENC_MEDIA_PLAYLIST_DATA = + ("#EXTM3U\n" + + "#EXT-X-TARGETDURATION:10\n" + + "#EXT-X-VERSION:3\n" + + "#EXT-X-MEDIA-SEQUENCE:0\n" + + "#EXT-X-PLAYLIST-TYPE:VOD\n" + + "#EXT-X-KEY:METHOD=AES-128,URI=\"enc.key\"\n" + + "#EXTINF:9.97667,\n" + + "fileSequence0.ts\n" + + "#EXTINF:9.97667,\n" + + "fileSequence1.ts\n" + + "#EXT-X-KEY:METHOD=AES-128,URI=\"enc2.key\"\n" + + "#EXTINF:9.97667,\n" + + "fileSequence2.ts\n" + + "#EXT-X-ENDLIST").getBytes(); + +} diff --git a/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloaderTest.java b/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloaderTest.java new file mode 100644 index 0000000000..28afe450eb --- /dev/null +++ b/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloaderTest.java @@ -0,0 +1,210 @@ +/* + * 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.hls.offline; + +import static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.ENC_MEDIA_PLAYLIST_DATA; +import static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.ENC_MEDIA_PLAYLIST_URI; +import static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.MASTER_PLAYLIST_DATA; +import static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.MASTER_PLAYLIST_URI; +import static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.MEDIA_PLAYLIST_0_DIR; +import static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.MEDIA_PLAYLIST_0_URI; +import static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.MEDIA_PLAYLIST_1_DIR; +import static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.MEDIA_PLAYLIST_1_URI; +import static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.MEDIA_PLAYLIST_2_DIR; +import static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.MEDIA_PLAYLIST_2_URI; +import static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.MEDIA_PLAYLIST_3_DIR; +import static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.MEDIA_PLAYLIST_3_URI; +import static com.google.android.exoplayer2.source.hls.offline.HlsDownloadTestData.MEDIA_PLAYLIST_DATA; +import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCacheEmpty; +import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCachedData; + +import android.net.Uri; +import android.test.InstrumentationTestCase; +import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; +import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist; +import com.google.android.exoplayer2.testutil.FakeDataSet; +import com.google.android.exoplayer2.testutil.FakeDataSource.Factory; +import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor; +import com.google.android.exoplayer2.upstream.cache.SimpleCache; +import com.google.android.exoplayer2.util.ClosedSource; +import com.google.android.exoplayer2.util.Util; +import java.io.File; + +/** Unit tests for {@link HlsDownloader}. */ +@ClosedSource(reason = "Not ready yet") +public class HlsDownloaderTest extends InstrumentationTestCase { + + private SimpleCache cache; + private File tempFolder; + private FakeDataSet fakeDataSet; + private HlsDownloader hlsDownloader; + + @Override + public void setUp() throws Exception { + super.setUp(); + tempFolder = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest"); + cache = new SimpleCache(tempFolder, new NoOpCacheEvictor()); + + fakeDataSet = new FakeDataSet() + .setData(MASTER_PLAYLIST_URI, MASTER_PLAYLIST_DATA) + .setData(MEDIA_PLAYLIST_1_URI, MEDIA_PLAYLIST_DATA) + .setRandomData(MEDIA_PLAYLIST_1_DIR + "fileSequence0.ts", 10) + .setRandomData(MEDIA_PLAYLIST_1_DIR + "fileSequence1.ts", 11) + .setRandomData(MEDIA_PLAYLIST_1_DIR + "fileSequence2.ts", 12) + .setData(MEDIA_PLAYLIST_2_URI, MEDIA_PLAYLIST_DATA) + .setRandomData(MEDIA_PLAYLIST_2_DIR + "fileSequence0.ts", 13) + .setRandomData(MEDIA_PLAYLIST_2_DIR + "fileSequence1.ts", 14) + .setRandomData(MEDIA_PLAYLIST_2_DIR + "fileSequence2.ts", 15); + hlsDownloader = getHlsDownloader(MASTER_PLAYLIST_URI); + } + + @Override + public void tearDown() throws Exception { + Util.recursiveDelete(tempFolder); + super.tearDown(); + } + + public void testDownloadManifest() throws Exception { + HlsMasterPlaylist manifest = hlsDownloader.getManifest(); + + assertNotNull(manifest); + assertCachedData(cache, fakeDataSet, MASTER_PLAYLIST_URI); + } + + public void testSelectRepresentationsClearsPreviousSelection() throws Exception { + hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI}); + hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_2_URI}); + hlsDownloader.download(null); + + assertCachedData(cache, fakeDataSet, MASTER_PLAYLIST_URI, MEDIA_PLAYLIST_2_URI, + MEDIA_PLAYLIST_2_DIR + "fileSequence0.ts", + MEDIA_PLAYLIST_2_DIR + "fileSequence1.ts", + MEDIA_PLAYLIST_2_DIR + "fileSequence2.ts"); + } + + public void testCounterMethods() throws Exception { + hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI}); + hlsDownloader.download(null); + + assertEquals(4, hlsDownloader.getTotalSegments()); + assertEquals(4, hlsDownloader.getDownloadedSegments()); + assertEquals(MEDIA_PLAYLIST_DATA.length + 10 + 11 + 12, hlsDownloader.getDownloadedBytes()); + } + + public void testInitStatus() throws Exception { + hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI}); + hlsDownloader.download(null); + + HlsDownloader newHlsDownloader = + getHlsDownloader(MASTER_PLAYLIST_URI); + newHlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI}); + newHlsDownloader.init(); + + assertEquals(4, newHlsDownloader.getTotalSegments()); + assertEquals(4, newHlsDownloader.getDownloadedSegments()); + assertEquals(MEDIA_PLAYLIST_DATA.length + 10 + 11 + 12, newHlsDownloader.getDownloadedBytes()); + } + + public void testDownloadRepresentation() throws Exception { + hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI}); + hlsDownloader.download(null); + + assertCachedData(cache, fakeDataSet, MASTER_PLAYLIST_URI, MEDIA_PLAYLIST_1_URI, + MEDIA_PLAYLIST_1_DIR + "fileSequence0.ts", + MEDIA_PLAYLIST_1_DIR + "fileSequence1.ts", + MEDIA_PLAYLIST_1_DIR + "fileSequence2.ts"); + } + + public void testDownloadMultipleRepresentations() throws Exception { + hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI, MEDIA_PLAYLIST_2_URI}); + hlsDownloader.download(null); + + assertCachedData(cache, fakeDataSet); + } + + public void testDownloadAllRepresentations() throws Exception { + // Add data for the rest of the playlists + fakeDataSet.setData(MEDIA_PLAYLIST_0_URI, MEDIA_PLAYLIST_DATA) + .setRandomData(MEDIA_PLAYLIST_0_DIR + "fileSequence0.ts", 10) + .setRandomData(MEDIA_PLAYLIST_0_DIR + "fileSequence1.ts", 11) + .setRandomData(MEDIA_PLAYLIST_0_DIR + "fileSequence2.ts", 12) + .setData(MEDIA_PLAYLIST_3_URI, MEDIA_PLAYLIST_DATA) + .setRandomData(MEDIA_PLAYLIST_3_DIR + "fileSequence0.ts", 13) + .setRandomData(MEDIA_PLAYLIST_3_DIR + "fileSequence1.ts", 14) + .setRandomData(MEDIA_PLAYLIST_3_DIR + "fileSequence2.ts", 15); + hlsDownloader = getHlsDownloader(MASTER_PLAYLIST_URI); + + // hlsDownloader.selectRepresentations() isn't called + hlsDownloader.download(null); + assertCachedData(cache, fakeDataSet); + hlsDownloader.remove(); + + // select something random + hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI}); + // clear selection + hlsDownloader.selectRepresentations(null); + hlsDownloader.download(null); + assertCachedData(cache, fakeDataSet); + hlsDownloader.remove(); + + hlsDownloader.selectRepresentations(new String[0]); + hlsDownloader.download(null); + assertCachedData(cache, fakeDataSet); + hlsDownloader.remove(); + } + + public void testRemoveAll() throws Exception { + hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI, MEDIA_PLAYLIST_2_URI}); + hlsDownloader.download(null); + hlsDownloader.remove(); + + assertCacheEmpty(cache); + } + + public void testDownloadMediaPlaylist() throws Exception { + hlsDownloader = getHlsDownloader(MEDIA_PLAYLIST_1_URI); + hlsDownloader.selectRepresentations(new String[] {MEDIA_PLAYLIST_1_URI}); + hlsDownloader.download(null); + + assertCachedData(cache, fakeDataSet, MEDIA_PLAYLIST_1_URI, + MEDIA_PLAYLIST_1_DIR + "fileSequence0.ts", + MEDIA_PLAYLIST_1_DIR + "fileSequence1.ts", + MEDIA_PLAYLIST_1_DIR + "fileSequence2.ts"); + } + + public void testDownloadEncMediaPlaylist() throws Exception { + fakeDataSet = new FakeDataSet() + .setData(ENC_MEDIA_PLAYLIST_URI, ENC_MEDIA_PLAYLIST_DATA) + .setRandomData("enc.key", 8) + .setRandomData("enc2.key", 9) + .setRandomData("fileSequence0.ts", 10) + .setRandomData("fileSequence1.ts", 11) + .setRandomData("fileSequence2.ts", 12); + hlsDownloader = + getHlsDownloader(ENC_MEDIA_PLAYLIST_URI); + hlsDownloader.selectRepresentations(new String[] {ENC_MEDIA_PLAYLIST_URI}); + hlsDownloader.download(null); + + assertCachedData(cache, fakeDataSet); + } + + private HlsDownloader getHlsDownloader(String mediaPlaylistUri) { + Factory factory = new Factory(null).setFakeDataSet(fakeDataSet); + return new HlsDownloader(Uri.parse(mediaPlaylistUri), + new DownloaderConstructorHelper(cache, factory)); + } + +} diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadAction.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadAction.java new file mode 100644 index 0000000000..3c23e25796 --- /dev/null +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadAction.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.source.hls.offline; + +import android.net.Uri; +import com.google.android.exoplayer2.offline.DownloadAction; +import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; +import com.google.android.exoplayer2.offline.SegmentDownloadAction; +import com.google.android.exoplayer2.util.ClosedSource; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +/** An action to download or remove downloaded HLS streams. */ +@ClosedSource(reason = "Not ready yet") +public final class HlsDownloadAction extends SegmentDownloadAction { + + public static final Deserializer DESERIALIZER = new SegmentDownloadActionDeserializer() { + + @Override + public String getType() { + return TYPE; + } + + @Override + protected String readKey(DataInputStream input) throws IOException { + return input.readUTF(); + } + + @Override + protected String[] createKeyArray(int keyCount) { + return new String[0]; + } + + @Override + protected DownloadAction createDownloadAction(Uri manifestUri, boolean removeAction, + String[] keys) { + return new HlsDownloadAction(manifestUri, removeAction, keys); + } + + }; + + private static final String TYPE = "HlsDownloadAction"; + + /** @see SegmentDownloadAction#SegmentDownloadAction(Uri, boolean, Object[]) */ + public HlsDownloadAction(Uri manifestUri, boolean removeAction, String... keys) { + super(manifestUri, removeAction, keys); + } + + @Override + public String getType() { + return TYPE; + } + + @Override + public HlsDownloader createDownloader(DownloaderConstructorHelper constructorHelper) + throws IOException { + HlsDownloader downloader = new HlsDownloader(manifestUri, constructorHelper); + if (!isRemoveAction()) { + downloader.selectRepresentations(keys); + } + return downloader; + } + + @Override + protected void writeKey(DataOutputStream output, String key) throws IOException { + output.writeUTF(key); + } + +} diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java new file mode 100644 index 0000000000..488b85e78a --- /dev/null +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java @@ -0,0 +1,139 @@ +/* + * 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.hls.offline; + +import android.net.Uri; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; +import com.google.android.exoplayer2.offline.SegmentDownloader; +import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist; +import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl; +import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; +import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylist; +import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.upstream.ParsingLoadable; +import com.google.android.exoplayer2.util.ClosedSource; +import com.google.android.exoplayer2.util.UriUtil; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; + +/** + * Helper class to download HLS streams. + * + * A subset of renditions can be downloaded by selecting them using {@link + * #selectRepresentations(Object[])}. As key, string form of the rendition's url is used. The urls + * can be absolute or relative to the master playlist url. + */ +@ClosedSource(reason = "Not ready yet") +public final class HlsDownloader extends SegmentDownloader { + + /** + * @see SegmentDownloader#SegmentDownloader(Uri, DownloaderConstructorHelper) + */ + public HlsDownloader(Uri manifestUri, DownloaderConstructorHelper constructorHelper) { + super(manifestUri, constructorHelper); + } + + @Override + protected HlsMasterPlaylist getManifest(DataSource dataSource, Uri uri) throws IOException { + HlsPlaylist hlsPlaylist = loadManifest(dataSource, uri); + if (hlsPlaylist instanceof HlsMasterPlaylist) { + return (HlsMasterPlaylist) hlsPlaylist; + } else { + return HlsMasterPlaylist.createSingleVariantMasterPlaylist(hlsPlaylist.baseUri); + } + } + + @Override + protected List getAllSegments(DataSource dataSource, HlsMasterPlaylist manifest, + boolean allowIndexLoadErrors) throws InterruptedException, IOException { + ArrayList urls = new ArrayList<>(); + extractUrls(manifest.variants, urls); + extractUrls(manifest.audios, urls); + extractUrls(manifest.subtitles, urls); + return getSegments(dataSource, manifest, urls.toArray(new String[urls.size()]), + allowIndexLoadErrors); + } + + @Override + protected List getSegments(DataSource dataSource, HlsMasterPlaylist manifest, + String[] keys, boolean allowIndexLoadErrors) throws InterruptedException, IOException { + HashSet encryptionKeyUris = new HashSet<>(); + ArrayList segments = new ArrayList<>(); + for (String playlistUrl : keys) { + HlsMediaPlaylist mediaPlaylist = null; + Uri uri = UriUtil.resolveToUri(manifest.baseUri, playlistUrl); + try { + mediaPlaylist = (HlsMediaPlaylist) loadManifest(dataSource, uri); + } catch (IOException e) { + if (!allowIndexLoadErrors) { + throw e; + } + } + segments.add(new Segment(mediaPlaylist != null ? mediaPlaylist.startTimeUs : Long.MIN_VALUE, + new DataSpec(uri))); + if (mediaPlaylist == null) { + continue; + } + + HlsMediaPlaylist.Segment initSegment = mediaPlaylist.initializationSegment; + if (initSegment != null) { + addSegment(segments, mediaPlaylist, initSegment, encryptionKeyUris); + } + + List hlsSegments = mediaPlaylist.segments; + for (int i = 0; i < hlsSegments.size(); i++) { + addSegment(segments, mediaPlaylist, hlsSegments.get(i), encryptionKeyUris); + } + } + return segments; + } + + private HlsPlaylist loadManifest(DataSource dataSource, Uri uri) throws IOException { + DataSpec dataSpec = new DataSpec(uri, + DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH | DataSpec.FLAG_ALLOW_GZIP); + ParsingLoadable loadable = new ParsingLoadable<>(dataSource, dataSpec, + C.DATA_TYPE_MANIFEST, new HlsPlaylistParser()); + loadable.load(); + return loadable.getResult(); + } + + private static void addSegment(ArrayList segments, HlsMediaPlaylist mediaPlaylist, + HlsMediaPlaylist.Segment hlsSegment, HashSet encryptionKeyUris) + throws IOException, InterruptedException { + long startTimeUs = mediaPlaylist.startTimeUs + hlsSegment.relativeStartTimeUs; + if (hlsSegment.isEncrypted) { + Uri keyUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, hlsSegment.encryptionKeyUri); + if (encryptionKeyUris.add(keyUri)) { + segments.add(new Segment(startTimeUs, new DataSpec(keyUri))); + } + } + Uri resolvedUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, hlsSegment.url); + segments.add(new Segment(startTimeUs, + new DataSpec(resolvedUri, hlsSegment.byterangeOffset, hlsSegment.byterangeLength, null))); + } + + private static void extractUrls(List hlsUrls, ArrayList urls) { + for (int i = 0; i < hlsUrls.size(); i++) { + urls.add(hlsUrls.get(i).url); + } + } + +} diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadAction.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadAction.java new file mode 100644 index 0000000000..7478062ef8 --- /dev/null +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadAction.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.source.smoothstreaming.offline; + +import android.net.Uri; +import com.google.android.exoplayer2.offline.DownloadAction; +import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; +import com.google.android.exoplayer2.offline.SegmentDownloadAction; +import com.google.android.exoplayer2.source.smoothstreaming.manifest.TrackKey; +import com.google.android.exoplayer2.util.ClosedSource; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +/** An action to download or remove downloaded SmoothStreaming streams. */ +@ClosedSource(reason = "Not ready yet") +public final class SsDownloadAction extends SegmentDownloadAction { + + public static final Deserializer DESERIALIZER = + new SegmentDownloadActionDeserializer() { + + @Override + public String getType() { + return TYPE; + } + + @Override + protected TrackKey readKey(DataInputStream input) throws IOException { + return new TrackKey(input.readInt(), input.readInt()); + } + + @Override + protected TrackKey[] createKeyArray(int keyCount) { + return new TrackKey[keyCount]; + } + + @Override + protected DownloadAction createDownloadAction(Uri manifestUri, boolean removeAction, + TrackKey[] keys) { + return new SsDownloadAction(manifestUri, removeAction, keys); + } + + }; + + private static final String TYPE = "SsDownloadAction"; + + /** @see SegmentDownloadAction#SegmentDownloadAction(Uri, boolean, Object[]) */ + public SsDownloadAction(Uri manifestUri, boolean removeAction, TrackKey... keys) { + super(manifestUri, removeAction, keys); + } + + @Override + public String getType() { + return TYPE; + } + + @Override + public SsDownloader createDownloader(DownloaderConstructorHelper constructorHelper) + throws IOException { + SsDownloader downloader = new SsDownloader(manifestUri, constructorHelper); + if (!isRemoveAction()) { + downloader.selectRepresentations(keys); + } + return downloader; + } + + @Override + protected void writeKey(DataOutputStream output, TrackKey key) throws IOException { + output.writeInt(key.streamElementIndex); + output.writeInt(key.trackIndex); + } + +} diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java new file mode 100644 index 0000000000..fe9c21d855 --- /dev/null +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java @@ -0,0 +1,111 @@ +/* + * 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.smoothstreaming.offline; + +import android.net.Uri; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; +import com.google.android.exoplayer2.offline.SegmentDownloader; +import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest; +import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest.StreamElement; +import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifestParser; +import com.google.android.exoplayer2.source.smoothstreaming.manifest.TrackKey; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.upstream.ParsingLoadable; +import com.google.android.exoplayer2.util.ClosedSource; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * Helper class to download SmoothStreaming streams. + * + *

        Except {@link #getTotalSegments()}, {@link #getDownloadedSegments()} and {@link + * #getDownloadedBytes()}, this class isn't thread safe. + * + *

        Example usage: + * + *

        + * {@code
        + * SimpleCache cache = new SimpleCache(downloadFolder, new NoOpCacheEvictor());
        + * DefaultHttpDataSourceFactory factory = new DefaultHttpDataSourceFactory("ExoPlayer", null);
        + * DownloaderConstructorHelper constructorHelper =
        + *     new DownloaderConstructorHelper(cache, factory);
        + * SsDownloader ssDownloader = new SsDownloader(manifestUrl, constructorHelper);
        + * // Select the first track of the first stream element
        + * ssDownloader.selectRepresentations(new TrackKey[] {new TrackKey(0, 0)});
        + * ssDownloader.download(new ProgressListener() {
        + *   @Override
        + *   public void onDownloadProgress(Downloader downloader, float downloadPercentage,
        + *       long downloadedBytes) {
        + *     // Invoked periodically during the download.
        + *   }
        + * });
        + * // Access downloaded data using CacheDataSource
        + * CacheDataSource cacheDataSource =
        + *     new CacheDataSource(cache, factory.createDataSource(), CacheDataSource.FLAG_BLOCK_ON_CACHE);}
        + * 
        + */ +@ClosedSource(reason = "Not ready yet") +public final class SsDownloader extends SegmentDownloader { + + /** + * @see SegmentDownloader#SegmentDownloader(Uri, DownloaderConstructorHelper) + */ + public SsDownloader(Uri manifestUri, DownloaderConstructorHelper constructorHelper) { + super(manifestUri, constructorHelper); + } + + @Override + public SsManifest getManifest(DataSource dataSource, Uri uri) throws IOException { + DataSpec dataSpec = new DataSpec(uri, + DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH | DataSpec.FLAG_ALLOW_GZIP); + ParsingLoadable loadable = new ParsingLoadable<>(dataSource, dataSpec, + C.DATA_TYPE_MANIFEST, new SsManifestParser()); + loadable.load(); + return loadable.getResult(); + } + + @Override + protected List getAllSegments(DataSource dataSource, SsManifest manifest, + boolean allowIndexLoadErrors) throws InterruptedException, IOException { + ArrayList segments = new ArrayList<>(); + for (int i = 0; i < manifest.streamElements.length; i++) { + StreamElement streamElement = manifest.streamElements[i]; + for (int j = 0; j < streamElement.formats.length; j++) { + segments.addAll(getSegments(dataSource, manifest, new TrackKey[] {new TrackKey(i, j)}, + allowIndexLoadErrors)); + } + } + return segments; + } + + @Override + protected List getSegments(DataSource dataSource, SsManifest manifest, + TrackKey[] keys, boolean allowIndexLoadErrors) throws InterruptedException, IOException { + ArrayList segments = new ArrayList<>(); + for (TrackKey key : keys) { + StreamElement streamElement = manifest.streamElements[key.streamElementIndex]; + for (int i = 0; i < streamElement.chunkCount; i++) { + segments.add(new Segment(streamElement.getStartTimeUs(i), + new DataSpec(streamElement.buildRequestUri(key.trackIndex, i)))); + } + } + return segments; + } + +} diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java index 3e6b5bf158..acb6e3e7cd 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java @@ -675,9 +675,11 @@ public class PlaybackControlView extends FrameLayout { timeline.getWindow(windowIndex, window); isSeekable = window.isSeekable; enablePrevious = isSeekable || !window.isDynamic - || timeline.getPreviousWindowIndex(windowIndex, player.getRepeatMode()) != C.INDEX_UNSET; + || timeline.getPreviousWindowIndex(windowIndex, player.getRepeatMode(), false) + != C.INDEX_UNSET; enableNext = window.isDynamic - || timeline.getNextWindowIndex(windowIndex, player.getRepeatMode()) != C.INDEX_UNSET; + || timeline.getNextWindowIndex(windowIndex, player.getRepeatMode(), false) + != C.INDEX_UNSET; if (player.isPlayingAd()) { // Always hide player controls during ads. hide(); @@ -861,7 +863,8 @@ public class PlaybackControlView extends FrameLayout { } int windowIndex = player.getCurrentWindowIndex(); timeline.getWindow(windowIndex, window); - int previousWindowIndex = timeline.getPreviousWindowIndex(windowIndex, player.getRepeatMode()); + int previousWindowIndex = timeline.getPreviousWindowIndex(windowIndex, player.getRepeatMode(), + false); if (previousWindowIndex != C.INDEX_UNSET && (player.getCurrentPosition() <= MAX_POSITION_FOR_SEEK_TO_PREVIOUS || (window.isDynamic && !window.isSeekable))) { @@ -877,7 +880,7 @@ public class PlaybackControlView extends FrameLayout { return; } int windowIndex = player.getCurrentWindowIndex(); - int nextWindowIndex = timeline.getNextWindowIndex(windowIndex, player.getRepeatMode()); + int nextWindowIndex = timeline.getNextWindowIndex(windowIndex, player.getRepeatMode(), false); if (nextWindowIndex != C.INDEX_UNSET) { seekTo(nextWindowIndex, C.TIME_UNSET); } else if (timeline.getWindow(windowIndex, window, false).isDynamic) { diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashDownloadTest.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashDownloadTest.java new file mode 100644 index 0000000000..706dd72166 --- /dev/null +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashDownloadTest.java @@ -0,0 +1,192 @@ +/* + * 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.playbacktests.gts; + +import android.net.Uri; +import android.test.ActivityInstrumentationTestCase2; +import com.google.android.exoplayer2.offline.Downloader; +import com.google.android.exoplayer2.offline.Downloader.ProgressListener; +import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; +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.Representation; +import com.google.android.exoplayer2.source.dash.manifest.RepresentationKey; +import com.google.android.exoplayer2.source.dash.offline.DashDownloader; +import com.google.android.exoplayer2.testutil.HostActivity; +import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; +import com.google.android.exoplayer2.upstream.DummyDataSource; +import com.google.android.exoplayer2.upstream.cache.CacheDataSource; +import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory; +import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor; +import com.google.android.exoplayer2.upstream.cache.SimpleCache; +import com.google.android.exoplayer2.util.ClosedSource; +import com.google.android.exoplayer2.util.Util; +import java.io.File; +import java.io.IOException; +import java.io.InterruptedIOException; +import java.util.ArrayList; +import java.util.List; + +/** + * Tests downloaded DASH playbacks. + */ +@ClosedSource(reason = "Not ready yet") +public final class DashDownloadTest extends ActivityInstrumentationTestCase2 { + + private static final String TAG = "DashDownloadTest"; + + private DashTestRunner testRunner; + private File tempFolder; + private SimpleCache cache; + + public DashDownloadTest() { + super(HostActivity.class); + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + testRunner = new DashTestRunner(TAG, getActivity(), getInstrumentation()) + .setManifestUrl(DashTestData.H264_MANIFEST) + .setFullPlaybackNoSeeking(true) + .setCanIncludeAdditionalVideoFormats(false) + .setAudioVideoFormats(DashTestData.AAC_AUDIO_REPRESENTATION_ID, + DashTestData.H264_CDD_FIXED); + tempFolder = Util.createTempDirectory(getActivity(), "ExoPlayerTest"); + cache = new SimpleCache(tempFolder, new NoOpCacheEvictor()); + } + + @Override + protected void tearDown() throws Exception { + testRunner = null; + Util.recursiveDelete(tempFolder); + cache = null; + super.tearDown(); + } + + // Download tests + + public void testDownload() throws Exception { + if (Util.SDK_INT < 16) { + return; // Pass. + } + + // Download manifest only + createDashDownloader(false).getManifest(); + long manifestLength = cache.getCacheSpace(); + + // Download representations + DashDownloader dashDownloader = downloadContent(false, Float.NaN); + assertEquals(cache.getCacheSpace() - manifestLength, dashDownloader.getDownloadedBytes()); + + testRunner.setStreamName("test_h264_fixed_download"). + setDataSourceFactory(newOfflineCacheDataSourceFactory()).run(); + + dashDownloader.remove(); + + assertEquals("There should be no content left.", 0, cache.getKeys().size()); + assertEquals("There should be no content left.", 0, cache.getCacheSpace()); + } + + public void testPartialDownload() throws Exception { + if (Util.SDK_INT < 16) { + return; // Pass. + } + + // Just download the first half and manifest + downloadContent(false, 0.5f); + + // Download the rest + DashDownloader dashDownloader = downloadContent(false, Float.NaN); + long downloadedBytes = dashDownloader.getDownloadedBytes(); + + // Make sure it doesn't download any data + dashDownloader = downloadContent(true, Float.NaN); + assertEquals(downloadedBytes, dashDownloader.getDownloadedBytes()); + + testRunner.setStreamName("test_h264_fixed_partial_download") + .setDataSourceFactory(newOfflineCacheDataSourceFactory()).run(); + } + + private DashDownloader downloadContent(boolean offline, float stopAt) throws Exception { + DashDownloader dashDownloader = createDashDownloader(offline); + DashManifest dashManifest = dashDownloader.getManifest(); + try { + ArrayList keys = new ArrayList<>(); + for (int pIndex = 0; pIndex < dashManifest.getPeriodCount(); pIndex++) { + List adaptationSets = dashManifest.getPeriod(pIndex).adaptationSets; + for (int aIndex = 0; aIndex < adaptationSets.size(); aIndex++) { + AdaptationSet adaptationSet = adaptationSets.get(aIndex); + List representations = adaptationSet.representations; + for (int rIndex = 0; rIndex < representations.size(); rIndex++) { + String id = representations.get(rIndex).format.id; + if (DashTestData.AAC_AUDIO_REPRESENTATION_ID.equals(id) + || DashTestData.H264_CDD_FIXED.equals(id)) { + keys.add(new RepresentationKey(pIndex, aIndex, rIndex)); + } + } + } + dashDownloader.selectRepresentations(keys.toArray(new RepresentationKey[keys.size()])); + TestProgressListener listener = new TestProgressListener(stopAt); + dashDownloader.download(listener); + } + } catch (InterruptedException e) { + // do nothing + } catch (IOException e) { + Throwable exception = e; + while (!(exception instanceof InterruptedIOException)) { + if (exception == null) { + throw e; + } + exception = exception.getCause(); + } + // else do nothing + } + return dashDownloader; + } + + private DashDownloader createDashDownloader(boolean offline) { + DownloaderConstructorHelper constructorHelper = new DownloaderConstructorHelper(cache, + offline ? DummyDataSource.FACTORY : new DefaultHttpDataSourceFactory("ExoPlayer", null)); + return new DashDownloader(Uri.parse(DashTestData.H264_MANIFEST), constructorHelper); + } + + private CacheDataSourceFactory newOfflineCacheDataSourceFactory() { + return new CacheDataSourceFactory(cache, DummyDataSource.FACTORY, + CacheDataSource.FLAG_BLOCK_ON_CACHE); + } + + private static class TestProgressListener implements ProgressListener { + + private float stopAt; + + private TestProgressListener(float stopAt) { + this.stopAt = stopAt; + } + + @Override + public void onDownloadProgress(Downloader downloader, float downloadPercentage, + long downloadedBytes) { + System.out.printf("onDownloadProgress downloadPercentage = [%g], downloadedData = [%d]%n", + downloadPercentage, downloadedBytes); + if (downloadPercentage >= stopAt) { + Thread.currentThread().interrupt(); + } + } + + } + +} diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java index 8357ce70c7..36d72db48b 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java @@ -74,7 +74,7 @@ public final class TimelineAsserts { @Player.RepeatMode int repeatMode, int... expectedPreviousWindowIndices) { for (int i = 0; i < timeline.getWindowCount(); i++) { assertEquals(expectedPreviousWindowIndices[i], - timeline.getPreviousWindowIndex(i, repeatMode)); + timeline.getPreviousWindowIndex(i, repeatMode, false)); } } @@ -86,14 +86,14 @@ public final class TimelineAsserts { int... expectedNextWindowIndices) { for (int i = 0; i < timeline.getWindowCount(); i++) { assertEquals(expectedNextWindowIndices[i], - timeline.getNextWindowIndex(i, repeatMode)); + timeline.getNextWindowIndex(i, repeatMode, false)); } } /** * Asserts that period counts for each window are set correctly. Also asserts that * {@link Window#firstPeriodIndex} and {@link Window#lastPeriodIndex} are set correctly, and it - * asserts the correct behavior of {@link Timeline#getNextWindowIndex(int, int)}. + * asserts the correct behavior of {@link Timeline#getNextWindowIndex(int, int, boolean)}. */ public static void assertPeriodCounts(Timeline timeline, int... expectedPeriodCounts) { int windowCount = timeline.getWindowCount(); @@ -119,16 +119,19 @@ public final class TimelineAsserts { } assertEquals(expectedWindowIndex, period.windowIndex); if (i < accumulatedPeriodCounts[expectedWindowIndex + 1] - 1) { - assertEquals(i + 1, timeline.getNextPeriodIndex(i, period, window, Player.REPEAT_MODE_OFF)); - assertEquals(i + 1, timeline.getNextPeriodIndex(i, period, window, Player.REPEAT_MODE_ONE)); - assertEquals(i + 1, timeline.getNextPeriodIndex(i, period, window, Player.REPEAT_MODE_ALL)); + assertEquals(i + 1, timeline.getNextPeriodIndex(i, period, window, Player.REPEAT_MODE_OFF, + false)); + assertEquals(i + 1, timeline.getNextPeriodIndex(i, period, window, Player.REPEAT_MODE_ONE, + false)); + assertEquals(i + 1, timeline.getNextPeriodIndex(i, period, window, Player.REPEAT_MODE_ALL, + false)); } else { int nextWindowOff = timeline.getNextWindowIndex(expectedWindowIndex, - Player.REPEAT_MODE_OFF); + Player.REPEAT_MODE_OFF, false); int nextWindowOne = timeline.getNextWindowIndex(expectedWindowIndex, - Player.REPEAT_MODE_ONE); + Player.REPEAT_MODE_ONE, false); int nextWindowAll = timeline.getNextWindowIndex(expectedWindowIndex, - Player.REPEAT_MODE_ALL); + Player.REPEAT_MODE_ALL, false); int nextPeriodOff = nextWindowOff == C.INDEX_UNSET ? C.INDEX_UNSET : accumulatedPeriodCounts[nextWindowOff]; int nextPeriodOne = nextWindowOne == C.INDEX_UNSET ? C.INDEX_UNSET @@ -136,11 +139,11 @@ public final class TimelineAsserts { int nextPeriodAll = nextWindowAll == C.INDEX_UNSET ? C.INDEX_UNSET : accumulatedPeriodCounts[nextWindowAll]; assertEquals(nextPeriodOff, timeline.getNextPeriodIndex(i, period, window, - Player.REPEAT_MODE_OFF)); + Player.REPEAT_MODE_OFF, false)); assertEquals(nextPeriodOne, timeline.getNextPeriodIndex(i, period, window, - Player.REPEAT_MODE_ONE)); + Player.REPEAT_MODE_ONE, false)); assertEquals(nextPeriodAll, timeline.getNextPeriodIndex(i, period, window, - Player.REPEAT_MODE_ALL)); + Player.REPEAT_MODE_ALL, false)); } } } From 2c8d5f846e4c0e01b772c46f9626954a836a9a47 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 23 Aug 2017 03:27:40 -0700 Subject: [PATCH 0294/2472] Pass shuffle mode to timeline assertion helper methods. This allows to test the expected behaviour of timeline with different shuffle modes. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166181091 --- .../android/exoplayer2/TimelineTest.java | 26 ++++--- .../source/ClippingMediaSourceTest.java | 15 ++-- .../source/ConcatenatingMediaSourceTest.java | 76 ++++++++++--------- .../DynamicConcatenatingMediaSourceTest.java | 15 ++-- .../source/LoopingMediaSourceTest.java | 39 +++++----- .../exoplayer2/testutil/TimelineAsserts.java | 55 +++++--------- 6 files changed, 113 insertions(+), 113 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/TimelineTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/TimelineTest.java index d9ee27bd62..f5c33843a1 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/TimelineTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/TimelineTest.java @@ -33,23 +33,25 @@ public class TimelineTest extends TestCase { Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(1, 111)); TimelineAsserts.assertWindowIds(timeline, 111); TimelineAsserts.assertPeriodCounts(timeline, 1); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, 0); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, 0); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, + C.INDEX_UNSET); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 0); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 0); } public void testMultiPeriodTimeline() { Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(5, 111)); TimelineAsserts.assertWindowIds(timeline, 111); TimelineAsserts.assertPeriodCounts(timeline, 5); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, 0); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, 0); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, + C.INDEX_UNSET); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 0); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 0); } } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java index 66b0337450..5e615dbc7f 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java @@ -109,13 +109,14 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase { TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US); TimelineAsserts.assertWindowIds(clippedTimeline, 111); TimelineAsserts.assertPeriodCounts(clippedTimeline, 1); - TimelineAsserts.assertPreviousWindowIndices( - clippedTimeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET); - TimelineAsserts.assertPreviousWindowIndices(clippedTimeline, Player.REPEAT_MODE_ONE, 0); - TimelineAsserts.assertPreviousWindowIndices(clippedTimeline, Player.REPEAT_MODE_ALL, 0); - TimelineAsserts.assertNextWindowIndices(clippedTimeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices(clippedTimeline, Player.REPEAT_MODE_ONE, 0); - TimelineAsserts.assertNextWindowIndices(clippedTimeline, Player.REPEAT_MODE_ALL, 0); + TimelineAsserts.assertPreviousWindowIndices(clippedTimeline, Player.REPEAT_MODE_OFF, false, + C.INDEX_UNSET); + TimelineAsserts.assertPreviousWindowIndices(clippedTimeline, Player.REPEAT_MODE_ONE, false, 0); + TimelineAsserts.assertPreviousWindowIndices(clippedTimeline, Player.REPEAT_MODE_ALL, false, 0); + TimelineAsserts.assertNextWindowIndices(clippedTimeline, Player.REPEAT_MODE_OFF, false, + C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(clippedTimeline, Player.REPEAT_MODE_ONE, false, 0); + TimelineAsserts.assertNextWindowIndices(clippedTimeline, Player.REPEAT_MODE_ALL, false, 0); } /** diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java index 3bf89f9bcc..34f44b47f7 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java @@ -34,22 +34,26 @@ public final class ConcatenatingMediaSourceTest extends TestCase { Timeline timeline = getConcatenatedTimeline(false, createFakeTimeline(3, 111)); TimelineAsserts.assertWindowIds(timeline, 111); TimelineAsserts.assertPeriodCounts(timeline, 3); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, 0); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, 0); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, + C.INDEX_UNSET); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 0); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, + C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 0); timeline = getConcatenatedTimeline(true, createFakeTimeline(3, 111)); TimelineAsserts.assertWindowIds(timeline, 111); TimelineAsserts.assertPeriodCounts(timeline, 3); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, 0); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, 0); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, + C.INDEX_UNSET); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 0); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, + C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 0); } public void testMultipleMediaSources() { @@ -58,24 +62,26 @@ public final class ConcatenatingMediaSourceTest extends TestCase { Timeline timeline = getConcatenatedTimeline(false, timelines); TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); TimelineAsserts.assertPeriodCounts(timeline, 3, 1, 3); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET, 0, 1); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0, 1, 2); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, 2, 0, 1); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, 1, 2, C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0, 1, 2); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, 1, 2, 0); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, + C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 2, 0, 1); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, + 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 1, 2, 0); timeline = getConcatenatedTimeline(true, timelines); TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); TimelineAsserts.assertPeriodCounts(timeline, 3, 1, 3); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET, 0, 1); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, 2, 0, 1); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, 2, 0, 1); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, 1, 2, C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, 1, 2, 0); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, 1, 2, 0); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, + C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 2, 0, 1); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 2, 0, 1); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, + 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 1, 2, 0); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 1, 2, 0); } public void testNestedMediaSources() { @@ -84,14 +90,16 @@ public final class ConcatenatingMediaSourceTest extends TestCase { getConcatenatedTimeline(true, createFakeTimeline(1, 333), createFakeTimeline(1, 444))); TimelineAsserts.assertWindowIds(timeline, 111, 222, 333, 444); TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1, 1); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET, 0, 1, 2); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0, 1, 3, 2); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, 3, 0, 1, 2); - TimelineAsserts.assertNextWindowIndices( - timeline, Player.REPEAT_MODE_OFF, 1, 2, 3, C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0, 1, 3, 2); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, 1, 2, 3, 0); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, + C.INDEX_UNSET, 0, 1, 2); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, + 0, 1, 3, 2); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, + 3, 0, 1, 2); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, + 1, 2, 3, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 3, 2); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 1, 2, 3, 0); } /** diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java index 8d29a95d89..35233febf5 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java @@ -120,13 +120,14 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { } // Assert correct next and previous indices behavior after some insertions and removals. - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, 1, 2, C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0, 1, 2); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, 1, 2, 0); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET, 0, 1); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0, 1, 2); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, 2, 0, 1); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, + 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 1, 2, 0); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, + C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 2, 0, 1); // Remove at front of queue. mediaSource.removeMediaSource(0); diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java index d2045c29a5..c32f5cb624 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java @@ -42,30 +42,31 @@ public class LoopingMediaSourceTest extends TestCase { Timeline timeline = getLoopingTimeline(multiWindowTimeline, 1); TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1); - TimelineAsserts.assertPreviousWindowIndices( - timeline, Player.REPEAT_MODE_OFF, C.INDEX_UNSET, 0, 1); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0, 1, 2); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, 2, 0, 1); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, 1, 2, C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0, 1, 2); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, 1, 2, 0); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, + C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 2, 0, 1); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, + 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 1, 2, 0); } public void testMultiLoop() { Timeline timeline = getLoopingTimeline(multiWindowTimeline, 3); TimelineAsserts.assertWindowIds(timeline, 111, 222, 333, 111, 222, 333, 111, 222, 333); TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1, 1, 1, 1, 1, 1, 1); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, C.INDEX_UNSET, 0, 1, 2, 3, 4, 5, 6, 7, 8); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2, 3, 4, 5, 6, 7, 8); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 8, 0, 1, 2, 3, 4, 5, 6, 7); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, 1, 2, 3, 4, 5, 6, 7, 8, C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2, 3, 4, 5, 6, 7, 8); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 1, 2, 3, 4, 5, 6, 7, 8, 0); } @@ -73,12 +74,12 @@ public class LoopingMediaSourceTest extends TestCase { Timeline timeline = getLoopingTimeline(multiWindowTimeline, Integer.MAX_VALUE); TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, 2, 0, 1); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0, 1, 2); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, 2, 0, 1); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, 1, 2, 0); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, 0, 1, 2); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, 1, 2, 0); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, 2, 0, 1); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 2, 0, 1); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, 1, 2, 0); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 1, 2, 0); } /** diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java index 36d72db48b..74129a0e69 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java @@ -67,26 +67,27 @@ public final class TimelineAsserts { } /** - * Asserts that previous window indices for each window are set correctly depending on the repeat - * mode. + * Asserts that previous window indices for each window depending on the repeat mode and the + * shuffle mode are equal to the given sequence. */ public static void assertPreviousWindowIndices(Timeline timeline, - @Player.RepeatMode int repeatMode, int... expectedPreviousWindowIndices) { + @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled, + int... expectedPreviousWindowIndices) { for (int i = 0; i < timeline.getWindowCount(); i++) { assertEquals(expectedPreviousWindowIndices[i], - timeline.getPreviousWindowIndex(i, repeatMode, false)); + timeline.getPreviousWindowIndex(i, repeatMode, shuffleModeEnabled)); } } /** - * Asserts that next window indices for each window are set correctly depending on the repeat - * mode. + * Asserts that next window indices for each window depending on the repeat mode and the + * shuffle mode are equal to the given sequence. */ public static void assertNextWindowIndices(Timeline timeline, @Player.RepeatMode int repeatMode, - int... expectedNextWindowIndices) { + boolean shuffleModeEnabled, int... expectedNextWindowIndices) { for (int i = 0; i < timeline.getWindowCount(); i++) { assertEquals(expectedNextWindowIndices[i], - timeline.getNextWindowIndex(i, repeatMode, false)); + timeline.getNextWindowIndex(i, repeatMode, shuffleModeEnabled)); } } @@ -118,34 +119,20 @@ public final class TimelineAsserts { expectedWindowIndex++; } assertEquals(expectedWindowIndex, period.windowIndex); - if (i < accumulatedPeriodCounts[expectedWindowIndex + 1] - 1) { - assertEquals(i + 1, timeline.getNextPeriodIndex(i, period, window, Player.REPEAT_MODE_OFF, - false)); - assertEquals(i + 1, timeline.getNextPeriodIndex(i, period, window, Player.REPEAT_MODE_ONE, - false)); - assertEquals(i + 1, timeline.getNextPeriodIndex(i, period, window, Player.REPEAT_MODE_ALL, - false)); - } else { - int nextWindowOff = timeline.getNextWindowIndex(expectedWindowIndex, - Player.REPEAT_MODE_OFF, false); - int nextWindowOne = timeline.getNextWindowIndex(expectedWindowIndex, - Player.REPEAT_MODE_ONE, false); - int nextWindowAll = timeline.getNextWindowIndex(expectedWindowIndex, - Player.REPEAT_MODE_ALL, false); - int nextPeriodOff = nextWindowOff == C.INDEX_UNSET ? C.INDEX_UNSET - : accumulatedPeriodCounts[nextWindowOff]; - int nextPeriodOne = nextWindowOne == C.INDEX_UNSET ? C.INDEX_UNSET - : accumulatedPeriodCounts[nextWindowOne]; - int nextPeriodAll = nextWindowAll == C.INDEX_UNSET ? C.INDEX_UNSET - : accumulatedPeriodCounts[nextWindowAll]; - assertEquals(nextPeriodOff, timeline.getNextPeriodIndex(i, period, window, - Player.REPEAT_MODE_OFF, false)); - assertEquals(nextPeriodOne, timeline.getNextPeriodIndex(i, period, window, - Player.REPEAT_MODE_ONE, false)); - assertEquals(nextPeriodAll, timeline.getNextPeriodIndex(i, period, window, - Player.REPEAT_MODE_ALL, false)); + for (@Player.RepeatMode int repeatMode + : new int[] { Player.REPEAT_MODE_OFF, Player.REPEAT_MODE_ONE, Player.REPEAT_MODE_ALL }) { + if (i < accumulatedPeriodCounts[expectedWindowIndex + 1] - 1) { + assertEquals(i + 1, timeline.getNextPeriodIndex(i, period, window, repeatMode, false)); + } else { + int nextWindow = timeline.getNextWindowIndex(expectedWindowIndex, repeatMode, false); + int nextPeriod = nextWindow == C.INDEX_UNSET ? C.INDEX_UNSET + : accumulatedPeriodCounts[nextWindow]; + assertEquals(nextPeriod, timeline.getNextPeriodIndex(i, period, window, repeatMode, + false)); + } } } } } + From bd81181892879b3ca0a573b669cfbce14015cd24 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 23 Aug 2017 03:29:21 -0700 Subject: [PATCH 0295/2472] Add shortcut methods to query next or previous window index. This functionality is most likely needed by UI modules which currently need to obtain the timeline, the current repeat and shuffle modes and are only then able to query the next/previous window index using this information. Adding these methods simplifies these cumbersome requests. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166181202 --- .../android/exoplayer2/ext/cast/CastPlayer.java | 10 ++++++++++ .../ext/mediasession/TimelineQueueNavigator.java | 7 +++---- .../DynamicConcatenatingMediaSourceTest.java | 10 ++++++++++ .../google/android/exoplayer2/ExoPlayerImpl.java | 12 ++++++++++++ .../java/com/google/android/exoplayer2/Player.java | 14 ++++++++++++++ .../google/android/exoplayer2/SimpleExoPlayer.java | 10 ++++++++++ .../android/exoplayer2/ui/PlaybackControlView.java | 13 +++++-------- .../exoplayer2/testutil/FakeSimpleExoPlayer.java | 10 ++++++++++ 8 files changed, 74 insertions(+), 12 deletions(-) diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java index 50ae7ea5ba..e79fef74d5 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java @@ -371,6 +371,16 @@ public final class CastPlayer implements Player { return 0; } + @Override + public int getNextWindowIndex() { + return C.INDEX_UNSET; + } + + @Override + public int getPreviousWindowIndex() { + return C.INDEX_UNSET; + } + @Override public long getDuration() { return currentTimeline.isEmpty() ? C.TIME_UNSET diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java index bd3f3f2820..9d7ed75c83 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java @@ -126,8 +126,7 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu if (timeline.isEmpty()) { return; } - int previousWindowIndex = timeline.getPreviousWindowIndex(player.getCurrentWindowIndex(), - player.getRepeatMode(), false); + int previousWindowIndex = player.getPreviousWindowIndex(); if (player.getCurrentPosition() > MAX_POSITION_FOR_SEEK_TO_PREVIOUS || previousWindowIndex == C.INDEX_UNSET) { player.seekTo(0); @@ -154,8 +153,7 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu if (timeline.isEmpty()) { return; } - int nextWindowIndex = timeline.getNextWindowIndex(player.getCurrentWindowIndex(), - player.getRepeatMode(), false); + int nextWindowIndex = player.getNextWindowIndex(); if (nextWindowIndex != C.INDEX_UNSET) { player.seekTo(nextWindowIndex, C.TIME_UNSET); } @@ -186,3 +184,4 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu } } + diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java index 35233febf5..9cdb461d7b 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java @@ -502,6 +502,16 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { throw new UnsupportedOperationException(); } + @Override + public int getNextWindowIndex() { + throw new UnsupportedOperationException(); + } + + @Override + public int getPreviousWindowIndex() { + throw new UnsupportedOperationException(); + } + @Override public long getDuration() { throw new UnsupportedOperationException(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index 0ce920a16f..7bf0cd5a02 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -317,6 +317,18 @@ import java.util.concurrent.CopyOnWriteArraySet; } } + @Override + public int getNextWindowIndex() { + return timeline.getNextWindowIndex(getCurrentWindowIndex(), getRepeatMode(), + getShuffleModeEnabled()); + } + + @Override + public int getPreviousWindowIndex() { + return timeline.getPreviousWindowIndex(getCurrentWindowIndex(), getRepeatMode(), + getShuffleModeEnabled()); + } + @Override public long getDuration() { if (timeline.isEmpty()) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Player.java b/library/core/src/main/java/com/google/android/exoplayer2/Player.java index 6eee930018..ae2785f6f8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Player.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Player.java @@ -363,6 +363,20 @@ public interface Player { */ int getCurrentWindowIndex(); + /** + * Returns the index of the next timeline window to be played, which may depend on the current + * repeat mode and whether shuffle mode is enabled. Returns {@link C#INDEX_UNSET} if the window + * currently being played is the last window. + */ + int getNextWindowIndex(); + + /** + * Returns the index of the previous timeline window to be played, which may depend on the current + * repeat mode and whether shuffle mode is enabled. Returns {@link C#INDEX_UNSET} if the window + * currently being played is the first window. + */ + int getPreviousWindowIndex(); + /** * Returns the duration of the current window in milliseconds, or {@link C#TIME_UNSET} if the * duration is not known. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 9fcc4d2128..1c35adb917 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -754,6 +754,16 @@ public class SimpleExoPlayer implements ExoPlayer { return player.getCurrentWindowIndex(); } + @Override + public int getNextWindowIndex() { + return player.getNextWindowIndex(); + } + + @Override + public int getPreviousWindowIndex() { + return player.getPreviousWindowIndex(); + } + @Override public long getDuration() { return player.getDuration(); diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java index acb6e3e7cd..105dbc2495 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java @@ -675,11 +675,8 @@ public class PlaybackControlView extends FrameLayout { timeline.getWindow(windowIndex, window); isSeekable = window.isSeekable; enablePrevious = isSeekable || !window.isDynamic - || timeline.getPreviousWindowIndex(windowIndex, player.getRepeatMode(), false) - != C.INDEX_UNSET; - enableNext = window.isDynamic - || timeline.getNextWindowIndex(windowIndex, player.getRepeatMode(), false) - != C.INDEX_UNSET; + || player.getPreviousWindowIndex() != C.INDEX_UNSET; + enableNext = window.isDynamic || player.getNextWindowIndex() != C.INDEX_UNSET; if (player.isPlayingAd()) { // Always hide player controls during ads. hide(); @@ -863,8 +860,7 @@ public class PlaybackControlView extends FrameLayout { } int windowIndex = player.getCurrentWindowIndex(); timeline.getWindow(windowIndex, window); - int previousWindowIndex = timeline.getPreviousWindowIndex(windowIndex, player.getRepeatMode(), - false); + int previousWindowIndex = player.getPreviousWindowIndex(); if (previousWindowIndex != C.INDEX_UNSET && (player.getCurrentPosition() <= MAX_POSITION_FOR_SEEK_TO_PREVIOUS || (window.isDynamic && !window.isSeekable))) { @@ -880,7 +876,7 @@ public class PlaybackControlView extends FrameLayout { return; } int windowIndex = player.getCurrentWindowIndex(); - int nextWindowIndex = timeline.getNextWindowIndex(windowIndex, player.getRepeatMode(), false); + int nextWindowIndex = player.getNextWindowIndex(); if (nextWindowIndex != C.INDEX_UNSET) { seekTo(nextWindowIndex, C.TIME_UNSET); } else if (timeline.getWindow(windowIndex, window, false).isDynamic) { @@ -1146,3 +1142,4 @@ public class PlaybackControlView extends FrameLayout { } } + diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java index 7edaa6b13e..67a83b84e1 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java @@ -250,6 +250,16 @@ public class FakeSimpleExoPlayer extends SimpleExoPlayer { return 0; } + @Override + public int getNextWindowIndex() { + return C.INDEX_UNSET; + } + + @Override + public int getPreviousWindowIndex() { + return C.INDEX_UNSET; + } + @Override public long getDuration() { return C.usToMs(durationUs); From 4883a9ba9a44479d02d4c280648a213bdb741e47 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 23 Aug 2017 05:59:32 -0700 Subject: [PATCH 0296/2472] Add shuffle support to infinitely looping timeline. In addition, let unit test assert window indices for both shuffle modes. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166191069 --- .../source/LoopingMediaSourceTest.java | 64 +++++++++++-------- .../exoplayer2/source/LoopingMediaSource.java | 5 +- 2 files changed, 41 insertions(+), 28 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java index c32f5cb624..52c313ed47 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java @@ -42,44 +42,55 @@ public class LoopingMediaSourceTest extends TestCase { Timeline timeline = getLoopingTimeline(multiWindowTimeline, 1); TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, - C.INDEX_UNSET, 0, 1); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 2, 0, 1); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, - 1, 2, C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 1, 2, 0); + for (boolean shuffled : new boolean[] { false, true }) { + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled, + C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, + 0, 1, 2); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, + 2, 0, 1); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled, + 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, 0, 1, 2); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, 1, 2, 0); + } } public void testMultiLoop() { Timeline timeline = getLoopingTimeline(multiWindowTimeline, 3); TimelineAsserts.assertWindowIds(timeline, 111, 222, 333, 111, 222, 333, 111, 222, 333); TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1, 1, 1, 1, 1, 1, 1); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, - C.INDEX_UNSET, 0, 1, 2, 3, 4, 5, 6, 7, 8); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, - 0, 1, 2, 3, 4, 5, 6, 7, 8); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, - 8, 0, 1, 2, 3, 4, 5, 6, 7); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, - 1, 2, 3, 4, 5, 6, 7, 8, C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, - 0, 1, 2, 3, 4, 5, 6, 7, 8); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, - 1, 2, 3, 4, 5, 6, 7, 8, 0); + for (boolean shuffled : new boolean[] { false, true }) { + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled, + C.INDEX_UNSET, 0, 1, 2, 3, 4, 5, 6, 7, 8); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, + 0, 1, 2, 3, 4, 5, 6, 7, 8); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, + 8, 0, 1, 2, 3, 4, 5, 6, 7); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled, + 1, 2, 3, 4, 5, 6, 7, 8, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, + 0, 1, 2, 3, 4, 5, 6, 7, 8); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, + 1, 2, 3, 4, 5, 6, 7, 8, 0); + } } public void testInfiniteLoop() { Timeline timeline = getLoopingTimeline(multiWindowTimeline, Integer.MAX_VALUE); TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, 2, 0, 1); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 2, 0, 1); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, 1, 2, 0); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 1, 2, 0); + for (boolean shuffled : new boolean[] { false, true }) { + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled, + 2, 0, 1); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, + 0, 1, 2); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, + 2, 0, 1); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled, 1, 2, 0); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, 0, 1, 2); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, 1, 2, 0); + } } /** @@ -93,3 +104,4 @@ public class LoopingMediaSourceTest extends TestCase { } } + diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java index 1c9c181914..28380e50b2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java @@ -171,7 +171,8 @@ public final class LoopingMediaSource implements MediaSource { boolean shuffleModeEnabled) { int childNextWindowIndex = timeline.getNextWindowIndex(windowIndex, repeatMode, shuffleModeEnabled); - return childNextWindowIndex == C.INDEX_UNSET ? 0 : childNextWindowIndex; + return childNextWindowIndex == C.INDEX_UNSET ? getFirstWindowIndex(shuffleModeEnabled) + : childNextWindowIndex; } @Override @@ -179,7 +180,7 @@ public final class LoopingMediaSource implements MediaSource { boolean shuffleModeEnabled) { int childPreviousWindowIndex = timeline.getPreviousWindowIndex(windowIndex, repeatMode, shuffleModeEnabled); - return childPreviousWindowIndex == C.INDEX_UNSET ? getWindowCount() - 1 + return childPreviousWindowIndex == C.INDEX_UNSET ? getLastWindowIndex(shuffleModeEnabled) : childPreviousWindowIndex; } From 9fd19d0e7c20818269c488e8fdd4d17d9528e5e0 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 23 Aug 2017 06:00:51 -0700 Subject: [PATCH 0297/2472] Add shuffle logic to concatenated timelines. The implementation in the abstract base class takes care to forward the queries to the correct methods given the shuffle mode and a given shuffle order. All concatenated timeline implementations use an unshuffled order so far. The handling of the shuffle orders will follow in other changes. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166191165 --- .../source/AbstractConcatenatedTimeline.java | 63 +++++++++++++------ .../source/ConcatenatingMediaSource.java | 3 +- .../DynamicConcatenatingMediaSource.java | 3 +- .../exoplayer2/source/LoopingMediaSource.java | 3 +- 4 files changed, 50 insertions(+), 22 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java b/library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java index 9c2be39576..07813ff046 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java @@ -26,9 +26,17 @@ import com.google.android.exoplayer2.Timeline; /* package */ abstract class AbstractConcatenatedTimeline extends Timeline { private final int childCount; + private final ShuffleOrder shuffleOrder; - public AbstractConcatenatedTimeline(int childCount) { - this.childCount = childCount; + /** + * Sets up a concatenated timeline with a shuffle order of child timelines. + * + * @param shuffleOrder A shuffle order of child timelines. The number of child timelines must + * match the number of elements in the shuffle order. + */ + public AbstractConcatenatedTimeline(ShuffleOrder shuffleOrder) { + this.shuffleOrder = shuffleOrder; + this.childCount = shuffleOrder.getLength(); } @Override @@ -42,16 +50,17 @@ import com.google.android.exoplayer2.Timeline; shuffleModeEnabled); if (nextWindowIndexInChild != C.INDEX_UNSET) { return firstWindowIndexInChild + nextWindowIndexInChild; - } else { - int nextChildIndex = childIndex + 1; - if (nextChildIndex < childCount) { - return getFirstWindowIndexByChildIndex(nextChildIndex); - } else if (repeatMode == Player.REPEAT_MODE_ALL) { - return 0; - } else { - return C.INDEX_UNSET; - } } + int nextChildIndex = shuffleModeEnabled ? shuffleOrder.getNextIndex(childIndex) + : childIndex + 1; + if (nextChildIndex != C.INDEX_UNSET && nextChildIndex < childCount) { + return getFirstWindowIndexByChildIndex(nextChildIndex) + + getTimelineByChildIndex(nextChildIndex).getFirstWindowIndex(shuffleModeEnabled); + } + if (repeatMode == Player.REPEAT_MODE_ALL) { + return getFirstWindowIndex(shuffleModeEnabled); + } + return C.INDEX_UNSET; } @Override @@ -65,15 +74,31 @@ import com.google.android.exoplayer2.Timeline; shuffleModeEnabled); if (previousWindowIndexInChild != C.INDEX_UNSET) { return firstWindowIndexInChild + previousWindowIndexInChild; - } else { - if (firstWindowIndexInChild > 0) { - return firstWindowIndexInChild - 1; - } else if (repeatMode == Player.REPEAT_MODE_ALL) { - return getWindowCount() - 1; - } else { - return C.INDEX_UNSET; - } } + int previousChildIndex = shuffleModeEnabled ? shuffleOrder.getPreviousIndex(childIndex) + : childIndex - 1; + if (previousChildIndex != C.INDEX_UNSET && previousChildIndex >= 0) { + return getFirstWindowIndexByChildIndex(previousChildIndex) + + getTimelineByChildIndex(previousChildIndex).getLastWindowIndex(shuffleModeEnabled); + } + if (repeatMode == Player.REPEAT_MODE_ALL) { + return getLastWindowIndex(shuffleModeEnabled); + } + return C.INDEX_UNSET; + } + + @Override + public int getLastWindowIndex(boolean shuffleModeEnabled) { + int lastChildIndex = shuffleModeEnabled ? shuffleOrder.getLastIndex() : childCount - 1; + return getFirstWindowIndexByChildIndex(lastChildIndex) + + getTimelineByChildIndex(lastChildIndex).getLastWindowIndex(shuffleModeEnabled); + } + + @Override + public int getFirstWindowIndex(boolean shuffleModeEnabled) { + int firstChildIndex = shuffleModeEnabled ? shuffleOrder.getFirstIndex() : 0; + return getFirstWindowIndexByChildIndex(firstChildIndex) + + getTimelineByChildIndex(firstChildIndex).getFirstWindowIndex(shuffleModeEnabled); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java index 0f6f1b345d..f12e1c006f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java @@ -19,6 +19,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.ShuffleOrder.UnshuffledShuffleOrder; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; @@ -167,7 +168,7 @@ public final class ConcatenatingMediaSource implements MediaSource { private final boolean isRepeatOneAtomic; public ConcatenatedTimeline(Timeline[] timelines, boolean isRepeatOneAtomic) { - super(timelines.length); + super(new UnshuffledShuffleOrder(timelines.length)); int[] sourcePeriodOffsets = new int[timelines.length]; int[] sourceWindowOffsets = new int[timelines.length]; long periodCount = 0; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java index c2d2e5f11e..02d4bad2bf 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java @@ -23,6 +23,7 @@ import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayer.ExoPlayerComponent; import com.google.android.exoplayer2.ExoPlayer.ExoPlayerMessage; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.ShuffleOrder.UnshuffledShuffleOrder; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.util.Assertions; @@ -397,7 +398,7 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl public ConcatenatedTimeline(Collection mediaSourceHolders, int windowCount, int periodCount) { - super(mediaSourceHolders.size()); + super(new UnshuffledShuffleOrder(mediaSourceHolders.size())); this.windowCount = windowCount; this.periodCount = periodCount; int childCount = mediaSourceHolders.size(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java index 28380e50b2..00e3c50506 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java @@ -19,6 +19,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.ShuffleOrder.UnshuffledShuffleOrder; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; @@ -101,7 +102,7 @@ public final class LoopingMediaSource implements MediaSource { private final int loopCount; public LoopingTimeline(Timeline childTimeline, int loopCount) { - super(loopCount); + super(new UnshuffledShuffleOrder(loopCount)); this.childTimeline = childTimeline; childPeriodCount = childTimeline.getPeriodCount(); childWindowCount = childTimeline.getWindowCount(); From b27c25e88072007b39a7a3c283eed50c2d0efdcd Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 23 Aug 2017 06:42:24 -0700 Subject: [PATCH 0298/2472] Add fake shuffle order. This just implements a reverse order which is different from the original order but still deterministic for simplified testing. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166194311 --- .../exoplayer2/testutil/FakeShuffleOrder.java | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeShuffleOrder.java diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeShuffleOrder.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeShuffleOrder.java new file mode 100644 index 0000000000..0664f47023 --- /dev/null +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeShuffleOrder.java @@ -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. + */ +package com.google.android.exoplayer2.testutil; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.source.ShuffleOrder; + +/** + * Fake {@link ShuffleOrder} which returns a reverse order. This order is thus deterministic but + * different from the original order. + */ +public final class FakeShuffleOrder implements ShuffleOrder { + + private final int length; + + public FakeShuffleOrder(int length) { + this.length = length; + } + + @Override + public int getLength() { + return length; + } + + @Override + public int getNextIndex(int index) { + return index > 0 ? index - 1 : C.INDEX_UNSET; + } + + @Override + public int getPreviousIndex(int index) { + return index < length - 1 ? index + 1 : C.INDEX_UNSET; + } + + @Override + public int getLastIndex() { + return length > 0 ? 0 : C.INDEX_UNSET; + } + + @Override + public int getFirstIndex() { + return length > 0 ? length - 1 : C.INDEX_UNSET; + } + + @Override + public ShuffleOrder cloneAndInsert(int insertionIndex, int insertionCount) { + return new FakeShuffleOrder(length + insertionCount); + } + + @Override + public ShuffleOrder cloneAndRemove(int removalIndex) { + return new FakeShuffleOrder(length - 1); + } + +} From 8115e11489cff49dec9cd15c208bc65fc2dfea41 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 23 Aug 2017 06:53:11 -0700 Subject: [PATCH 0299/2472] Allow subclasses to customize the MediaFormat Make getMediaFormat protected so that subclasses can set additional MediaFormat keys. For example, if the decoder output needs to be read back via an ImageReader as YUV data it is necessary to set KEY_COLOR_FORMAT to COLOR_FormatYUV420Flexible. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166195211 --- .../video/MediaCodecVideoRenderer.java | 56 +++++++++++-------- 1 file changed, 34 insertions(+), 22 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index 8fe3476351..c11c415cd7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -737,28 +737,6 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { return earlyUs < -30000; } - @SuppressLint("InlinedApi") - private static MediaFormat getMediaFormat(Format format, CodecMaxValues codecMaxValues, - boolean deviceNeedsAutoFrcWorkaround, int tunnelingAudioSessionId) { - MediaFormat frameworkMediaFormat = format.getFrameworkMediaFormatV16(); - // Set the maximum adaptive video dimensions. - frameworkMediaFormat.setInteger(MediaFormat.KEY_MAX_WIDTH, codecMaxValues.width); - frameworkMediaFormat.setInteger(MediaFormat.KEY_MAX_HEIGHT, codecMaxValues.height); - // Set the maximum input size. - if (codecMaxValues.inputSize != Format.NO_VALUE) { - frameworkMediaFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, codecMaxValues.inputSize); - } - // Set FRC workaround. - if (deviceNeedsAutoFrcWorkaround) { - frameworkMediaFormat.setInteger("auto-frc", 0); - } - // Configure tunneling if enabled. - if (tunnelingAudioSessionId != C.AUDIO_SESSION_ID_UNSET) { - configureTunnelingV21(frameworkMediaFormat, tunnelingAudioSessionId); - } - return frameworkMediaFormat; - } - @TargetApi(23) private static void setOutputSurfaceV23(MediaCodec codec, Surface surface) { codec.setOutputSurface(surface); @@ -814,6 +792,40 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { return new CodecMaxValues(maxWidth, maxHeight, maxInputSize); } + /** + * Returns the framework {@link MediaFormat} that should be used to configure the decoder when + * playing media in the specified input format. + * + * @param format The format of input media. + * @param codecMaxValues The codec's maximum supported values. + * @param deviceNeedsAutoFrcWorkaround Whether the device is known to enable frame-rate conversion + * logic that negatively impacts ExoPlayer. + * @param tunnelingAudioSessionId The audio session id to use for tunneling, or + * {@link C#AUDIO_SESSION_ID_UNSET} if tunneling should not be enabled. + * @return The framework {@link MediaFormat} that should be used to configure the decoder. + */ + @SuppressLint("InlinedApi") + protected MediaFormat getMediaFormat(Format format, CodecMaxValues codecMaxValues, + boolean deviceNeedsAutoFrcWorkaround, int tunnelingAudioSessionId) { + MediaFormat frameworkMediaFormat = format.getFrameworkMediaFormatV16(); + // Set the maximum adaptive video dimensions. + frameworkMediaFormat.setInteger(MediaFormat.KEY_MAX_WIDTH, codecMaxValues.width); + frameworkMediaFormat.setInteger(MediaFormat.KEY_MAX_HEIGHT, codecMaxValues.height); + // Set the maximum input size. + if (codecMaxValues.inputSize != Format.NO_VALUE) { + frameworkMediaFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, codecMaxValues.inputSize); + } + // Set FRC workaround. + if (deviceNeedsAutoFrcWorkaround) { + frameworkMediaFormat.setInteger("auto-frc", 0); + } + // Configure tunneling if enabled. + if (tunnelingAudioSessionId != C.AUDIO_SESSION_ID_UNSET) { + configureTunnelingV21(frameworkMediaFormat, tunnelingAudioSessionId); + } + return frameworkMediaFormat; + } + /** * Returns a maximum video size to use when configuring a codec for {@code format} in a way * that will allow possible adaptation to other compatible formats that are expected to have the From e15633e9060d409996ff0f98e4bf274d052e821f Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 23 Aug 2017 07:15:41 -0700 Subject: [PATCH 0300/2472] Add shuffle support to ConcatenatingMediaSource. The media source is initialized with a DefaultShuffleOrder which can be changed at any time. This shuffle order is then used within the corresponding timeline. The isRepeatOneAtomic flag is extended to also suppress shuffling (now called isAtomic only). ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166197184 --- .../source/ConcatenatingMediaSourceTest.java | 86 +++++++++++++------ .../source/ConcatenatingMediaSource.java | 58 +++++++++---- 2 files changed, 103 insertions(+), 41 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java index 34f44b47f7..fd0acf2ab3 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java @@ -19,6 +19,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.testutil.FakeMediaSource; +import com.google.android.exoplayer2.testutil.FakeShuffleOrder; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.testutil.TestUtil; @@ -34,26 +35,30 @@ public final class ConcatenatingMediaSourceTest extends TestCase { Timeline timeline = getConcatenatedTimeline(false, createFakeTimeline(3, 111)); TimelineAsserts.assertWindowIds(timeline, 111); TimelineAsserts.assertPeriodCounts(timeline, 3); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, - C.INDEX_UNSET); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 0); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, - C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 0); + for (boolean shuffled : new boolean[] { false, true }) { + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled, + C.INDEX_UNSET); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, 0); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, 0); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled, + C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, 0); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, 0); + } timeline = getConcatenatedTimeline(true, createFakeTimeline(3, 111)); TimelineAsserts.assertWindowIds(timeline, 111); TimelineAsserts.assertPeriodCounts(timeline, 3); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, - C.INDEX_UNSET); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 0); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, - C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 0); + for (boolean shuffled : new boolean[] { false, true }) { + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled, + C.INDEX_UNSET); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, 0); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, 0); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled, + C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, 0); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, 0); + } } public void testMultipleMediaSources() { @@ -70,18 +75,36 @@ public final class ConcatenatingMediaSourceTest extends TestCase { 1, 2, C.INDEX_UNSET); TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2); TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 1, 2, 0); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, true, + 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 2); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 1, 2, 0); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, true, + C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 2); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 2, 0, 1); + assertEquals(0, timeline.getFirstWindowIndex(false)); + assertEquals(2, timeline.getLastWindowIndex(false)); + assertEquals(2, timeline.getFirstWindowIndex(true)); + assertEquals(0, timeline.getLastWindowIndex(true)); timeline = getConcatenatedTimeline(true, timelines); TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); TimelineAsserts.assertPeriodCounts(timeline, 3, 1, 3); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, - C.INDEX_UNSET, 0, 1); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 2, 0, 1); - TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 2, 0, 1); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, - 1, 2, C.INDEX_UNSET); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 1, 2, 0); - TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 1, 2, 0); + for (boolean shuffled : new boolean[] { false, true }) { + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled, + C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, + 2, 0, 1); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, + 2, 0, 1); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled, + 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, 1, 2, 0); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, 1, 2, 0); + assertEquals(0, timeline.getFirstWindowIndex(shuffled)); + assertEquals(2, timeline.getLastWindowIndex(shuffled)); + } } public void testNestedMediaSources() { @@ -100,6 +123,16 @@ public final class ConcatenatingMediaSourceTest extends TestCase { 1, 2, 3, C.INDEX_UNSET); TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 3, 2); TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 1, 2, 3, 0); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, true, + 1, 3, C.INDEX_UNSET, 2); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, + 0, 1, 3, 2); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, + 1, 3, 0, 2); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, true, + C.INDEX_UNSET, 0, 3, 1); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 3, 2); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 2, 0, 3, 1); } /** @@ -112,8 +145,9 @@ public final class ConcatenatingMediaSourceTest extends TestCase { for (int i = 0; i < timelines.length; i++) { mediaSources[i] = new FakeMediaSource(timelines[i], null); } - return TestUtil.extractTimelineFromMediaSource( - new ConcatenatingMediaSource(isRepeatOneAtomic, mediaSources)); + ConcatenatingMediaSource mediaSource = new ConcatenatingMediaSource(isRepeatOneAtomic, + new FakeShuffleOrder(mediaSources.length), mediaSources); + return TestUtil.extractTimelineFromMediaSource(mediaSource); } private static FakeTimeline createFakeTimeline(int periodCount, int windowId) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java index f12e1c006f..0c7bcece68 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java @@ -19,7 +19,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.source.ShuffleOrder.UnshuffledShuffleOrder; +import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; @@ -39,10 +39,11 @@ public final class ConcatenatingMediaSource implements MediaSource { private final Object[] manifests; private final Map sourceIndexByMediaPeriod; private final boolean[] duplicateFlags; - private final boolean isRepeatOneAtomic; + private final boolean isAtomic; private Listener listener; private ConcatenatedTimeline timeline; + private ShuffleOrder shuffleOrder; /** * @param mediaSources The {@link MediaSource}s to concatenate. It is valid for the same @@ -53,17 +54,33 @@ public final class ConcatenatingMediaSource implements MediaSource { } /** - * @param isRepeatOneAtomic Whether the concatenated media source shall be treated as atomic - * (i.e., repeated in its entirety) when repeat mode is set to {@code Player.REPEAT_MODE_ONE}. + * @param isAtomic Whether the concatenated media source shall be treated as atomic, + * i.e., treated as a single item for repeating and shuffling. * @param mediaSources The {@link MediaSource}s to concatenate. It is valid for the same * {@link MediaSource} instance to be present more than once in the array. */ - public ConcatenatingMediaSource(boolean isRepeatOneAtomic, MediaSource... mediaSources) { + public ConcatenatingMediaSource(boolean isAtomic, MediaSource... mediaSources) { + this(isAtomic, new DefaultShuffleOrder(mediaSources.length), mediaSources); + } + + /** + * @param isAtomic Whether the concatenated media source shall be treated as atomic, + * i.e., treated as a single item for repeating and shuffling. + * @param shuffleOrder The {@link ShuffleOrder} to use when shuffling the child media sources. The + * number of elements in the shuffle order must match the number of concatenated + * {@link MediaSource}s. + * @param mediaSources The {@link MediaSource}s to concatenate. It is valid for the same + * {@link MediaSource} instance to be present more than once in the array. + */ + public ConcatenatingMediaSource(boolean isAtomic, ShuffleOrder shuffleOrder, + MediaSource... mediaSources) { for (MediaSource mediaSource : mediaSources) { Assertions.checkNotNull(mediaSource); } + Assertions.checkArgument(shuffleOrder.getLength() == mediaSources.length); this.mediaSources = mediaSources; - this.isRepeatOneAtomic = isRepeatOneAtomic; + this.isAtomic = isAtomic; + this.shuffleOrder = shuffleOrder; timelines = new Timeline[mediaSources.length]; manifests = new Object[mediaSources.length]; sourceIndexByMediaPeriod = new HashMap<>(); @@ -139,7 +156,7 @@ public final class ConcatenatingMediaSource implements MediaSource { return; } } - timeline = new ConcatenatedTimeline(timelines.clone(), isRepeatOneAtomic); + timeline = new ConcatenatedTimeline(timelines.clone(), isAtomic, shuffleOrder); listener.onSourceInfoRefreshed(timeline, manifests.clone()); } @@ -165,10 +182,10 @@ public final class ConcatenatingMediaSource implements MediaSource { private final Timeline[] timelines; private final int[] sourcePeriodOffsets; private final int[] sourceWindowOffsets; - private final boolean isRepeatOneAtomic; + private final boolean isAtomic; - public ConcatenatedTimeline(Timeline[] timelines, boolean isRepeatOneAtomic) { - super(new UnshuffledShuffleOrder(timelines.length)); + public ConcatenatedTimeline(Timeline[] timelines, boolean isAtomic, ShuffleOrder shuffleOrder) { + super(shuffleOrder); int[] sourcePeriodOffsets = new int[timelines.length]; int[] sourceWindowOffsets = new int[timelines.length]; long periodCount = 0; @@ -185,7 +202,7 @@ public final class ConcatenatingMediaSource implements MediaSource { this.timelines = timelines; this.sourcePeriodOffsets = sourcePeriodOffsets; this.sourceWindowOffsets = sourceWindowOffsets; - this.isRepeatOneAtomic = isRepeatOneAtomic; + this.isAtomic = isAtomic; } @Override @@ -201,19 +218,29 @@ public final class ConcatenatingMediaSource implements MediaSource { @Override public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled) { - if (isRepeatOneAtomic && repeatMode == Player.REPEAT_MODE_ONE) { + if (isAtomic && repeatMode == Player.REPEAT_MODE_ONE) { repeatMode = Player.REPEAT_MODE_ALL; } - return super.getNextWindowIndex(windowIndex, repeatMode, shuffleModeEnabled); + return super.getNextWindowIndex(windowIndex, repeatMode, !isAtomic && shuffleModeEnabled); } @Override public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled) { - if (isRepeatOneAtomic && repeatMode == Player.REPEAT_MODE_ONE) { + if (isAtomic && repeatMode == Player.REPEAT_MODE_ONE) { repeatMode = Player.REPEAT_MODE_ALL; } - return super.getPreviousWindowIndex(windowIndex, repeatMode, shuffleModeEnabled); + return super.getPreviousWindowIndex(windowIndex, repeatMode, !isAtomic && shuffleModeEnabled); + } + + @Override + public int getLastWindowIndex(boolean shuffleModeEnabled) { + return super.getLastWindowIndex(!isAtomic && shuffleModeEnabled); + } + + @Override + public int getFirstWindowIndex(boolean shuffleModeEnabled) { + return super.getFirstWindowIndex(!isAtomic && shuffleModeEnabled); } @Override @@ -257,3 +284,4 @@ public final class ConcatenatingMediaSource implements MediaSource { } } + From f7eba77ee0378a0886ad9cb9d2b9ddeda8ed2285 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 23 Aug 2017 07:30:29 -0700 Subject: [PATCH 0301/2472] Add shuffle support to dynamic concatenating media source. The media source is initialized with a DefaultShuffleOrder which can be changed at any time. Whenever the list of media source is changed, the shuffle order is adapted accordingly (either on the app thread if the player is not prepared yet, or on the player thread). The shuffle order is then used to construct the timeline. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166198488 --- .../DynamicConcatenatingMediaSourceTest.java | 27 ++++++++++++++-- .../DynamicConcatenatingMediaSource.java | 32 ++++++++++++++++--- 2 files changed, 52 insertions(+), 7 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java index 9cdb461d7b..0e07e99978 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java @@ -27,6 +27,7 @@ import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaSource.Listener; import com.google.android.exoplayer2.testutil.FakeMediaSource; +import com.google.android.exoplayer2.testutil.FakeShuffleOrder; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.testutil.TimelineAsserts; @@ -49,7 +50,8 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { public void testPlaylistChangesAfterPreparation() throws InterruptedException { timeline = null; FakeMediaSource[] childSources = createMediaSources(7); - DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); + DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource( + new FakeShuffleOrder(0)); prepareAndListenToTimelineUpdates(mediaSource); waitForTimelineUpdate(); TimelineAsserts.assertEmpty(timeline); @@ -128,6 +130,18 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { C.INDEX_UNSET, 0, 1); TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2); TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 2, 0, 1); + assertEquals(0, timeline.getFirstWindowIndex(false)); + assertEquals(timeline.getWindowCount() - 1, timeline.getLastWindowIndex(false)); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, true, + C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 2); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 2, 0, 1); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, true, + 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 2); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 1, 2, 0); + assertEquals(timeline.getWindowCount() - 1, timeline.getFirstWindowIndex(true)); + assertEquals(0, timeline.getLastWindowIndex(true)); // Remove at front of queue. mediaSource.removeMediaSource(0); @@ -153,7 +167,8 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { public void testPlaylistChangesBeforePreparation() throws InterruptedException { timeline = null; FakeMediaSource[] childSources = createMediaSources(4); - DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); + DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource( + new FakeShuffleOrder(0)); mediaSource.addMediaSource(childSources[0]); mediaSource.addMediaSource(childSources[1]); mediaSource.addMediaSource(0, childSources[2]); @@ -168,6 +183,14 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { assertNotNull(timeline); TimelineAsserts.assertPeriodCounts(timeline, 3, 4, 2); TimelineAsserts.assertWindowIds(timeline, 333, 444, 222); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, + 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, + C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, true, + C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, true, + 1, 2, C.INDEX_UNSET); mediaSource.releaseSource(); for (int i = 1; i < 4; i++) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java index 02d4bad2bf..3d0df7dcb3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java @@ -23,7 +23,7 @@ import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayer.ExoPlayerComponent; import com.google.android.exoplayer2.ExoPlayer.ExoPlayerMessage; import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.source.ShuffleOrder.UnshuffledShuffleOrder; +import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.util.Assertions; @@ -58,11 +58,26 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl private ExoPlayer player; private Listener listener; + private ShuffleOrder shuffleOrder; private boolean preventListenerNotification; private int windowCount; private int periodCount; + /** + * Creates a new dynamic concatenating media source. + */ public DynamicConcatenatingMediaSource() { + this(new DefaultShuffleOrder(0)); + } + + /** + * Creates a new dynamic concatenating media source with a custom shuffle order. + * + * @param shuffleOrder The {@link ShuffleOrder} to use when shuffling the child media sources. + * This shuffle order must be empty. + */ + public DynamicConcatenatingMediaSource(ShuffleOrder shuffleOrder) { + this.shuffleOrder = shuffleOrder; this.mediaSourceByMediaPeriod = new IdentityHashMap<>(); this.mediaSourcesPublic = new ArrayList<>(); this.mediaSourceHolders = new ArrayList<>(); @@ -180,6 +195,7 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl this.player = player; this.listener = listener; preventListenerNotification = true; + shuffleOrder = shuffleOrder.cloneAndInsert(0, mediaSourcesPublic.size()); addMediaSourcesInternal(0, mediaSourcesPublic); preventListenerNotification = false; maybeNotifyListener(); @@ -234,21 +250,26 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl switch (messageType) { case MSG_ADD: { Pair messageData = (Pair) message; + shuffleOrder = shuffleOrder.cloneAndInsert(messageData.first, 1); addMediaSourceInternal(messageData.first, messageData.second); break; } case MSG_ADD_MULTIPLE: { Pair> messageData = (Pair>) message; + shuffleOrder = shuffleOrder.cloneAndInsert(messageData.first, messageData.second.size()); addMediaSourcesInternal(messageData.first, messageData.second); break; } case MSG_REMOVE: { + shuffleOrder = shuffleOrder.cloneAndRemove((Integer) message); removeMediaSourceInternal((Integer) message); break; } case MSG_MOVE: { Pair messageData = (Pair) message; + shuffleOrder = shuffleOrder.cloneAndRemove(messageData.first); + shuffleOrder = shuffleOrder.cloneAndInsert(messageData.second, 1); moveMediaSourceInternal(messageData.first, messageData.second); break; } @@ -262,8 +283,8 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl private void maybeNotifyListener() { if (!preventListenerNotification) { - listener.onSourceInfoRefreshed( - new ConcatenatedTimeline(mediaSourceHolders, windowCount, periodCount), null); + listener.onSourceInfoRefreshed(new ConcatenatedTimeline(mediaSourceHolders, windowCount, + periodCount, shuffleOrder), null); } } @@ -397,8 +418,8 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl private final SparseIntArray childIndexByUid; public ConcatenatedTimeline(Collection mediaSourceHolders, int windowCount, - int periodCount) { - super(new UnshuffledShuffleOrder(mediaSourceHolders.size())); + int periodCount, ShuffleOrder shuffleOrder) { + super(shuffleOrder); this.windowCount = windowCount; this.periodCount = periodCount; int childCount = mediaSourceHolders.size(); @@ -638,3 +659,4 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl } } + From 1305b1155b73cc106c616bebd59ab8592b47acc2 Mon Sep 17 00:00:00 2001 From: bachinger Date: Wed, 23 Aug 2017 07:32:53 -0700 Subject: [PATCH 0302/2472] Migrate MediaSessionConnector to API 26 for shuffle mode. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166198698 --- .../ext/mediasession/MediaSessionConnector.java | 10 +++++----- .../ext/mediasession/TimelineQueueNavigator.java | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java index 4dc1100c1e..3a4a80733d 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java @@ -232,9 +232,9 @@ public final class MediaSessionConnector { */ void onSkipToNext(Player player); /** - * See {@link MediaSessionCompat.Callback#onSetShuffleModeEnabled(boolean)}. + * See {@link MediaSessionCompat.Callback#onSetShuffleMode(int)}. */ - void onSetShuffleModeEnabled(Player player, boolean enabled); + void onSetShuffleMode(Player player, int shuffleMode); } /** @@ -803,15 +803,15 @@ public final class MediaSessionConnector { @Override public void onSetShuffleModeEnabled(boolean enabled) { if (canDispatchToQueueNavigator(PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE_ENABLED)) { - queueNavigator.onSetShuffleModeEnabled(player, enabled); + queueNavigator.onSetShuffleMode(player, enabled + ? PlaybackStateCompat.SHUFFLE_MODE_ALL : PlaybackStateCompat.SHUFFLE_MODE_NONE); } } @Override public void onSetShuffleMode(int shuffleMode) { if (canDispatchToQueueNavigator(PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE_ENABLED)) { - queueNavigator.onSetShuffleModeEnabled(player, - shuffleMode != PlaybackStateCompat.SHUFFLE_MODE_NONE); + queueNavigator.onSetShuffleMode(player, shuffleMode); } } diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java index 9d7ed75c83..8c7d3be114 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java @@ -160,8 +160,8 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu } @Override - public void onSetShuffleModeEnabled(Player player, boolean enabled) { - player.setShuffleModeEnabled(enabled); + public void onSetShuffleMode(Player player, int shuffleMode) { + player.setShuffleModeEnabled(shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_ALL); } private void publishFloatingQueueWindow(Player player) { From eeebb3968b2491539aaacd310d9ebc276faddfb7 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 23 Aug 2017 07:40:05 -0700 Subject: [PATCH 0303/2472] Implement shuffle mode logic in ExoPlayerImplInternal. This is mostly connecting the already stored shuffleMode with the timeline queries for the playback order. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166199330 --- .../android/exoplayer2/ExoPlayerTest.java | 25 +++++++++++++++ .../exoplayer2/ExoPlayerImplInternal.java | 31 ++++++++++++------- .../exoplayer2/MediaPeriodInfoSequence.java | 4 +-- .../android/exoplayer2/testutil/Action.java | 23 ++++++++++++++ .../exoplayer2/testutil/ActionSchedule.java | 10 ++++++ 5 files changed, 79 insertions(+), 14 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index bc72ebc060..9f172dc802 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2; +import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; @@ -24,6 +25,7 @@ import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner.Builder; import com.google.android.exoplayer2.testutil.FakeMediaClockRenderer; import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeRenderer; +import com.google.android.exoplayer2.testutil.FakeShuffleOrder; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import java.util.concurrent.CountDownLatch; @@ -234,4 +236,27 @@ public final class ExoPlayerTest extends TestCase { assertTrue(renderer.isEnded); } + public void testShuffleModeEnabledChanges() throws Exception { + Timeline fakeTimeline = new FakeTimeline(new TimelineWindowDefinition(true, false, 100000)); + MediaSource[] fakeMediaSources = { + new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT), + new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT), + new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT) + }; + ConcatenatingMediaSource mediaSource = new ConcatenatingMediaSource(false, + new FakeShuffleOrder(3), fakeMediaSources); + FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); + ActionSchedule actionSchedule = new ActionSchedule.Builder("testShuffleModeEnabled") + .setRepeatMode(Player.REPEAT_MODE_ALL).waitForPositionDiscontinuity() // 0 -> 1 + .setShuffleModeEnabled(true).waitForPositionDiscontinuity() // 1 -> 0 + .waitForPositionDiscontinuity().waitForPositionDiscontinuity() // 0 -> 2 -> 1 + .setShuffleModeEnabled(false).setRepeatMode(Player.REPEAT_MODE_OFF) // 1 -> 2 -> end + .build(); + ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() + .setMediaSource(mediaSource).setRenderers(renderer).setActionSchedule(actionSchedule) + .build().start().blockUntilEnded(TIMEOUT_MS); + testRunner.assertPlayedPeriodIndices(0, 1, 0, 2, 1, 2); + assertTrue(renderer.isEnded); + } + } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 7035ed637e..5d55652f61 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -488,7 +488,7 @@ import java.io.IOException; } while (true) { int nextPeriodIndex = timeline.getNextPeriodIndex(lastValidPeriodHolder.info.id.periodIndex, - period, window, repeatMode, false); + period, window, repeatMode, shuffleModeEnabled); while (lastValidPeriodHolder.next != null && !lastValidPeriodHolder.info.isLastInTimelinePeriod) { lastValidPeriodHolder = lastValidPeriodHolder.next; @@ -686,13 +686,15 @@ import java.io.IOException; Pair periodPosition = resolveSeekPosition(seekPosition); if (periodPosition == null) { + int firstPeriodIndex = timeline.isEmpty() ? 0 : timeline.getWindow( + timeline.getFirstWindowIndex(shuffleModeEnabled), window).firstPeriodIndex; // The seek position was valid for the timeline that it was performed into, but the // timeline has changed and a suitable seek position could not be resolved in the new one. - playbackInfo = new PlaybackInfo(0, 0); + playbackInfo = new PlaybackInfo(firstPeriodIndex, 0); eventHandler.obtainMessage(MSG_SEEK_ACK, 1, 0, playbackInfo).sendToTarget(); - // Set the internal position to (0,TIME_UNSET) so that a subsequent seek to (0,0) isn't - // ignored. - playbackInfo = new PlaybackInfo(0, C.TIME_UNSET); + // Set the internal position to (firstPeriodIndex,TIME_UNSET) so that a subsequent seek to + // (firstPeriodIndex,0) isn't ignored. + playbackInfo = new PlaybackInfo(firstPeriodIndex, C.TIME_UNSET); setState(Player.STATE_ENDED); // Reset, but retain the source so that it can still be used should a seek occur. resetInternal(false); @@ -1029,7 +1031,8 @@ import java.io.IOException; if (timeline.isEmpty()) { handleSourceInfoRefreshEndedPlayback(manifest); } else { - Pair defaultPosition = getPeriodPosition(0, C.TIME_UNSET); + Pair defaultPosition = getPeriodPosition( + timeline.getFirstWindowIndex(shuffleModeEnabled), C.TIME_UNSET); int periodIndex = defaultPosition.first; long startPositionUs = defaultPosition.second; MediaPeriodId periodId = mediaPeriodInfoSequence.resolvePeriodPositionForAds(periodIndex, @@ -1122,7 +1125,8 @@ import java.io.IOException; while (periodHolder.next != null) { MediaPeriodHolder previousPeriodHolder = periodHolder; periodHolder = periodHolder.next; - periodIndex = timeline.getNextPeriodIndex(periodIndex, period, window, repeatMode, false); + periodIndex = timeline.getNextPeriodIndex(periodIndex, period, window, repeatMode, + shuffleModeEnabled); if (periodIndex != C.INDEX_UNSET && periodHolder.uid.equals(timeline.getPeriod(periodIndex, period, true).uid)) { // The holder is consistent with the new timeline. Update its index and continue. @@ -1170,11 +1174,14 @@ import java.io.IOException; private void handleSourceInfoRefreshEndedPlayback(Object manifest, int processedInitialSeekCount) { - // Set the playback position to (0,0) for notifying the eventHandler. - playbackInfo = new PlaybackInfo(0, 0); + int firstPeriodIndex = timeline.isEmpty() ? 0 : timeline.getWindow( + timeline.getFirstWindowIndex(shuffleModeEnabled), window).firstPeriodIndex; + // Set the playback position to (firstPeriodIndex,0) for notifying the eventHandler. + playbackInfo = new PlaybackInfo(firstPeriodIndex, 0); notifySourceInfoRefresh(manifest, processedInitialSeekCount); - // Set the internal position to (0,TIME_UNSET) so that a subsequent seek to (0,0) isn't ignored. - playbackInfo = new PlaybackInfo(0, C.TIME_UNSET); + // Set the internal position to (firstPeriodIndex,TIME_UNSET) so that a subsequent seek to + // (firstPeriodIndex,0) isn't ignored. + playbackInfo = new PlaybackInfo(firstPeriodIndex, C.TIME_UNSET); setState(Player.STATE_ENDED); // Reset, but retain the source so that it can still be used should a seek occur. resetInternal(false); @@ -1205,7 +1212,7 @@ import java.io.IOException; int maxIterations = oldTimeline.getPeriodCount(); for (int i = 0; i < maxIterations && newPeriodIndex == C.INDEX_UNSET; i++) { oldPeriodIndex = oldTimeline.getNextPeriodIndex(oldPeriodIndex, period, window, repeatMode, - false); + shuffleModeEnabled); if (oldPeriodIndex == C.INDEX_UNSET) { // We've reached the end of the old timeline. break; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfoSequence.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfoSequence.java index d7821ed705..6fd0d48e57 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfoSequence.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfoSequence.java @@ -162,7 +162,7 @@ import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; // timeline is updated, to avoid repeatedly checking the same timeline. if (currentMediaPeriodInfo.isLastInTimelinePeriod) { int nextPeriodIndex = timeline.getNextPeriodIndex(currentMediaPeriodInfo.id.periodIndex, - period, window, repeatMode, false); + period, window, repeatMode, shuffleModeEnabled); if (nextPeriodIndex == C.INDEX_UNSET) { // We can't create a next period yet. return null; @@ -353,7 +353,7 @@ import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; private boolean isLastInTimeline(MediaPeriodId id, boolean isLastMediaPeriodInPeriod) { int windowIndex = timeline.getPeriod(id.periodIndex, period).windowIndex; return !timeline.getWindow(windowIndex, window).isDynamic - && timeline.isLastPeriod(id.periodIndex, period, window, repeatMode, false) + && timeline.isLastPeriod(id.periodIndex, period, window, repeatMode, shuffleModeEnabled) && isLastMediaPeriodInPeriod; } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java index ab1f448afd..bc16e105da 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java @@ -284,6 +284,29 @@ public abstract class Action { } + /** + * Calls {@link Player#setShuffleModeEnabled(boolean)}. + */ + public static final class SetShuffleModeEnabled extends Action { + + private final boolean shuffleModeEnabled; + + /** + * @param tag A tag to use for logging. + */ + public SetShuffleModeEnabled(String tag, boolean shuffleModeEnabled) { + super(tag, "SetShuffleModeEnabled:" + shuffleModeEnabled); + this.shuffleModeEnabled = shuffleModeEnabled; + } + + @Override + protected void doActionImpl(SimpleExoPlayer player, MappingTrackSelector trackSelector, + Surface surface) { + player.setShuffleModeEnabled(shuffleModeEnabled); + } + + } + /** * Waits for {@link Player.EventListener#onTimelineChanged(Timeline, Object)}. */ diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java index 4392dd9d3f..c9ae02c957 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java @@ -30,6 +30,7 @@ import com.google.android.exoplayer2.testutil.Action.Seek; import com.google.android.exoplayer2.testutil.Action.SetPlayWhenReady; import com.google.android.exoplayer2.testutil.Action.SetRendererDisabled; import com.google.android.exoplayer2.testutil.Action.SetRepeatMode; +import com.google.android.exoplayer2.testutil.Action.SetShuffleModeEnabled; import com.google.android.exoplayer2.testutil.Action.SetVideoSurface; import com.google.android.exoplayer2.testutil.Action.Stop; import com.google.android.exoplayer2.testutil.Action.WaitForPositionDiscontinuity; @@ -217,6 +218,15 @@ public final class ActionSchedule { return apply(new SetRepeatMode(tag, repeatMode)); } + /** + * Schedules a shuffle setting action to be executed. + * + * @return The builder, for convenience. + */ + public Builder setShuffleModeEnabled(boolean shuffleModeEnabled) { + return apply(new SetShuffleModeEnabled(tag, shuffleModeEnabled)); + } + /** * Schedules a delay until the timeline changed to a specified expected timeline. * From aa712114c66b5e92d73d0529e1cbdb7242e9cd50 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 23 Aug 2017 09:04:29 -0700 Subject: [PATCH 0304/2472] Force stop hosted test after timeout. When hosted tests run into a timeout, the outer test method stops. However, the hosted test itself may continue running and needs to be forced-stopped to ensure it does not block any resources needed by subsequent test methods. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166208204 --- .../google/android/exoplayer2/testutil/HostActivity.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/HostActivity.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/HostActivity.java index 66b992e652..54087c4461 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/HostActivity.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/HostActivity.java @@ -137,6 +137,12 @@ public final class HostActivity extends Activity implements SurfaceHolder.Callba fail(message); } } else { + runOnUiThread(new Runnable() { + @Override + public void run() { + hostedTest.forceStop(); + } + }); String message = "Test timed out after " + timeoutMs + " ms."; Log.e(TAG, message); if (failOnTimeout) { From 57bad31e4c17883de178f7a743798c42b6c88dc4 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 24 Aug 2017 03:37:32 -0700 Subject: [PATCH 0305/2472] Update documentation with new demo app location Plus a few misc doc fixes / adjustments ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166323135 --- README.md | 4 ++-- .../com/google/android/exoplayer2/ExoPlayer.java | 6 ++++++ .../source/DynamicConcatenatingMediaSource.java | 16 ++++++++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c67fb09d73..aff473c488 100644 --- a/README.md +++ b/README.md @@ -71,12 +71,12 @@ individually. In addition to library modules, ExoPlayer has multiple extension modules that depend on external libraries to provide additional functionality. Some extensions are available from JCenter, whereas others must be built manaully. -Browse the [extensions directory] and their individual READMEs for details. +Browse the [extensions directory][] and their individual READMEs for details. More information on the library and extension modules that are available from JCenter can be found on [Bintray][]. -[extensions directory][]: https://github.com/google/ExoPlayer/tree/release-v2/extensions/ +[extensions directory]: https://github.com/google/ExoPlayer/tree/release-v2/extensions/ [Bintray]: https://bintray.com/google/exoplayer ### Locally ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java index b096b5ae12..915a083657 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java @@ -211,12 +211,18 @@ public interface ExoPlayer extends Player { /** * Prepares the player to play the provided {@link MediaSource}. Equivalent to * {@code prepare(mediaSource, true, true)}. + *

        + * Note: {@link MediaSource} instances are not designed to be re-used. If you want to prepare a + * player more than once with the same piece of media, use a new instance each time. */ void prepare(MediaSource mediaSource); /** * Prepares the player to play the provided {@link MediaSource}, optionally resetting the playback * position the default position in the first {@link Timeline.Window}. + *

        + * Note: {@link MediaSource} instances are not designed to be re-used. If you want to prepare a + * player more than once with the same piece of media, use a new instance each time. * * @param mediaSource The {@link MediaSource} to play. * @param resetPosition Whether the playback position should be reset to the default position in diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java index 3d0df7dcb3..9c1e7ec1ba 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java @@ -87,6 +87,9 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl /** * Appends a {@link MediaSource} to the playlist. + *

        + * Note: {@link MediaSource} instances are not designed to be re-used. If you want to add the same + * piece of media multiple times, use a new instance each time. * * @param mediaSource The {@link MediaSource} to be added to the list. */ @@ -96,6 +99,9 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl /** * Adds a {@link MediaSource} to the playlist. + *

        + * Note: {@link MediaSource} instances are not designed to be re-used. If you want to add the same + * piece of media multiple times, use a new instance each time. * * @param index The index at which the new {@link MediaSource} will be inserted. This index must * be in the range of 0 <= index <= {@link #getSize()}. @@ -112,6 +118,9 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl /** * Appends multiple {@link MediaSource}s to the playlist. + *

        + * Note: {@link MediaSource} instances are not designed to be re-used. If you want to add the same + * piece of media multiple times, use a new instance each time. * * @param mediaSources A collection of {@link MediaSource}s to be added to the list. The media * sources are added in the order in which they appear in this collection. @@ -122,6 +131,9 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl /** * Adds multiple {@link MediaSource}s to the playlist. + *

        + * Note: {@link MediaSource} instances are not designed to be re-used. If you want to add the same + * piece of media multiple times, use a new instance each time. * * @param index The index at which the new {@link MediaSource}s will be inserted. This index must * be in the range of 0 <= index <= {@link #getSize()}. @@ -142,6 +154,10 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl /** * Removes a {@link MediaSource} from the playlist. + *

        + * Note: {@link MediaSource} instances are not designed to be re-used, and so the instance being + * removed should not be re-added. If you want to move the instance use + * {@link #moveMediaSource(int, int)} instead. * * @param index The index at which the media source will be removed. This index must be in the * range of 0 <= index < {@link #getSize()}. From 6e03dcdfa1fa18a7865085186b7e33790489d46c Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 24 Aug 2017 05:30:36 -0700 Subject: [PATCH 0306/2472] Add LocalMediaDrmCallback. Useful for providing local keys. Issue: #3178 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166330215 --- .../exoplayer2/drm/LocalMediaDrmCallback.java | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/drm/LocalMediaDrmCallback.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/LocalMediaDrmCallback.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/LocalMediaDrmCallback.java new file mode 100644 index 0000000000..7b9aeca30a --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/LocalMediaDrmCallback.java @@ -0,0 +1,51 @@ +/* + * 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.drm; + +import com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest; +import com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest; +import com.google.android.exoplayer2.util.Assertions; +import java.io.IOException; +import java.util.UUID; + +/** + * A {@link MediaDrmCallback} that provides a fixed response to key requests. Provisioning is not + * supported. This implementation is primarily useful for providing locally stored keys to decrypt + * ClearKey protected content. It is not suitable for use with Widevine or PlayReady protected + * content. + */ +public final class LocalMediaDrmCallback implements MediaDrmCallback { + + private final byte[] keyResponse; + + /** + * @param keyResponse The fixed response for all key requests. + */ + public LocalMediaDrmCallback(byte[] keyResponse) { + this.keyResponse = Assertions.checkNotNull(keyResponse); + } + + @Override + public byte[] executeProvisionRequest(UUID uuid, ProvisionRequest request) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public byte[] executeKeyRequest(UUID uuid, KeyRequest request) throws Exception { + return keyResponse; + } + +} From 6907ffb2852d56556c5a4186d6d7cfb00d8d7553 Mon Sep 17 00:00:00 2001 From: olly Date: Sun, 23 Jul 2017 09:55:50 +0100 Subject: [PATCH 0307/2472] Remove unnecessary view casts findViewById is now defined using generics, which allows the types to be inferred. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166355555 --- .../android/exoplayer2/castdemo/MainActivity.java | 6 +++--- .../google/android/exoplayer2/demo/PlayerActivity.java | 8 ++++---- .../android/exoplayer2/demo/SampleChooserActivity.java | 2 +- .../android/exoplayer2/ui/PlaybackControlView.java | 8 ++++---- .../android/exoplayer2/ui/SimpleExoPlayerView.java | 10 +++++----- .../android/exoplayer2/testutil/HostActivity.java | 2 +- 6 files changed, 18 insertions(+), 18 deletions(-) diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java index e1367858aa..e1c7519a05 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java @@ -49,12 +49,12 @@ public class MainActivity extends AppCompatActivity { setContentView(R.layout.main_activity); - simpleExoPlayerView = (SimpleExoPlayerView) findViewById(R.id.player_view); + simpleExoPlayerView = findViewById(R.id.player_view); simpleExoPlayerView.requestFocus(); - castControlView = (PlaybackControlView) findViewById(R.id.cast_control_view); + castControlView = findViewById(R.id.cast_control_view); - ListView sampleList = (ListView) findViewById(R.id.sample_list); + ListView sampleList = findViewById(R.id.sample_list); sampleList.setAdapter(new SampleListAdapter()); sampleList.setOnItemClickListener(new SampleClickListener()); } diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index 6d733c9f97..b2750a93bb 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -148,12 +148,12 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi setContentView(R.layout.player_activity); View rootView = findViewById(R.id.root); rootView.setOnClickListener(this); - debugRootView = (LinearLayout) findViewById(R.id.controls_root); - debugTextView = (TextView) findViewById(R.id.debug_text_view); - retryButton = (Button) findViewById(R.id.retry_button); + debugRootView = findViewById(R.id.controls_root); + debugTextView = findViewById(R.id.debug_text_view); + retryButton = findViewById(R.id.retry_button); retryButton.setOnClickListener(this); - simpleExoPlayerView = (SimpleExoPlayerView) findViewById(R.id.player_view); + simpleExoPlayerView = findViewById(R.id.player_view); simpleExoPlayerView.setControllerVisibilityListener(this); simpleExoPlayerView.requestFocus(); } diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java index 382c783598..c0edb1d1b8 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java @@ -90,7 +90,7 @@ public class SampleChooserActivity extends Activity { Toast.makeText(getApplicationContext(), R.string.sample_list_load_error, Toast.LENGTH_LONG) .show(); } - ExpandableListView sampleList = (ExpandableListView) findViewById(R.id.sample_list); + ExpandableListView sampleList = findViewById(R.id.sample_list); sampleList.setAdapter(new SampleAdapter(this, groups)); sampleList.setOnChildClickListener(new OnChildClickListener() { @Override diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java index 105dbc2495..91308848f9 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java @@ -374,9 +374,9 @@ public class PlaybackControlView extends FrameLayout { LayoutInflater.from(context).inflate(controllerLayoutId, this); setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); - durationView = (TextView) findViewById(R.id.exo_duration); - positionView = (TextView) findViewById(R.id.exo_position); - timeBar = (TimeBar) findViewById(R.id.exo_progress); + durationView = findViewById(R.id.exo_duration); + positionView = findViewById(R.id.exo_position); + timeBar = findViewById(R.id.exo_progress); if (timeBar != null) { timeBar.setListener(componentListener); } @@ -404,7 +404,7 @@ public class PlaybackControlView extends FrameLayout { if (fastForwardButton != null) { fastForwardButton.setOnClickListener(componentListener); } - repeatToggleButton = (ImageView) findViewById(R.id.exo_repeat_toggle); + repeatToggleButton = findViewById(R.id.exo_repeat_toggle); if (repeatToggleButton != null) { repeatToggleButton.setOnClickListener(componentListener); } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index bdbdf34331..0a531375c1 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -286,7 +286,7 @@ public final class SimpleExoPlayerView extends FrameLayout { setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); // Content frame. - contentFrame = (AspectRatioFrameLayout) findViewById(R.id.exo_content_frame); + contentFrame = findViewById(R.id.exo_content_frame); if (contentFrame != null) { setResizeModeRaw(contentFrame, resizeMode); } @@ -307,24 +307,24 @@ public final class SimpleExoPlayerView extends FrameLayout { } // Overlay frame layout. - overlayFrameLayout = (FrameLayout) findViewById(R.id.exo_overlay); + overlayFrameLayout = findViewById(R.id.exo_overlay); // Artwork view. - artworkView = (ImageView) findViewById(R.id.exo_artwork); + artworkView = findViewById(R.id.exo_artwork); this.useArtwork = useArtwork && artworkView != null; if (defaultArtworkId != 0) { defaultArtwork = BitmapFactory.decodeResource(context.getResources(), defaultArtworkId); } // Subtitle view. - subtitleView = (SubtitleView) findViewById(R.id.exo_subtitles); + subtitleView = findViewById(R.id.exo_subtitles); if (subtitleView != null) { subtitleView.setUserDefaultStyle(); subtitleView.setUserDefaultTextSize(); } // Playback control view. - PlaybackControlView customController = (PlaybackControlView) findViewById(R.id.exo_controller); + PlaybackControlView customController = findViewById(R.id.exo_controller); View controllerPlaceholder = findViewById(R.id.exo_controller_placeholder); if (customController != null) { this.controller = customController; diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/HostActivity.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/HostActivity.java index 54087c4461..299cb10815 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/HostActivity.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/HostActivity.java @@ -159,7 +159,7 @@ public final class HostActivity extends Activity implements SurfaceHolder.Callba super.onCreate(savedInstanceState); requestWindowFeature(Window.FEATURE_NO_TITLE); setContentView(getResources().getIdentifier("host_activity", "layout", getPackageName())); - surfaceView = (SurfaceView) findViewById( + surfaceView = findViewById( getResources().getIdentifier("surface_view", "id", getPackageName())); surfaceView.getHolder().addCallback(this); } From 1b9c904dbae7009fcb448b0db3b2286289f14925 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 25 Aug 2017 02:52:35 -0700 Subject: [PATCH 0308/2472] Add UI for shuffle mode. This includes an option to show and hide the shuffle mode button. When pressing the button, the shuffle mode of the player is toggled. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166455759 --- .../exoplayer2/ui/PlaybackControlView.java | 73 ++++++++++++++++++- .../exoplayer2/ui/SimpleExoPlayerView.java | 10 +++ .../res/layout/exo_playback_control_view.xml | 3 + library/ui/src/main/res/values/attrs.xml | 2 + 4 files changed, 87 insertions(+), 1 deletion(-) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java index 91308848f9..f83bab5770 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java @@ -84,6 +84,12 @@ import java.util.Locale; *

      • Default: {@link PlaybackControlView#DEFAULT_REPEAT_TOGGLE_MODES}
      • *
      *
    • + *
    • {@code show_shuffle_button} - Whether the shuffle button is shown. + *
        + *
      • Corresponding method: {@link #setShowShuffleButton(boolean)}
      • + *
      • Default: false
      • + *
      + *
    • *
    • {@code controller_layout_id} - Specifies the id of the layout to be inflated. See * below for more details. *
        @@ -136,6 +142,11 @@ import java.util.Locale; *
      • Type: {@link View}
      • *
      *
    • + *
    • {@code exo_shuffle} - The shuffle button. + *
        + *
      • Type: {@link View}
      • + *
      + *
    • *
    • {@code exo_position} - Text view displaying the current playback position. *
        *
      • Type: {@link TextView}
      • @@ -221,6 +232,15 @@ public class PlaybackControlView extends FrameLayout { */ boolean dispatchSetRepeatMode(Player player, @RepeatMode int repeatMode); + /** + * Dispatches a {@link Player#setShuffleModeEnabled(boolean)} operation. + * + * @param player The {@link Player} to which the operation should be dispatched. + * @param shuffleModeEnabled Whether shuffling is enabled. + * @return True if the operation was dispatched. False if suppressed. + */ + boolean dispatchSetShuffleModeEnabled(Player player, boolean shuffleModeEnabled); + } /** @@ -247,6 +267,12 @@ public class PlaybackControlView extends FrameLayout { return true; } + @Override + public boolean dispatchSetShuffleModeEnabled(Player player, boolean shuffleModeEnabled) { + player.setShuffleModeEnabled(shuffleModeEnabled); + return true; + } + }; /** @@ -282,6 +308,7 @@ public class PlaybackControlView extends FrameLayout { private final View fastForwardButton; private final View rewindButton; private final ImageView repeatToggleButton; + private final View shuffleButton; private final TextView durationView; private final TextView positionView; private final TimeBar timeBar; @@ -309,6 +336,7 @@ public class PlaybackControlView extends FrameLayout { private int fastForwardMs; private int showTimeoutMs; private @RepeatModeUtil.RepeatToggleModes int repeatToggleModes; + private boolean showShuffleButton; private long hideAtMs; private long[] adGroupTimesMs; private boolean[] playedAdGroups; @@ -345,6 +373,7 @@ public class PlaybackControlView extends FrameLayout { fastForwardMs = DEFAULT_FAST_FORWARD_MS; showTimeoutMs = DEFAULT_SHOW_TIMEOUT_MS; repeatToggleModes = DEFAULT_REPEAT_TOGGLE_MODES; + showShuffleButton = false; if (attrs != null) { TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.PlaybackControlView, 0, 0); @@ -356,6 +385,8 @@ public class PlaybackControlView extends FrameLayout { controllerLayoutId = a.getResourceId(R.styleable.PlaybackControlView_controller_layout_id, controllerLayoutId); repeatToggleModes = getRepeatToggleModes(a, repeatToggleModes); + showShuffleButton = a.getBoolean(R.styleable.PlaybackControlView_show_shuffle_button, + showShuffleButton); } finally { a.recycle(); } @@ -408,6 +439,10 @@ public class PlaybackControlView extends FrameLayout { if (repeatToggleButton != null) { repeatToggleButton.setOnClickListener(componentListener); } + shuffleButton = findViewById(R.id.exo_shuffle); + if (shuffleButton != null) { + shuffleButton.setOnClickListener(componentListener); + } Resources resources = context.getResources(); repeatOffButtonDrawable = resources.getDrawable(R.drawable.exo_controls_repeat_off); repeatOneButtonDrawable = resources.getDrawable(R.drawable.exo_controls_repeat_one); @@ -584,6 +619,23 @@ public class PlaybackControlView extends FrameLayout { } } + /** + * Returns whether the shuffle button is shown. + */ + public boolean getShowShuffleButton() { + return showShuffleButton; + } + + /** + * Sets whether the shuffle button is shown. + * + * @param showShuffleButton Whether the shuffle button is shown. + */ + public void setShowShuffleButton(boolean showShuffleButton) { + this.showShuffleButton = showShuffleButton; + updateShuffleButton(); + } + /** * Shows the playback controls. If {@link #getShowTimeoutMs()} is positive then the controls will * be automatically hidden after this duration of time has elapsed without user input. @@ -639,6 +691,7 @@ public class PlaybackControlView extends FrameLayout { updatePlayPauseButton(); updateNavigation(); updateRepeatModeButton(); + updateShuffleButton(); updateProgress(); } @@ -721,6 +774,21 @@ public class PlaybackControlView extends FrameLayout { repeatToggleButton.setVisibility(View.VISIBLE); } + private void updateShuffleButton() { + if (!isVisible() || !isAttachedToWindow || shuffleButton == null) { + return; + } + if (!showShuffleButton) { + shuffleButton.setVisibility(View.GONE); + } else if (player == null) { + setButtonEnabled(false, shuffleButton); + } else { + shuffleButton.setAlpha(player.getShuffleModeEnabled() ? 1f : 0.3f); + shuffleButton.setEnabled(true); + shuffleButton.setVisibility(View.VISIBLE); + } + } + private void updateTimeBarMode() { if (player == null) { return; @@ -1080,7 +1148,8 @@ public class PlaybackControlView extends FrameLayout { @Override public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { - // TODO: Update UI. + updateShuffleButton(); + updateNavigation(); } @Override @@ -1134,6 +1203,8 @@ public class PlaybackControlView extends FrameLayout { } else if (repeatToggleButton == view) { controlDispatcher.dispatchSetRepeatMode(player, RepeatModeUtil.getNextRepeatMode( player.getRepeatMode(), repeatToggleModes)); + } else if (shuffleButton == view) { + controlDispatcher.dispatchSetShuffleModeEnabled(player, !player.getShuffleModeEnabled()); } } hideAfterTimeout(); diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index 0a531375c1..a8926f9ecc 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -655,6 +655,16 @@ public final class SimpleExoPlayerView extends FrameLayout { controller.setRepeatToggleModes(repeatToggleModes); } + /** + * Sets whether the shuffle button is shown. + * + * @param showShuffleButton Whether the shuffle button is shown. + */ + public void setShowShuffleButton(boolean showShuffleButton) { + Assertions.checkState(controller != null); + controller.setShowShuffleButton(showShuffleButton); + } + /** * Sets whether the time bar should show all windows, as opposed to just the current one. * diff --git a/library/ui/src/main/res/layout/exo_playback_control_view.xml b/library/ui/src/main/res/layout/exo_playback_control_view.xml index 407329890d..159844c234 100644 --- a/library/ui/src/main/res/layout/exo_playback_control_view.xml +++ b/library/ui/src/main/res/layout/exo_playback_control_view.xml @@ -34,6 +34,9 @@ + + diff --git a/library/ui/src/main/res/values/attrs.xml b/library/ui/src/main/res/values/attrs.xml index d1f45228b1..9b701d5ba5 100644 --- a/library/ui/src/main/res/values/attrs.xml +++ b/library/ui/src/main/res/values/attrs.xml @@ -39,6 +39,7 @@ + @@ -64,6 +65,7 @@ + From 01f481984437ce94bed16e71131d0ba084a2b70c Mon Sep 17 00:00:00 2001 From: bachinger Date: Fri, 25 Aug 2017 07:43:15 -0700 Subject: [PATCH 0309/2472] Introduce MediaSessionConnector.CommandReceiver interface and add TimelineQueueEditor. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166475351 --- .../DefaultPlaybackController.java | 12 + .../mediasession/MediaSessionConnector.java | 86 +++++-- .../ext/mediasession/TimelineQueueEditor.java | 226 ++++++++++++++++++ .../mediasession/TimelineQueueNavigator.java | 14 ++ 4 files changed, 316 insertions(+), 22 deletions(-) create mode 100644 extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueEditor.java diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/DefaultPlaybackController.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/DefaultPlaybackController.java index c3586b29e6..e01d6a48db 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/DefaultPlaybackController.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/DefaultPlaybackController.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.ext.mediasession; +import android.os.Bundle; +import android.os.ResultReceiver; import android.support.v4.media.session.PlaybackStateCompat; import com.google.android.exoplayer2.C; @@ -125,4 +127,14 @@ public class DefaultPlaybackController implements MediaSessionConnector.Playback player.stop(); } + @Override + public String[] getCommands() { + return null; + } + + @Override + public void onCommand(Player player, String command, Bundle extras, ResultReceiver cb) { + // Do nothing. + } + } diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java index 3a4a80733d..a64f163733 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java @@ -79,10 +79,24 @@ public final class MediaSessionConnector { private static final int EDITOR_MEDIA_SESSION_FLAGS = BASE_MEDIA_SESSION_FLAGS | MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS; + /** + * Receiver of media commands sent by a media controller. + */ + public interface CommandReceiver { + /** + * Returns the commands the receiver handles, or {@code null} if no commands need to be handled. + */ + String[] getCommands(); + /** + * See {@link MediaSessionCompat.Callback#onCommand(String, Bundle, ResultReceiver)}. + */ + void onCommand(Player player, String command, Bundle extras, ResultReceiver cb); + } + /** * Interface to which playback preparation actions are delegated. */ - public interface PlaybackPreparer { + public interface PlaybackPreparer extends CommandReceiver { long ACTIONS = PlaybackStateCompat.ACTION_PREPARE | PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID @@ -121,16 +135,12 @@ public final class MediaSessionConnector { * See {@link MediaSessionCompat.Callback#onPrepareFromUri(Uri, Bundle)}. */ void onPrepareFromUri(Uri uri, Bundle extras); - /** - * See {@link MediaSessionCompat.Callback#onCommand(String, Bundle, ResultReceiver)}. - */ - void onCommand(String command, Bundle extras, ResultReceiver cb); } /** * Interface to which playback actions are delegated. */ - public interface PlaybackController { + public interface PlaybackController extends CommandReceiver { long ACTIONS = PlaybackStateCompat.ACTION_PLAY_PAUSE | PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PAUSE | PlaybackStateCompat.ACTION_SEEK_TO @@ -178,7 +188,7 @@ public final class MediaSessionConnector { * Handles queue navigation actions, and updates the media session queue by calling * {@code MediaSessionCompat.setQueue()}. */ - public interface QueueNavigator { + public interface QueueNavigator extends CommandReceiver { long ACTIONS = PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM | PlaybackStateCompat.ACTION_SKIP_TO_NEXT | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS @@ -240,7 +250,7 @@ public final class MediaSessionConnector { /** * Handles media session queue edits. */ - public interface QueueEditor { + public interface QueueEditor extends CommandReceiver { long ACTIONS = PlaybackStateCompat.ACTION_SET_RATING; @@ -309,6 +319,7 @@ public final class MediaSessionConnector { private final ExoPlayerEventListener exoPlayerEventListener; private final MediaSessionCallback mediaSessionCallback; private final PlaybackController playbackController; + private final Map commandMap; private Player player; private CustomActionProvider[] customActionProviders; @@ -328,7 +339,7 @@ public final class MediaSessionConnector { * @param mediaSession The {@link MediaSessionCompat} to connect to. */ public MediaSessionConnector(MediaSessionCompat mediaSession) { - this(mediaSession, new DefaultPlaybackController()); + this(mediaSession, null); } /** @@ -350,7 +361,8 @@ public final class MediaSessionConnector { * instances passed to {@link #setPlayer(Player, PlaybackPreparer, CustomActionProvider...)}. * * @param mediaSession The {@link MediaSessionCompat} to connect to. - * @param playbackController A {@link PlaybackController} for handling playback actions. + * @param playbackController A {@link PlaybackController} for handling playback actions, or + * {@code null} if the connector should handle playback actions directly. * @param doMaintainMetadata Whether the connector should maintain the metadata of the session. If * {@code false}, you need to maintain the metadata of the media session yourself (provide at * least the duration to allow clients to show a progress bar). @@ -358,7 +370,8 @@ public final class MediaSessionConnector { public MediaSessionConnector(MediaSessionCompat mediaSession, PlaybackController playbackController, boolean doMaintainMetadata) { this.mediaSession = mediaSession; - this.playbackController = playbackController; + this.playbackController = playbackController != null ? playbackController + : new DefaultPlaybackController(); this.handler = new Handler(Looper.myLooper() != null ? Looper.myLooper() : Looper.getMainLooper()); this.doMaintainMetadata = doMaintainMetadata; @@ -367,6 +380,8 @@ public final class MediaSessionConnector { mediaSessionCallback = new MediaSessionCallback(); exoPlayerEventListener = new ExoPlayerEventListener(); customActionMap = Collections.emptyMap(); + commandMap = new HashMap<>(); + registerCommandReceiver(playbackController); } /** @@ -386,8 +401,12 @@ public final class MediaSessionConnector { this.player.removeListener(exoPlayerEventListener); mediaSession.setCallback(null); } - this.playbackPreparer = playbackPreparer; + unregisterCommandReceiver(this.playbackPreparer); + this.player = player; + this.playbackPreparer = playbackPreparer; + registerCommandReceiver(playbackPreparer); + this.customActionProviders = (player != null && customActionProviders != null) ? customActionProviders : new CustomActionProvider[0]; if (player != null) { @@ -416,7 +435,9 @@ public final class MediaSessionConnector { * @param queueNavigator The queue navigator. */ public void setQueueNavigator(QueueNavigator queueNavigator) { + unregisterCommandReceiver(this.queueNavigator); this.queueNavigator = queueNavigator; + registerCommandReceiver(queueNavigator); } /** @@ -425,11 +446,29 @@ public final class MediaSessionConnector { * @param queueEditor The queue editor. */ public void setQueueEditor(QueueEditor queueEditor) { + unregisterCommandReceiver(this.queueEditor); this.queueEditor = queueEditor; + registerCommandReceiver(queueEditor); mediaSession.setFlags(queueEditor == null ? BASE_MEDIA_SESSION_FLAGS : EDITOR_MEDIA_SESSION_FLAGS); } + private void registerCommandReceiver(CommandReceiver commandReceiver) { + if (commandReceiver != null && commandReceiver.getCommands() != null) { + for (String command : commandReceiver.getCommands()) { + commandMap.put(command, commandReceiver); + } + } + } + + private void unregisterCommandReceiver(CommandReceiver commandReceiver) { + if (commandReceiver != null && commandReceiver.getCommands() != null) { + for (String command : commandReceiver.getCommands()) { + commandMap.remove(command); + } + } + } + private void updateMediaSessionPlaybackState() { PlaybackStateCompat.Builder builder = new PlaybackStateCompat.Builder(); if (player == null) { @@ -473,11 +512,8 @@ public final class MediaSessionConnector { } private long buildPlaybackActions() { - long actions = 0; - if (playbackController != null) { - actions |= (PlaybackController.ACTIONS & playbackController - .getSupportedPlaybackActions(player)); - } + long actions = (PlaybackController.ACTIONS + & playbackController.getSupportedPlaybackActions(player)); if (playbackPreparer != null) { actions |= (PlaybackPreparer.ACTIONS & playbackPreparer.getSupportedPrepareActions()); } @@ -562,7 +598,7 @@ public final class MediaSessionConnector { } private boolean canDispatchToPlaybackController(long action) { - return playbackController != null && (playbackController.getSupportedPlaybackActions(player) + return (playbackController.getSupportedPlaybackActions(player) & PlaybackController.ACTIONS & action) != 0; } @@ -583,10 +619,15 @@ public final class MediaSessionConnector { @Override public void onTimelineChanged(Timeline timeline, Object manifest) { + int windowCount = player.getCurrentTimeline().getWindowCount(); + int windowIndex = player.getCurrentWindowIndex(); if (queueNavigator != null) { queueNavigator.onTimelineChanged(player); + updateMediaSessionPlaybackState(); + } else if (currentWindowCount != windowCount || currentWindowIndex != windowIndex) { + // active queue item and queue navigation actions may need to be updated + updateMediaSessionPlaybackState(); } - int windowCount = player.getCurrentTimeline().getWindowCount(); if (currentWindowCount != windowCount) { // active queue item and queue navigation actions may need to be updated updateMediaSessionPlaybackState(); @@ -638,8 +679,8 @@ public final class MediaSessionConnector { if (queueNavigator != null) { queueNavigator.onCurrentWindowIndexChanged(player); } - updateMediaSessionMetadata(); currentWindowIndex = player.getCurrentWindowIndex(); + updateMediaSessionMetadata(); } updateMediaSessionPlaybackState(); } @@ -732,8 +773,9 @@ public final class MediaSessionConnector { @Override public void onCommand(String command, Bundle extras, ResultReceiver cb) { - if (playbackPreparer != null) { - playbackPreparer.onCommand(command, extras, cb); + CommandReceiver commandReceiver = commandMap.get(command); + if (commandReceiver != null) { + commandReceiver.onCommand(player, command, extras, cb); } } diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueEditor.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueEditor.java new file mode 100644 index 0000000000..65090a3c1c --- /dev/null +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueEditor.java @@ -0,0 +1,226 @@ +/* + * 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.ext.mediasession; + +import android.os.Bundle; +import android.os.ResultReceiver; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.media.MediaDescriptionCompat; +import android.support.v4.media.RatingCompat; +import android.support.v4.media.session.MediaControllerCompat; +import android.support.v4.media.session.MediaSessionCompat; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.util.Util; +import java.util.List; + +/** + * A {@link MediaSessionConnector.QueueEditor} implementation based on the + * {@link DynamicConcatenatingMediaSource}. + *

        + * This class implements the {@link MediaSessionConnector.CommandReceiver} interface and handles + * the {@link #COMMAND_MOVE_QUEUE_ITEM} to move a queue item instead of removing and inserting it. + * This allows to move the currently playing window without interrupting playback. + */ +public final class TimelineQueueEditor implements MediaSessionConnector.QueueEditor, + MediaSessionConnector.CommandReceiver { + + public static final String COMMAND_MOVE_QUEUE_ITEM = "exo_move_window"; + public static final String EXTRA_FROM_INDEX = "from_index"; + public static final String EXTRA_TO_INDEX = "to_index"; + + /** + * Factory to create {@link MediaSource}s. + */ + public interface MediaSourceFactory { + /** + * Creates a {@link MediaSource} for the given {@link MediaDescriptionCompat}. + * + * @param description The {@link MediaDescriptionCompat} to create a media source for. + * @return A {@link MediaSource} or {@code null} if no source can be created for the given + * description. + */ + @Nullable MediaSource createMediaSource(MediaDescriptionCompat description); + } + + /** + * Adapter to get {@link MediaDescriptionCompat} of items in the queue and to notify the + * application about changes in the queue to sync the data structure backing the + * {@link MediaSessionConnector}. + */ + public interface QueueDataAdapter { + /** + * Gets the {@link MediaDescriptionCompat} for a {@code position}. + * + * @param position The position in the queue for which to provide a description. + * @return A {@link MediaDescriptionCompat}. + */ + MediaDescriptionCompat getMediaDescription(int position); + /** + * Adds a {@link MediaDescriptionCompat} at the given {@code position}. + * + * @param position The position at which to add. + * @param description The {@link MediaDescriptionCompat} to be added. + */ + void add(int position, MediaDescriptionCompat description); + /** + * Removes the item at the given {@code position}. + * + * @param position The position at which to remove the item. + */ + void remove(int position); + /** + * Moves a queue item from position {@code from} to position {@code to}. + * + * @param from The position from which to remove the item. + * @param to The target position to which to move the item. + */ + void move(int from, int to); + } + + /** + * Used to evaluate whether two {@link MediaDescriptionCompat} are considered equal. + */ + interface MediaDescriptionEqualityChecker { + /** + * Returns {@code true} whether the descriptions are considered equal. + * + * @param d1 The first {@link MediaDescriptionCompat}. + * @param d2 The second {@link MediaDescriptionCompat}. + * @return {@code true} if considered equal. + */ + boolean equals(MediaDescriptionCompat d1, MediaDescriptionCompat d2); + } + + /** + * Media description comparator comparing the media IDs. Media IDs are considered equals if both + * are {@code null}. + */ + public static final class MediaIdEqualityChecker implements MediaDescriptionEqualityChecker { + + @Override + public boolean equals(MediaDescriptionCompat d1, MediaDescriptionCompat d2) { + return Util.areEqual(d1.getMediaId(), d2.getMediaId()); + } + + } + + private final MediaControllerCompat mediaController; + private final QueueDataAdapter queueDataAdapter; + private final MediaSourceFactory sourceFactory; + private final MediaDescriptionEqualityChecker equalityChecker; + private final DynamicConcatenatingMediaSource queueMediaSource; + + /** + * Creates a new {@link TimelineQueueEditor} with a given mediaSourceFactory. + * + * @param mediaController A {@link MediaControllerCompat} to read the current queue. + * @param queueMediaSource The {@link DynamicConcatenatingMediaSource} to + * manipulate. + * @param queueDataAdapter A {@link QueueDataAdapter} to change the backing data. + * @param sourceFactory The {@link MediaSourceFactory} to build media sources. + */ + public TimelineQueueEditor(@NonNull MediaControllerCompat mediaController, + @NonNull DynamicConcatenatingMediaSource queueMediaSource, + @NonNull QueueDataAdapter queueDataAdapter, @NonNull MediaSourceFactory sourceFactory) { + this(mediaController, queueMediaSource, queueDataAdapter, sourceFactory, + new MediaIdEqualityChecker()); + } + + /** + * Creates a new {@link TimelineQueueEditor} with a given mediaSourceFactory. + * + * @param mediaController A {@link MediaControllerCompat} to read the current queue. + * @param queueMediaSource The {@link DynamicConcatenatingMediaSource} to + * manipulate. + * @param queueDataAdapter A {@link QueueDataAdapter} to change the backing data. + * @param sourceFactory The {@link MediaSourceFactory} to build media sources. + * @param equalityChecker The {@link MediaDescriptionEqualityChecker} to match queue items. + */ + public TimelineQueueEditor(@NonNull MediaControllerCompat mediaController, + @NonNull DynamicConcatenatingMediaSource queueMediaSource, + @NonNull QueueDataAdapter queueDataAdapter, @NonNull MediaSourceFactory sourceFactory, + @NonNull MediaDescriptionEqualityChecker equalityChecker) { + this.mediaController = mediaController; + this.queueMediaSource = queueMediaSource; + this.queueDataAdapter = queueDataAdapter; + this.sourceFactory = sourceFactory; + this.equalityChecker = equalityChecker; + } + + @Override + public long getSupportedQueueEditorActions(@Nullable Player player) { + return 0; + } + + @Override + public void onAddQueueItem(Player player, MediaDescriptionCompat description) { + onAddQueueItem(player, description, player.getCurrentTimeline().getWindowCount()); + } + + @Override + public void onAddQueueItem(Player player, MediaDescriptionCompat description, int index) { + MediaSource mediaSource = sourceFactory.createMediaSource(description); + if (mediaSource != null) { + queueDataAdapter.add(index, description); + queueMediaSource.addMediaSource(index, mediaSource); + } + } + + @Override + public void onRemoveQueueItem(Player player, MediaDescriptionCompat description) { + List queue = mediaController.getQueue(); + for (int i = 0; i < queue.size(); i++) { + if (equalityChecker.equals(queue.get(i).getDescription(), description)) { + onRemoveQueueItemAt(player, i); + return; + } + } + } + + @Override + public void onRemoveQueueItemAt(Player player, int index) { + queueDataAdapter.remove(index); + queueMediaSource.removeMediaSource(index); + } + + @Override + public void onSetRating(Player player, RatingCompat rating) { + // Do nothing. + } + + // CommandReceiver implementation. + + @NonNull + @Override + public String[] getCommands() { + return new String[] {COMMAND_MOVE_QUEUE_ITEM}; + } + + @Override + public void onCommand(Player player, String command, Bundle extras, ResultReceiver cb) { + int from = extras.getInt(EXTRA_FROM_INDEX, C.INDEX_UNSET); + int to = extras.getInt(EXTRA_TO_INDEX, C.INDEX_UNSET); + if (from != C.INDEX_UNSET && to != C.INDEX_UNSET) { + queueDataAdapter.move(from, to); + queueMediaSource.moveMediaSource(from, to); + } + } + +} diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java index 8c7d3be114..777949863d 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.ext.mediasession; +import android.os.Bundle; +import android.os.ResultReceiver; import android.support.annotation.Nullable; import android.support.v4.media.MediaDescriptionCompat; import android.support.v4.media.session.MediaSessionCompat; @@ -164,6 +166,18 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu player.setShuffleModeEnabled(shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_ALL); } + // CommandReceiver implementation. + + @Override + public String[] getCommands() { + return null; + } + + @Override + public void onCommand(Player player, String command, Bundle extras, ResultReceiver cb) { + // Do nothing. + } + private void publishFloatingQueueWindow(Player player) { if (player.getCurrentTimeline().isEmpty()) { mediaSession.setQueue(Collections.emptyList()); From 30b31b56792664ebe181bc497282f075f6add1e5 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 25 Aug 2017 08:37:38 -0700 Subject: [PATCH 0310/2472] Support empty concatenations and empty timelines in concatenations. Both cases were not supported so far. Added tests which all failed in the previous code version and adapted the concatenated media sources to cope with empty timelines and empty concatenations. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166480344 --- .../source/ConcatenatingMediaSourceTest.java | 67 ++++++++++- .../DynamicConcatenatingMediaSourceTest.java | 110 +++++++++++++++++- .../source/LoopingMediaSourceTest.java | 17 ++- .../google/android/exoplayer2/Timeline.java | 10 +- .../source/AbstractConcatenatedTimeline.java | 55 ++++++++- .../source/ConcatenatingMediaSource.java | 26 +++-- .../DynamicConcatenatingMediaSource.java | 13 ++- .../exoplayer2/source/LoopingMediaSource.java | 6 +- .../exoplayer2/testutil/TimelineAsserts.java | 7 +- 9 files changed, 275 insertions(+), 36 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java index fd0acf2ab3..53111e83ac 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java @@ -31,11 +31,24 @@ import junit.framework.TestCase; */ public final class ConcatenatingMediaSourceTest extends TestCase { + public void testEmptyConcatenation() { + for (boolean atomic : new boolean[] {false, true}) { + Timeline timeline = getConcatenatedTimeline(atomic); + TimelineAsserts.assertEmpty(timeline); + + timeline = getConcatenatedTimeline(atomic, Timeline.EMPTY); + TimelineAsserts.assertEmpty(timeline); + + timeline = getConcatenatedTimeline(atomic, Timeline.EMPTY, Timeline.EMPTY, Timeline.EMPTY); + TimelineAsserts.assertEmpty(timeline); + } + } + public void testSingleMediaSource() { Timeline timeline = getConcatenatedTimeline(false, createFakeTimeline(3, 111)); TimelineAsserts.assertWindowIds(timeline, 111); TimelineAsserts.assertPeriodCounts(timeline, 3); - for (boolean shuffled : new boolean[] { false, true }) { + for (boolean shuffled : new boolean[] {false, true}) { TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled, C.INDEX_UNSET); TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, 0); @@ -49,7 +62,7 @@ public final class ConcatenatingMediaSourceTest extends TestCase { timeline = getConcatenatedTimeline(true, createFakeTimeline(3, 111)); TimelineAsserts.assertWindowIds(timeline, 111); TimelineAsserts.assertPeriodCounts(timeline, 3); - for (boolean shuffled : new boolean[] { false, true }) { + for (boolean shuffled : new boolean[] {false, true}) { TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled, C.INDEX_UNSET); TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, 0); @@ -91,7 +104,7 @@ public final class ConcatenatingMediaSourceTest extends TestCase { timeline = getConcatenatedTimeline(true, timelines); TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); TimelineAsserts.assertPeriodCounts(timeline, 3, 1, 3); - for (boolean shuffled : new boolean[] { false, true }) { + for (boolean shuffled : new boolean[] {false, true}) { TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled, C.INDEX_UNSET, 0, 1); TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, @@ -135,6 +148,54 @@ public final class ConcatenatingMediaSourceTest extends TestCase { TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 2, 0, 3, 1); } + public void testEmptyTimelineMediaSources() { + // Empty timelines in the front, back, and the middle (single and multiple in a row). + Timeline[] timelines = { Timeline.EMPTY, createFakeTimeline(1, 111), Timeline.EMPTY, + Timeline.EMPTY, createFakeTimeline(2, 222), Timeline.EMPTY, createFakeTimeline(3, 333), + Timeline.EMPTY }; + Timeline timeline = getConcatenatedTimeline(false, timelines); + TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); + TimelineAsserts.assertPeriodCounts(timeline, 1, 2, 3); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, + C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 2, 0, 1); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, + 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 1, 2, 0); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, true, + 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 2); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 1, 2, 0); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, true, + C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 2); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 2, 0, 1); + assertEquals(0, timeline.getFirstWindowIndex(false)); + assertEquals(2, timeline.getLastWindowIndex(false)); + assertEquals(2, timeline.getFirstWindowIndex(true)); + assertEquals(0, timeline.getLastWindowIndex(true)); + + timeline = getConcatenatedTimeline(true, timelines); + TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); + TimelineAsserts.assertPeriodCounts(timeline, 1, 2, 3); + for (boolean shuffled : new boolean[] {false, true}) { + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled, + C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, + 2, 0, 1); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, + 2, 0, 1); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled, + 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, 1, 2, 0); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, shuffled, 1, 2, 0); + assertEquals(0, timeline.getFirstWindowIndex(shuffled)); + assertEquals(2, timeline.getLastWindowIndex(shuffled)); + } + } + /** * Wraps the specified timelines in a {@link ConcatenatingMediaSource} and returns * the concatenated timeline. diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java index 0e07e99978..86c03d1ce8 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source; +import android.os.ConditionVariable; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; @@ -25,7 +26,10 @@ import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.MediaPeriod.Callback; import com.google.android.exoplayer2.source.MediaSource.Listener; +import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import com.google.android.exoplayer2.testutil.FakeMediaPeriod; import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeShuffleOrder; import com.google.android.exoplayer2.testutil.FakeTimeline; @@ -53,13 +57,13 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource( new FakeShuffleOrder(0)); prepareAndListenToTimelineUpdates(mediaSource); + assertNotNull(timeline); waitForTimelineUpdate(); TimelineAsserts.assertEmpty(timeline); // Add first source. mediaSource.addMediaSource(childSources[0]); waitForTimelineUpdate(); - assertNotNull(timeline); TimelineAsserts.assertPeriodCounts(timeline, 1); TimelineAsserts.assertWindowIds(timeline, 111); @@ -143,6 +147,9 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { assertEquals(timeline.getWindowCount() - 1, timeline.getFirstWindowIndex(true)); assertEquals(0, timeline.getLastWindowIndex(true)); + // Assert all periods can be prepared. + assertAllPeriodsCanBeCreatedPreparedAndReleased(mediaSource, timeline.getPeriodCount()); + // Remove at front of queue. mediaSource.removeMediaSource(0); waitForTimelineUpdate(); @@ -192,6 +199,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, true, 1, 2, C.INDEX_UNSET); + assertAllPeriodsCanBeCreatedPreparedAndReleased(mediaSource, timeline.getPeriodCount()); mediaSource.releaseSource(); for (int i = 1; i < 4; i++) { childSources[i].assertReleased(); @@ -225,8 +233,9 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { TimelineAsserts.assertPeriodCounts(timeline, 1, 9); TimelineAsserts.assertWindowIds(timeline, 111, 999); TimelineAsserts.assertWindowIsDynamic(timeline, false, false); + assertAllPeriodsCanBeCreatedPreparedAndReleased(mediaSource, timeline.getPeriodCount()); - //Add lazy sources after preparation + //Add lazy sources after preparation (and also try to prepare media period from lazy source). mediaSource.addMediaSource(1, lazySources[2]); waitForTimelineUpdate(); mediaSource.addMediaSource(2, childSources[1]); @@ -239,17 +248,90 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { TimelineAsserts.assertWindowIds(timeline, null, 111, 222, 999); TimelineAsserts.assertWindowIsDynamic(timeline, true, false, false, false); + MediaPeriod lazyPeriod = mediaSource.createPeriod(new MediaPeriodId(0), null); + assertNotNull(lazyPeriod); + final ConditionVariable lazyPeriodPrepared = new ConditionVariable(); + lazyPeriod.prepare(new Callback() { + @Override + public void onPrepared(MediaPeriod mediaPeriod) { + lazyPeriodPrepared.open(); + } + @Override + public void onContinueLoadingRequested(MediaPeriod source) {} + }, 0); + assertFalse(lazyPeriodPrepared.block(1)); + MediaPeriod secondLazyPeriod = mediaSource.createPeriod(new MediaPeriodId(0), null); + assertNotNull(secondLazyPeriod); + mediaSource.releasePeriod(secondLazyPeriod); + lazySources[3].triggerTimelineUpdate(createFakeTimeline(7)); waitForTimelineUpdate(); TimelineAsserts.assertPeriodCounts(timeline, 8, 1, 2, 9); TimelineAsserts.assertWindowIds(timeline, 888, 111, 222, 999); TimelineAsserts.assertWindowIsDynamic(timeline, false, false, false, false); + assertTrue(lazyPeriodPrepared.block(TIMEOUT_MS)); + mediaSource.releasePeriod(lazyPeriod); mediaSource.releaseSource(); childSources[0].assertReleased(); childSources[1].assertReleased(); } + public void testEmptyTimelineMediaSource() throws InterruptedException { + timeline = null; + DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource( + new FakeShuffleOrder(0)); + prepareAndListenToTimelineUpdates(mediaSource); + assertNotNull(timeline); + waitForTimelineUpdate(); + TimelineAsserts.assertEmpty(timeline); + + mediaSource.addMediaSource(new FakeMediaSource(Timeline.EMPTY, null)); + waitForTimelineUpdate(); + TimelineAsserts.assertEmpty(timeline); + + mediaSource.addMediaSources(Arrays.asList(new MediaSource[] { + new FakeMediaSource(Timeline.EMPTY, null), new FakeMediaSource(Timeline.EMPTY, null), + new FakeMediaSource(Timeline.EMPTY, null), new FakeMediaSource(Timeline.EMPTY, null), + new FakeMediaSource(Timeline.EMPTY, null), new FakeMediaSource(Timeline.EMPTY, null) + })); + waitForTimelineUpdate(); + TimelineAsserts.assertEmpty(timeline); + + // Insert non-empty media source to leave empty sources at the start, the end, and the middle + // (with single and multiple empty sources in a row). + MediaSource[] mediaSources = createMediaSources(3); + mediaSource.addMediaSource(1, mediaSources[0]); + waitForTimelineUpdate(); + mediaSource.addMediaSource(4, mediaSources[1]); + waitForTimelineUpdate(); + mediaSource.addMediaSource(6, mediaSources[2]); + waitForTimelineUpdate(); + TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); + TimelineAsserts.assertPeriodCounts(timeline, 1, 2, 3); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, + C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 2, 0, 1); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, + 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, false, 0, 1, 2); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, false, 1, 2, 0); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, true, + 1, 2, C.INDEX_UNSET); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 2); + TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 1, 2, 0); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, true, + C.INDEX_UNSET, 0, 1); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ONE, true, 0, 1, 2); + TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 2, 0, 1); + assertEquals(0, timeline.getFirstWindowIndex(false)); + assertEquals(2, timeline.getLastWindowIndex(false)); + assertEquals(2, timeline.getFirstWindowIndex(true)); + assertEquals(0, timeline.getLastWindowIndex(true)); + assertAllPeriodsCanBeCreatedPreparedAndReleased(mediaSource, timeline.getPeriodCount()); + } + public void testIllegalArguments() { DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); MediaSource validSource = new FakeMediaSource(createFakeTimeline(1), null); @@ -325,6 +407,28 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { return new FakeTimeline(new TimelineWindowDefinition(index + 1, (index + 1) * 111)); } + private static void assertAllPeriodsCanBeCreatedPreparedAndReleased(MediaSource mediaSource, + int periodCount) { + for (int i = 0; i < periodCount; i++) { + MediaPeriod mediaPeriod = mediaSource.createPeriod(new MediaPeriodId(i), null); + assertNotNull(mediaPeriod); + final ConditionVariable mediaPeriodPrepared = new ConditionVariable(); + mediaPeriod.prepare(new Callback() { + @Override + public void onPrepared(MediaPeriod mediaPeriod) { + mediaPeriodPrepared.open(); + } + @Override + public void onContinueLoadingRequested(MediaPeriod source) {} + }, 0); + assertTrue(mediaPeriodPrepared.block(TIMEOUT_MS)); + MediaPeriod secondMediaPeriod = mediaSource.createPeriod(new MediaPeriodId(i), null); + assertNotNull(secondMediaPeriod); + mediaSource.releasePeriod(secondMediaPeriod); + mediaSource.releasePeriod(mediaPeriod); + } + } + private static class LazyMediaSource implements MediaSource { private Listener listener; @@ -344,7 +448,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { @Override public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { - return null; + return new FakeMediaPeriod(TrackGroupArray.EMPTY); } @Override diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java index 52c313ed47..2c8deb74b4 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java @@ -42,7 +42,7 @@ public class LoopingMediaSourceTest extends TestCase { Timeline timeline = getLoopingTimeline(multiWindowTimeline, 1); TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1); - for (boolean shuffled : new boolean[] { false, true }) { + for (boolean shuffled : new boolean[] {false, true}) { TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled, C.INDEX_UNSET, 0, 1); TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, @@ -60,7 +60,7 @@ public class LoopingMediaSourceTest extends TestCase { Timeline timeline = getLoopingTimeline(multiWindowTimeline, 3); TimelineAsserts.assertWindowIds(timeline, 111, 222, 333, 111, 222, 333, 111, 222, 333); TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1, 1, 1, 1, 1, 1, 1); - for (boolean shuffled : new boolean[] { false, true }) { + for (boolean shuffled : new boolean[] {false, true}) { TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled, C.INDEX_UNSET, 0, 1, 2, 3, 4, 5, 6, 7, 8); TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, @@ -80,7 +80,7 @@ public class LoopingMediaSourceTest extends TestCase { Timeline timeline = getLoopingTimeline(multiWindowTimeline, Integer.MAX_VALUE); TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1); - for (boolean shuffled : new boolean[] { false, true }) { + for (boolean shuffled : new boolean[] {false, true}) { TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, shuffled, 2, 0, 1); TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_ONE, shuffled, @@ -93,6 +93,17 @@ public class LoopingMediaSourceTest extends TestCase { } } + public void testEmptyTimelineLoop() { + Timeline timeline = getLoopingTimeline(Timeline.EMPTY, 1); + TimelineAsserts.assertEmpty(timeline); + + timeline = getLoopingTimeline(Timeline.EMPTY, 3); + TimelineAsserts.assertEmpty(timeline); + + timeline = getLoopingTimeline(Timeline.EMPTY, Integer.MAX_VALUE); + TimelineAsserts.assertEmpty(timeline); + } + /** * Wraps the specified timeline in a {@link LoopingMediaSource} and returns * the looping timeline. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java index 8a1d7964ee..b83a99295a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java @@ -606,10 +606,11 @@ public abstract class Timeline { * enabled. * * @param shuffleModeEnabled Whether shuffling is enabled. - * @return The index of the last window in the playback order. + * @return The index of the last window in the playback order, or {@link C#INDEX_UNSET} if the + * timeline is empty. */ public int getLastWindowIndex(boolean shuffleModeEnabled) { - return getWindowCount() - 1; + return isEmpty() ? C.INDEX_UNSET : getWindowCount() - 1; } /** @@ -617,10 +618,11 @@ public abstract class Timeline { * enabled. * * @param shuffleModeEnabled Whether shuffling is enabled. - * @return The index of the first window in the playback order. + * @return The index of the first window in the playback order, or {@link C#INDEX_UNSET} if the + * timeline is empty. */ public int getFirstWindowIndex(boolean shuffleModeEnabled) { - return 0; + return isEmpty() ? C.INDEX_UNSET : 0; } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java b/library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java index 07813ff046..35234753b0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/AbstractConcatenatedTimeline.java @@ -42,6 +42,7 @@ import com.google.android.exoplayer2.Timeline; @Override public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled) { + // Find next window within current child. int childIndex = getChildIndexByWindowIndex(windowIndex); int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex); int nextWindowIndexInChild = getTimelineByChildIndex(childIndex).getNextWindowIndex( @@ -51,12 +52,16 @@ import com.google.android.exoplayer2.Timeline; if (nextWindowIndexInChild != C.INDEX_UNSET) { return firstWindowIndexInChild + nextWindowIndexInChild; } - int nextChildIndex = shuffleModeEnabled ? shuffleOrder.getNextIndex(childIndex) - : childIndex + 1; - if (nextChildIndex != C.INDEX_UNSET && nextChildIndex < childCount) { + // If not found, find first window of next non-empty child. + int nextChildIndex = getNextChildIndex(childIndex, shuffleModeEnabled); + while (nextChildIndex != C.INDEX_UNSET && getTimelineByChildIndex(nextChildIndex).isEmpty()) { + nextChildIndex = getNextChildIndex(nextChildIndex, shuffleModeEnabled); + } + if (nextChildIndex != C.INDEX_UNSET) { return getFirstWindowIndexByChildIndex(nextChildIndex) + getTimelineByChildIndex(nextChildIndex).getFirstWindowIndex(shuffleModeEnabled); } + // If not found, this is the last window. if (repeatMode == Player.REPEAT_MODE_ALL) { return getFirstWindowIndex(shuffleModeEnabled); } @@ -66,6 +71,7 @@ import com.google.android.exoplayer2.Timeline; @Override public int getPreviousWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled) { + // Find previous window within current child. int childIndex = getChildIndexByWindowIndex(windowIndex); int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex); int previousWindowIndexInChild = getTimelineByChildIndex(childIndex).getPreviousWindowIndex( @@ -75,12 +81,17 @@ import com.google.android.exoplayer2.Timeline; if (previousWindowIndexInChild != C.INDEX_UNSET) { return firstWindowIndexInChild + previousWindowIndexInChild; } - int previousChildIndex = shuffleModeEnabled ? shuffleOrder.getPreviousIndex(childIndex) - : childIndex - 1; - if (previousChildIndex != C.INDEX_UNSET && previousChildIndex >= 0) { + // If not found, find last window of previous non-empty child. + int previousChildIndex = getPreviousChildIndex(childIndex, shuffleModeEnabled); + while (previousChildIndex != C.INDEX_UNSET + && getTimelineByChildIndex(previousChildIndex).isEmpty()) { + previousChildIndex = getPreviousChildIndex(previousChildIndex, shuffleModeEnabled); + } + if (previousChildIndex != C.INDEX_UNSET) { return getFirstWindowIndexByChildIndex(previousChildIndex) + getTimelineByChildIndex(previousChildIndex).getLastWindowIndex(shuffleModeEnabled); } + // If not found, this is the first window. if (repeatMode == Player.REPEAT_MODE_ALL) { return getLastWindowIndex(shuffleModeEnabled); } @@ -89,14 +100,36 @@ import com.google.android.exoplayer2.Timeline; @Override public int getLastWindowIndex(boolean shuffleModeEnabled) { + if (childCount == 0) { + return C.INDEX_UNSET; + } + // Find last non-empty child. int lastChildIndex = shuffleModeEnabled ? shuffleOrder.getLastIndex() : childCount - 1; + while (getTimelineByChildIndex(lastChildIndex).isEmpty()) { + lastChildIndex = getPreviousChildIndex(lastChildIndex, shuffleModeEnabled); + if (lastChildIndex == C.INDEX_UNSET) { + // All children are empty. + return C.INDEX_UNSET; + } + } return getFirstWindowIndexByChildIndex(lastChildIndex) + getTimelineByChildIndex(lastChildIndex).getLastWindowIndex(shuffleModeEnabled); } @Override public int getFirstWindowIndex(boolean shuffleModeEnabled) { + if (childCount == 0) { + return C.INDEX_UNSET; + } + // Find first non-empty child. int firstChildIndex = shuffleModeEnabled ? shuffleOrder.getFirstIndex() : 0; + while (getTimelineByChildIndex(firstChildIndex).isEmpty()) { + firstChildIndex = getNextChildIndex(firstChildIndex, shuffleModeEnabled); + if (firstChildIndex == C.INDEX_UNSET) { + // All children are empty. + return C.INDEX_UNSET; + } + } return getFirstWindowIndexByChildIndex(firstChildIndex) + getTimelineByChildIndex(firstChildIndex).getFirstWindowIndex(shuffleModeEnabled); } @@ -196,4 +229,14 @@ import com.google.android.exoplayer2.Timeline; */ protected abstract Object getChildUidByChildIndex(int childIndex); + private int getNextChildIndex(int childIndex, boolean shuffleModeEnabled) { + return shuffleModeEnabled ? shuffleOrder.getNextIndex(childIndex) + : childIndex < childCount - 1 ? childIndex + 1 : C.INDEX_UNSET; + } + + private int getPreviousChildIndex(int childIndex, boolean shuffleModeEnabled) { + return shuffleModeEnabled ? shuffleOrder.getPreviousIndex(childIndex) + : childIndex > 0 ? childIndex - 1 : C.INDEX_UNSET; + } + } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java index 0c7bcece68..4cf3843ea1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java @@ -90,15 +90,19 @@ public final class ConcatenatingMediaSource implements MediaSource { @Override public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { this.listener = listener; - for (int i = 0; i < mediaSources.length; i++) { - if (!duplicateFlags[i]) { - final int index = i; - mediaSources[i].prepareSource(player, false, new Listener() { - @Override - public void onSourceInfoRefreshed(Timeline timeline, Object manifest) { - handleSourceInfoRefreshed(index, timeline, manifest); - } - }); + if (mediaSources.length == 0) { + listener.onSourceInfoRefreshed(Timeline.EMPTY, null); + } else { + for (int i = 0; i < mediaSources.length; i++) { + if (!duplicateFlags[i]) { + final int index = i; + mediaSources[i].prepareSource(player, false, new Listener() { + @Override + public void onSourceInfoRefreshed(Timeline timeline, Object manifest) { + handleSourceInfoRefreshed(index, timeline, manifest); + } + }); + } } } } @@ -245,12 +249,12 @@ public final class ConcatenatingMediaSource implements MediaSource { @Override protected int getChildIndexByPeriodIndex(int periodIndex) { - return Util.binarySearchFloor(sourcePeriodOffsets, periodIndex, true, false) + 1; + return Util.binarySearchFloor(sourcePeriodOffsets, periodIndex + 1, false, false) + 1; } @Override protected int getChildIndexByWindowIndex(int windowIndex) { - return Util.binarySearchFloor(sourceWindowOffsets, windowIndex, true, false) + 1; + return Util.binarySearchFloor(sourceWindowOffsets, windowIndex + 1, false, false) + 1; } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java index 9c1e7ec1ba..8614cf9c85 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java @@ -395,7 +395,14 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl private int findMediaSourceHolderByPeriodIndex(int periodIndex) { query.firstPeriodIndexInChild = periodIndex; int index = Collections.binarySearch(mediaSourceHolders, query); - return index >= 0 ? index : -index - 2; + if (index < 0) { + return -index - 2; + } + while (index < mediaSourceHolders.size() - 1 + && mediaSourceHolders.get(index + 1).firstPeriodIndexInChild == periodIndex) { + index++; + } + return index; } private static final class MediaSourceHolder implements Comparable { @@ -456,12 +463,12 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl @Override protected int getChildIndexByPeriodIndex(int periodIndex) { - return Util.binarySearchFloor(firstPeriodInChildIndices, periodIndex, true, false); + return Util.binarySearchFloor(firstPeriodInChildIndices, periodIndex + 1, false, false); } @Override protected int getChildIndexByWindowIndex(int windowIndex) { - return Util.binarySearchFloor(firstWindowInChildIndices, windowIndex, true, false); + return Util.binarySearchFloor(firstWindowInChildIndices, windowIndex + 1, false, false); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java index 00e3c50506..b23b36dcf3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java @@ -107,8 +107,10 @@ public final class LoopingMediaSource implements MediaSource { childPeriodCount = childTimeline.getPeriodCount(); childWindowCount = childTimeline.getWindowCount(); this.loopCount = loopCount; - Assertions.checkState(loopCount <= Integer.MAX_VALUE / childPeriodCount, - "LoopingMediaSource contains too many periods"); + if (childPeriodCount > 0) { + Assertions.checkState(loopCount <= Integer.MAX_VALUE / childPeriodCount, + "LoopingMediaSource contains too many periods"); + } } @Override diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java index 74129a0e69..c61aac708c 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java @@ -36,6 +36,10 @@ public final class TimelineAsserts { public static void assertEmpty(Timeline timeline) { assertWindowIds(timeline); assertPeriodCounts(timeline); + for (boolean shuffled : new boolean[] {false, true}) { + assertEquals(C.INDEX_UNSET, timeline.getFirstWindowIndex(shuffled)); + assertEquals(C.INDEX_UNSET, timeline.getLastWindowIndex(shuffled)); + } } /** @@ -119,8 +123,9 @@ public final class TimelineAsserts { expectedWindowIndex++; } assertEquals(expectedWindowIndex, period.windowIndex); + assertEquals(i, timeline.getIndexOfPeriod(period.uid)); for (@Player.RepeatMode int repeatMode - : new int[] { Player.REPEAT_MODE_OFF, Player.REPEAT_MODE_ONE, Player.REPEAT_MODE_ALL }) { + : new int[] {Player.REPEAT_MODE_OFF, Player.REPEAT_MODE_ONE, Player.REPEAT_MODE_ALL}) { if (i < accumulatedPeriodCounts[expectedWindowIndex + 1] - 1) { assertEquals(i + 1, timeline.getNextPeriodIndex(i, period, window, repeatMode, false)); } else { From cc58b515b773be93ba4572d5ac221cae856ee1ae Mon Sep 17 00:00:00 2001 From: eguven Date: Fri, 25 Aug 2017 09:35:10 -0700 Subject: [PATCH 0311/2472] Make Downloaders open source ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166486294 --- .../google/android/exoplayer2/offline/DownloadException.java | 2 -- .../java/com/google/android/exoplayer2/offline/Downloader.java | 2 -- .../exoplayer2/offline/DownloaderConstructorHelper.java | 2 -- .../android/exoplayer2/offline/ProgressiveDownloader.java | 2 -- .../google/android/exoplayer2/offline/SegmentDownloader.java | 2 -- .../exoplayer2/source/hls/offline/HlsDownloadTestData.java | 3 --- .../exoplayer2/source/hls/offline/HlsDownloaderTest.java | 2 -- .../android/exoplayer2/source/hls/offline/HlsDownloader.java | 2 -- .../source/smoothstreaming/offline/SsDownloader.java | 2 -- .../android/exoplayer2/playbacktests/gts/DashDownloadTest.java | 2 -- 10 files changed, 21 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadException.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadException.java index 239195892c..730ce2d3e8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadException.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadException.java @@ -15,11 +15,9 @@ */ package com.google.android.exoplayer2.offline; -import com.google.android.exoplayer2.util.ClosedSource; import java.io.IOException; /** Thrown on an error during downloading. */ -@ClosedSource(reason = "Not ready yet") public final class DownloadException extends IOException { /** @param message The message for the exception. */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/Downloader.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/Downloader.java index a130bb4052..b8d9432c63 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/Downloader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/Downloader.java @@ -17,13 +17,11 @@ package com.google.android.exoplayer2.offline; import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.util.ClosedSource; import java.io.IOException; /** * An interface for stream downloaders. */ -@ClosedSource(reason = "Not ready yet") public interface Downloader { /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloaderConstructorHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloaderConstructorHelper.java index 5f9a4d973a..9ef9366397 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloaderConstructorHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloaderConstructorHelper.java @@ -27,11 +27,9 @@ import com.google.android.exoplayer2.upstream.cache.Cache; import com.google.android.exoplayer2.upstream.cache.CacheDataSink; import com.google.android.exoplayer2.upstream.cache.CacheDataSource; import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.ClosedSource; import com.google.android.exoplayer2.util.PriorityTaskManager; /** A helper class that holds necessary parameters for {@link Downloader} construction. */ -@ClosedSource(reason = "Not ready yet") public final class DownloaderConstructorHelper { private final Cache cache; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java index c6bb3bc432..e5aa429424 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java @@ -23,14 +23,12 @@ import com.google.android.exoplayer2.upstream.cache.Cache; import com.google.android.exoplayer2.upstream.cache.CacheDataSource; import com.google.android.exoplayer2.upstream.cache.CacheUtil; import com.google.android.exoplayer2.upstream.cache.CacheUtil.CachingCounters; -import com.google.android.exoplayer2.util.ClosedSource; import com.google.android.exoplayer2.util.PriorityTaskManager; import java.io.IOException; /** * A downloader for progressive media streams. */ -@ClosedSource(reason = "Not ready yet") public final class ProgressiveDownloader implements Downloader { private static final int BUFFER_SIZE_BYTES = 128 * 1024; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java index 93e7c57470..d81df90b81 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java @@ -25,7 +25,6 @@ import com.google.android.exoplayer2.upstream.cache.Cache; import com.google.android.exoplayer2.upstream.cache.CacheDataSource; import com.google.android.exoplayer2.upstream.cache.CacheUtil; import com.google.android.exoplayer2.upstream.cache.CacheUtil.CachingCounters; -import com.google.android.exoplayer2.util.ClosedSource; import com.google.android.exoplayer2.util.PriorityTaskManager; import java.io.IOException; import java.util.Collections; @@ -40,7 +39,6 @@ import java.util.List; * @param The type of the manifest object. * @param The type of the representation key object. */ -@ClosedSource(reason = "Not ready yet") public abstract class SegmentDownloader implements Downloader { /** Smallest unit of content to be downloaded. */ diff --git a/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadTestData.java b/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadTestData.java index 133bf19dba..ec70fb1200 100644 --- a/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadTestData.java +++ b/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadTestData.java @@ -15,12 +15,9 @@ */ package com.google.android.exoplayer2.source.hls.offline; -import com.google.android.exoplayer2.util.ClosedSource; - /** * Data for HLS downloading tests. */ -@ClosedSource(reason = "Not ready yet") /* package */ interface HlsDownloadTestData { String MASTER_PLAYLIST_URI = "test.m3u8"; diff --git a/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloaderTest.java b/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloaderTest.java index 28afe450eb..ebf73ebfd7 100644 --- a/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloaderTest.java +++ b/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloaderTest.java @@ -39,12 +39,10 @@ import com.google.android.exoplayer2.testutil.FakeDataSet; import com.google.android.exoplayer2.testutil.FakeDataSource.Factory; import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor; import com.google.android.exoplayer2.upstream.cache.SimpleCache; -import com.google.android.exoplayer2.util.ClosedSource; import com.google.android.exoplayer2.util.Util; import java.io.File; /** Unit tests for {@link HlsDownloader}. */ -@ClosedSource(reason = "Not ready yet") public class HlsDownloaderTest extends InstrumentationTestCase { private SimpleCache cache; diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java index 488b85e78a..ac8ec5ee5e 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java @@ -27,7 +27,6 @@ import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.ParsingLoadable; -import com.google.android.exoplayer2.util.ClosedSource; import com.google.android.exoplayer2.util.UriUtil; import java.io.IOException; import java.util.ArrayList; @@ -41,7 +40,6 @@ import java.util.List; * #selectRepresentations(Object[])}. As key, string form of the rendition's url is used. The urls * can be absolute or relative to the master playlist url. */ -@ClosedSource(reason = "Not ready yet") public final class HlsDownloader extends SegmentDownloader { /** diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java index fe9c21d855..21cacdc6f3 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java @@ -26,7 +26,6 @@ import com.google.android.exoplayer2.source.smoothstreaming.manifest.TrackKey; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.ParsingLoadable; -import com.google.android.exoplayer2.util.ClosedSource; import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -60,7 +59,6 @@ import java.util.List; * new CacheDataSource(cache, factory.createDataSource(), CacheDataSource.FLAG_BLOCK_ON_CACHE);} * */ -@ClosedSource(reason = "Not ready yet") public final class SsDownloader extends SegmentDownloader { /** diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashDownloadTest.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashDownloadTest.java index 706dd72166..66884f3e5b 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashDownloadTest.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashDownloadTest.java @@ -32,7 +32,6 @@ import com.google.android.exoplayer2.upstream.cache.CacheDataSource; import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory; import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor; import com.google.android.exoplayer2.upstream.cache.SimpleCache; -import com.google.android.exoplayer2.util.ClosedSource; import com.google.android.exoplayer2.util.Util; import java.io.File; import java.io.IOException; @@ -43,7 +42,6 @@ import java.util.List; /** * Tests downloaded DASH playbacks. */ -@ClosedSource(reason = "Not ready yet") public final class DashDownloadTest extends ActivityInstrumentationTestCase2 { private static final String TAG = "DashDownloadTest"; From ba6d208fe95b9c9eef431fdd6f6185794154e1a7 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 29 Aug 2017 02:15:08 -0700 Subject: [PATCH 0312/2472] Use Math.abs in Sonic.java ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166820970 --- .../main/java/com/google/android/exoplayer2/audio/Sonic.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index ef7877ae1e..5c5ac06da3 100644 --- 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 @@ -241,7 +241,7 @@ import java.util.Arrays; 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; + diff += Math.abs(sVal - pVal); } // 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 From 3e762270062726a690be50d285474175c81ae91f Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 29 Aug 2017 07:17:24 -0700 Subject: [PATCH 0313/2472] Don't copy primary-track format to non-primary tracks Copying non-primary-track formats to non-primary tracks looks non-trivial (I tried; went down a dead-end), so leaving that for now. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166843123 --- .../source/chunk/BaseMediaChunkOutput.java | 27 +++++++++++++++---- .../source/chunk/ChunkExtractorWrapper.java | 24 ++++++++++++----- .../source/chunk/ChunkSampleStream.java | 10 ++----- 3 files changed, 41 insertions(+), 20 deletions(-) diff --git a/library/core/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 index 9531aaf32e..0b6c196d7c 100644 --- a/library/core/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 @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source.chunk; +import android.support.annotation.Nullable; import android.util.Log; import com.google.android.exoplayer2.extractor.DummyTrackOutput; import com.google.android.exoplayer2.extractor.TrackOutput; @@ -32,12 +33,23 @@ import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper.TrackOut private final SampleQueue[] sampleQueues; /** - * @param trackTypes The track types of the individual track outputs. - * @param sampleQueues The individual sample queues. + * @param primaryTrackType The type of the primary track. + * @param primarySampleQueue The primary track sample queues. + * @param embeddedTrackTypes The types of any embedded tracks, or null. + * @param embeddedSampleQueues The track sample queues for any embedded tracks, or null. */ - public BaseMediaChunkOutput(int[] trackTypes, SampleQueue[] sampleQueues) { - this.trackTypes = trackTypes; - this.sampleQueues = sampleQueues; + @SuppressWarnings("ConstantConditions") + public BaseMediaChunkOutput(int primaryTrackType, SampleQueue primarySampleQueue, + @Nullable int[] embeddedTrackTypes, @Nullable SampleQueue[] embeddedSampleQueues) { + int embeddedTrackCount = embeddedTrackTypes == null ? 0 : embeddedTrackTypes.length; + trackTypes = new int[1 + embeddedTrackCount]; + sampleQueues = new SampleQueue[1 + embeddedTrackCount]; + trackTypes[0] = primaryTrackType; + sampleQueues[0] = primarySampleQueue; + for (int i = 0; i < embeddedTrackCount; i++) { + trackTypes[i + 1] = embeddedTrackTypes[i]; + sampleQueues[i + 1] = embeddedSampleQueues[i]; + } } @Override @@ -51,6 +63,11 @@ import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper.TrackOut return new DummyTrackOutput(); } + @Override + public boolean isPrimaryTrack(int type) { + return type == trackTypes[0]; + } + /** * Returns the current absolute write indices of the individual sample queues. */ diff --git a/library/core/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 index 07d1cce8cb..eda9ed3cf7 100644 --- a/library/core/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 @@ -45,13 +45,22 @@ public final class ChunkExtractorWrapper implements ExtractorOutput { *

        * The same {@link TrackOutput} is returned if multiple calls are made with the same {@code id}. * - * @param id A track identifier. - * @param type The type of the track. Typically one of the - * {@link com.google.android.exoplayer2.C} {@code TRACK_TYPE_*} constants. + * @param id The track identifier. + * @param type The track type. Typically one of the {@link com.google.android.exoplayer2.C} + * {@code TRACK_TYPE_*} constants. * @return The {@link TrackOutput} for the given track identifier. */ TrackOutput track(int id, int type); + /** + * Returns whether the specified type corresponds to the primary track. + * + * @param type The track type. Typically one of the {@link com.google.android.exoplayer2.C} + * {@code TRACK_TYPE_*} constants. + * @return Whether {@code type} corresponds to the primary track. + */ + boolean isPrimaryTrack(int type); + } public final Extractor extractor; @@ -146,6 +155,7 @@ public final class ChunkExtractorWrapper implements ExtractorOutput { private final Format manifestFormat; public Format sampleFormat; + private boolean isPrimaryTrack; private TrackOutput trackOutput; public BindingTrackOutput(int id, int type, Format manifestFormat) { @@ -159,17 +169,17 @@ public final class ChunkExtractorWrapper implements ExtractorOutput { trackOutput = new DummyTrackOutput(); return; } + isPrimaryTrack = trackOutputProvider.isPrimaryTrack(type); trackOutput = trackOutputProvider.track(id, type); - if (trackOutput != null) { + if (sampleFormat != null) { trackOutput.format(sampleFormat); } } @Override public void format(Format format) { - // TODO: This should only happen for the primary track. Additional metadata/text tracks need - // to be copied with different manifest derived formats. - sampleFormat = format.copyWithManifestFormatInfo(manifestFormat); + // TODO: Non-primary tracks should be copied with data from their own manifest formats. + sampleFormat = isPrimaryTrack ? format.copyWithManifestFormatInfo(manifestFormat) : format; trackOutput.format(sampleFormat); } diff --git a/library/core/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 index f2609a0ffd..e8586f7230 100644 --- a/library/core/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 @@ -87,21 +87,15 @@ public class ChunkSampleStream implements SampleStream, S int embeddedTrackCount = embeddedTrackTypes == null ? 0 : embeddedTrackTypes.length; embeddedSampleQueues = new SampleQueue[embeddedTrackCount]; embeddedTracksSelected = new boolean[embeddedTrackCount]; - int[] trackTypes = new int[1 + embeddedTrackCount]; - SampleQueue[] sampleQueues = new SampleQueue[1 + embeddedTrackCount]; primarySampleQueue = new SampleQueue(allocator); - trackTypes[0] = primaryTrackType; - sampleQueues[0] = primarySampleQueue; - for (int i = 0; i < embeddedTrackCount; i++) { SampleQueue sampleQueue = new SampleQueue(allocator); embeddedSampleQueues[i] = sampleQueue; - sampleQueues[i + 1] = sampleQueue; - trackTypes[i + 1] = embeddedTrackTypes[i]; } - mediaChunkOutput = new BaseMediaChunkOutput(trackTypes, sampleQueues); + mediaChunkOutput = new BaseMediaChunkOutput(primaryTrackType, primarySampleQueue, + embeddedTrackTypes, embeddedSampleQueues); pendingResetPositionUs = positionUs; lastSeekPositionUs = positionUs; } From f44e30c7543e63bb8aec54f27626d4c70e808c48 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 29 Aug 2017 07:19:48 -0700 Subject: [PATCH 0314/2472] Fix mapping CLEARKEY_UUID to COMMON_PSSH_UUID This mapping when we call into platform components also needs to be applied when creating the MediaCrypto instance. The fix is to stop propagating the UUID through all the createMediaCrypto methods. This is unnecessary, since the eventual target already knows its own UUID! Issue: #3138 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166843372 --- .../android/exoplayer2/drm/DefaultDrmSession.java | 2 +- .../google/android/exoplayer2/drm/DrmInitData.java | 2 +- .../google/android/exoplayer2/drm/ExoMediaDrm.java | 4 +--- .../android/exoplayer2/drm/FrameworkMediaDrm.java | 11 +++++------ 4 files changed, 8 insertions(+), 11 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java index cfb2cf9d8a..b4dab7b971 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java @@ -226,7 +226,7 @@ import java.util.UUID; try { sessionId = mediaDrm.openSession(); - mediaCrypto = mediaDrm.createMediaCrypto(uuid, sessionId); + mediaCrypto = mediaDrm.createMediaCrypto(sessionId); state = STATE_OPENED; return true; } catch (NotProvisionedException e) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java index 9fa6547a00..d814ece31d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java @@ -60,7 +60,7 @@ public final class DrmInitData implements Comparator, Parcelable { if (cloneSchemeDatas) { schemeDatas = schemeDatas.clone(); } - // Sorting ensures that universal scheme data(i.e. data that applies to all schemes) is matched + // Sorting ensures that universal scheme data (i.e. data that applies to all schemes) is matched // last. It's also required by the equals and hashcode implementations. Arrays.sort(schemeDatas, this); // Check for no duplicates. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java index 3d765dbef5..63387f19e1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java @@ -22,7 +22,6 @@ import android.media.NotProvisionedException; import android.media.ResourceBusyException; import java.util.HashMap; import java.util.Map; -import java.util.UUID; /** * Used to obtain keys for decrypting protected media streams. See {@link android.media.MediaDrm}. @@ -137,11 +136,10 @@ public interface ExoMediaDrm { /** * @see android.media.MediaCrypto#MediaCrypto(UUID, byte[]) * - * @param uuid The UUID of the crypto scheme. * @param initData Opaque initialization data specific to the crypto scheme. * @return An object extends {@link ExoMediaCrypto}, using opaque crypto scheme specific data. * @throws MediaCryptoException */ - T createMediaCrypto(UUID uuid, byte[] initData) throws MediaCryptoException; + T createMediaCrypto(byte[] initData) throws MediaCryptoException; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java index 5d0cb038d4..d664cb69a9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java @@ -37,6 +37,7 @@ import java.util.UUID; @TargetApi(18) public final class FrameworkMediaDrm implements ExoMediaDrm { + private final UUID uuid; private final MediaDrm mediaDrm; /** @@ -59,10 +60,9 @@ public final class FrameworkMediaDrm implements ExoMediaDrm Date: Tue, 29 Aug 2017 08:16:11 -0700 Subject: [PATCH 0315/2472] Add media queue support to CastPlayer Also workaround the non-repeatable queue and fix other minor issues. Issue:#2283 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166848894 --- .../exoplayer2/castdemo/CastDemoUtil.java | 8 +- .../exoplayer2/castdemo/PlayerManager.java | 32 +- .../src/main/res/layout/main_activity.xml | 5 +- .../exoplayer2/ext/cast/CastPlayer.java | 520 ++++++++++++------ .../exoplayer2/ext/cast/CastTimeline.java | 114 ++++ .../DynamicConcatenatingMediaSource.java | 2 +- 6 files changed, 494 insertions(+), 187 deletions(-) create mode 100644 extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimeline.java diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/CastDemoUtil.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/CastDemoUtil.java index f819e54e50..68c5904362 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/CastDemoUtil.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/CastDemoUtil.java @@ -52,17 +52,17 @@ import java.util.List; /** * The mime type of the media sample, as required by {@link MediaInfo#setContentType}. */ - public final String type; + public final String mimeType; /** * @param uri See {@link #uri}. * @param name See {@link #name}. - * @param type See {@link #type}. + * @param mimeType See {@link #mimeType}. */ - public Sample(String uri, String name, String type) { + public Sample(String uri, String name, String mimeType) { this.uri = uri; this.name = name; - this.type = type; + this.mimeType = mimeType; } @Override diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java index 741df7eff1..8b461ec65c 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java @@ -37,6 +37,9 @@ import com.google.android.exoplayer2.ui.PlaybackControlView; import com.google.android.exoplayer2.ui.SimpleExoPlayerView; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; +import com.google.android.gms.cast.MediaInfo; +import com.google.android.gms.cast.MediaMetadata; +import com.google.android.gms.cast.MediaQueueItem; import com.google.android.gms.cast.framework.CastContext; /** @@ -95,12 +98,12 @@ import com.google.android.gms.cast.framework.CastContext; boolean playWhenReady) { this.currentSample = currentSample; if (playbackLocation == PLAYBACK_REMOTE) { - castPlayer.load(currentSample.name, currentSample.uri, currentSample.type, positionMs, - playWhenReady); + castPlayer.loadItem(buildMediaQueueItem(currentSample), positionMs); + castPlayer.setPlayWhenReady(playWhenReady); } else /* playbackLocation == PLAYBACK_LOCAL */ { + exoPlayer.prepare(buildMediaSource(currentSample), true, true); exoPlayer.setPlayWhenReady(playWhenReady); exoPlayer.seekTo(positionMs); - exoPlayer.prepare(buildMediaSource(currentSample), true, true); } } @@ -143,9 +146,18 @@ import com.google.android.gms.cast.framework.CastContext; // Internal methods. + private static MediaQueueItem buildMediaQueueItem(CastDemoUtil.Sample sample) { + MediaMetadata movieMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE); + movieMetadata.putString(MediaMetadata.KEY_TITLE, sample.name); + MediaInfo mediaInfo = new MediaInfo.Builder(sample.uri) + .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED).setContentType(sample.mimeType) + .setMetadata(movieMetadata).build(); + return new MediaQueueItem.Builder(mediaInfo).build(); + } + private static MediaSource buildMediaSource(CastDemoUtil.Sample sample) { Uri uri = Uri.parse(sample.uri); - switch (sample.type) { + switch (sample.mimeType) { case CastDemoUtil.MIME_TYPE_SS: return new SsMediaSource(uri, DATA_SOURCE_FACTORY, new DefaultSsChunkSource.Factory(DATA_SOURCE_FACTORY), null, null); @@ -158,7 +170,7 @@ import com.google.android.gms.cast.framework.CastContext; return new ExtractorMediaSource(uri, DATA_SOURCE_FACTORY, new DefaultExtractorsFactory(), null, null); default: { - throw new IllegalStateException("Unsupported type: " + sample.type); + throw new IllegalStateException("Unsupported type: " + sample.mimeType); } } } @@ -177,14 +189,16 @@ import com.google.android.gms.cast.framework.CastContext; castControlView.show(); } - long playbackPositionMs = 0; - boolean playWhenReady = true; - if (exoPlayer != null) { + long playbackPositionMs; + boolean playWhenReady; + if (this.playbackLocation == PLAYBACK_LOCAL) { playbackPositionMs = exoPlayer.getCurrentPosition(); playWhenReady = exoPlayer.getPlayWhenReady(); - } else if (this.playbackLocation == PLAYBACK_REMOTE) { + exoPlayer.stop(); + } else /* this.playbackLocation == PLAYBACK_REMOTE */ { playbackPositionMs = castPlayer.getCurrentPosition(); playWhenReady = castPlayer.getPlayWhenReady(); + castPlayer.stop(); } this.playbackLocation = playbackLocation; diff --git a/demos/cast/src/main/res/layout/main_activity.xml b/demos/cast/src/main/res/layout/main_activity.xml index 7e39320e3b..5d94931b64 100644 --- a/demos/cast/src/main/res/layout/main_activity.xml +++ b/demos/cast/src/main/res/layout/main_activity.xml @@ -35,7 +35,8 @@ android:id="@+id/cast_control_view" android:layout_width="match_parent" android:layout_height="0dp" - app:show_timeout="-1" android:layout_weight="2" - android:visibility="gone"/> + android:visibility="gone" + app:repeat_toggle_modes="all|one" + app:show_timeout="-1"/> diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java index e79fef74d5..234b8384f9 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java @@ -15,23 +15,24 @@ */ package com.google.android.exoplayer2.ext.cast; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Log; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.source.SinglePeriodTimeline; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.FixedTrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import com.google.android.gms.cast.CastStatusCodes; import com.google.android.gms.cast.MediaInfo; -import com.google.android.gms.cast.MediaMetadata; +import com.google.android.gms.cast.MediaQueueItem; import com.google.android.gms.cast.MediaStatus; import com.google.android.gms.cast.MediaTrack; import com.google.android.gms.cast.framework.CastContext; @@ -41,6 +42,7 @@ import com.google.android.gms.cast.framework.SessionManagerListener; import com.google.android.gms.cast.framework.media.RemoteMediaClient; import com.google.android.gms.cast.framework.media.RemoteMediaClient.MediaChannelResult; import com.google.android.gms.common.api.CommonStatusCodes; +import com.google.android.gms.common.api.PendingResult; import com.google.android.gms.common.api.ResultCallback; import java.util.List; import java.util.concurrent.CopyOnWriteArraySet; @@ -48,19 +50,16 @@ import java.util.concurrent.CopyOnWriteArraySet; /** * {@link Player} implementation that communicates with a Cast receiver app. * - *

        Calls to the methods in this class depend on the availability of an underlying cast session. - * If no session is available, method calls have no effect. To keep track of the underyling session, + *

        The behavior of this class depends on the underlying Cast session, which is obtained from the + * Cast context passed to {@link #CastPlayer}. To keep track of the session, * {@link #isCastSessionAvailable()} can be queried and {@link SessionAvailabilityListener} can be - * implemented and attached to the player. + * implemented and attached to the player.

        * - *

        Methods should be called on the application's main thread. + *

        If no session is available, the player state will remain unchanged and calls to methods that + * alter it will be ignored. Querying the player state is possible even when no session is + * available, in which case, the last observed receiver app state is reported.

        * - *

        Known issues: - *

          - *
        • Part of the Cast API is not exposed through this interface. For instance, volume settings - * and track selection.
        • - *
        • Repeat mode is not working. See [internal: b/64137174].
        • - *
        + *

        Methods should be called on the application's main thread.

        */ public final class CastPlayer implements Player { @@ -95,10 +94,12 @@ public final class CastPlayer implements Player { private final CastContext castContext; private final Timeline.Window window; + private final Timeline.Period period; + + private RemoteMediaClient remoteMediaClient; // Result callbacks. private final StatusListener statusListener; - private final RepeatModeResultCallback repeatModeResultCallback; private final SeekResultCallback seekResultCallback; // Listeners. @@ -106,11 +107,15 @@ public final class CastPlayer implements Player { private SessionAvailabilityListener sessionAvailabilityListener; // Internal state. - private RemoteMediaClient remoteMediaClient; - private Timeline currentTimeline; + private CastTimeline currentTimeline; private TrackGroupArray currentTrackGroups; private TrackSelectionArray currentTrackSelection; + private int playbackState; + private int repeatMode; + private int currentWindowIndex; + private boolean playWhenReady; private long lastReportedPositionMs; + private int pendingSeekWindowIndex; private long pendingSeekPositionMs; /** @@ -119,41 +124,142 @@ public final class CastPlayer implements Player { public CastPlayer(CastContext castContext) { this.castContext = castContext; window = new Timeline.Window(); + period = new Timeline.Period(); statusListener = new StatusListener(); - repeatModeResultCallback = new RepeatModeResultCallback(); seekResultCallback = new SeekResultCallback(); listeners = new CopyOnWriteArraySet<>(); + SessionManager sessionManager = castContext.getSessionManager(); sessionManager.addSessionManagerListener(statusListener, CastSession.class); CastSession session = sessionManager.getCurrentCastSession(); remoteMediaClient = session != null ? session.getRemoteMediaClient() : null; + + playbackState = STATE_IDLE; + repeatMode = REPEAT_MODE_OFF; + currentTimeline = CastTimeline.EMPTY_CAST_TIMELINE; + currentTrackGroups = EMPTY_TRACK_GROUP_ARRAY; + currentTrackSelection = EMPTY_TRACK_SELECTION_ARRAY; + pendingSeekWindowIndex = C.INDEX_UNSET; pendingSeekPositionMs = C.TIME_UNSET; updateInternalState(); } + // Media Queue manipulation methods. + /** - * Loads media into the receiver app. + * Loads a single item media queue. If no session is available, does nothing. * - * @param title The title of the media sample. - * @param url The url from which the media is obtained. - * @param contentMimeType The mime type of the content to play. - * @param positionMs The position at which the playback should start in milliseconds. - * @param playWhenReady Whether the player should start playback as soon as it is ready to do so. + * @param item The item to load. + * @param positionMs The position at which the playback should start in milliseconds relative to + * the start of the item at {@code startIndex}. + * @return The Cast {@code PendingResult}, or null if no session is available. */ - public void load(String title, String url, String contentMimeType, long positionMs, - boolean playWhenReady) { - lastReportedPositionMs = 0; - if (remoteMediaClient != null) { - MediaMetadata movieMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE); - movieMetadata.putString(MediaMetadata.KEY_TITLE, title); - MediaInfo mediaInfo = new MediaInfo.Builder(url).setStreamType(MediaInfo.STREAM_TYPE_BUFFERED) - .setContentType(contentMimeType).setMetadata(movieMetadata).build(); - remoteMediaClient.load(mediaInfo, playWhenReady, positionMs); - } + public PendingResult loadItem(MediaQueueItem item, long positionMs) { + return loadItems(new MediaQueueItem[] {item}, 0, positionMs, REPEAT_MODE_OFF); } /** - * Returns whether a cast session is available for playback. + * Loads a media queue. If no session is available, does nothing. + * + * @param items The items to load. + * @param startIndex The index of the item at which playback should start. + * @param positionMs The position at which the playback should start in milliseconds relative to + * the start of the item at {@code startIndex}. + * @param repeatMode The repeat mode for the created media queue. + * @return The Cast {@code PendingResult}, or null if no session is available. + */ + public PendingResult loadItems(MediaQueueItem[] items, int startIndex, + long positionMs, @RepeatMode int repeatMode) { + if (remoteMediaClient != null) { + return remoteMediaClient.queueLoad(items, startIndex, getCastRepeatMode(repeatMode), + positionMs, null); + } + return null; + } + + /** + * Appends a sequence of items to the media queue. If no media queue exists, does nothing. + * + * @param items The items to append. + * @return The Cast {@code PendingResult}, or null if no media queue exists. + */ + public PendingResult addItems(MediaQueueItem... items) { + return addItems(MediaQueueItem.INVALID_ITEM_ID, items); + } + + /** + * Inserts a sequence of items into the media queue. If no media queue or period with id + * {@code periodId} exist, does nothing. + * + * @param periodId The id of the period ({@see #getCurrentTimeline}) that corresponds to the item + * that will follow immediately after the inserted items. + * @param items The items to insert. + * @return The Cast {@code PendingResult}, or null if no media queue or no period with id + * {@code periodId} exist. + */ + public PendingResult addItems(int periodId, MediaQueueItem... items) { + if (getMediaStatus() != null && (periodId == MediaQueueItem.INVALID_ITEM_ID + || currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET)) { + return remoteMediaClient.queueInsertItems(items, periodId, null); + } + return null; + } + + /** + * Removes an item from the media queue. If no media queue or period with id {@code periodId} + * exist, does nothing. + * + * @param periodId The id of the period ({@see #getCurrentTimeline}) that corresponds to the item + * to remove. + * @return The Cast {@code PendingResult}, or null if no media queue or no period with id + * {@code periodId} exist. + */ + public PendingResult removeItem(int periodId) { + if (getMediaStatus() != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET) { + return remoteMediaClient.queueRemoveItem(periodId, null); + } + return null; + } + + /** + * Moves an existing item within the media queue. If no media queue or period with id + * {@code periodId} exist, does nothing. + * + * @param periodId The id of the period ({@see #getCurrentTimeline}) that corresponds to the item + * to move. + * @param newIndex The target index of the item in the media queue. Must be in the range + * 0 <= index < {@link Timeline#getPeriodCount()}, as provided by + * {@link #getCurrentTimeline()}. + * @return The Cast {@code PendingResult}, or null if no media queue or no period with id + * {@code periodId} exist. + */ + public PendingResult moveItem(int periodId, int newIndex) { + Assertions.checkArgument(newIndex >= 0 && newIndex < currentTimeline.getPeriodCount()); + if (getMediaStatus() != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET) { + return remoteMediaClient.queueMoveItemToNewIndex(periodId, newIndex, null); + } + return null; + } + + /** + * Returns the item that corresponds to the period with the given id, or null if no media queue or + * period with id {@code periodId} exist. + * + * @param periodId The id of the period ({@see #getCurrentTimeline}) that corresponds to the item + * to get. + * @return The item that corresponds to the period with the given id, or null if no media queue or + * period with id {@code periodId} exist. + */ + public MediaQueueItem getItem(int periodId) { + MediaStatus mediaStatus = getMediaStatus(); + return mediaStatus != null && currentTimeline.getIndexOfPeriod(periodId) != C.INDEX_UNSET + ? mediaStatus.getItemById(periodId) : null; + } + + // CastSession methods. + + /** + * Returns whether a cast session is available. */ public boolean isCastSessionAvailable() { return remoteMediaClient != null; @@ -182,21 +288,7 @@ public final class CastPlayer implements Player { @Override public int getPlaybackState() { - if (remoteMediaClient == null) { - return STATE_IDLE; - } - int receiverAppStatus = remoteMediaClient.getPlayerState(); - switch (receiverAppStatus) { - case MediaStatus.PLAYER_STATE_BUFFERING: - return STATE_BUFFERING; - case MediaStatus.PLAYER_STATE_PLAYING: - case MediaStatus.PLAYER_STATE_PAUSED: - return STATE_READY; - case MediaStatus.PLAYER_STATE_IDLE: - case MediaStatus.PLAYER_STATE_UNKNOWN: - default: - return STATE_IDLE; - } + return playbackState; } @Override @@ -213,7 +305,7 @@ public final class CastPlayer implements Player { @Override public boolean getPlayWhenReady() { - return remoteMediaClient != null && !remoteMediaClient.isPaused(); + return playWhenReady; } @Override @@ -228,13 +320,20 @@ public final class CastPlayer implements Player { @Override public void seekTo(long positionMs) { - seekTo(0, positionMs); + seekTo(getCurrentWindowIndex(), positionMs); } @Override public void seekTo(int windowIndex, long positionMs) { - if (remoteMediaClient != null) { - remoteMediaClient.seek(positionMs).setResultCallback(seekResultCallback); + MediaStatus mediaStatus = getMediaStatus(); + if (mediaStatus != null) { + if (getCurrentWindowIndex() != windowIndex) { + remoteMediaClient.queueJumpToItem((int) currentTimeline.getPeriod(windowIndex, period).uid, + positionMs, null).setResultCallback(seekResultCallback); + } else { + remoteMediaClient.seek(positionMs).setResultCallback(seekResultCallback); + } + pendingSeekWindowIndex = windowIndex; pendingSeekPositionMs = positionMs; for (EventListener listener : listeners) { listener.onPositionDiscontinuity(); @@ -287,47 +386,13 @@ public final class CastPlayer implements Player { @Override public void setRepeatMode(@RepeatMode int repeatMode) { if (remoteMediaClient != null) { - int castRepeatMode; - switch (repeatMode) { - case REPEAT_MODE_ONE: - castRepeatMode = MediaStatus.REPEAT_MODE_REPEAT_SINGLE; - break; - case REPEAT_MODE_ALL: - castRepeatMode = MediaStatus.REPEAT_MODE_REPEAT_ALL; - break; - case REPEAT_MODE_OFF: - castRepeatMode = MediaStatus.REPEAT_MODE_REPEAT_OFF; - break; - default: - throw new IllegalArgumentException(); - } - remoteMediaClient.queueSetRepeatMode(castRepeatMode, null) - .setResultCallback(repeatModeResultCallback); + remoteMediaClient.queueSetRepeatMode(getCastRepeatMode(repeatMode), null); } } @Override @RepeatMode public int getRepeatMode() { - if (remoteMediaClient == null) { - return REPEAT_MODE_OFF; - } - MediaStatus mediaStatus = getMediaStatus(); - if (mediaStatus == null) { - // No media session active, yet. - return REPEAT_MODE_OFF; - } - int castRepeatMode = mediaStatus.getQueueRepeatMode(); - switch (castRepeatMode) { - case MediaStatus.REPEAT_MODE_REPEAT_SINGLE: - return REPEAT_MODE_ONE; - case MediaStatus.REPEAT_MODE_REPEAT_ALL: - case MediaStatus.REPEAT_MODE_REPEAT_ALL_AND_SHUFFLE: - return REPEAT_MODE_ALL; - case MediaStatus.REPEAT_MODE_REPEAT_OFF: - return REPEAT_MODE_OFF; - default: - throw new IllegalStateException(); - } + return repeatMode; } @Override @@ -363,12 +428,12 @@ public final class CastPlayer implements Player { @Override public int getCurrentPeriodIndex() { - return 0; + return getCurrentWindowIndex(); } @Override public int getCurrentWindowIndex() { - return 0; + return pendingSeekWindowIndex != C.INDEX_UNSET ? pendingSeekWindowIndex : currentWindowIndex; } @Override @@ -384,14 +449,14 @@ public final class CastPlayer implements Player { @Override public long getDuration() { return currentTimeline.isEmpty() ? C.TIME_UNSET - : currentTimeline.getWindow(0, window).getDurationMs(); + : currentTimeline.getWindow(getCurrentWindowIndex(), window).getDurationMs(); } @Override public long getCurrentPosition() { - return remoteMediaClient == null ? lastReportedPositionMs - : pendingSeekPositionMs != C.TIME_UNSET ? pendingSeekPositionMs - : remoteMediaClient.getApproximateStreamPosition(); + return pendingSeekPositionMs != C.TIME_UNSET ? pendingSeekPositionMs + : remoteMediaClient != null ? remoteMediaClient.getApproximateStreamPosition() + : lastReportedPositionMs; } @Override @@ -447,6 +512,121 @@ public final class CastPlayer implements Player { // Internal methods. + public void updateInternalState() { + if (remoteMediaClient == null) { + // There is no session. We leave the state of the player as it is now. + return; + } + + int playbackState = fetchPlaybackState(remoteMediaClient); + boolean playWhenReady = !remoteMediaClient.isPaused(); + if (this.playbackState != playbackState + || this.playWhenReady != playWhenReady) { + this.playbackState = playbackState; + this.playWhenReady = playWhenReady; + for (EventListener listener : listeners) { + listener.onPlayerStateChanged(this.playWhenReady, this.playbackState); + } + } + @RepeatMode int repeatMode = fetchRepeatMode(remoteMediaClient); + if (this.repeatMode != repeatMode) { + this.repeatMode = repeatMode; + for (EventListener listener : listeners) { + listener.onRepeatModeChanged(repeatMode); + } + } + int currentWindowIndex = fetchCurrentWindowIndex(getMediaStatus()); + if (this.currentWindowIndex != currentWindowIndex) { + this.currentWindowIndex = currentWindowIndex; + for (EventListener listener : listeners) { + listener.onPositionDiscontinuity(); + } + } + if (updateTracksAndSelections()) { + for (EventListener listener : listeners) { + listener.onTracksChanged(currentTrackGroups, currentTrackSelection); + } + } + maybeUpdateTimelineAndNotify(); + } + + private void maybeUpdateTimelineAndNotify() { + if (updateTimeline()) { + for (EventListener listener : listeners) { + listener.onTimelineChanged(currentTimeline, null); + } + } + } + + /** + * Updates the current timeline and returns whether it has changed. + */ + private boolean updateTimeline() { + MediaStatus mediaStatus = getMediaStatus(); + if (mediaStatus == null) { + boolean hasChanged = currentTimeline != CastTimeline.EMPTY_CAST_TIMELINE; + currentTimeline = CastTimeline.EMPTY_CAST_TIMELINE; + return hasChanged; + } + + List items = mediaStatus.getQueueItems(); + if (!currentTimeline.represents(items)) { + currentTimeline = !items.isEmpty() ? new CastTimeline(mediaStatus.getQueueItems()) + : CastTimeline.EMPTY_CAST_TIMELINE; + return true; + } + return false; + } + + /** + * Updates the internal tracks and selection and returns whether they have changed. + */ + private boolean updateTracksAndSelections() { + if (remoteMediaClient == null) { + // There is no session. We leave the state of the player as it is now. + return false; + } + + MediaStatus mediaStatus = getMediaStatus(); + MediaInfo mediaInfo = mediaStatus != null ? mediaStatus.getMediaInfo() : null; + List castMediaTracks = mediaInfo != null ? mediaInfo.getMediaTracks() : null; + if (castMediaTracks == null || castMediaTracks.isEmpty()) { + boolean hasChanged = currentTrackGroups != EMPTY_TRACK_GROUP_ARRAY; + currentTrackGroups = EMPTY_TRACK_GROUP_ARRAY; + currentTrackSelection = EMPTY_TRACK_SELECTION_ARRAY; + return hasChanged; + } + long[] activeTrackIds = mediaStatus.getActiveTrackIds(); + if (activeTrackIds == null) { + activeTrackIds = EMPTY_TRACK_ID_ARRAY; + } + + TrackGroup[] trackGroups = new TrackGroup[castMediaTracks.size()]; + TrackSelection[] trackSelections = new TrackSelection[RENDERER_COUNT]; + for (int i = 0; i < castMediaTracks.size(); i++) { + MediaTrack mediaTrack = castMediaTracks.get(i); + trackGroups[i] = new TrackGroup(CastUtils.mediaTrackToFormat(mediaTrack)); + + long id = mediaTrack.getId(); + int trackType = MimeTypes.getTrackType(mediaTrack.getContentType()); + int rendererIndex = getRendererIndexForTrackType(trackType); + if (isTrackActive(id, activeTrackIds) && rendererIndex != C.INDEX_UNSET + && trackSelections[rendererIndex] == null) { + trackSelections[rendererIndex] = new FixedTrackSelection(trackGroups[i], 0); + } + } + TrackGroupArray newTrackGroups = new TrackGroupArray(trackGroups); + TrackSelectionArray newTrackSelections = new TrackSelectionArray(trackSelections); + + if (!newTrackGroups.equals(currentTrackGroups) + || !newTrackSelections.equals(currentTrackSelection)) { + currentTrackSelection = new TrackSelectionArray(trackSelections); + currentTrackGroups = new TrackGroupArray(trackGroups); + return true; + } + return false; + } + private void setRemoteMediaClient(@Nullable RemoteMediaClient remoteMediaClient) { if (this.remoteMediaClient == remoteMediaClient) { // Do nothing. @@ -463,6 +643,7 @@ public final class CastPlayer implements Player { } remoteMediaClient.addListener(statusListener); remoteMediaClient.addProgressListener(statusListener, PROGRESS_REPORT_PERIOD_MS); + updateInternalState(); } else { if (sessionAvailabilityListener != null) { sessionAvailabilityListener.onCastSessionUnavailable(); @@ -474,50 +655,58 @@ public final class CastPlayer implements Player { return remoteMediaClient != null ? remoteMediaClient.getMediaStatus() : null; } - private @Nullable MediaInfo getMediaInfo() { - return remoteMediaClient != null ? remoteMediaClient.getMediaInfo() : null; + /** + * Retrieves the playback state from {@code remoteMediaClient} and maps it into a {@link Player} + * state + */ + private static int fetchPlaybackState(RemoteMediaClient remoteMediaClient) { + int receiverAppStatus = remoteMediaClient.getPlayerState(); + switch (receiverAppStatus) { + case MediaStatus.PLAYER_STATE_BUFFERING: + return STATE_BUFFERING; + case MediaStatus.PLAYER_STATE_PLAYING: + case MediaStatus.PLAYER_STATE_PAUSED: + return STATE_READY; + case MediaStatus.PLAYER_STATE_IDLE: + case MediaStatus.PLAYER_STATE_UNKNOWN: + default: + return STATE_IDLE; + } } - private void updateInternalState() { - currentTimeline = Timeline.EMPTY; - currentTrackGroups = EMPTY_TRACK_GROUP_ARRAY; - currentTrackSelection = EMPTY_TRACK_SELECTION_ARRAY; - MediaInfo mediaInfo = getMediaInfo(); - if (mediaInfo == null) { - return; + /** + * Retrieves the repeat mode from {@code remoteMediaClient} and maps it into a + * {@link Player.RepeatMode}. + */ + @RepeatMode + private static int fetchRepeatMode(RemoteMediaClient remoteMediaClient) { + MediaStatus mediaStatus = remoteMediaClient.getMediaStatus(); + if (mediaStatus == null) { + // No media session active, yet. + return REPEAT_MODE_OFF; } - long streamDurationMs = mediaInfo.getStreamDuration(); - boolean isSeekable = streamDurationMs != MediaInfo.UNKNOWN_DURATION; - currentTimeline = new SinglePeriodTimeline( - isSeekable ? C.msToUs(streamDurationMs) : C.TIME_UNSET, isSeekable); - - List tracks = mediaInfo.getMediaTracks(); - if (tracks == null) { - return; + int castRepeatMode = mediaStatus.getQueueRepeatMode(); + switch (castRepeatMode) { + case MediaStatus.REPEAT_MODE_REPEAT_SINGLE: + return REPEAT_MODE_ONE; + case MediaStatus.REPEAT_MODE_REPEAT_ALL: + case MediaStatus.REPEAT_MODE_REPEAT_ALL_AND_SHUFFLE: + return REPEAT_MODE_ALL; + case MediaStatus.REPEAT_MODE_REPEAT_OFF: + return REPEAT_MODE_OFF; + default: + throw new IllegalStateException(); } + } - MediaStatus mediaStatus = getMediaStatus(); - long[] activeTrackIds = mediaStatus != null ? mediaStatus.getActiveTrackIds() : null; - if (activeTrackIds == null) { - activeTrackIds = EMPTY_TRACK_ID_ARRAY; - } - - TrackGroup[] trackGroups = new TrackGroup[tracks.size()]; - TrackSelection[] trackSelections = new TrackSelection[RENDERER_COUNT]; - for (int i = 0; i < tracks.size(); i++) { - MediaTrack mediaTrack = tracks.get(i); - trackGroups[i] = new TrackGroup(CastUtils.mediaTrackToFormat(mediaTrack)); - - long id = mediaTrack.getId(); - int trackType = MimeTypes.getTrackType(mediaTrack.getContentType()); - int rendererIndex = getRendererIndexForTrackType(trackType); - if (isTrackActive(id, activeTrackIds) && rendererIndex != C.INDEX_UNSET - && trackSelections[rendererIndex] == null) { - trackSelections[rendererIndex] = new FixedTrackSelection(trackGroups[i], 0); - } - } - currentTrackSelection = new TrackSelectionArray(trackSelections); - currentTrackGroups = new TrackGroupArray(trackGroups); + /** + * Retrieves the current item index from {@code mediaStatus} and maps it into a window index. If + * there is no media session, returns 0. + */ + private static int fetchCurrentWindowIndex(@Nullable MediaStatus mediaStatus) { + Integer currentItemId = mediaStatus != null + ? mediaStatus.getIndexById(mediaStatus.getCurrentItemId()) : null; + return currentItemId != null ? currentItemId : 0; } private static boolean isTrackActive(long id, long[] activeTrackIds) { @@ -536,6 +725,19 @@ public final class CastPlayer implements Player { : C.INDEX_UNSET; } + private static int getCastRepeatMode(@RepeatMode int repeatMode) { + switch (repeatMode) { + case REPEAT_MODE_ONE: + return MediaStatus.REPEAT_MODE_REPEAT_SINGLE; + case REPEAT_MODE_ALL: + return MediaStatus.REPEAT_MODE_REPEAT_ALL; + case REPEAT_MODE_OFF: + return MediaStatus.REPEAT_MODE_REPEAT_OFF; + default: + throw new IllegalArgumentException(); + } + } + private final class StatusListener implements RemoteMediaClient.Listener, SessionManagerListener, RemoteMediaClient.ProgressListener { @@ -550,24 +752,16 @@ public final class CastPlayer implements Player { @Override public void onStatusUpdated() { - boolean playWhenReady = getPlayWhenReady(); - int playbackState = getPlaybackState(); - for (EventListener listener : listeners) { - listener.onPlayerStateChanged(playWhenReady, playbackState); - } - } - - @Override - public void onMetadataUpdated() { updateInternalState(); - for (EventListener listener : listeners) { - listener.onTracksChanged(currentTrackGroups, currentTrackSelection); - listener.onTimelineChanged(currentTimeline, null); - } } @Override - public void onQueueStatusUpdated() {} + public void onMetadataUpdated() {} + + @Override + public void onQueueStatusUpdated() { + maybeUpdateTimelineAndNotify(); + } @Override public void onPreloadStatusUpdated() {} @@ -632,36 +826,20 @@ public final class CastPlayer implements Player { // Result callbacks hooks. - private final class RepeatModeResultCallback implements ResultCallback { - - @Override - public void onResult(MediaChannelResult result) { - int statusCode = result.getStatus().getStatusCode(); - if (statusCode == CommonStatusCodes.SUCCESS) { - int repeatMode = getRepeatMode(); - for (EventListener listener : listeners) { - listener.onRepeatModeChanged(repeatMode); - } - } else { - Log.e(TAG, "Set repeat mode failed. Error code " + statusCode + ": " - + CastUtils.getLogString(statusCode)); - } - } - - } - private final class SeekResultCallback implements ResultCallback { @Override - public void onResult(MediaChannelResult result) { + public void onResult(@NonNull MediaChannelResult result) { int statusCode = result.getStatus().getStatusCode(); - if (statusCode == CommonStatusCodes.SUCCESS) { - pendingSeekPositionMs = C.TIME_UNSET; - } else if (statusCode == CastStatusCodes.REPLACED) { + if (statusCode == CastStatusCodes.REPLACED) { // A seek was executed before this one completed. Do nothing. } else { - Log.e(TAG, "Seek failed. Error code " + statusCode + ": " - + CastUtils.getLogString(statusCode)); + pendingSeekWindowIndex = C.INDEX_UNSET; + pendingSeekPositionMs = C.TIME_UNSET; + if (statusCode != CommonStatusCodes.SUCCESS) { + Log.e(TAG, "Seek failed. Error code " + statusCode + ": " + + CastUtils.getLogString(statusCode)); + } } } diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimeline.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimeline.java new file mode 100644 index 0000000000..39b57148b2 --- /dev/null +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimeline.java @@ -0,0 +1,114 @@ +/* + * 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.ext.cast; + +import android.util.SparseIntArray; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Timeline; +import com.google.android.gms.cast.MediaInfo; +import com.google.android.gms.cast.MediaQueueItem; +import java.util.Collections; +import java.util.List; + +/** + * A {@link Timeline} for Cast media queues. + */ +/* package */ final class CastTimeline extends Timeline { + + public static final CastTimeline EMPTY_CAST_TIMELINE = + new CastTimeline(Collections.emptyList()); + + private final SparseIntArray idsToIndex; + private final int[] ids; + private final long[] durationsUs; + private final long[] defaultPositionsUs; + + public CastTimeline(List items) { + int itemCount = items.size(); + int index = 0; + idsToIndex = new SparseIntArray(itemCount); + ids = new int[itemCount]; + durationsUs = new long[itemCount]; + defaultPositionsUs = new long[itemCount]; + for (MediaQueueItem item : items) { + int itemId = item.getItemId(); + ids[index] = itemId; + idsToIndex.put(itemId, index); + durationsUs[index] = getStreamDurationUs(item.getMedia()); + defaultPositionsUs[index] = (long) (item.getStartTime() * C.MICROS_PER_SECOND); + index++; + } + } + + @Override + public int getWindowCount() { + return ids.length; + } + + @Override + public Window getWindow(int windowIndex, Window window, boolean setIds, + long defaultPositionProjectionUs) { + long durationUs = durationsUs[windowIndex]; + boolean isDynamic = durationUs == C.TIME_UNSET; + return window.set(ids[windowIndex], C.TIME_UNSET, C.TIME_UNSET, !isDynamic, isDynamic, + defaultPositionsUs[windowIndex], durationUs, windowIndex, windowIndex, 0); + } + + @Override + public int getPeriodCount() { + return ids.length; + } + + @Override + public Period getPeriod(int periodIndex, Period period, boolean setIds) { + int id = ids[periodIndex]; + return period.set(id, id, periodIndex, durationsUs[periodIndex], 0); + } + + @Override + public int getIndexOfPeriod(Object uid) { + return uid instanceof Integer ? idsToIndex.get((int) uid, C.INDEX_UNSET) : C.INDEX_UNSET; + } + + /** + * Returns whether the timeline represents a given {@code MediaQueueItem} list. + * + * @param items The {@code MediaQueueItem} list. + * @return Whether the timeline represents {@code items}. + */ + /* package */ boolean represents(List items) { + if (ids.length != items.size()) { + return false; + } + int index = 0; + for (MediaQueueItem item : items) { + if (ids[index] != item.getItemId() + || durationsUs[index] != getStreamDurationUs(item.getMedia()) + || defaultPositionsUs[index] != (long) (item.getStartTime() * C.MICROS_PER_SECOND)) { + return false; + } + index++; + } + return true; + } + + private static long getStreamDurationUs(MediaInfo mediaInfo) { + long durationMs = mediaInfo != null ? mediaInfo.getStreamDuration() + : MediaInfo.UNKNOWN_DURATION; + return durationMs != MediaInfo.UNKNOWN_DURATION ? C.msToUs(durationMs) : C.TIME_UNSET; + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java index 8614cf9c85..36c674f81a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java @@ -198,7 +198,7 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl /** * Returns the {@link MediaSource} at a specified index. * - * @param index A index in the range of 0 <= index <= {@link #getSize()}. + * @param index An index in the range of 0 <= index <= {@link #getSize()}. * @return The {@link MediaSource} at this index. */ public synchronized MediaSource getMediaSource(int index) { From b2c245281a59973def165e09e9961cd310f1ab8b Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 29 Aug 2017 08:22:30 -0700 Subject: [PATCH 0316/2472] Parse SchemeData from urn:mpeg:dash:mp4protection:2011 element Issue: #3138 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166849631 --- .../source/dash/manifest/DashManifestParser.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/library/dash/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 index 2e85f3a1ad..2f4724c258 100644 --- a/library/dash/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 @@ -347,6 +347,16 @@ public class DashManifestParser extends DefaultHandler byte[] data = null; UUID uuid = null; boolean requiresSecureDecoder = false; + + if ("urn:mpeg:dash:mp4protection:2011".equals(schemeIdUri)) { + String defaultKid = xpp.getAttributeValue(null, "cenc:default_KID"); + if (defaultKid != null) { + UUID keyId = UUID.fromString(defaultKid); + data = PsshAtomUtil.buildPsshAtom(C.COMMON_PSSH_UUID, new UUID[] {keyId}, null); + uuid = C.COMMON_PSSH_UUID; + } + } + do { xpp.next(); if (data == null && XmlPullParserUtil.isStartTag(xpp, "cenc:pssh") From 44dc3c3ab3ed599145fff5cf0986ee30880396fe Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 29 Aug 2017 08:49:51 -0700 Subject: [PATCH 0317/2472] Make all renderers DRM aware ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166852758 --- .../ext/ffmpeg/FfmpegAudioRenderer.java | 16 ++++++--- .../ext/flac/LibflacAudioRenderer.java | 14 ++++++-- .../ext/opus/LibopusAudioRenderer.java | 13 +++++-- .../ext/vp9/LibvpxVideoRenderer.java | 8 +++-- .../android/exoplayer2/BaseRenderer.java | 24 +++++++++++++ .../exoplayer2/RendererCapabilities.java | 18 ++++++---- .../audio/MediaCodecAudioRenderer.java | 24 ++++++++++--- .../audio/SimpleDecoderAudioRenderer.java | 8 +++-- .../mediacodec/MediaCodecRenderer.java | 36 ++++--------------- .../exoplayer2/metadata/MetadataRenderer.java | 6 +++- .../android/exoplayer2/text/TextRenderer.java | 10 ++++-- .../video/MediaCodecVideoRenderer.java | 10 ++++-- 12 files changed, 123 insertions(+), 64 deletions(-) diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java index 453a18476e..ed8a5b0eac 100644 --- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java +++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java @@ -22,6 +22,7 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.audio.AudioProcessor; import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer; +import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.ExoMediaCrypto; import com.google.android.exoplayer2.util.MimeTypes; @@ -58,13 +59,18 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer { } @Override - protected int supportsFormatInternal(Format format) { - if (!FfmpegLibrary.isAvailable()) { + protected int supportsFormatInternal(DrmSessionManager drmSessionManager, + Format format) { + String sampleMimeType = format.sampleMimeType; + if (!FfmpegLibrary.isAvailable() || !MimeTypes.isAudio(sampleMimeType)) { return FORMAT_UNSUPPORTED_TYPE; + } else if (!FfmpegLibrary.supportsFormat(sampleMimeType)) { + return FORMAT_UNSUPPORTED_SUBTYPE; + } else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) { + return FORMAT_UNSUPPORTED_DRM; + } else { + return FORMAT_HANDLED; } - String mimeType = format.sampleMimeType; - return FfmpegLibrary.supportsFormat(mimeType) ? FORMAT_HANDLED - : MimeTypes.isAudio(mimeType) ? FORMAT_UNSUPPORTED_SUBTYPE : FORMAT_UNSUPPORTED_TYPE; } @Override diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java index 246cde9d2f..dc376d2ea4 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java @@ -20,6 +20,7 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.audio.AudioProcessor; import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer; +import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.ExoMediaCrypto; import com.google.android.exoplayer2.util.MimeTypes; @@ -46,9 +47,16 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer { } @Override - protected int supportsFormatInternal(Format format) { - return FlacLibrary.isAvailable() && MimeTypes.AUDIO_FLAC.equalsIgnoreCase(format.sampleMimeType) - ? FORMAT_HANDLED : FORMAT_UNSUPPORTED_TYPE; + protected int supportsFormatInternal(DrmSessionManager drmSessionManager, + Format format) { + if (!FlacLibrary.isAvailable() + || !MimeTypes.AUDIO_FLAC.equalsIgnoreCase(format.sampleMimeType)) { + return FORMAT_UNSUPPORTED_TYPE; + } else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) { + return FORMAT_UNSUPPORTED_DRM; + } else { + return FORMAT_HANDLED; + } } @Override diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java index 730473ddad..e4745d0c29 100644 --- a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java +++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java @@ -71,9 +71,16 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer { } @Override - protected int supportsFormatInternal(Format format) { - return OpusLibrary.isAvailable() && MimeTypes.AUDIO_OPUS.equalsIgnoreCase(format.sampleMimeType) - ? FORMAT_HANDLED : FORMAT_UNSUPPORTED_TYPE; + protected int supportsFormatInternal(DrmSessionManager drmSessionManager, + Format format) { + if (!OpusLibrary.isAvailable() + || !MimeTypes.AUDIO_OPUS.equalsIgnoreCase(format.sampleMimeType)) { + return FORMAT_UNSUPPORTED_TYPE; + } else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) { + return FORMAT_UNSUPPORTED_DRM; + } else { + return FORMAT_HANDLED; + } } @Override 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 a947378de5..100ca6f00f 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 @@ -194,8 +194,12 @@ public final class LibvpxVideoRenderer extends BaseRenderer { @Override public int supportsFormat(Format format) { - return VpxLibrary.isAvailable() && MimeTypes.VIDEO_VP9.equalsIgnoreCase(format.sampleMimeType) - ? (FORMAT_HANDLED | ADAPTIVE_SEAMLESS) : FORMAT_UNSUPPORTED_TYPE; + if (!VpxLibrary.isAvailable() || !MimeTypes.VIDEO_VP9.equalsIgnoreCase(format.sampleMimeType)) { + return FORMAT_UNSUPPORTED_TYPE; + } else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) { + return FORMAT_UNSUPPORTED_DRM; + } + return FORMAT_HANDLED | ADAPTIVE_SEAMLESS; } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java index 7f14837965..a4103787d1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java @@ -15,7 +15,10 @@ */ package com.google.android.exoplayer2; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import com.google.android.exoplayer2.drm.DrmInitData; +import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MediaClock; @@ -309,4 +312,25 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { return readEndOfStream ? streamIsFinal : stream.isReady(); } + /** + * Returns whether {@code drmSessionManager} supports the specified {@code drmInitData}, or true + * if {@code drmInitData} is null. + * + * @param drmSessionManager The drm session manager. + * @param drmInitData {@link DrmInitData} of the format to check for support. + * @return Whether {@code drmSessionManager} supports the specified {@code drmInitData}, or + * true if {@code drmInitData} is null. + */ + protected static boolean supportsFormatDrm(@Nullable DrmSessionManager drmSessionManager, + @Nullable DrmInitData drmInitData) { + if (drmInitData == null) { + // Content is unencrypted. + return true; + } else if (drmSessionManager == null) { + // Content is encrypted, but no drm session manager is available. + return false; + } + return drmSessionManager.canAcquireSession(drmInitData); + } + } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java b/library/core/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java index 3f1be20cfb..de0d481386 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java @@ -34,7 +34,9 @@ public interface RendererCapabilities { int FORMAT_HANDLED = 0b100; /** * The {@link Renderer} is capable of rendering formats with the same mime type, but the - * properties of the format exceed the renderer's capability. + * properties of the format exceed the renderer's capabilities. There is a chance the renderer + * will be able to play the format in practice because some renderers report their capabilities + * conservatively, but the expected outcome is that playback will fail. *

        * Example: The {@link Renderer} is capable of rendering H264 and the format's mime type is * {@link MimeTypes#VIDEO_H264}, but the format's resolution exceeds the maximum limit supported @@ -42,12 +44,12 @@ public interface RendererCapabilities { */ int FORMAT_EXCEEDS_CAPABILITIES = 0b011; /** - * The {@link Renderer} is capable of rendering formats with the same mime type, but the - * drm scheme used is not supported. + * The {@link Renderer} is capable of rendering formats with the same mime type, but is not + * capable of rendering the format because the format's drm protection is not supported. *

        * Example: The {@link Renderer} is capable of rendering H264 and the format's mime type is - * {@link MimeTypes#VIDEO_H264}, but the format indicates cbcs encryption, which is not supported - * by the underlying content decryption module. + * {@link MimeTypes#VIDEO_H264}, but the format indicates PlayReady drm protection where-as the + * renderer only supports Widevine. */ int FORMAT_UNSUPPORTED_DRM = 0b010; /** @@ -121,9 +123,11 @@ public interface RendererCapabilities { * {@link #FORMAT_UNSUPPORTED_SUBTYPE} and {@link #FORMAT_UNSUPPORTED_TYPE}. *

      • The level of support for adapting from the format to another format of the same mime type. * One of {@link #ADAPTIVE_SEAMLESS}, {@link #ADAPTIVE_NOT_SEAMLESS} and - * {@link #ADAPTIVE_NOT_SUPPORTED}.
      • + * {@link #ADAPTIVE_NOT_SUPPORTED}. Only set if the level of support for the format itself is + * {@link #FORMAT_HANDLED} or {@link #FORMAT_EXCEEDS_CAPABILITIES}. *
      • The level of support for tunneling. One of {@link #TUNNELING_SUPPORTED} and - * {@link #TUNNELING_NOT_SUPPORTED}.
      • + * {@link #TUNNELING_NOT_SUPPORTED}. Only set if the level of support for the format itself is + * {@link #FORMAT_HANDLED} or {@link #FORMAT_EXCEEDS_CAPABILITIES}. *
      * The individual properties can be retrieved by performing a bitwise AND with * {@link #FORMAT_SUPPORT_MASK}, {@link #ADAPTIVE_SUPPORT_MASK} and diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index e146238dcc..7f157e5866 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java @@ -27,6 +27,7 @@ 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.DrmInitData; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.mediacodec.MediaCodecInfo; @@ -138,19 +139,34 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } @Override - protected int supportsFormat(MediaCodecSelector mediaCodecSelector, Format format) + protected int supportsFormat(MediaCodecSelector mediaCodecSelector, + DrmSessionManager drmSessionManager, Format format) throws DecoderQueryException { String mimeType = format.sampleMimeType; if (!MimeTypes.isAudio(mimeType)) { return FORMAT_UNSUPPORTED_TYPE; } int tunnelingSupport = Util.SDK_INT >= 21 ? TUNNELING_SUPPORTED : TUNNELING_NOT_SUPPORTED; - if (allowPassthrough(mimeType) && mediaCodecSelector.getPassthroughDecoderInfo() != null) { + boolean supportsFormatDrm = supportsFormatDrm(drmSessionManager, format.drmInitData); + if (supportsFormatDrm && allowPassthrough(mimeType) + && mediaCodecSelector.getPassthroughDecoderInfo() != null) { return ADAPTIVE_NOT_SEAMLESS | tunnelingSupport | FORMAT_HANDLED; } - MediaCodecInfo decoderInfo = mediaCodecSelector.getDecoderInfo(mimeType, false); + boolean requiresSecureDecryption = false; + DrmInitData drmInitData = format.drmInitData; + if (drmInitData != null) { + for (int i = 0; i < drmInitData.schemeDataCount; i++) { + requiresSecureDecryption |= drmInitData.get(i).requiresSecureDecryption; + } + } + MediaCodecInfo decoderInfo = mediaCodecSelector.getDecoderInfo(mimeType, + requiresSecureDecryption); if (decoderInfo == null) { - return FORMAT_UNSUPPORTED_SUBTYPE; + return requiresSecureDecryption && mediaCodecSelector.getDecoderInfo(mimeType, false) != null + ? FORMAT_UNSUPPORTED_DRM : FORMAT_UNSUPPORTED_SUBTYPE; + } + if (!supportsFormatDrm) { + return FORMAT_UNSUPPORTED_DRM; } // Note: We assume support for unknown sampleRate and channelCount. boolean decoderCapable = Util.SDK_INT < 21 diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java index c4a55eeb02..012f06da39 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java @@ -159,8 +159,8 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements @Override public final int supportsFormat(Format format) { - int formatSupport = supportsFormatInternal(format); - if (formatSupport == FORMAT_UNSUPPORTED_TYPE || formatSupport == FORMAT_UNSUPPORTED_SUBTYPE) { + int formatSupport = supportsFormatInternal(drmSessionManager, format); + if (formatSupport <= FORMAT_UNSUPPORTED_DRM) { return formatSupport; } int tunnelingSupport = Util.SDK_INT >= 21 ? TUNNELING_SUPPORTED : TUNNELING_NOT_SUPPORTED; @@ -171,10 +171,12 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements * Returns the {@link #FORMAT_SUPPORT_MASK} component of the return value for * {@link #supportsFormat(Format)}. * + * @param drmSessionManager The renderer's {@link DrmSessionManager}. * @param format The format. * @return The extent to which the renderer supports the format itself. */ - protected abstract int supportsFormatInternal(Format format); + protected abstract int supportsFormatInternal(DrmSessionManager drmSessionManager, + Format format); @Override public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java index 31c6a824ef..d6725e373a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java @@ -32,7 +32,6 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; -import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.drm.DrmSession; import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException; import com.google.android.exoplayer2.drm.DrmSessionManager; @@ -169,7 +168,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer { private static final int ADAPTATION_WORKAROUND_SLICE_WIDTH_HEIGHT = 32; private final MediaCodecSelector mediaCodecSelector; - @Nullable private final DrmSessionManager drmSessionManager; + @Nullable + private final DrmSessionManager drmSessionManager; private final boolean playClearSamplesWithoutKeys; private final DecoderInputBuffer buffer; private final DecoderInputBuffer flagsOnlyBuffer; @@ -247,14 +247,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { @Override public final int supportsFormat(Format format) throws ExoPlaybackException { try { - int formatSupport = supportsFormat(mediaCodecSelector, format); - if ((formatSupport & FORMAT_SUPPORT_MASK) > FORMAT_UNSUPPORTED_DRM - && !isDrmSchemeSupported(drmSessionManager, format.drmInitData)) { - // The renderer advertises higher support than FORMAT_UNSUPPORTED_DRM but the DRM scheme is - // not supported. The format support is truncated to reflect this. - formatSupport = (formatSupport & ~FORMAT_SUPPORT_MASK) | FORMAT_UNSUPPORTED_DRM; - } - return formatSupport; + return supportsFormat(mediaCodecSelector, drmSessionManager, format); } catch (DecoderQueryException e) { throw ExoPlaybackException.createForRenderer(e, getIndex()); } @@ -264,12 +257,14 @@ public abstract class MediaCodecRenderer extends BaseRenderer { * Returns the extent to which the renderer is capable of supporting a given format. * * @param mediaCodecSelector The decoder selector. + * @param drmSessionManager The renderer's {@link DrmSessionManager}. * @param format The format. * @return The extent to which the renderer is capable of supporting the given format. See * {@link #supportsFormat(Format)} for more detail. * @throws DecoderQueryException If there was an error querying decoders. */ - protected abstract int supportsFormat(MediaCodecSelector mediaCodecSelector, Format format) + protected abstract int supportsFormat(MediaCodecSelector mediaCodecSelector, + DrmSessionManager drmSessionManager, Format format) throws DecoderQueryException; /** @@ -1083,25 +1078,6 @@ public abstract class MediaCodecRenderer extends BaseRenderer { return false; } - /** - * Returns whether the encryption scheme is supported, or true if {@code drmInitData} is null. - * - * @param drmSessionManager The drm session manager associated with the renderer. - * @param drmInitData {@link DrmInitData} of the format to check for support. - * @return Whether the encryption scheme is supported, or true if {@code drmInitData} is null. - */ - private static boolean isDrmSchemeSupported(@Nullable DrmSessionManager drmSessionManager, - @Nullable DrmInitData drmInitData) { - if (drmInitData == null) { - // Content is unencrypted. - return true; - } else if (drmSessionManager == null) { - // Content is encrypted, but no drm session manager is available. - return false; - } - return drmSessionManager.canAcquireSession(drmInitData); - } - /** * Returns whether the decoder is known to fail when flushed. *

      diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java index f46dd467c8..869e9306a5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java @@ -92,7 +92,11 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { @Override public int supportsFormat(Format format) { - return decoderFactory.supportsFormat(format) ? FORMAT_HANDLED : FORMAT_UNSUPPORTED_TYPE; + if (decoderFactory.supportsFormat(format)) { + return supportsFormatDrm(null, format.drmInitData) ? FORMAT_HANDLED : FORMAT_UNSUPPORTED_DRM; + } else { + return FORMAT_UNSUPPORTED_TYPE; + } } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java index 8e1966305e..c3dc2383ef 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java @@ -117,9 +117,13 @@ public final class TextRenderer extends BaseRenderer implements Callback { @Override public int supportsFormat(Format format) { - return decoderFactory.supportsFormat(format) ? FORMAT_HANDLED - : (MimeTypes.isText(format.sampleMimeType) ? FORMAT_UNSUPPORTED_SUBTYPE - : FORMAT_UNSUPPORTED_TYPE); + if (decoderFactory.supportsFormat(format)) { + return supportsFormatDrm(null, format.drmInitData) ? FORMAT_HANDLED : FORMAT_UNSUPPORTED_DRM; + } else if (MimeTypes.isText(format.sampleMimeType)) { + return FORMAT_UNSUPPORTED_SUBTYPE; + } else { + return FORMAT_UNSUPPORTED_TYPE; + } } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index c11c415cd7..f70d74e413 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -186,7 +186,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } @Override - protected int supportsFormat(MediaCodecSelector mediaCodecSelector, Format format) + protected int supportsFormat(MediaCodecSelector mediaCodecSelector, + DrmSessionManager drmSessionManager, Format format) throws DecoderQueryException { String mimeType = format.sampleMimeType; if (!MimeTypes.isVideo(mimeType)) { @@ -202,9 +203,12 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { MediaCodecInfo decoderInfo = mediaCodecSelector.getDecoderInfo(mimeType, requiresSecureDecryption); if (decoderInfo == null) { - return FORMAT_UNSUPPORTED_SUBTYPE; + return requiresSecureDecryption && mediaCodecSelector.getDecoderInfo(mimeType, false) != null + ? FORMAT_UNSUPPORTED_DRM : FORMAT_UNSUPPORTED_SUBTYPE; + } + if (!supportsFormatDrm(drmSessionManager, drmInitData)) { + return FORMAT_UNSUPPORTED_DRM; } - boolean decoderCapable = decoderInfo.isCodecSupported(format.codecs); if (decoderCapable && format.width > 0 && format.height > 0) { if (Util.SDK_INT >= 21) { From 55e928f75acb78107b1defb702e08aa6f3aa42e5 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 29 Aug 2017 08:50:17 -0700 Subject: [PATCH 0318/2472] Make LeanbackPlayerAdapter use a ControlDispatcher + Misc cleanup 1. Make LeanbackPlayerAdapter use a ControlDispatcher. This allows apps to suppress control events in some circumstances, and is in-line with our mobile controls. 2. Misc simplifications and cleanup to LeanbackPlayerAdapter. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166852816 --- .../ext/leanback/LeanbackPlayerAdapter.java | 126 ++++++++++-------- .../android/exoplayer2/ControlDispatcher.java | 67 ++++++++++ .../exoplayer2/DefaultControlDispatcher.java | 50 +++++++ .../exoplayer2/ui/PlaybackControlView.java | 107 +++------------ .../exoplayer2/ui/SimpleExoPlayerView.java | 7 +- 5 files changed, 215 insertions(+), 142 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/ControlDispatcher.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/DefaultControlDispatcher.java diff --git a/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java b/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java index 8a207bea8f..f5ef8b2ca4 100644 --- a/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java +++ b/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.ext.leanback; import android.content.Context; import android.os.Handler; +import android.support.annotation.Nullable; import android.support.v17.leanback.R; import android.support.v17.leanback.media.PlaybackGlueHost; import android.support.v17.leanback.media.PlayerAdapter; @@ -25,6 +26,8 @@ import android.util.Pair; import android.view.Surface; import android.view.SurfaceHolder; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ControlDispatcher; +import com.google.android.exoplayer2.DefaultControlDispatcher; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.PlaybackParameters; @@ -48,13 +51,13 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { private final SimpleExoPlayer player; private final Handler handler; private final ComponentListener componentListener; - private final Runnable updatePlayerRunnable; + private final Runnable updateProgressRunnable; + private ControlDispatcher controlDispatcher; private ErrorMessageProvider errorMessageProvider; private SurfaceHolderGlueHost surfaceHolderGlueHost; - private boolean initialized; private boolean hasSurface; - private boolean isBuffering; + private boolean lastNotifiedPreparedState; /** * Builds an instance. Note that the {@code PlayerAdapter} does not manage the lifecycle of the @@ -70,7 +73,8 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { this.player = player; handler = new Handler(); componentListener = new ComponentListener(); - updatePlayerRunnable = new Runnable() { + controlDispatcher = new DefaultControlDispatcher(); + updateProgressRunnable = new Runnable() { @Override public void run() { Callback callback = getCallback(); @@ -81,34 +85,15 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { }; } - @Override - public void onAttachedToHost(PlaybackGlueHost host) { - if (host instanceof SurfaceHolderGlueHost) { - surfaceHolderGlueHost = ((SurfaceHolderGlueHost) host); - surfaceHolderGlueHost.setSurfaceHolderCallback(componentListener); - } - notifyListeners(); - player.addListener(componentListener); - player.addVideoListener(componentListener); - } - - private void notifyListeners() { - boolean oldIsPrepared = isPrepared(); - int playbackState = player.getPlaybackState(); - boolean isInitialized = playbackState != Player.STATE_IDLE; - isBuffering = playbackState == Player.STATE_BUFFERING; - boolean hasEnded = playbackState == Player.STATE_ENDED; - - initialized = isInitialized; - Callback callback = getCallback(); - if (oldIsPrepared != isPrepared()) { - callback.onPreparedStateChanged(this); - } - callback.onPlayStateChanged(this); - callback.onBufferingStateChanged(this, isBuffering || !initialized); - if (hasEnded) { - callback.onPlayCompleted(this); - } + /** + * Sets the {@link ControlDispatcher}. + * + * @param controlDispatcher The {@link ControlDispatcher}, or null to use + * {@link DefaultControlDispatcher}. + */ + public void setControlDispatcher(@Nullable ControlDispatcher controlDispatcher) { + this.controlDispatcher = controlDispatcher == null ? new DefaultControlDispatcher() + : controlDispatcher; } /** @@ -121,6 +106,19 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { this.errorMessageProvider = errorMessageProvider; } + // PlayerAdapter implementation. + + @Override + public void onAttachedToHost(PlaybackGlueHost host) { + if (host instanceof SurfaceHolderGlueHost) { + surfaceHolderGlueHost = ((SurfaceHolderGlueHost) host); + surfaceHolderGlueHost.setSurfaceHolderCallback(componentListener); + } + notifyStateChanged(); + player.addListener(componentListener); + player.addVideoListener(componentListener); + } + @Override public void onDetachedFromHost() { player.removeListener(componentListener); @@ -129,56 +127,59 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { surfaceHolderGlueHost.setSurfaceHolderCallback(null); surfaceHolderGlueHost = null; } - initialized = false; hasSurface = false; Callback callback = getCallback(); callback.onBufferingStateChanged(this, false); callback.onPlayStateChanged(this); - callback.onPreparedStateChanged(this); + maybeNotifyPreparedStateChanged(callback); } @Override - public void setProgressUpdatingEnabled(final boolean enabled) { - handler.removeCallbacks(updatePlayerRunnable); + public void setProgressUpdatingEnabled(boolean enabled) { + handler.removeCallbacks(updateProgressRunnable); if (enabled) { - handler.post(updatePlayerRunnable); + handler.post(updateProgressRunnable); } } @Override public boolean isPlaying() { - return initialized && player.getPlayWhenReady(); + int playbackState = player.getPlaybackState(); + return playbackState != Player.STATE_IDLE && playbackState != Player.STATE_ENDED + && player.getPlayWhenReady(); } @Override public long getDuration() { long durationMs = player.getDuration(); - return durationMs != C.TIME_UNSET ? durationMs : -1; + return durationMs == C.TIME_UNSET ? -1 : durationMs; } @Override public long getCurrentPosition() { - return initialized ? player.getCurrentPosition() : -1; + return player.getPlaybackState() == Player.STATE_IDLE ? -1 : player.getCurrentPosition(); } @Override public void play() { if (player.getPlaybackState() == Player.STATE_ENDED) { - player.seekToDefaultPosition(); + controlDispatcher.dispatchSeekTo(player, player.getCurrentWindowIndex(), C.TIME_UNSET); + } + if (controlDispatcher.dispatchSetPlayWhenReady(player, true)) { + getCallback().onPlayStateChanged(this); } - player.setPlayWhenReady(true); - getCallback().onPlayStateChanged(this); } @Override public void pause() { - player.setPlayWhenReady(false); - getCallback().onPlayStateChanged(this); + if (controlDispatcher.dispatchSetPlayWhenReady(player, false)) { + getCallback().onPlayStateChanged(this); + } } @Override public void seekTo(long positionMs) { - player.seekTo(positionMs); + controlDispatcher.dispatchSeekTo(player, player.getCurrentWindowIndex(), positionMs); } @Override @@ -188,13 +189,35 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { @Override public boolean isPrepared() { - return initialized && (surfaceHolderGlueHost == null || hasSurface); + return player.getPlaybackState() != Player.STATE_IDLE + && (surfaceHolderGlueHost == null || hasSurface); } - private void setVideoSurface(Surface surface) { + // Internal methods. + + /* package */ void setVideoSurface(Surface surface) { hasSurface = surface != null; player.setVideoSurface(surface); - getCallback().onPreparedStateChanged(this); + maybeNotifyPreparedStateChanged(getCallback()); + } + + /* package */ void notifyStateChanged() { + int playbackState = player.getPlaybackState(); + Callback callback = getCallback(); + maybeNotifyPreparedStateChanged(callback); + callback.onPlayStateChanged(this); + callback.onBufferingStateChanged(this, playbackState == Player.STATE_BUFFERING); + if (playbackState == Player.STATE_ENDED) { + callback.onPlayCompleted(this); + } + } + + private void maybeNotifyPreparedStateChanged(Callback callback) { + boolean isPrepared = isPrepared(); + if (lastNotifiedPreparedState != isPrepared) { + lastNotifiedPreparedState = isPrepared; + callback.onPreparedStateChanged(this); + } } private final class ComponentListener implements Player.EventListener, @@ -208,7 +231,7 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { } @Override - public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) { + public void surfaceChanged(SurfaceHolder surfaceHolder, int format, int width, int height) { // Do nothing. } @@ -221,7 +244,7 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { @Override public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { - notifyListeners(); + notifyStateChanged(); } @Override @@ -292,4 +315,3 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { } } - diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ControlDispatcher.java b/library/core/src/main/java/com/google/android/exoplayer2/ControlDispatcher.java new file mode 100644 index 0000000000..21c596e6d4 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/ControlDispatcher.java @@ -0,0 +1,67 @@ +/* + * 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 com.google.android.exoplayer2.Player.RepeatMode; + +/** + * Dispatches operations to the {@link 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 ControlDispatcher { + + /** + * Dispatches a {@link Player#setPlayWhenReady(boolean)} operation. + * + * @param player The {@link 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(Player player, boolean playWhenReady); + + /** + * Dispatches a {@link Player#seekTo(int, long)} operation. + * + * @param player The {@link 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 operation was dispatched. False if suppressed. + */ + boolean dispatchSeekTo(Player player, int windowIndex, long positionMs); + + /** + * Dispatches a {@link Player#setRepeatMode(int)} operation. + * + * @param player The {@link Player} to which the operation should be dispatched. + * @param repeatMode The repeat mode. + * @return True if the operation was dispatched. False if suppressed. + */ + boolean dispatchSetRepeatMode(Player player, @RepeatMode int repeatMode); + + /** + * Dispatches a {@link Player#setShuffleModeEnabled(boolean)} operation. + * + * @param player The {@link Player} to which the operation should be dispatched. + * @param shuffleModeEnabled Whether shuffling is enabled. + * @return True if the operation was dispatched. False if suppressed. + */ + boolean dispatchSetShuffleModeEnabled(Player player, boolean shuffleModeEnabled); + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultControlDispatcher.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultControlDispatcher.java new file mode 100644 index 0000000000..84711d752a --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultControlDispatcher.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; + +import com.google.android.exoplayer2.Player.RepeatMode; + +/** + * Default {@link ControlDispatcher} that dispatches all operations to the player without + * modification. + */ +public class DefaultControlDispatcher implements ControlDispatcher { + + @Override + public boolean dispatchSetPlayWhenReady(Player player, boolean playWhenReady) { + player.setPlayWhenReady(playWhenReady); + return true; + } + + @Override + public boolean dispatchSeekTo(Player player, int windowIndex, long positionMs) { + player.seekTo(windowIndex, positionMs); + return true; + } + + @Override + public boolean dispatchSetRepeatMode(Player player, @RepeatMode int repeatMode) { + player.setRepeatMode(repeatMode); + return true; + } + + @Override + public boolean dispatchSetShuffleModeEnabled(Player player, boolean shuffleModeEnabled) { + player.setShuffleModeEnabled(shuffleModeEnabled); + return true; + } + +} diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java index f83bab5770..c89feaebf5 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java @@ -34,7 +34,6 @@ import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.Player.RepeatMode; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; @@ -180,6 +179,12 @@ public class PlaybackControlView extends FrameLayout { ExoPlayerLibraryInfo.registerModule("goog.exo.ui"); } + /** + * @deprecated Use {@link com.google.android.exoplayer2.ControlDispatcher}. + */ + @Deprecated + public interface ControlDispatcher extends com.google.android.exoplayer2.ControlDispatcher {} + /** * Listener to be notified about changes of the visibility of the UI control. */ @@ -194,86 +199,13 @@ public class PlaybackControlView extends FrameLayout { } + private static final class DefaultControlDispatcher + extends com.google.android.exoplayer2.DefaultControlDispatcher implements ControlDispatcher {} /** - * Dispatches operations to the {@link 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. + * @deprecated Use {@link com.google.android.exoplayer2.DefaultControlDispatcher}. */ - public interface ControlDispatcher { - - /** - * Dispatches a {@link Player#setPlayWhenReady(boolean)} operation. - * - * @param player The {@link 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(Player player, boolean playWhenReady); - - /** - * Dispatches a {@link Player#seekTo(int, long)} operation. - * - * @param player The {@link 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 operation was dispatched. False if suppressed. - */ - boolean dispatchSeekTo(Player player, int windowIndex, long positionMs); - - /** - * Dispatches a {@link Player#setRepeatMode(int)} operation. - * - * @param player The {@link Player} to which the operation should be dispatched. - * @param repeatMode The repeat mode. - * @return True if the operation was dispatched. False if suppressed. - */ - boolean dispatchSetRepeatMode(Player player, @RepeatMode int repeatMode); - - /** - * Dispatches a {@link Player#setShuffleModeEnabled(boolean)} operation. - * - * @param player The {@link Player} to which the operation should be dispatched. - * @param shuffleModeEnabled Whether shuffling is enabled. - * @return True if the operation was dispatched. False if suppressed. - */ - boolean dispatchSetShuffleModeEnabled(Player player, boolean shuffleModeEnabled); - - } - - /** - * Default {@link ControlDispatcher} that dispatches operations to the player without - * modification. - */ - public static final ControlDispatcher DEFAULT_CONTROL_DISPATCHER = new ControlDispatcher() { - - @Override - public boolean dispatchSetPlayWhenReady(Player player, boolean playWhenReady) { - player.setPlayWhenReady(playWhenReady); - return true; - } - - @Override - public boolean dispatchSeekTo(Player player, int windowIndex, long positionMs) { - player.seekTo(windowIndex, positionMs); - return true; - } - - @Override - public boolean dispatchSetRepeatMode(Player player, @RepeatMode int repeatMode) { - player.setRepeatMode(repeatMode); - return true; - } - - @Override - public boolean dispatchSetShuffleModeEnabled(Player player, boolean shuffleModeEnabled) { - player.setShuffleModeEnabled(shuffleModeEnabled); - return true; - } - - }; + @Deprecated + public static final ControlDispatcher DEFAULT_CONTROL_DISPATCHER = new DefaultControlDispatcher(); /** * The default fast forward increment, in milliseconds. @@ -325,7 +257,7 @@ public class PlaybackControlView extends FrameLayout { private final String repeatAllButtonContentDescription; private Player player; - private ControlDispatcher controlDispatcher; + private com.google.android.exoplayer2.ControlDispatcher controlDispatcher; private VisibilityListener visibilityListener; private boolean isAttachedToWindow; @@ -400,7 +332,7 @@ public class PlaybackControlView extends FrameLayout { extraAdGroupTimesMs = new long[0]; extraPlayedAdGroups = new boolean[0]; componentListener = new ComponentListener(); - controlDispatcher = DEFAULT_CONTROL_DISPATCHER; + controlDispatcher = new com.google.android.exoplayer2.DefaultControlDispatcher(); LayoutInflater.from(context).inflate(controllerLayoutId, this); setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); @@ -534,14 +466,15 @@ public class PlaybackControlView extends FrameLayout { } /** - * Sets the {@link ControlDispatcher}. + * Sets the {@link com.google.android.exoplayer2.ControlDispatcher}. * - * @param controlDispatcher The {@link ControlDispatcher}, or null to use - * {@link #DEFAULT_CONTROL_DISPATCHER}. + * @param controlDispatcher The {@link com.google.android.exoplayer2.ControlDispatcher}, or null + * to use {@link com.google.android.exoplayer2.DefaultControlDispatcher}. */ - public void setControlDispatcher(ControlDispatcher controlDispatcher) { - this.controlDispatcher = controlDispatcher == null ? DEFAULT_CONTROL_DISPATCHER - : controlDispatcher; + public void setControlDispatcher( + @Nullable com.google.android.exoplayer2.ControlDispatcher controlDispatcher) { + this.controlDispatcher = controlDispatcher == null + ? new com.google.android.exoplayer2.DefaultControlDispatcher() : controlDispatcher; } /** diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index a8926f9ecc..5b6e11c5e4 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -34,6 +34,8 @@ import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.ImageView; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ControlDispatcher; +import com.google.android.exoplayer2.DefaultControlDispatcher; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; @@ -47,7 +49,6 @@ import com.google.android.exoplayer2.text.TextOutput; 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.ControlDispatcher; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.RepeatModeUtil; import com.google.android.exoplayer2.util.Util; @@ -616,9 +617,9 @@ public final class SimpleExoPlayerView extends FrameLayout { * Sets the {@link ControlDispatcher}. * * @param controlDispatcher The {@link ControlDispatcher}, or null to use - * {@link PlaybackControlView#DEFAULT_CONTROL_DISPATCHER}. + * {@link DefaultControlDispatcher}. */ - public void setControlDispatcher(ControlDispatcher controlDispatcher) { + public void setControlDispatcher(@Nullable ControlDispatcher controlDispatcher) { Assertions.checkState(controller != null); controller.setControlDispatcher(controlDispatcher); } From d9cd13ce746c496adb6c327cb749e74afcfe2f05 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 29 Aug 2017 12:15:50 -0700 Subject: [PATCH 0319/2472] Automated rollback of changelist 166843123. *** Reason for rollback *** Doesn't work because trackOutputProvider can be null when extracting init data. *** Original change description *** Don't copy primary-track format to non-primary tracks Copying non-primary-track formats to non-primary tracks looks non-trivial (I tried; went down a dead-end), so leaving that for now. *** ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166883654 --- .../source/chunk/BaseMediaChunkOutput.java | 27 ++++--------------- .../source/chunk/ChunkExtractorWrapper.java | 24 +++++------------ .../source/chunk/ChunkSampleStream.java | 10 +++++-- 3 files changed, 20 insertions(+), 41 deletions(-) diff --git a/library/core/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 index 0b6c196d7c..9531aaf32e 100644 --- a/library/core/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 @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.source.chunk; -import android.support.annotation.Nullable; import android.util.Log; import com.google.android.exoplayer2.extractor.DummyTrackOutput; import com.google.android.exoplayer2.extractor.TrackOutput; @@ -33,23 +32,12 @@ import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper.TrackOut private final SampleQueue[] sampleQueues; /** - * @param primaryTrackType The type of the primary track. - * @param primarySampleQueue The primary track sample queues. - * @param embeddedTrackTypes The types of any embedded tracks, or null. - * @param embeddedSampleQueues The track sample queues for any embedded tracks, or null. + * @param trackTypes The track types of the individual track outputs. + * @param sampleQueues The individual sample queues. */ - @SuppressWarnings("ConstantConditions") - public BaseMediaChunkOutput(int primaryTrackType, SampleQueue primarySampleQueue, - @Nullable int[] embeddedTrackTypes, @Nullable SampleQueue[] embeddedSampleQueues) { - int embeddedTrackCount = embeddedTrackTypes == null ? 0 : embeddedTrackTypes.length; - trackTypes = new int[1 + embeddedTrackCount]; - sampleQueues = new SampleQueue[1 + embeddedTrackCount]; - trackTypes[0] = primaryTrackType; - sampleQueues[0] = primarySampleQueue; - for (int i = 0; i < embeddedTrackCount; i++) { - trackTypes[i + 1] = embeddedTrackTypes[i]; - sampleQueues[i + 1] = embeddedSampleQueues[i]; - } + public BaseMediaChunkOutput(int[] trackTypes, SampleQueue[] sampleQueues) { + this.trackTypes = trackTypes; + this.sampleQueues = sampleQueues; } @Override @@ -63,11 +51,6 @@ import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper.TrackOut return new DummyTrackOutput(); } - @Override - public boolean isPrimaryTrack(int type) { - return type == trackTypes[0]; - } - /** * Returns the current absolute write indices of the individual sample queues. */ diff --git a/library/core/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 index eda9ed3cf7..07d1cce8cb 100644 --- a/library/core/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 @@ -45,22 +45,13 @@ public final class ChunkExtractorWrapper implements ExtractorOutput { *

      * The same {@link TrackOutput} is returned if multiple calls are made with the same {@code id}. * - * @param id The track identifier. - * @param type The track type. Typically one of the {@link com.google.android.exoplayer2.C} - * {@code TRACK_TYPE_*} constants. + * @param id A track identifier. + * @param type The type of the track. Typically one of the + * {@link com.google.android.exoplayer2.C} {@code TRACK_TYPE_*} constants. * @return The {@link TrackOutput} for the given track identifier. */ TrackOutput track(int id, int type); - /** - * Returns whether the specified type corresponds to the primary track. - * - * @param type The track type. Typically one of the {@link com.google.android.exoplayer2.C} - * {@code TRACK_TYPE_*} constants. - * @return Whether {@code type} corresponds to the primary track. - */ - boolean isPrimaryTrack(int type); - } public final Extractor extractor; @@ -155,7 +146,6 @@ public final class ChunkExtractorWrapper implements ExtractorOutput { private final Format manifestFormat; public Format sampleFormat; - private boolean isPrimaryTrack; private TrackOutput trackOutput; public BindingTrackOutput(int id, int type, Format manifestFormat) { @@ -169,17 +159,17 @@ public final class ChunkExtractorWrapper implements ExtractorOutput { trackOutput = new DummyTrackOutput(); return; } - isPrimaryTrack = trackOutputProvider.isPrimaryTrack(type); trackOutput = trackOutputProvider.track(id, type); - if (sampleFormat != null) { + if (trackOutput != null) { trackOutput.format(sampleFormat); } } @Override public void format(Format format) { - // TODO: Non-primary tracks should be copied with data from their own manifest formats. - sampleFormat = isPrimaryTrack ? format.copyWithManifestFormatInfo(manifestFormat) : format; + // TODO: This should only happen for the primary track. Additional metadata/text tracks need + // to be copied with different manifest derived formats. + sampleFormat = format.copyWithManifestFormatInfo(manifestFormat); trackOutput.format(sampleFormat); } diff --git a/library/core/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 index e8586f7230..f2609a0ffd 100644 --- a/library/core/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 @@ -87,15 +87,21 @@ public class ChunkSampleStream implements SampleStream, S int embeddedTrackCount = embeddedTrackTypes == null ? 0 : embeddedTrackTypes.length; embeddedSampleQueues = new SampleQueue[embeddedTrackCount]; embeddedTracksSelected = new boolean[embeddedTrackCount]; + int[] trackTypes = new int[1 + embeddedTrackCount]; + SampleQueue[] sampleQueues = new SampleQueue[1 + embeddedTrackCount]; primarySampleQueue = new SampleQueue(allocator); + trackTypes[0] = primaryTrackType; + sampleQueues[0] = primarySampleQueue; + for (int i = 0; i < embeddedTrackCount; i++) { SampleQueue sampleQueue = new SampleQueue(allocator); embeddedSampleQueues[i] = sampleQueue; + sampleQueues[i + 1] = sampleQueue; + trackTypes[i + 1] = embeddedTrackTypes[i]; } - mediaChunkOutput = new BaseMediaChunkOutput(primaryTrackType, primarySampleQueue, - embeddedTrackTypes, embeddedSampleQueues); + mediaChunkOutput = new BaseMediaChunkOutput(trackTypes, sampleQueues); pendingResetPositionUs = positionUs; lastSeekPositionUs = positionUs; } From 5bed2bf5032a4e5bea36a30a667a795ed172798a Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 29 Aug 2017 13:55:19 -0700 Subject: [PATCH 0320/2472] Don't copy primary-track format to non-primary tracks This time plumbing the track type in from the other side. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166898172 --- .../source/chunk/ChunkExtractorWrapper.java | 35 +++++++++------- .../exoplayer2/source/dash/DashUtil.java | 41 +++++++++++-------- .../source/dash/DefaultDashChunkSource.java | 10 ++--- .../smoothstreaming/DefaultSsChunkSource.java | 2 +- 4 files changed, 51 insertions(+), 37 deletions(-) diff --git a/library/core/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 index 07d1cce8cb..17eb30dee9 100644 --- a/library/core/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 @@ -29,9 +29,10 @@ import com.google.android.exoplayer2.util.ParsableByteArray; import java.io.IOException; /** - * An {@link Extractor} wrapper for loading chunks containing a single track. + * An {@link Extractor} wrapper for loading chunks that contain a single primary track, and possibly + * additional embedded tracks. *

      - * The wrapper allows switching of the {@link TrackOutput} that receives parsed data. + * The wrapper allows switching of the {@link TrackOutput}s that receive parsed data. */ public final class ChunkExtractorWrapper implements ExtractorOutput { @@ -56,7 +57,8 @@ public final class ChunkExtractorWrapper implements ExtractorOutput { public final Extractor extractor; - private final Format manifestFormat; + private final int primaryTrackType; + private final Format primaryTrackManifestFormat; private final SparseArray bindingTrackOutputs; private boolean extractorInitialized; @@ -66,12 +68,16 @@ public final class ChunkExtractorWrapper implements ExtractorOutput { /** * @param extractor The extractor to wrap. - * @param manifestFormat A manifest defined {@link Format} whose data should be merged into any - * sample {@link Format} output from the {@link Extractor}. + * @param primaryTrackType The type of the primary track. Typically one of the + * {@link com.google.android.exoplayer2.C} {@code TRACK_TYPE_*} constants. + * @param primaryTrackManifestFormat A manifest defined {@link Format} whose data should be merged + * into any sample {@link Format} output from the {@link Extractor} for the primary track. */ - public ChunkExtractorWrapper(Extractor extractor, Format manifestFormat) { + public ChunkExtractorWrapper(Extractor extractor, int primaryTrackType, + Format primaryTrackManifestFormat) { this.extractor = extractor; - this.manifestFormat = manifestFormat; + this.primaryTrackType = primaryTrackType; + this.primaryTrackManifestFormat = primaryTrackManifestFormat; bindingTrackOutputs = new SparseArray<>(); } @@ -90,8 +96,8 @@ public final class ChunkExtractorWrapper implements ExtractorOutput { } /** - * Initializes the extractor to output to the provided {@link TrackOutput}, and configures it to - * receive data from a new chunk. + * Initializes the wrapper to output to {@link TrackOutput}s provided by the specified + * {@link TrackOutputProvider}, and configures the extractor to receive data from a new chunk. * * @param trackOutputProvider The provider of {@link TrackOutput}s that will receive sample data. */ @@ -116,7 +122,9 @@ public final class ChunkExtractorWrapper implements ExtractorOutput { if (bindingTrackOutput == null) { // Assert that if we're seeing a new track we have not seen endTracks. Assertions.checkState(sampleFormats == null); - bindingTrackOutput = new BindingTrackOutput(id, type, manifestFormat); + // TODO: Manifest formats for embedded tracks should also be passed here. + bindingTrackOutput = new BindingTrackOutput(id, type, + type == primaryTrackType ? primaryTrackManifestFormat : null); bindingTrackOutput.bind(trackOutputProvider); bindingTrackOutputs.put(id, bindingTrackOutput); } @@ -160,16 +168,15 @@ public final class ChunkExtractorWrapper implements ExtractorOutput { return; } trackOutput = trackOutputProvider.track(id, type); - if (trackOutput != null) { + if (sampleFormat != null) { trackOutput.format(sampleFormat); } } @Override public void format(Format format) { - // TODO: This should only happen for the primary track. Additional metadata/text tracks need - // to be copied with different manifest derived formats. - sampleFormat = format.copyWithManifestFormatInfo(manifestFormat); + sampleFormat = manifestFormat != null ? format.copyWithManifestFormatInfo(manifestFormat) + : format; trackOutput.format(sampleFormat); } diff --git a/library/dash/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 index 59fbfb18fe..ed2f916b87 100644 --- a/library/dash/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 @@ -72,9 +72,11 @@ public final class DashUtil { */ public static DrmInitData loadDrmInitData(DataSource dataSource, Period period) throws IOException, InterruptedException { - Representation representation = getFirstRepresentation(period, C.TRACK_TYPE_VIDEO); + int primaryTrackType = C.TRACK_TYPE_VIDEO; + Representation representation = getFirstRepresentation(period, primaryTrackType); if (representation == null) { - representation = getFirstRepresentation(period, C.TRACK_TYPE_AUDIO); + primaryTrackType = C.TRACK_TYPE_AUDIO; + representation = getFirstRepresentation(period, primaryTrackType); if (representation == null) { return null; } @@ -85,7 +87,7 @@ public final class DashUtil { // as per DASH IF Interoperability Recommendations V3.0, 7.5.3. return drmInitData; } - Format sampleFormat = DashUtil.loadSampleFormat(dataSource, representation); + Format sampleFormat = DashUtil.loadSampleFormat(dataSource, primaryTrackType, representation); return sampleFormat == null ? null : sampleFormat.drmInitData; } @@ -93,15 +95,17 @@ public final class DashUtil { * 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 trackType The type of the representation. Typically one of the + * {@link com.google.android.exoplayer2.C} {@code TRACK_TYPE_*} constants. * @param representation The representation which initialization chunk belongs to. * @return the sample {@link Format} of the given representation. * @throws IOException Thrown when there is an error while loading. * @throws InterruptedException Thrown if the thread was interrupted. */ - public static Format loadSampleFormat(DataSource dataSource, Representation representation) - throws IOException, InterruptedException { - ChunkExtractorWrapper extractorWrapper = loadInitializationData(dataSource, representation, - false); + public static Format loadSampleFormat(DataSource dataSource, int trackType, + Representation representation) throws IOException, InterruptedException { + ChunkExtractorWrapper extractorWrapper = loadInitializationData(dataSource, trackType, + representation, false); return extractorWrapper == null ? null : extractorWrapper.getSampleFormats()[0]; } @@ -110,16 +114,18 @@ public final class DashUtil { * ChunkIndex}. * * @param dataSource The source from which the data should be loaded. + * @param trackType The type of the representation. Typically one of the + * {@link com.google.android.exoplayer2.C} {@code TRACK_TYPE_*} constants. * @param representation The representation which initialization chunk belongs to. * @return The {@link ChunkIndex} of the given representation, or null if no initialization or * index data exists. * @throws IOException Thrown when there is an error while loading. * @throws InterruptedException Thrown if the thread was interrupted. */ - public static ChunkIndex loadChunkIndex(DataSource dataSource, Representation representation) - throws IOException, InterruptedException { - ChunkExtractorWrapper extractorWrapper = loadInitializationData(dataSource, representation, - true); + public static ChunkIndex loadChunkIndex(DataSource dataSource, int trackType, + Representation representation) throws IOException, InterruptedException { + ChunkExtractorWrapper extractorWrapper = loadInitializationData(dataSource, trackType, + representation, true); return extractorWrapper == null ? null : (ChunkIndex) extractorWrapper.getSeekMap(); } @@ -128,6 +134,8 @@ public final class DashUtil { * returns a {@link ChunkExtractorWrapper} which contains the output. * * @param dataSource The source from which the data should be loaded. + * @param trackType The type of the representation. Typically one of the + * {@link com.google.android.exoplayer2.C} {@code TRACK_TYPE_*} constants. * @param representation The representation which initialization chunk belongs to. * @param loadIndex Whether to load index data too. * @return A {@link ChunkExtractorWrapper} for the {@code representation}, or null if no @@ -135,14 +143,13 @@ public final class DashUtil { * @throws IOException Thrown when there is an error while loading. * @throws InterruptedException Thrown if the thread was interrupted. */ - private static ChunkExtractorWrapper loadInitializationData(DataSource dataSource, - Representation representation, boolean loadIndex) - throws IOException, InterruptedException { + private static ChunkExtractorWrapper loadInitializationData(DataSource dataSource, int trackType, + Representation representation, boolean loadIndex) throws IOException, InterruptedException { RangedUri initializationUri = representation.getInitializationUri(); if (initializationUri == null) { return null; } - ChunkExtractorWrapper extractorWrapper = newWrappedExtractor(representation.format); + ChunkExtractorWrapper extractorWrapper = newWrappedExtractor(trackType, representation.format); RangedUri requestUri; if (loadIndex) { RangedUri indexUri = representation.getIndexUri(); @@ -174,12 +181,12 @@ public final class DashUtil { initializationChunk.load(); } - private static ChunkExtractorWrapper newWrappedExtractor(Format format) { + private static ChunkExtractorWrapper newWrappedExtractor(int trackType, Format format) { String mimeType = format.containerMimeType; boolean isWebm = mimeType.startsWith(MimeTypes.VIDEO_WEBM) || mimeType.startsWith(MimeTypes.AUDIO_WEBM); Extractor extractor = isWebm ? new MatroskaExtractor() : new FragmentedMp4Extractor(); - return new ChunkExtractorWrapper(extractor, format); + return new ChunkExtractorWrapper(extractor, trackType, format); } private static Representation getFirstRepresentation(Period period, int type) { diff --git a/library/dash/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 index dd62d47621..c6c1461001 100644 --- a/library/dash/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 @@ -134,8 +134,8 @@ public class DefaultDashChunkSource implements DashChunkSource { representationHolders = new RepresentationHolder[trackSelection.length()]; for (int i = 0; i < representationHolders.length; i++) { Representation representation = representations.get(trackSelection.getIndexInTrackGroup(i)); - representationHolders[i] = new RepresentationHolder(periodDurationUs, representation, - enableEventMessageTrack, enableCea608Track); + representationHolders[i] = new RepresentationHolder(periodDurationUs, trackType, + representation, enableEventMessageTrack, enableCea608Track); } } @@ -390,8 +390,8 @@ public class DefaultDashChunkSource implements DashChunkSource { private long periodDurationUs; private int segmentNumShift; - /* package */ RepresentationHolder(long periodDurationUs, Representation representation, - boolean enableEventMessageTrack, boolean enableCea608Track) { + /* package */ RepresentationHolder(long periodDurationUs, int trackType, + Representation representation, boolean enableEventMessageTrack, boolean enableCea608Track) { this.periodDurationUs = periodDurationUs; this.representation = representation; String containerMimeType = representation.format.containerMimeType; @@ -415,7 +415,7 @@ public class DefaultDashChunkSource implements DashChunkSource { } // Prefer drmInitData obtained from the manifest over drmInitData obtained from the stream, // as per DASH IF Interoperability Recommendations V3.0, 7.5.3. - extractorWrapper = new ChunkExtractorWrapper(extractor, representation.format); + extractorWrapper = new ChunkExtractorWrapper(extractor, trackType, representation.format); } segmentIndex = representation.getIndex(); } diff --git a/library/smoothstreaming/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 index f2e4c57298..1069527989 100644 --- a/library/smoothstreaming/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 @@ -102,7 +102,7 @@ public class DefaultSsChunkSource implements SsChunkSource { FragmentedMp4Extractor extractor = new FragmentedMp4Extractor( FragmentedMp4Extractor.FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME | FragmentedMp4Extractor.FLAG_WORKAROUND_IGNORE_TFDT_BOX, null, track); - extractorWrappers[i] = new ChunkExtractorWrapper(extractor, format); + extractorWrappers[i] = new ChunkExtractorWrapper(extractor, streamElement.type, format); } } From b0df6dce9823e11c45c881abf31ac8cb9a9ba92f Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 29 Aug 2017 15:38:04 -0700 Subject: [PATCH 0321/2472] Fix moe config ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166914821 --- .../ProgressiveDownloadActionTest.java | 123 ------ .../dash/offline/DashDownloadTestData.java | 102 +++++ .../dash/offline/DashDownloaderTest.java | 406 ++++++++++++++++++ .../source/dash/offline/DashDownloader.java | 173 ++++++++ .../source/hls/offline/HlsDownloadAction.java | 83 ---- .../offline/SsDownloadAction.java | 86 ---- 6 files changed, 681 insertions(+), 292 deletions(-) delete mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/offline/ProgressiveDownloadActionTest.java create mode 100644 library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadTestData.java create mode 100644 library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java create mode 100644 library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloader.java delete mode 100644 library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadAction.java delete mode 100644 library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadAction.java diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/offline/ProgressiveDownloadActionTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/offline/ProgressiveDownloadActionTest.java deleted file mode 100644 index ec45ea01c7..0000000000 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/offline/ProgressiveDownloadActionTest.java +++ /dev/null @@ -1,123 +0,0 @@ -/* - * 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.offline; - -import android.test.InstrumentationTestCase; -import com.google.android.exoplayer2.testutil.TestUtil; -import com.google.android.exoplayer2.upstream.DummyDataSource; -import com.google.android.exoplayer2.upstream.cache.Cache; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; -import org.mockito.Mockito; - -/** - * Unit tests for {@link ProgressiveDownloadAction}. - */ -public class ProgressiveDownloadActionTest extends InstrumentationTestCase { - - public void testDownloadActionIsNotRemoveAction() throws Exception { - ProgressiveDownloadAction action = new ProgressiveDownloadAction("uri", null, false); - assertFalse(action.isRemoveAction()); - } - - public void testRemoveActionIsRemoveAction() throws Exception { - ProgressiveDownloadAction action2 = new ProgressiveDownloadAction("uri", null, true); - assertTrue(action2.isRemoveAction()); - } - - public void testCreateDownloader() throws Exception { - TestUtil.setUpMockito(this); - ProgressiveDownloadAction action = new ProgressiveDownloadAction("uri", null, false); - DownloaderConstructorHelper constructorHelper = new DownloaderConstructorHelper( - Mockito.mock(Cache.class), DummyDataSource.FACTORY); - assertNotNull(action.createDownloader(constructorHelper)); - } - - public void testSameUriCacheKeyDifferentAction_IsSameMedia() throws Exception { - ProgressiveDownloadAction action1 = new ProgressiveDownloadAction("uri", null, true); - ProgressiveDownloadAction action2 = new ProgressiveDownloadAction("uri", null, false); - assertTrue(action1.isSameMedia(action2)); - } - - public void testNullCacheKeyDifferentUriAction_IsNotSameMedia() throws Exception { - ProgressiveDownloadAction action3 = new ProgressiveDownloadAction("uri2", null, true); - ProgressiveDownloadAction action4 = new ProgressiveDownloadAction("uri", null, false); - assertFalse(action3.isSameMedia(action4)); - } - - public void testSameCacheKeyDifferentUriAction_IsSameMedia() throws Exception { - ProgressiveDownloadAction action5 = new ProgressiveDownloadAction("uri2", "key", true); - ProgressiveDownloadAction action6 = new ProgressiveDownloadAction("uri", "key", false); - assertTrue(action5.isSameMedia(action6)); - } - - public void testSameUriDifferentCacheKeyAction_IsNotSameMedia() throws Exception { - ProgressiveDownloadAction action7 = new ProgressiveDownloadAction("uri", "key", true); - ProgressiveDownloadAction action8 = new ProgressiveDownloadAction("uri", "key2", false); - assertFalse(action7.isSameMedia(action8)); - } - - public void testEquals() throws Exception { - ProgressiveDownloadAction action1 = new ProgressiveDownloadAction("uri", null, true); - assertTrue(action1.equals(action1)); - - ProgressiveDownloadAction action2 = new ProgressiveDownloadAction("uri", null, true); - ProgressiveDownloadAction action3 = new ProgressiveDownloadAction("uri", null, true); - assertTrue(action2.equals(action3)); - - ProgressiveDownloadAction action4 = new ProgressiveDownloadAction("uri", null, true); - ProgressiveDownloadAction action5 = new ProgressiveDownloadAction("uri", null, false); - assertFalse(action4.equals(action5)); - - ProgressiveDownloadAction action6 = new ProgressiveDownloadAction("uri", null, true); - ProgressiveDownloadAction action7 = new ProgressiveDownloadAction("uri", "key", true); - assertFalse(action6.equals(action7)); - - ProgressiveDownloadAction action8 = new ProgressiveDownloadAction("uri", "key2", true); - ProgressiveDownloadAction action9 = new ProgressiveDownloadAction("uri", "key", true); - assertFalse(action8.equals(action9)); - - ProgressiveDownloadAction action10 = new ProgressiveDownloadAction("uri", null, true); - ProgressiveDownloadAction action11 = new ProgressiveDownloadAction("uri2", null, true); - assertFalse(action10.equals(action11)); - } - - public void testSerializerGetType() throws Exception { - ProgressiveDownloadAction action = new ProgressiveDownloadAction("uri", null, false); - assertNotNull(action.getType()); - } - - public void testSerializerWriteRead() throws Exception { - doTestSerializationRoundTrip(new ProgressiveDownloadAction("uri1", null, false)); - doTestSerializationRoundTrip(new ProgressiveDownloadAction("uri2", "key", true)); - } - - private void doTestSerializationRoundTrip(ProgressiveDownloadAction action1) throws IOException { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - DataOutputStream output = new DataOutputStream(out); - action1.writeToStream(output); - - ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); - DataInputStream input = new DataInputStream(in); - DownloadAction action2 = ProgressiveDownloadAction.DESERIALIZER.readFromStream(input); - - assertEquals(action1, action2); - } - -} diff --git a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadTestData.java b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadTestData.java new file mode 100644 index 0000000000..220adfb3c5 --- /dev/null +++ b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadTestData.java @@ -0,0 +1,102 @@ +/* + * 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.offline; + +import android.net.Uri; +import com.google.android.exoplayer2.util.ClosedSource; + +/** + * Data for DASH downloading tests. + */ +@ClosedSource(reason = "Not ready yet") +/* package */ interface DashDownloadTestData { + + Uri TEST_MPD_URI = Uri.parse("test.mpd"); + + byte[] TEST_MPD = + ("\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + // Bounded range data + + " \n" + // Unbounded range data + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + // This segment list has a 1 second offset to make sure the progressive download order + + " \n" + + " \n" + + " \n" // 1s offset + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "").getBytes(); + + byte[] TEST_MPD_NO_INDEX = + ("\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "").getBytes(); +} diff --git a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java new file mode 100644 index 0000000000..f5e00d2cec --- /dev/null +++ b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java @@ -0,0 +1,406 @@ +/* + * 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.offline; + +import static com.google.android.exoplayer2.source.dash.offline.DashDownloadTestData.TEST_MPD; +import static com.google.android.exoplayer2.source.dash.offline.DashDownloadTestData.TEST_MPD_NO_INDEX; +import static com.google.android.exoplayer2.source.dash.offline.DashDownloadTestData.TEST_MPD_URI; +import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCacheEmpty; +import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCachedData; +import static com.google.android.exoplayer2.testutil.CacheAsserts.assertDataCached; + +import android.test.InstrumentationTestCase; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.offline.DownloadException; +import com.google.android.exoplayer2.offline.Downloader.ProgressListener; +import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; +import com.google.android.exoplayer2.source.dash.manifest.DashManifest; +import com.google.android.exoplayer2.source.dash.manifest.RepresentationKey; +import com.google.android.exoplayer2.testutil.FakeDataSet; +import com.google.android.exoplayer2.testutil.FakeDataSource; +import com.google.android.exoplayer2.testutil.FakeDataSource.Factory; +import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor; +import com.google.android.exoplayer2.upstream.cache.SimpleCache; +import com.google.android.exoplayer2.util.Util; +import java.io.File; +import java.io.IOException; +import java.util.Arrays; +import org.mockito.InOrder; +import org.mockito.Mockito; + +/** + * Unit tests for {@link DashDownloader}. + */ +public class DashDownloaderTest extends InstrumentationTestCase { + + private SimpleCache cache; + private File tempFolder; + + @Override + public void setUp() throws Exception { + super.setUp(); + TestUtil.setUpMockito(this); + tempFolder = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest"); + cache = new SimpleCache(tempFolder, new NoOpCacheEvictor()); + } + + @Override + public void tearDown() throws Exception { + Util.recursiveDelete(tempFolder); + super.tearDown(); + } + + public void testGetManifest() throws Exception { + FakeDataSet fakeDataSet = new FakeDataSet() + .setData(TEST_MPD_URI, TEST_MPD); + DashDownloader dashDownloader = getDashDownloader(fakeDataSet); + + DashManifest manifest = dashDownloader.getManifest(); + + assertNotNull(manifest); + assertCachedData(cache, fakeDataSet); + } + + public void testDownloadManifestFailure() throws Exception { + byte[] testMpdFirstPart = Arrays.copyOf(TEST_MPD, 10); + byte[] testMpdSecondPart = Arrays.copyOfRange(TEST_MPD, 10, TEST_MPD.length); + FakeDataSet fakeDataSet = new FakeDataSet() + .newData(TEST_MPD_URI) + .appendReadData(testMpdFirstPart) + .appendReadError(new IOException()) + .appendReadData(testMpdSecondPart) + .endData(); + DashDownloader dashDownloader = getDashDownloader(fakeDataSet); + + // fails on the first try + try { + dashDownloader.getManifest(); + fail(); + } catch (IOException e) { + // ignore + } + assertDataCached(cache, TEST_MPD_URI, testMpdFirstPart); + + // on the second try it downloads the rest of the data + DashManifest manifest = dashDownloader.getManifest(); + + assertNotNull(manifest); + assertCachedData(cache, fakeDataSet); + } + + public void testDownloadRepresentation() throws Exception { + FakeDataSet fakeDataSet = new FakeDataSet() + .setData(TEST_MPD_URI, TEST_MPD) + .setRandomData("audio_init_data", 10) + .setRandomData("audio_segment_1", 4) + .setRandomData("audio_segment_2", 5) + .setRandomData("audio_segment_3", 6); + DashDownloader dashDownloader = getDashDownloader(fakeDataSet); + + dashDownloader.selectRepresentations(new RepresentationKey[] {new RepresentationKey(0, 0, 0)}); + dashDownloader.download(null); + + assertCachedData(cache, fakeDataSet); + } + + public void testDownloadRepresentationInSmallParts() throws Exception { + FakeDataSet fakeDataSet = new FakeDataSet() + .setData(TEST_MPD_URI, TEST_MPD) + .setRandomData("audio_init_data", 10) + .newData("audio_segment_1") + .appendReadData(TestUtil.buildTestData(10)) + .appendReadData(TestUtil.buildTestData(10)) + .appendReadData(TestUtil.buildTestData(10)) + .endData() + .setRandomData("audio_segment_2", 5) + .setRandomData("audio_segment_3", 6); + DashDownloader dashDownloader = getDashDownloader(fakeDataSet); + + dashDownloader.selectRepresentations(new RepresentationKey[] {new RepresentationKey(0, 0, 0)}); + dashDownloader.download(null); + + assertCachedData(cache, fakeDataSet); + } + + public void testDownloadRepresentations() throws Exception { + FakeDataSet fakeDataSet = new FakeDataSet() + .setData(TEST_MPD_URI, TEST_MPD) + .setRandomData("audio_init_data", 10) + .setRandomData("audio_segment_1", 4) + .setRandomData("audio_segment_2", 5) + .setRandomData("audio_segment_3", 6) + .setRandomData("text_segment_1", 1) + .setRandomData("text_segment_2", 2) + .setRandomData("text_segment_3", 3); + DashDownloader dashDownloader = getDashDownloader(fakeDataSet); + + dashDownloader.selectRepresentations( + new RepresentationKey[] {new RepresentationKey(0, 0, 0), new RepresentationKey(0, 1, 0)}); + dashDownloader.download(null); + + assertCachedData(cache, fakeDataSet); + } + + public void testDownloadAllRepresentations() throws Exception { + FakeDataSet fakeDataSet = new FakeDataSet() + .setData(TEST_MPD_URI, TEST_MPD) + .setRandomData("audio_init_data", 10) + .setRandomData("audio_segment_1", 4) + .setRandomData("audio_segment_2", 5) + .setRandomData("audio_segment_3", 6) + .setRandomData("text_segment_1", 1) + .setRandomData("text_segment_2", 2) + .setRandomData("text_segment_3", 3) + .setRandomData("period_2_segment_1", 1) + .setRandomData("period_2_segment_2", 2) + .setRandomData("period_2_segment_3", 3); + DashDownloader dashDownloader = getDashDownloader(fakeDataSet); + + // dashDownloader.selectRepresentations() isn't called + dashDownloader.download(null); + assertCachedData(cache, fakeDataSet); + dashDownloader.remove(); + + // select something random + dashDownloader.selectRepresentations(new RepresentationKey[] {new RepresentationKey(0, 0, 0)}); + // clear selection + dashDownloader.selectRepresentations(null); + dashDownloader.download(null); + assertCachedData(cache, fakeDataSet); + dashDownloader.remove(); + + dashDownloader.selectRepresentations(new RepresentationKey[0]); + dashDownloader.download(null); + assertCachedData(cache, fakeDataSet); + dashDownloader.remove(); + } + + public void testProgressiveDownload() throws Exception { + FakeDataSet fakeDataSet = new FakeDataSet() + .setData(TEST_MPD_URI, TEST_MPD) + .setRandomData("audio_init_data", 10) + .setRandomData("audio_segment_1", 4) + .setRandomData("audio_segment_2", 5) + .setRandomData("audio_segment_3", 6) + .setRandomData("text_segment_1", 1) + .setRandomData("text_segment_2", 2) + .setRandomData("text_segment_3", 3); + FakeDataSource fakeDataSource = new FakeDataSource(fakeDataSet); + Factory factory = Mockito.mock(Factory.class); + Mockito.when(factory.createDataSource()).thenReturn(fakeDataSource); + DashDownloader dashDownloader = new DashDownloader(TEST_MPD_URI, + new DownloaderConstructorHelper(cache, factory)); + + dashDownloader.selectRepresentations( + new RepresentationKey[] {new RepresentationKey(0, 0, 0), new RepresentationKey(0, 1, 0)}); + dashDownloader.download(null); + + DataSpec[] openedDataSpecs = fakeDataSource.getAndClearOpenedDataSpecs(); + assertEquals(8, openedDataSpecs.length); + assertEquals(TEST_MPD_URI, openedDataSpecs[0].uri); + assertEquals("audio_init_data", openedDataSpecs[1].uri.getPath()); + assertEquals("audio_segment_1", openedDataSpecs[2].uri.getPath()); + assertEquals("text_segment_1", openedDataSpecs[3].uri.getPath()); + assertEquals("audio_segment_2", openedDataSpecs[4].uri.getPath()); + assertEquals("text_segment_2", openedDataSpecs[5].uri.getPath()); + assertEquals("audio_segment_3", openedDataSpecs[6].uri.getPath()); + assertEquals("text_segment_3", openedDataSpecs[7].uri.getPath()); + } + + public void testProgressiveDownloadSeparatePeriods() throws Exception { + FakeDataSet fakeDataSet = new FakeDataSet() + .setData(TEST_MPD_URI, TEST_MPD) + .setRandomData("audio_init_data", 10) + .setRandomData("audio_segment_1", 4) + .setRandomData("audio_segment_2", 5) + .setRandomData("audio_segment_3", 6) + .setRandomData("period_2_segment_1", 1) + .setRandomData("period_2_segment_2", 2) + .setRandomData("period_2_segment_3", 3); + FakeDataSource fakeDataSource = new FakeDataSource(fakeDataSet); + Factory factory = Mockito.mock(Factory.class); + Mockito.when(factory.createDataSource()).thenReturn(fakeDataSource); + DashDownloader dashDownloader = new DashDownloader(TEST_MPD_URI, + new DownloaderConstructorHelper(cache, factory)); + + dashDownloader.selectRepresentations( + new RepresentationKey[] {new RepresentationKey(0, 0, 0), new RepresentationKey(1, 0, 0)}); + dashDownloader.download(null); + + DataSpec[] openedDataSpecs = fakeDataSource.getAndClearOpenedDataSpecs(); + assertEquals(8, openedDataSpecs.length); + assertEquals(TEST_MPD_URI, openedDataSpecs[0].uri); + assertEquals("audio_init_data", openedDataSpecs[1].uri.getPath()); + assertEquals("audio_segment_1", openedDataSpecs[2].uri.getPath()); + assertEquals("audio_segment_2", openedDataSpecs[3].uri.getPath()); + assertEquals("audio_segment_3", openedDataSpecs[4].uri.getPath()); + assertEquals("period_2_segment_1", openedDataSpecs[5].uri.getPath()); + assertEquals("period_2_segment_2", openedDataSpecs[6].uri.getPath()); + assertEquals("period_2_segment_3", openedDataSpecs[7].uri.getPath()); + } + + public void testDownloadRepresentationFailure() throws Exception { + FakeDataSet fakeDataSet = new FakeDataSet() + .setData(TEST_MPD_URI, TEST_MPD) + .setRandomData("audio_init_data", 10) + .setRandomData("audio_segment_1", 4) + .newData("audio_segment_2") + .appendReadData(TestUtil.buildTestData(2)) + .appendReadError(new IOException()) + .appendReadData(TestUtil.buildTestData(3)) + .endData() + .setRandomData("audio_segment_3", 6); + DashDownloader dashDownloader = getDashDownloader(fakeDataSet); + + dashDownloader.selectRepresentations(new RepresentationKey[] {new RepresentationKey(0, 0, 0)}); + // downloadRepresentations fails on the first try + try { + dashDownloader.download(null); + fail(); + } catch (IOException e) { + // ignore + } + dashDownloader.download(null); + + assertCachedData(cache, fakeDataSet); + } + + public void testCounters() throws Exception { + FakeDataSet fakeDataSet = new FakeDataSet() + .setData(TEST_MPD_URI, TEST_MPD) + .setRandomData("audio_init_data", 10) + .setRandomData("audio_segment_1", 4) + .newData("audio_segment_2") + .appendReadData(TestUtil.buildTestData(2)) + .appendReadError(new IOException()) + .appendReadData(TestUtil.buildTestData(3)) + .endData() + .setRandomData("audio_segment_3", 6); + DashDownloader dashDownloader = getDashDownloader(fakeDataSet); + + assertCounters(dashDownloader, C.LENGTH_UNSET, C.LENGTH_UNSET, C.LENGTH_UNSET); + + dashDownloader.selectRepresentations(new RepresentationKey[] {new RepresentationKey(0, 0, 0)}); + dashDownloader.init(); + assertCounters(dashDownloader, C.LENGTH_UNSET, C.LENGTH_UNSET, C.LENGTH_UNSET); + + // downloadRepresentations fails after downloading init data, segment 1 and 2 bytes in segment 2 + try { + dashDownloader.download(null); + fail(); + } catch (IOException e) { + // ignore + } + dashDownloader.init(); + assertCounters(dashDownloader, 4, 2, 10 + 4 + 2); + + dashDownloader.download(null); + + assertCounters(dashDownloader, 4, 4, 10 + 4 + 5 + 6); + } + + public void testListener() throws Exception { + FakeDataSet fakeDataSet = new FakeDataSet() + .setData(TEST_MPD_URI, TEST_MPD) + .setRandomData("audio_init_data", 10) + .setRandomData("audio_segment_1", 4) + .setRandomData("audio_segment_2", 5) + .setRandomData("audio_segment_3", 6); + DashDownloader dashDownloader = getDashDownloader(fakeDataSet); + + dashDownloader.selectRepresentations(new RepresentationKey[] {new RepresentationKey(0, 0, 0)}); + ProgressListener mockListener = Mockito.mock(ProgressListener.class); + dashDownloader.download(mockListener); + InOrder inOrder = Mockito.inOrder(mockListener); + inOrder.verify(mockListener).onDownloadProgress(dashDownloader, 0.0f, 0); + inOrder.verify(mockListener).onDownloadProgress(dashDownloader, 25.0f, 10); + inOrder.verify(mockListener).onDownloadProgress(dashDownloader, 50.0f, 14); + inOrder.verify(mockListener).onDownloadProgress(dashDownloader, 75.0f, 19); + inOrder.verify(mockListener).onDownloadProgress(dashDownloader, 100.0f, 25); + inOrder.verifyNoMoreInteractions(); + } + + public void testRemoveAll() throws Exception { + FakeDataSet fakeDataSet = new FakeDataSet() + .setData(TEST_MPD_URI, TEST_MPD) + .setRandomData("audio_init_data", 10) + .setRandomData("audio_segment_1", 4) + .setRandomData("audio_segment_2", 5) + .setRandomData("audio_segment_3", 6) + .setRandomData("text_segment_1", 1) + .setRandomData("text_segment_2", 2) + .setRandomData("text_segment_3", 3); + DashDownloader dashDownloader = getDashDownloader(fakeDataSet); + dashDownloader.selectRepresentations( + new RepresentationKey[] {new RepresentationKey(0, 0, 0), new RepresentationKey(0, 1, 0)}); + dashDownloader.download(null); + + dashDownloader.remove(); + + assertCacheEmpty(cache); + } + + public void testRepresentationWithoutIndex() throws Exception { + FakeDataSet fakeDataSet = new FakeDataSet() + .setData(TEST_MPD_URI, TEST_MPD_NO_INDEX) + .setRandomData("test_segment_1", 4); + DashDownloader dashDownloader = getDashDownloader(fakeDataSet); + + dashDownloader.selectRepresentations(new RepresentationKey[] {new RepresentationKey(0, 0, 0)}); + dashDownloader.init(); + try { + dashDownloader.download(null); + fail(); + } catch (DownloadException e) { + // expected exception. + } + dashDownloader.remove(); + + assertCacheEmpty(cache); + } + + public void testSelectRepresentationsClearsPreviousSelection() throws Exception { + FakeDataSet fakeDataSet = new FakeDataSet() + .setData(TEST_MPD_URI, TEST_MPD) + .setRandomData("audio_init_data", 10) + .setRandomData("audio_segment_1", 4) + .setRandomData("audio_segment_2", 5) + .setRandomData("audio_segment_3", 6); + DashDownloader dashDownloader = getDashDownloader(fakeDataSet); + + dashDownloader.selectRepresentations( + new RepresentationKey[] {new RepresentationKey(0, 0, 0), new RepresentationKey(0, 1, 0)}); + dashDownloader.selectRepresentations(new RepresentationKey[] {new RepresentationKey(0, 0, 0)}); + dashDownloader.download(null); + + assertCachedData(cache, fakeDataSet); + } + + private DashDownloader getDashDownloader(FakeDataSet fakeDataSet) { + Factory factory = new Factory(null).setFakeDataSet(fakeDataSet); + return new DashDownloader(TEST_MPD_URI, new DownloaderConstructorHelper(cache, factory)); + } + + private static void assertCounters(DashDownloader dashDownloader, int totalSegments, + int downloadedSegments, int downloadedBytes) { + assertEquals(totalSegments, dashDownloader.getTotalSegments()); + assertEquals(downloadedSegments, dashDownloader.getDownloadedSegments()); + assertEquals(downloadedBytes, dashDownloader.getDownloadedBytes()); + } + +} diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloader.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloader.java new file mode 100644 index 0000000000..558adca7bd --- /dev/null +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloader.java @@ -0,0 +1,173 @@ +/* + * 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.offline; + +import android.net.Uri; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.extractor.ChunkIndex; +import com.google.android.exoplayer2.offline.DownloadException; +import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; +import com.google.android.exoplayer2.offline.SegmentDownloader; +import com.google.android.exoplayer2.source.dash.DashSegmentIndex; +import com.google.android.exoplayer2.source.dash.DashUtil; +import com.google.android.exoplayer2.source.dash.DashWrappingSegmentIndex; +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.RangedUri; +import com.google.android.exoplayer2.source.dash.manifest.Representation; +import com.google.android.exoplayer2.source.dash.manifest.RepresentationKey; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DataSpec; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * Helper class to download DASH streams. + * + *

      Except {@link #getTotalSegments()}, {@link #getDownloadedSegments()} and {@link + * #getDownloadedBytes()}, this class isn't thread safe. + * + *

      Example usage: + * + *

      + * {@code
      + * SimpleCache cache = new SimpleCache(downloadFolder, new NoOpCacheEvictor());
      + * DefaultHttpDataSourceFactory factory = new DefaultHttpDataSourceFactory("ExoPlayer", null);
      + * DownloaderConstructorHelper constructorHelper =
      + *     new DownloaderConstructorHelper(cache, factory);
      + * DashDownloader dashDownloader = new DashDownloader(manifestUrl, constructorHelper);
      + * // Select the first representation of the first adaptation set of the first period
      + * dashDownloader.selectRepresentations(new RepresentationKey[] {new RepresentationKey(0, 0, 0)});
      + * dashDownloader.download(new ProgressListener() {
      + *   @Override
      + *   public void onDownloadProgress(Downloader downloader, float downloadPercentage,
      + *       long downloadedBytes) {
      + *     // Invoked periodically during the download.
      + *   }
      + * });
      + * // Access downloaded data using CacheDataSource
      + * CacheDataSource cacheDataSource =
      + *     new CacheDataSource(cache, factory.createDataSource(), CacheDataSource.FLAG_BLOCK_ON_CACHE);}
      + * 
      + */ +public final class DashDownloader extends SegmentDownloader { + + /** + * @see SegmentDownloader#SegmentDownloader(Uri, DownloaderConstructorHelper) + */ + public DashDownloader(Uri manifestUri, DownloaderConstructorHelper constructorHelper) { + super(manifestUri, constructorHelper); + } + + @Override + public DashManifest getManifest(DataSource dataSource, Uri uri) throws IOException { + return DashUtil.loadManifest(dataSource, uri); + } + + @Override + protected List getAllSegments(DataSource dataSource, DashManifest manifest, + boolean allowIndexLoadErrors) throws InterruptedException, IOException { + ArrayList segments = new ArrayList<>(); + for (int periodIndex = 0; periodIndex < manifest.getPeriodCount(); periodIndex++) { + List adaptationSets = manifest.getPeriod(periodIndex).adaptationSets; + for (int adaptationIndex = 0; adaptationIndex < adaptationSets.size(); adaptationIndex++) { + AdaptationSet adaptationSet = adaptationSets.get(adaptationIndex); + RepresentationKey[] keys = new RepresentationKey[adaptationSet.representations.size()]; + for (int i = 0; i < keys.length; i++) { + keys[i] = new RepresentationKey(periodIndex, adaptationIndex, i); + } + segments.addAll(getSegments(dataSource, manifest, keys, allowIndexLoadErrors)); + } + } + return segments; + } + + @Override + protected List getSegments(DataSource dataSource, DashManifest manifest, + RepresentationKey[] keys, boolean allowIndexLoadErrors) + throws InterruptedException, IOException { + ArrayList segments = new ArrayList<>(); + for (RepresentationKey key : keys) { + DashSegmentIndex index; + try { + index = getSegmentIndex(dataSource, manifest, key); + if (index == null) { + // Loading succeeded but there was no index. This is always a failure. + throw new DownloadException("No index for representation: " + key); + } + } catch (IOException e) { + if (allowIndexLoadErrors) { + // Loading failed, but load errors are allowed. Advance to the next key. + continue; + } else { + throw e; + } + } + + int segmentCount = index.getSegmentCount(C.TIME_UNSET); + if (segmentCount == DashSegmentIndex.INDEX_UNBOUNDED) { + throw new DownloadException("Unbounded index for representation: " + key); + } + + Period period = manifest.getPeriod(key.periodIndex); + Representation representation = period.adaptationSets.get(key.adaptationSetIndex) + .representations.get(key.representationIndex); + long startUs = C.msToUs(period.startMs); + String baseUrl = representation.baseUrl; + RangedUri initializationUri = representation.getInitializationUri(); + if (initializationUri != null) { + addSegment(segments, startUs, baseUrl, initializationUri); + } + RangedUri indexUri = representation.getIndexUri(); + if (indexUri != null) { + addSegment(segments, startUs, baseUrl, indexUri); + } + + int firstSegmentNum = index.getFirstSegmentNum(); + int lastSegmentNum = firstSegmentNum + segmentCount - 1; + for (int j = firstSegmentNum; j <= lastSegmentNum; j++) { + addSegment(segments, startUs + index.getTimeUs(j), baseUrl, index.getSegmentUrl(j)); + } + } + return segments; + } + + /** + * Returns DashSegmentIndex for given representation. + */ + private DashSegmentIndex getSegmentIndex(DataSource dataSource, DashManifest manifest, + RepresentationKey key) throws IOException, InterruptedException { + AdaptationSet adaptationSet = manifest.getPeriod(key.periodIndex).adaptationSets.get( + key.adaptationSetIndex); + Representation representation = adaptationSet.representations.get(key.representationIndex); + DashSegmentIndex index = representation.getIndex(); + if (index != null) { + return index; + } + ChunkIndex seekMap = DashUtil.loadChunkIndex(dataSource, adaptationSet.type, representation); + return seekMap == null ? null : new DashWrappingSegmentIndex(seekMap); + } + + private static void addSegment(ArrayList segments, long startTimeUs, String baseUrl, + RangedUri rangedUri) { + DataSpec dataSpec = new DataSpec(rangedUri.resolveUri(baseUrl), rangedUri.start, + rangedUri.length, null); + segments.add(new Segment(startTimeUs, dataSpec)); + } + +} diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadAction.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadAction.java deleted file mode 100644 index 3c23e25796..0000000000 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadAction.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * 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.hls.offline; - -import android.net.Uri; -import com.google.android.exoplayer2.offline.DownloadAction; -import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; -import com.google.android.exoplayer2.offline.SegmentDownloadAction; -import com.google.android.exoplayer2.util.ClosedSource; -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; - -/** An action to download or remove downloaded HLS streams. */ -@ClosedSource(reason = "Not ready yet") -public final class HlsDownloadAction extends SegmentDownloadAction { - - public static final Deserializer DESERIALIZER = new SegmentDownloadActionDeserializer() { - - @Override - public String getType() { - return TYPE; - } - - @Override - protected String readKey(DataInputStream input) throws IOException { - return input.readUTF(); - } - - @Override - protected String[] createKeyArray(int keyCount) { - return new String[0]; - } - - @Override - protected DownloadAction createDownloadAction(Uri manifestUri, boolean removeAction, - String[] keys) { - return new HlsDownloadAction(manifestUri, removeAction, keys); - } - - }; - - private static final String TYPE = "HlsDownloadAction"; - - /** @see SegmentDownloadAction#SegmentDownloadAction(Uri, boolean, Object[]) */ - public HlsDownloadAction(Uri manifestUri, boolean removeAction, String... keys) { - super(manifestUri, removeAction, keys); - } - - @Override - public String getType() { - return TYPE; - } - - @Override - public HlsDownloader createDownloader(DownloaderConstructorHelper constructorHelper) - throws IOException { - HlsDownloader downloader = new HlsDownloader(manifestUri, constructorHelper); - if (!isRemoveAction()) { - downloader.selectRepresentations(keys); - } - return downloader; - } - - @Override - protected void writeKey(DataOutputStream output, String key) throws IOException { - output.writeUTF(key); - } - -} diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadAction.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadAction.java deleted file mode 100644 index 7478062ef8..0000000000 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadAction.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * 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.smoothstreaming.offline; - -import android.net.Uri; -import com.google.android.exoplayer2.offline.DownloadAction; -import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; -import com.google.android.exoplayer2.offline.SegmentDownloadAction; -import com.google.android.exoplayer2.source.smoothstreaming.manifest.TrackKey; -import com.google.android.exoplayer2.util.ClosedSource; -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; - -/** An action to download or remove downloaded SmoothStreaming streams. */ -@ClosedSource(reason = "Not ready yet") -public final class SsDownloadAction extends SegmentDownloadAction { - - public static final Deserializer DESERIALIZER = - new SegmentDownloadActionDeserializer() { - - @Override - public String getType() { - return TYPE; - } - - @Override - protected TrackKey readKey(DataInputStream input) throws IOException { - return new TrackKey(input.readInt(), input.readInt()); - } - - @Override - protected TrackKey[] createKeyArray(int keyCount) { - return new TrackKey[keyCount]; - } - - @Override - protected DownloadAction createDownloadAction(Uri manifestUri, boolean removeAction, - TrackKey[] keys) { - return new SsDownloadAction(manifestUri, removeAction, keys); - } - - }; - - private static final String TYPE = "SsDownloadAction"; - - /** @see SegmentDownloadAction#SegmentDownloadAction(Uri, boolean, Object[]) */ - public SsDownloadAction(Uri manifestUri, boolean removeAction, TrackKey... keys) { - super(manifestUri, removeAction, keys); - } - - @Override - public String getType() { - return TYPE; - } - - @Override - public SsDownloader createDownloader(DownloaderConstructorHelper constructorHelper) - throws IOException { - SsDownloader downloader = new SsDownloader(manifestUri, constructorHelper); - if (!isRemoveAction()) { - downloader.selectRepresentations(keys); - } - return downloader; - } - - @Override - protected void writeKey(DataOutputStream output, TrackKey key) throws IOException { - output.writeInt(key.streamElementIndex); - output.writeInt(key.trackIndex); - } - -} From 6bf0b7f3de32b34cc58786474a302198cc18e421 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 29 Aug 2017 15:51:36 -0700 Subject: [PATCH 0322/2472] Fix moe config II ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166916769 --- .../exoplayer2/source/dash/offline/DashDownloadTestData.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadTestData.java b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadTestData.java index 220adfb3c5..50752c8a72 100644 --- a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadTestData.java +++ b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadTestData.java @@ -16,12 +16,10 @@ package com.google.android.exoplayer2.source.dash.offline; import android.net.Uri; -import com.google.android.exoplayer2.util.ClosedSource; /** * Data for DASH downloading tests. */ -@ClosedSource(reason = "Not ready yet") /* package */ interface DashDownloadTestData { Uri TEST_MPD_URI = Uri.parse("test.mpd"); From 49c2926e45b2d9ee4fc41dd834074c61c80e2c76 Mon Sep 17 00:00:00 2001 From: Shyri Villar Date: Wed, 30 Aug 2017 16:25:50 +0200 Subject: [PATCH 0323/2472] Add support for new codecs parameter string --- .../java/com/google/android/exoplayer2/util/MimeTypes.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java b/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java index 2d4a1ec96f..1c8bb62a75 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java @@ -184,9 +184,9 @@ public final class MimeTypes { return MimeTypes.VIDEO_H264; } else if (codec.startsWith("hev1") || codec.startsWith("hvc1")) { return MimeTypes.VIDEO_H265; - } else if (codec.startsWith("vp9")) { + } else if (codec.startsWith("vp9") || codec.startsWith("vp09")) { return MimeTypes.VIDEO_VP9; - } else if (codec.startsWith("vp8")) { + } else if (codec.startsWith("vp8") || codec.startsWith("vp08")) { return MimeTypes.VIDEO_VP8; } else if (codec.startsWith("mp4a")) { return MimeTypes.AUDIO_AAC; From 0c7f11606f0eaf3660511d4a478fc80de4c2e63d Mon Sep 17 00:00:00 2001 From: Danny Brain Date: Thu, 31 Aug 2017 14:31:18 +1000 Subject: [PATCH 0324/2472] #3215 Additional secure DummySurface device exclusions --- .../android/exoplayer2/video/DummySurface.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java index a1820ed7a1..8551f2541d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java @@ -43,6 +43,7 @@ import static android.opengl.GLES20.glGenTextures; import android.annotation.TargetApi; import android.content.Context; +import android.content.pm.PackageManager; import android.graphics.SurfaceTexture; import android.graphics.SurfaceTexture.OnFrameAvailableListener; import android.opengl.EGL14; @@ -152,9 +153,16 @@ public final class DummySurface extends Surface { * * @param context Any {@link Context}. */ - @SuppressWarnings("unused") // Context may be needed in the future for better targeting. + @SuppressWarnings("unused") private static boolean deviceNeedsSecureDummySurfaceWorkaround(Context context) { - return Util.SDK_INT == 24 && "samsung".equals(Util.MANUFACTURER); + return (Util.SDK_INT == 24 && "samsung".equals(Util.MANUFACTURER)) + || (Util.SDK_INT >= 24 && Util.SDK_INT < 26 + && !hasVrModeHighPerformanceSystemFeatureV24(context.getPackageManager())); + } + + @TargetApi(24) + private static boolean hasVrModeHighPerformanceSystemFeatureV24(PackageManager packageManager) { + return packageManager.hasSystemFeature(PackageManager.FEATURE_VR_MODE_HIGH_PERFORMANCE); } private static class DummySurfaceThread extends HandlerThread implements OnFrameAvailableListener, From 049d41db2a4a697e23a39271a9a2758c8bd90fbd Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 30 Aug 2017 07:03:41 -0700 Subject: [PATCH 0325/2472] Add license notes for extensions with non-Google dependencies Issue: #3197 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166988657 --- extensions/ffmpeg/README.md | 8 ++++++++ extensions/flac/README.md | 8 ++++++++ extensions/okhttp/README.md | 8 ++++++++ extensions/opus/README.md | 8 ++++++++ extensions/rtmp/README.md | 8 ++++++++ extensions/vp9/README.md | 8 ++++++++ 6 files changed, 48 insertions(+) diff --git a/extensions/ffmpeg/README.md b/extensions/ffmpeg/README.md index 57b637d1e2..b29c836887 100644 --- a/extensions/ffmpeg/README.md +++ b/extensions/ffmpeg/README.md @@ -3,6 +3,14 @@ The FFmpeg extension provides `FfmpegAudioRenderer`, which uses FFmpeg for decoding and can render audio encoded in a variety of formats. +## License note ## + +Please note that whilst the code in this repository is licensed under +[Apache 2.0][], using this extension also requires building and including one or +more external libraries as described below. These are licensed separately. + +[Apache 2.0]: https://github.com/google/ExoPlayer/blob/release-v2/LICENSE + ## Build instructions ## To use this extension you need to clone the ExoPlayer repository and depend on diff --git a/extensions/flac/README.md b/extensions/flac/README.md index 113b41a93d..cd0f2efe47 100644 --- a/extensions/flac/README.md +++ b/extensions/flac/README.md @@ -3,6 +3,14 @@ The Flac extension provides `FlacExtractor` and `LibflacAudioRenderer`, which use libFLAC (the Flac decoding library) to extract and decode FLAC audio. +## License note ## + +Please note that whilst the code in this repository is licensed under +[Apache 2.0][], using this extension also requires building and including one or +more external libraries as described below. These are licensed separately. + +[Apache 2.0]: https://github.com/google/ExoPlayer/blob/release-v2/LICENSE + ## Build instructions ## To use this extension you need to clone the ExoPlayer repository and depend on diff --git a/extensions/okhttp/README.md b/extensions/okhttp/README.md index f84d0c35f2..e40535d4e8 100644 --- a/extensions/okhttp/README.md +++ b/extensions/okhttp/README.md @@ -6,6 +6,14 @@ The OkHttp extension is an [HttpDataSource][] implementation using Square's [HttpDataSource]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/upstream/HttpDataSource.html [OkHttp]: https://square.github.io/okhttp/ +## License note ## + +Please note that whilst the code in this repository is licensed under +[Apache 2.0][], using this extension requires depending on OkHttp, which is +licensed separately. + +[Apache 2.0]: https://github.com/google/ExoPlayer/blob/release-v2/LICENSE + ## Getting the extension ## The easiest way to use the extension is to add it as a gradle dependency: diff --git a/extensions/opus/README.md b/extensions/opus/README.md index d766e8c9c4..15c3e5413d 100644 --- a/extensions/opus/README.md +++ b/extensions/opus/README.md @@ -3,6 +3,14 @@ The Opus extension provides `LibopusAudioRenderer`, which uses libopus (the Opus decoding library) to decode Opus audio. +## License note ## + +Please note that whilst the code in this repository is licensed under +[Apache 2.0][], using this extension also requires building and including one or +more external libraries as described below. These are licensed separately. + +[Apache 2.0]: https://github.com/google/ExoPlayer/blob/release-v2/LICENSE + ## Build instructions ## To use this extension you need to clone the ExoPlayer repository and depend on diff --git a/extensions/rtmp/README.md b/extensions/rtmp/README.md index 7e6bc0d641..fb822b8326 100644 --- a/extensions/rtmp/README.md +++ b/extensions/rtmp/README.md @@ -7,6 +7,14 @@ streams using [LibRtmp Client for Android][]. [RTMP]: https://en.wikipedia.org/wiki/Real-Time_Messaging_Protocol [LibRtmp Client for Android]: https://github.com/ant-media/LibRtmp-Client-for-Android +## License note ## + +Please note that whilst the code in this repository is licensed under +[Apache 2.0][], using this extension requires depending on LibRtmp Client for +Android, which is licensed separately. + +[Apache 2.0]: https://github.com/google/ExoPlayer/blob/release-v2/LICENSE + ## Getting the extension ## The easiest way to use the extension is to add it as a gradle dependency: diff --git a/extensions/vp9/README.md b/extensions/vp9/README.md index 7bce4a2a25..941b413c09 100644 --- a/extensions/vp9/README.md +++ b/extensions/vp9/README.md @@ -3,6 +3,14 @@ The VP9 extension provides `LibvpxVideoRenderer`, which uses libvpx (the VPx decoding library) to decode VP9 video. +## License note ## + +Please note that whilst the code in this repository is licensed under +[Apache 2.0][], using this extension also requires building and including one or +more external libraries as described below. These are licensed separately. + +[Apache 2.0]: https://github.com/google/ExoPlayer/blob/release-v2/LICENSE + ## Build instructions ## To use this extension you need to clone the ExoPlayer repository and depend on From e80a93d7990db645707f579df58cb380abb1edd4 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 30 Aug 2017 10:20:37 -0700 Subject: [PATCH 0326/2472] Use UTF-8 everywhere UTF-8 is the default charset on Android so this should be a no-op change, but makes the code portable (in case it runs on another platform). ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167011583 --- .../com/google/android/exoplayer2/util/ParsableByteArray.java | 3 ++- .../src/main/java/com/google/android/exoplayer2/util/Util.java | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java b/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java index 2a907e5955..70cb584085 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.util; +import com.google.android.exoplayer2.C; import java.nio.ByteBuffer; import java.nio.charset.Charset; @@ -428,7 +429,7 @@ public final class ParsableByteArray { * @return The string encoded by the bytes. */ public String readString(int length) { - return readString(length, Charset.defaultCharset()); + return readString(length, Charset.forName(C.UTF8_NAME)); } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java index b958a54244..519919f129 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -253,7 +253,7 @@ public final class Util { * @return The code points encoding using UTF-8. */ public static byte[] getUtf8Bytes(String value) { - return value.getBytes(Charset.defaultCharset()); // UTF-8 is the default on Android. + return value.getBytes(Charset.forName(C.UTF8_NAME)); } /** From 84c13ccbf3daf7814099137f2cfa27957893c664 Mon Sep 17 00:00:00 2001 From: bachinger Date: Wed, 30 Aug 2017 10:32:47 -0700 Subject: [PATCH 0327/2472] Support setRepeatMode (and move shuffle action to PlaybackController) ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167013507 --- .../DefaultPlaybackController.java | 53 ++++++++++--- .../mediasession/MediaSessionConnector.java | 79 +++++++++++-------- .../RepeatModeActionProvider.java | 10 +-- .../mediasession/TimelineQueueNavigator.java | 5 -- 4 files changed, 93 insertions(+), 54 deletions(-) diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/DefaultPlaybackController.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/DefaultPlaybackController.java index e01d6a48db..95ebcde095 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/DefaultPlaybackController.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/DefaultPlaybackController.java @@ -21,6 +21,7 @@ import android.support.v4.media.session.PlaybackStateCompat; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.util.RepeatModeUtil; /** * A default implementation of {@link MediaSessionConnector.PlaybackController}. @@ -40,33 +41,37 @@ public class DefaultPlaybackController implements MediaSessionConnector.Playback private static final long BASE_ACTIONS = PlaybackStateCompat.ACTION_PLAY_PAUSE | PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PAUSE - | PlaybackStateCompat.ACTION_STOP; + | PlaybackStateCompat.ACTION_STOP | PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE_ENABLED + | PlaybackStateCompat.ACTION_SET_REPEAT_MODE; protected final long rewindIncrementMs; protected final long fastForwardIncrementMs; + protected final int repeatToggleModes; /** * Creates a new instance. *

      - * Equivalent to {@code DefaultPlaybackController( - * DefaultPlaybackController.DEFAULT_REWIND_MS, - * DefaultPlaybackController.DEFAULT_FAST_FORWARD_MS)}. + * Equivalent to {@code DefaultPlaybackController(DefaultPlaybackController.DEFAULT_REWIND_MS, + * DefaultPlaybackController.DEFAULT_FAST_FORWARD_MS, + * MediaSessionConnector.DEFAULT_REPEAT_TOGGLE_MODES)}. */ public DefaultPlaybackController() { - this(DEFAULT_REWIND_MS, DEFAULT_FAST_FORWARD_MS); + this(DEFAULT_REWIND_MS, DEFAULT_FAST_FORWARD_MS, + MediaSessionConnector.DEFAULT_REPEAT_TOGGLE_MODES); } /** * Creates a new instance with the given fast forward and rewind increments. - * - * @param rewindIncrementMs The rewind increment in milliseconds. A zero or negative value will + * @param rewindIncrementMs The rewind increment in milliseconds. A zero or negative value will * cause the rewind action to be disabled. * @param fastForwardIncrementMs The fast forward increment in milliseconds. A zero or negative - * value will cause the fast forward action to be removed. + * @param repeatToggleModes The available repeatToggleModes. */ - public DefaultPlaybackController(long rewindIncrementMs, long fastForwardIncrementMs) { + public DefaultPlaybackController(long rewindIncrementMs, long fastForwardIncrementMs, + @RepeatModeUtil.RepeatToggleModes int repeatToggleModes) { this.rewindIncrementMs = rewindIncrementMs; this.fastForwardIncrementMs = fastForwardIncrementMs; + this.repeatToggleModes = repeatToggleModes; } @Override @@ -127,6 +132,36 @@ public class DefaultPlaybackController implements MediaSessionConnector.Playback player.stop(); } + @Override + public void onSetShuffleMode(Player player, int shuffleMode) { + player.setShuffleModeEnabled(shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_ALL + || shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_GROUP); + } + + @Override + public void onSetRepeatMode(Player player, int repeatMode) { + int selectedExoPlayerRepeatMode = player.getRepeatMode(); + switch (repeatMode) { + case PlaybackStateCompat.REPEAT_MODE_ALL: + case PlaybackStateCompat.REPEAT_MODE_GROUP: + if ((repeatToggleModes & RepeatModeUtil.REPEAT_TOGGLE_MODE_ALL) != 0) { + selectedExoPlayerRepeatMode = Player.REPEAT_MODE_ALL; + } + break; + case PlaybackStateCompat.REPEAT_MODE_ONE: + if ((repeatToggleModes & RepeatModeUtil.REPEAT_TOGGLE_MODE_ONE) != 0) { + selectedExoPlayerRepeatMode = Player.REPEAT_MODE_ONE; + } + break; + default: + selectedExoPlayerRepeatMode = Player.REPEAT_MODE_OFF; + break; + } + player.setRepeatMode(selectedExoPlayerRepeatMode); + } + + // CommandReceiver implementation. + @Override public String[] getCommands() { return null; diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java index a64f163733..4c7ad123f3 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java @@ -39,6 +39,8 @@ 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.ErrorMessageProvider; +import com.google.android.exoplayer2.util.RepeatModeUtil; + import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -73,6 +75,13 @@ public final class MediaSessionConnector { ExoPlayerLibraryInfo.registerModule("goog.exo.mediasession"); } + /** + * The default repeat toggle modes which is the bitmask of + * {@link RepeatModeUtil#REPEAT_TOGGLE_MODE_ONE} and + * {@link RepeatModeUtil#REPEAT_TOGGLE_MODE_ALL}. + */ + public static final @RepeatModeUtil.RepeatToggleModes int DEFAULT_REPEAT_TOGGLE_MODES = + RepeatModeUtil.REPEAT_TOGGLE_MODE_ONE | RepeatModeUtil.REPEAT_TOGGLE_MODE_ALL; public static final String EXTRAS_PITCH = "EXO_PITCH"; private static final int BASE_MEDIA_SESSION_FLAGS = MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS; @@ -145,14 +154,17 @@ public final class MediaSessionConnector { long ACTIONS = PlaybackStateCompat.ACTION_PLAY_PAUSE | PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PAUSE | PlaybackStateCompat.ACTION_SEEK_TO | PlaybackStateCompat.ACTION_FAST_FORWARD | PlaybackStateCompat.ACTION_REWIND - | PlaybackStateCompat.ACTION_STOP; + | PlaybackStateCompat.ACTION_STOP | PlaybackStateCompat.ACTION_SET_REPEAT_MODE + | PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE_ENABLED; /** * Returns the actions which are supported by the controller. The supported actions must be a * bitmask combined out of {@link PlaybackStateCompat#ACTION_PLAY_PAUSE}, * {@link PlaybackStateCompat#ACTION_PLAY}, {@link PlaybackStateCompat#ACTION_PAUSE}, * {@link PlaybackStateCompat#ACTION_SEEK_TO}, {@link PlaybackStateCompat#ACTION_FAST_FORWARD}, - * {@link PlaybackStateCompat#ACTION_REWIND} and {@link PlaybackStateCompat#ACTION_STOP}. + * {@link PlaybackStateCompat#ACTION_REWIND}, {@link PlaybackStateCompat#ACTION_STOP}, + * {@link PlaybackStateCompat#ACTION_SET_REPEAT_MODE} and + * {@link PlaybackStateCompat#ACTION_SET_SHUFFLE_MODE_ENABLED}. * * @param player The player. * @return The bitmask of the supported media actions. @@ -182,6 +194,14 @@ public final class MediaSessionConnector { * See {@link MediaSessionCompat.Callback#onStop()}. */ void onStop(Player player); + /** + * See {@link MediaSessionCompat.Callback#onSetShuffleMode(int)}. + */ + void onSetShuffleMode(Player player, int shuffleMode); + /** + * See {@link MediaSessionCompat.Callback#onSetRepeatMode(int)}. + */ + void onSetRepeatMode(Player player, int repeatMode); } /** @@ -191,15 +211,13 @@ public final class MediaSessionConnector { public interface QueueNavigator extends CommandReceiver { long ACTIONS = PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM - | PlaybackStateCompat.ACTION_SKIP_TO_NEXT | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS - | PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE_ENABLED; + | PlaybackStateCompat.ACTION_SKIP_TO_NEXT | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS; /** * Returns the actions which are supported by the navigator. The supported actions must be a * bitmask combined out of {@link PlaybackStateCompat#ACTION_SKIP_TO_QUEUE_ITEM}, * {@link PlaybackStateCompat#ACTION_SKIP_TO_NEXT}, - * {@link PlaybackStateCompat#ACTION_SKIP_TO_PREVIOUS}, - * {@link PlaybackStateCompat#ACTION_SET_SHUFFLE_MODE_ENABLED}. + * {@link PlaybackStateCompat#ACTION_SKIP_TO_PREVIOUS}. * * @param player The {@link Player}. * @return The bitmask of the supported media actions. @@ -241,10 +259,6 @@ public final class MediaSessionConnector { * See {@link MediaSessionCompat.Callback#onSkipToNext()}. */ void onSkipToNext(Player player); - /** - * See {@link MediaSessionCompat.Callback#onSetShuffleMode(int)}. - */ - void onSetShuffleMode(Player player, int shuffleMode); } /** @@ -429,8 +443,7 @@ public final class MediaSessionConnector { /** * Sets the {@link QueueNavigator} to handle queue navigation actions {@code ACTION_SKIP_TO_NEXT}, - * {@code ACTION_SKIP_TO_PREVIOUS}, {@code ACTION_SKIP_TO_QUEUE_ITEM} and - * {@code ACTION_SET_SHUFFLE_MODE_ENABLED}. + * {@code ACTION_SKIP_TO_PREVIOUS} and {@code ACTION_SKIP_TO_QUEUE_ITEM}. * * @param queueNavigator The queue navigator. */ @@ -736,6 +749,28 @@ public final class MediaSessionConnector { } } + @Override + public void onSetShuffleModeEnabled(boolean enabled) { + if (canDispatchToPlaybackController(PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE_ENABLED)) { + playbackController.onSetShuffleMode(player, enabled + ? PlaybackStateCompat.SHUFFLE_MODE_ALL : PlaybackStateCompat.SHUFFLE_MODE_NONE); + } + } + + @Override + public void onSetShuffleMode(int shuffleMode) { + if (canDispatchToPlaybackController(PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE_ENABLED)) { + playbackController.onSetShuffleMode(player, shuffleMode); + } + } + + @Override + public void onSetRepeatMode(int repeatMode) { + if (canDispatchToPlaybackController(PlaybackStateCompat.ACTION_SET_REPEAT_MODE)) { + playbackController.onSetRepeatMode(player, repeatMode); + } + } + @Override public void onSkipToNext() { if (canDispatchToQueueNavigator(PlaybackStateCompat.ACTION_SKIP_TO_NEXT)) { @@ -757,11 +792,6 @@ public final class MediaSessionConnector { } } - @Override - public void onSetRepeatMode(int repeatMode) { - // implemented as custom action - } - @Override public void onCustomAction(@NonNull String action, @Nullable Bundle extras) { Map actionMap = customActionMap; @@ -842,21 +872,6 @@ public final class MediaSessionConnector { } } - @Override - public void onSetShuffleModeEnabled(boolean enabled) { - if (canDispatchToQueueNavigator(PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE_ENABLED)) { - queueNavigator.onSetShuffleMode(player, enabled - ? PlaybackStateCompat.SHUFFLE_MODE_ALL : PlaybackStateCompat.SHUFFLE_MODE_NONE); - } - } - - @Override - public void onSetShuffleMode(int shuffleMode) { - if (canDispatchToQueueNavigator(PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE_ENABLED)) { - queueNavigator.onSetShuffleMode(player, shuffleMode); - } - } - @Override public void onAddQueueItem(MediaDescriptionCompat description) { if (queueEditor != null) { diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java index db0190de0f..b4cb3c73d0 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java @@ -25,12 +25,6 @@ import com.google.android.exoplayer2.util.RepeatModeUtil; */ public final class RepeatModeActionProvider implements MediaSessionConnector.CustomActionProvider { - /** - * The default repeat toggle modes. - */ - public static final @RepeatModeUtil.RepeatToggleModes int DEFAULT_REPEAT_TOGGLE_MODES = - RepeatModeUtil.REPEAT_TOGGLE_MODE_ONE | RepeatModeUtil.REPEAT_TOGGLE_MODE_ALL; - private static final String ACTION_REPEAT_MODE = "ACTION_EXO_REPEAT_MODE"; private final Player player; @@ -44,13 +38,13 @@ public final class RepeatModeActionProvider implements MediaSessionConnector.Cus * Creates a new instance. *

      * Equivalent to {@code RepeatModeActionProvider(context, player, - * RepeatModeActionProvider.DEFAULT_REPEAT_TOGGLE_MODES)}. + * MediaSessionConnector.DEFAULT_REPEAT_TOGGLE_MODES)}. * * @param context The context. * @param player The player on which to toggle the repeat mode. */ public RepeatModeActionProvider(Context context, Player player) { - this(context, player, DEFAULT_REPEAT_TOGGLE_MODES); + this(context, player, MediaSessionConnector.DEFAULT_REPEAT_TOGGLE_MODES); } /** diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java index 777949863d..0484c0b641 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java @@ -161,11 +161,6 @@ public abstract class TimelineQueueNavigator implements MediaSessionConnector.Qu } } - @Override - public void onSetShuffleMode(Player player, int shuffleMode) { - player.setShuffleModeEnabled(shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_ALL); - } - // CommandReceiver implementation. @Override From 0b78837f35f54acc3ef41f5ceeb3b634df1cd142 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 30 Aug 2017 10:51:37 -0700 Subject: [PATCH 0328/2472] Fix ContentDataSource bytesRemaining calculation The bytesRemaining didn't always take into account any skipped bytes, which meant that reaching the end of the file was not correctly detected in read(). Issue: #3216 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167016672 --- .../upstream/ContentDataSourceTest.java | 27 +++++++++++++++++++ .../upstream/ContentDataSource.java | 10 ++++--- .../android/exoplayer2/testutil/TestUtil.java | 2 +- 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java index 834e7e1374..2b70c83ca5 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java @@ -23,9 +23,12 @@ import android.database.Cursor; import android.net.Uri; import android.support.annotation.NonNull; import android.test.InstrumentationTestCase; +import android.test.MoreAsserts; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.testutil.TestUtil; import java.io.FileNotFoundException; import java.io.IOException; +import java.util.Arrays; /** * Unit tests for {@link ContentDataSource}. @@ -35,6 +38,9 @@ public final class ContentDataSourceTest extends InstrumentationTestCase { private static final String AUTHORITY = "com.google.android.exoplayer2.core.test"; private static final String DATA_PATH = "binary/1024_incrementing_bytes.mp3"; + private static final int TEST_DATA_OFFSET = 1; + private static final int TEST_DATA_LENGTH = 1023; + public void testReadValidUri() throws Exception { ContentDataSource dataSource = new ContentDataSource(getInstrumentation().getContext()); Uri contentUri = new Uri.Builder() @@ -64,6 +70,27 @@ public final class ContentDataSourceTest extends InstrumentationTestCase { } } + public void testReadFromOffsetToEndOfInput() throws Exception { + ContentDataSource dataSource = new ContentDataSource(getInstrumentation().getContext()); + Uri contentUri = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(AUTHORITY) + .path(DATA_PATH).build(); + try { + DataSpec dataSpec = new DataSpec(contentUri, TEST_DATA_OFFSET, C.LENGTH_UNSET, null); + long length = dataSource.open(dataSpec); + assertEquals(TEST_DATA_LENGTH, length); + byte[] expectedData = Arrays.copyOfRange( + TestUtil.getByteArray(getInstrumentation(), DATA_PATH), TEST_DATA_OFFSET, + TEST_DATA_OFFSET + TEST_DATA_LENGTH); + byte[] readData = TestUtil.readToEnd(dataSource); + MoreAsserts.assertEquals(expectedData, readData); + assertEquals(C.RESULT_END_OF_INPUT, dataSource.read(new byte[1], 0, 1)); + } finally { + dataSource.close(); + } + } + /** * A {@link ContentProvider} for the test. */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java index d118b91378..c37599eccc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java @@ -76,8 +76,8 @@ public final class ContentDataSource implements DataSource { throw new FileNotFoundException("Could not open file descriptor for: " + uri); } inputStream = new FileInputStream(assetFileDescriptor.getFileDescriptor()); - long assertStartOffset = assetFileDescriptor.getStartOffset(); - long skipped = inputStream.skip(assertStartOffset + dataSpec.position) - assertStartOffset; + long assetStartOffset = assetFileDescriptor.getStartOffset(); + long skipped = inputStream.skip(assetStartOffset + dataSpec.position) - assetStartOffset; if (skipped != dataSpec.position) { // We expect the skip to be satisfied in full. If it isn't then we're probably trying to // skip beyond the end of the data. @@ -86,8 +86,8 @@ public final class ContentDataSource implements DataSource { if (dataSpec.length != C.LENGTH_UNSET) { bytesRemaining = dataSpec.length; } else { - bytesRemaining = assetFileDescriptor.getLength(); - if (bytesRemaining == AssetFileDescriptor.UNKNOWN_LENGTH) { + long assetFileDescriptorLength = assetFileDescriptor.getLength(); + if (assetFileDescriptorLength == AssetFileDescriptor.UNKNOWN_LENGTH) { // The asset must extend to the end of the file. bytesRemaining = inputStream.available(); if (bytesRemaining == 0) { @@ -96,6 +96,8 @@ public final class ContentDataSource implements DataSource { // case, so treat as unbounded. bytesRemaining = C.LENGTH_UNSET; } + } else { + bytesRemaining = assetFileDescriptorLength - skipped; } } } catch (IOException e) { diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java index 5819a4b711..2e59b33c0b 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java @@ -181,7 +181,7 @@ public class TestUtil { byte[] expectedData) throws IOException { try { long length = dataSource.open(dataSpec); - Assert.assertEquals(length, expectedData.length); + Assert.assertEquals(expectedData.length, length); byte[] readData = TestUtil.readToEnd(dataSource); MoreAsserts.assertEquals(expectedData, readData); } finally { From 6bd0ba887c70e500724b4fc87d40db74f8d0c019 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 31 Aug 2017 04:13:13 -0700 Subject: [PATCH 0329/2472] Allow more aggressive switching for HLS with independent segments We currently switch without downloading overlapping segments, but we do not actually switch more aggressively. This change fixes this. Note there's an implicit assumption made that if one media playlist declares independent segments, the others will too. This is almost certainly true in practice, and if it's not the penalty isn't too bad (the player may try and switch to a higher quality variant one segment's worth of buffer too soon). ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167120992 --- .../exoplayer2/source/hls/HlsChunkSource.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/library/hls/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 index 4fed33eee3..d0161d839c 100644 --- a/library/hls/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 @@ -92,6 +92,7 @@ import java.util.List; private byte[] scratchSpace; private IOException fatalError; private HlsUrl expectedPlaylistUrl; + private boolean independentSegments; private Uri encryptionKeyUri; private byte[] encryptionKey; @@ -206,10 +207,11 @@ import java.util.List; int oldVariantIndex = previous == null ? C.INDEX_UNSET : trackGroup.indexOf(previous.trackFormat); expectedPlaylistUrl = null; - // Use start time of the previous chunk rather than its end time because switching format will - // require downloading overlapping segments. - long bufferedDurationUs = previous == null ? 0 - : Math.max(0, previous.startTimeUs - playbackPositionUs); + // Unless segments are known to be independent, switching variant will require downloading + // overlapping segments. Hence we use the start time of the previous chunk rather than its end + // time for this case. + long bufferedDurationUs = previous == null ? 0 : Math.max(0, + (independentSegments ? previous.endTimeUs : previous.startTimeUs) - playbackPositionUs); // Select the variant. trackSelection.updateSelectedTrack(bufferedDurationUs); @@ -224,12 +226,13 @@ import java.util.List; return; } HlsMediaPlaylist mediaPlaylist = playlistTracker.getPlaylistSnapshot(selectedUrl); + independentSegments = mediaPlaylist.hasIndependentSegmentsTag; // Select the chunk. int chunkMediaSequence; if (previous == null || switchingVariant) { long targetPositionUs = previous == null ? playbackPositionUs - : mediaPlaylist.hasIndependentSegmentsTag ? previous.endTimeUs : previous.startTimeUs; + : independentSegments ? previous.endTimeUs : previous.startTimeUs; if (!mediaPlaylist.hasEndTag && targetPositionUs >= mediaPlaylist.getEndTimeUs()) { // If the playlist is too old to contain the chunk, we need to refresh it. chunkMediaSequence = mediaPlaylist.mediaSequence + mediaPlaylist.segments.size(); From f15ce81c4763e2a4e6c606c9aeb8d24024b86440 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 31 Aug 2017 09:33:55 -0700 Subject: [PATCH 0330/2472] Move some unit tests to use Robolectric ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167148146 --- constants.gradle | 3 + library/core/build.gradle | 7 + .../com/google/android/exoplayer2/CTest.java | 35 -- .../google/android/exoplayer2/FormatTest.java | 151 ------ .../exoplayer2/util/AtomicFileTest.java | 85 --- .../exoplayer2/util/ColorParserTest.java | 98 ---- .../exoplayer2/util/NalUnitUtilTest.java | 206 -------- .../exoplayer2/util/ParsableBitArrayTest.java | 177 ------- .../util/ParsableByteArrayTest.java | 492 ------------------ .../util/ParsableNalUnitBitArrayTest.java | 114 ---- .../ReusableBufferedOutputStreamTest.java | 46 -- .../android/exoplayer2/util/UriUtilTest.java | 98 ---- .../android/exoplayer2/util/UtilTest.java | 179 ------- 13 files changed, 10 insertions(+), 1681 deletions(-) delete mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/CTest.java delete mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/FormatTest.java delete mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/util/AtomicFileTest.java delete mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/util/ColorParserTest.java delete mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/util/NalUnitUtilTest.java delete mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/util/ParsableBitArrayTest.java delete mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/util/ParsableByteArrayTest.java delete mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/util/ParsableNalUnitBitArrayTest.java delete mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/util/ReusableBufferedOutputStreamTest.java delete mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/util/UriUtilTest.java delete mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/util/UtilTest.java diff --git a/constants.gradle b/constants.gradle index 4107faab4c..dcec53efba 100644 --- a/constants.gradle +++ b/constants.gradle @@ -25,6 +25,9 @@ project.ext { playServicesLibraryVersion = '11.0.2' dexmakerVersion = '1.2' mockitoVersion = '1.9.5' + junitVersion = '4.12' + truthVersion = '0.35' + robolectricVersion = '3.4.2' releaseVersion = 'r2.5.1' modulePrefix = ':' if (gradle.ext.has('exoplayerModulePrefix')) { diff --git a/library/core/build.gradle b/library/core/build.gradle index ecad1e58b5..d50834efd5 100644 --- a/library/core/build.gradle +++ b/library/core/build.gradle @@ -28,6 +28,9 @@ android { androidTest { java.srcDirs += "../../testutils/src/main/java/" } + test { + java.srcDirs += "../../testutils/src/main/java/" + } } buildTypes { @@ -44,6 +47,10 @@ dependencies { androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion androidTestCompile 'org.mockito:mockito-core:' + mockitoVersion + testCompile 'com.google.truth:truth:' + truthVersion + testCompile 'junit:junit:' + junitVersion + testCompile 'org.mockito:mockito-core:' + mockitoVersion + testCompile 'org.robolectric:robolectric:' + robolectricVersion } ext { diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/CTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/CTest.java deleted file mode 100644 index ddcdc4ac8a..0000000000 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/CTest.java +++ /dev/null @@ -1,35 +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; - -import android.annotation.SuppressLint; -import android.media.MediaCodec; -import junit.framework.TestCase; - -/** - * Unit test for {@link C}. - */ -public class CTest extends TestCase { - - @SuppressLint("InlinedApi") - public static void testConstants() { - // Sanity check that constant values match those defined by the platform. - assertEquals(MediaCodec.BUFFER_FLAG_KEY_FRAME, C.BUFFER_FLAG_KEY_FRAME); - assertEquals(MediaCodec.BUFFER_FLAG_END_OF_STREAM, C.BUFFER_FLAG_END_OF_STREAM); - assertEquals(MediaCodec.CRYPTO_MODE_AES_CTR, C.CRYPTO_MODE_AES_CTR); - } - -} diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/FormatTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/FormatTest.java deleted file mode 100644 index bdea08638b..0000000000 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/FormatTest.java +++ /dev/null @@ -1,151 +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; - -import static com.google.android.exoplayer2.C.WIDEVINE_UUID; -import static com.google.android.exoplayer2.util.MimeTypes.VIDEO_MP4; -import static com.google.android.exoplayer2.util.MimeTypes.VIDEO_WEBM; - -import android.annotation.SuppressLint; -import android.annotation.TargetApi; -import android.media.MediaFormat; -import android.os.Parcel; -import com.google.android.exoplayer2.drm.DrmInitData; -import com.google.android.exoplayer2.metadata.Metadata; -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; -import java.util.Collections; -import java.util.List; -import junit.framework.TestCase; - -/** - * Unit test for {@link Format}. - */ -public final class FormatTest extends TestCase { - - private static final List INIT_DATA; - static { - byte[] initData1 = new byte[] {1, 2, 3}; - byte[] initData2 = new byte[] {4, 5, 6}; - List initData = new ArrayList<>(); - initData.add(initData1); - initData.add(initData2); - INIT_DATA = Collections.unmodifiableList(initData); - } - - public void testParcelable() { - DrmInitData.SchemeData DRM_DATA_1 = new DrmInitData.SchemeData(WIDEVINE_UUID, "cenc", VIDEO_MP4, - TestUtil.buildTestData(128, 1 /* data seed */)); - DrmInitData.SchemeData DRM_DATA_2 = new DrmInitData.SchemeData(C.UUID_NIL, null, VIDEO_WEBM, - TestUtil.buildTestData(128, 1 /* data seed */)); - DrmInitData drmInitData = new DrmInitData(DRM_DATA_1, DRM_DATA_2); - byte[] projectionData = new byte[] {1, 2, 3}; - 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, 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); - parcel.setDataPosition(0); - - Format formatFromParcel = Format.CREATOR.createFromParcel(parcel); - assertEquals(formatToParcel, formatFromParcel); - - parcel.recycle(); - } - - public void testConversionToFrameworkMediaFormat() { - if (Util.SDK_INT < 16) { - // Test doesn't apply. - return; - } - - testConversionToFrameworkMediaFormatV16(Format.createVideoSampleFormat(null, "video/xyz", null, - 5000, 102400, 1280, 720, 30, INIT_DATA, null)); - testConversionToFrameworkMediaFormatV16(Format.createVideoSampleFormat(null, "video/xyz", null, - 5000, Format.NO_VALUE, 1280, 720, 30, null, null)); - testConversionToFrameworkMediaFormatV16(Format.createAudioSampleFormat(null, "audio/xyz", null, - 500, 128, 5, 44100, INIT_DATA, null, 0, null)); - testConversionToFrameworkMediaFormatV16(Format.createAudioSampleFormat(null, "audio/xyz", null, - 500, Format.NO_VALUE, 5, 44100, null, null, 0, null)); - testConversionToFrameworkMediaFormatV16(Format.createTextSampleFormat(null, "text/xyz", 0, - "eng")); - testConversionToFrameworkMediaFormatV16(Format.createTextSampleFormat(null, "text/xyz", 0, - null)); - } - - @SuppressLint("InlinedApi") - @TargetApi(16) - private static void testConversionToFrameworkMediaFormatV16(Format in) { - MediaFormat out = in.getFrameworkMediaFormatV16(); - assertEquals(in.sampleMimeType, out.getString(MediaFormat.KEY_MIME)); - assertOptionalV16(out, MediaFormat.KEY_LANGUAGE, in.language); - assertOptionalV16(out, MediaFormat.KEY_MAX_INPUT_SIZE, in.maxInputSize); - assertOptionalV16(out, MediaFormat.KEY_WIDTH, in.width); - assertOptionalV16(out, MediaFormat.KEY_HEIGHT, in.height); - assertOptionalV16(out, MediaFormat.KEY_CHANNEL_COUNT, in.channelCount); - assertOptionalV16(out, MediaFormat.KEY_SAMPLE_RATE, in.sampleRate); - assertOptionalV16(out, MediaFormat.KEY_FRAME_RATE, in.frameRate); - - for (int i = 0; i < in.initializationData.size(); i++) { - byte[] originalData = in.initializationData.get(i); - ByteBuffer frameworkBuffer = out.getByteBuffer("csd-" + i); - byte[] frameworkData = Arrays.copyOf(frameworkBuffer.array(), frameworkBuffer.limit()); - assertTrue(Arrays.equals(originalData, frameworkData)); - } - } - - @TargetApi(16) - private static void assertOptionalV16(MediaFormat format, String key, String value) { - if (value == null) { - assertFalse(format.containsKey(key)); - } else { - assertEquals(value, format.getString(key)); - } - } - - @TargetApi(16) - private static void assertOptionalV16(MediaFormat format, String key, int value) { - if (value == Format.NO_VALUE) { - assertFalse(format.containsKey(key)); - } else { - assertEquals(value, format.getInteger(key)); - } - } - - @TargetApi(16) - private static void assertOptionalV16(MediaFormat format, String key, float value) { - if (value == Format.NO_VALUE) { - assertFalse(format.containsKey(key)); - } else { - assertEquals(value, format.getFloat(key)); - } - } - -} diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/util/AtomicFileTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/util/AtomicFileTest.java deleted file mode 100644 index b4f1d50293..0000000000 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/util/AtomicFileTest.java +++ /dev/null @@ -1,85 +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.test.InstrumentationTestCase; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -/** - * Tests {@link AtomicFile}. - */ -public class AtomicFileTest extends InstrumentationTestCase { - - private File tempFolder; - private File file; - private AtomicFile atomicFile; - - @Override - public void setUp() throws Exception { - tempFolder = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest"); - file = new File(tempFolder, "atomicFile"); - atomicFile = new AtomicFile(file); - } - - @Override - protected void tearDown() throws Exception { - Util.recursiveDelete(tempFolder); - } - - public void testDelete() throws Exception { - assertTrue(file.createNewFile()); - atomicFile.delete(); - assertFalse(file.exists()); - } - - public void testWriteRead() throws Exception { - OutputStream output = atomicFile.startWrite(); - output.write(5); - atomicFile.endWrite(output); - output.close(); - - assertRead(); - - output = atomicFile.startWrite(); - output.write(5); - output.write(6); - output.close(); - - assertRead(); - - output = atomicFile.startWrite(); - output.write(6); - - assertRead(); - output.close(); - - output = atomicFile.startWrite(); - - assertRead(); - output.close(); - } - - private void assertRead() throws IOException { - InputStream input = atomicFile.openRead(); - assertEquals(5, input.read()); - assertEquals(-1, input.read()); - input.close(); - } - -} diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ColorParserTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ColorParserTest.java deleted file mode 100644 index 641b58b0ce..0000000000 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ColorParserTest.java +++ /dev/null @@ -1,98 +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.graphics.Color; -import android.test.InstrumentationTestCase; - -/** - * Unit test for ColorParser. - */ -public class ColorParserTest extends InstrumentationTestCase { - - // Negative tests. - - public void testParseUnknownColor() { - try { - ColorParser.parseTtmlColor("colorOfAnElectron"); - fail(); - } catch (IllegalArgumentException e) { - // expected - } - } - - public void testParseNull() { - try { - ColorParser.parseTtmlColor(null); - fail(); - } catch (IllegalArgumentException e) { - // expected - } - } - - public void testParseEmpty() { - try { - ColorParser.parseTtmlColor(""); - fail(); - } catch (IllegalArgumentException e) { - // expected - } - } - - public void testRgbColorParsingRgbValuesNegative() { - try { - ColorParser.parseTtmlColor("rgb(-4, 55, 209)"); - fail(); - } catch (IllegalArgumentException e) { - // expected - } - } - - // Positive tests. - - public void testHexCodeParsing() { - assertEquals(Color.WHITE, ColorParser.parseTtmlColor("#FFFFFF")); - assertEquals(Color.WHITE, ColorParser.parseTtmlColor("#FFFFFFFF")); - assertEquals(Color.parseColor("#FF123456"), ColorParser.parseTtmlColor("#123456")); - // Hex colors in ColorParser are RGBA, where-as {@link Color#parseColor} takes ARGB. - assertEquals(Color.parseColor("#00FFFFFF"), ColorParser.parseTtmlColor("#FFFFFF00")); - assertEquals(Color.parseColor("#78123456"), ColorParser.parseTtmlColor("#12345678")); - } - - public void testRgbColorParsing() { - assertEquals(Color.WHITE, ColorParser.parseTtmlColor("rgb(255,255,255)")); - // Spaces are ignored. - assertEquals(Color.WHITE, ColorParser.parseTtmlColor(" rgb ( 255, 255, 255)")); - } - - public void testRgbColorParsingRgbValuesOutOfBounds() { - int outOfBounds = ColorParser.parseTtmlColor("rgb(999, 999, 999)"); - int color = Color.rgb(999, 999, 999); - // Behave like the framework does. - assertEquals(color, outOfBounds); - } - - public void testRgbaColorParsing() { - assertEquals(Color.WHITE, ColorParser.parseTtmlColor("rgba(255,255,255,255)")); - assertEquals(Color.argb(255, 255, 255, 255), - ColorParser.parseTtmlColor("rgba(255,255,255,255)")); - assertEquals(Color.BLACK, ColorParser.parseTtmlColor("rgba(0, 0, 0, 255)")); - assertEquals(Color.argb(0, 0, 0, 255), ColorParser.parseTtmlColor("rgba(0, 0, 255, 0)")); - assertEquals(Color.RED, ColorParser.parseTtmlColor("rgba(255, 0, 0, 255)")); - assertEquals(Color.argb(0, 255, 0, 255), ColorParser.parseTtmlColor("rgba(255, 0, 255, 0)")); - assertEquals(Color.argb(205, 255, 0, 0), ColorParser.parseTtmlColor("rgba(255, 0, 0, 205)")); - } -} diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/util/NalUnitUtilTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/util/NalUnitUtilTest.java deleted file mode 100644 index 286013e83a..0000000000 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/util/NalUnitUtilTest.java +++ /dev/null @@ -1,206 +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 static com.google.android.exoplayer2.testutil.TestUtil.createByteArray; - -import java.nio.ByteBuffer; -import java.util.Arrays; -import junit.framework.TestCase; - -/** - * Tests for {@link NalUnitUtil}. - */ -public class NalUnitUtilTest extends TestCase { - - private static final int TEST_PARTIAL_NAL_POSITION = 4; - private static final int TEST_NAL_POSITION = 10; - private static final byte[] SPS_TEST_DATA = createByteArray(0x00, 0x00, 0x01, 0x67, 0x4D, 0x40, - 0x16, 0xEC, 0xA0, 0x50, 0x17, 0xFC, 0xB8, 0x08, 0x80, 0x00, 0x00, 0x03, 0x00, 0x80, 0x00, - 0x00, 0x0F, 0x47, 0x8B, 0x16, 0xCB); - private static final int SPS_TEST_DATA_OFFSET = 3; - - public void testFindNalUnit() { - byte[] data = buildTestData(); - - // Should find NAL unit. - int result = NalUnitUtil.findNalUnit(data, 0, data.length, null); - assertEquals(TEST_NAL_POSITION, result); - // Should find NAL unit whose prefix ends one byte before the limit. - result = NalUnitUtil.findNalUnit(data, 0, TEST_NAL_POSITION + 4, null); - assertEquals(TEST_NAL_POSITION, result); - // Shouldn't find NAL unit whose prefix ends at the limit (since the limit is exclusive). - result = NalUnitUtil.findNalUnit(data, 0, TEST_NAL_POSITION + 3, null); - assertEquals(TEST_NAL_POSITION + 3, result); - // Should find NAL unit whose prefix starts at the offset. - result = NalUnitUtil.findNalUnit(data, TEST_NAL_POSITION, data.length, null); - assertEquals(TEST_NAL_POSITION, result); - // Shouldn't find NAL unit whose prefix starts one byte past the offset. - result = NalUnitUtil.findNalUnit(data, TEST_NAL_POSITION + 1, data.length, null); - assertEquals(data.length, result); - } - - public void testFindNalUnitWithPrefix() { - byte[] data = buildTestData(); - - // First byte of NAL unit in data1, rest in data2. - boolean[] prefixFlags = new boolean[3]; - byte[] data1 = Arrays.copyOfRange(data, 0, TEST_NAL_POSITION + 1); - byte[] data2 = Arrays.copyOfRange(data, TEST_NAL_POSITION + 1, data.length); - int result = NalUnitUtil.findNalUnit(data1, 0, data1.length, prefixFlags); - assertEquals(data1.length, result); - result = NalUnitUtil.findNalUnit(data2, 0, data2.length, prefixFlags); - assertEquals(-1, result); - assertPrefixFlagsCleared(prefixFlags); - - // First three bytes of NAL unit in data1, rest in data2. - prefixFlags = new boolean[3]; - data1 = Arrays.copyOfRange(data, 0, TEST_NAL_POSITION + 3); - data2 = Arrays.copyOfRange(data, TEST_NAL_POSITION + 3, data.length); - result = NalUnitUtil.findNalUnit(data1, 0, data1.length, prefixFlags); - assertEquals(data1.length, result); - result = NalUnitUtil.findNalUnit(data2, 0, data2.length, prefixFlags); - assertEquals(-3, result); - assertPrefixFlagsCleared(prefixFlags); - - // First byte of NAL unit in data1, second byte in data2, rest in data3. - prefixFlags = new boolean[3]; - data1 = Arrays.copyOfRange(data, 0, TEST_NAL_POSITION + 1); - data2 = Arrays.copyOfRange(data, TEST_NAL_POSITION + 1, TEST_NAL_POSITION + 2); - byte[] data3 = Arrays.copyOfRange(data, TEST_NAL_POSITION + 2, data.length); - result = NalUnitUtil.findNalUnit(data1, 0, data1.length, prefixFlags); - assertEquals(data1.length, result); - result = NalUnitUtil.findNalUnit(data2, 0, data2.length, prefixFlags); - assertEquals(data2.length, result); - result = NalUnitUtil.findNalUnit(data3, 0, data3.length, prefixFlags); - assertEquals(-2, result); - assertPrefixFlagsCleared(prefixFlags); - - // NAL unit split with one byte in four arrays. - prefixFlags = new boolean[3]; - data1 = Arrays.copyOfRange(data, 0, TEST_NAL_POSITION + 1); - data2 = Arrays.copyOfRange(data, TEST_NAL_POSITION + 1, TEST_NAL_POSITION + 2); - data3 = Arrays.copyOfRange(data, TEST_NAL_POSITION + 2, TEST_NAL_POSITION + 3); - byte[] data4 = Arrays.copyOfRange(data, TEST_NAL_POSITION + 2, data.length); - result = NalUnitUtil.findNalUnit(data1, 0, data1.length, prefixFlags); - assertEquals(data1.length, result); - result = NalUnitUtil.findNalUnit(data2, 0, data2.length, prefixFlags); - assertEquals(data2.length, result); - result = NalUnitUtil.findNalUnit(data3, 0, data3.length, prefixFlags); - assertEquals(data3.length, result); - result = NalUnitUtil.findNalUnit(data4, 0, data4.length, prefixFlags); - assertEquals(-3, result); - assertPrefixFlagsCleared(prefixFlags); - - // NAL unit entirely in data2. data1 ends with partial prefix. - prefixFlags = new boolean[3]; - data1 = Arrays.copyOfRange(data, 0, TEST_PARTIAL_NAL_POSITION + 2); - data2 = Arrays.copyOfRange(data, TEST_PARTIAL_NAL_POSITION + 2, data.length); - result = NalUnitUtil.findNalUnit(data1, 0, data1.length, prefixFlags); - assertEquals(data1.length, result); - result = NalUnitUtil.findNalUnit(data2, 0, data2.length, prefixFlags); - assertEquals(4, result); - assertPrefixFlagsCleared(prefixFlags); - } - - public void testParseSpsNalUnit() { - NalUnitUtil.SpsData data = NalUnitUtil.parseSpsNalUnit(SPS_TEST_DATA, SPS_TEST_DATA_OFFSET, - SPS_TEST_DATA.length); - assertEquals(640, data.width); - assertEquals(360, data.height); - assertFalse(data.deltaPicOrderAlwaysZeroFlag); - assertTrue(data.frameMbsOnlyFlag); - assertEquals(4, data.frameNumLength); - assertEquals(6, data.picOrderCntLsbLength); - assertEquals(0, data.seqParameterSetId); - assertEquals(1.0f, data.pixelWidthAspectRatio); - assertEquals(0, data.picOrderCountType); - assertFalse(data.separateColorPlaneFlag); - } - - public void testUnescapeDoesNotModifyBuffersWithoutStartCodes() { - assertUnescapeDoesNotModify(""); - assertUnescapeDoesNotModify("0000"); - assertUnescapeDoesNotModify("172BF38A3C"); - assertUnescapeDoesNotModify("000004"); - } - - public void testUnescapeModifiesBuffersWithStartCodes() { - assertUnescapeMatchesExpected("00000301", "000001"); - assertUnescapeMatchesExpected("0000030200000300", "000002000000"); - } - - public void testDiscardToSps() { - assertDiscardToSpsMatchesExpected("", ""); - assertDiscardToSpsMatchesExpected("00", ""); - assertDiscardToSpsMatchesExpected("FFFF000001", ""); - assertDiscardToSpsMatchesExpected("00000001", ""); - assertDiscardToSpsMatchesExpected("00000001FF67", ""); - assertDiscardToSpsMatchesExpected("00000001000167", ""); - assertDiscardToSpsMatchesExpected("0000000167", "0000000167"); - assertDiscardToSpsMatchesExpected("0000000167FF", "0000000167FF"); - assertDiscardToSpsMatchesExpected("0000000167FF", "0000000167FF"); - assertDiscardToSpsMatchesExpected("0000000167FF000000016700", "0000000167FF000000016700"); - assertDiscardToSpsMatchesExpected("000000000167FF", "0000000167FF"); - assertDiscardToSpsMatchesExpected("0001670000000167FF", "0000000167FF"); - assertDiscardToSpsMatchesExpected("FF00000001660000000167FF", "0000000167FF"); - } - - private static byte[] buildTestData() { - byte[] data = new byte[20]; - for (int i = 0; i < data.length; i++) { - data[i] = (byte) 0xFF; - } - // Insert an incomplete NAL unit start code. - data[TEST_PARTIAL_NAL_POSITION] = 0; - data[TEST_PARTIAL_NAL_POSITION + 1] = 0; - // Insert a complete NAL unit start code. - data[TEST_NAL_POSITION] = 0; - data[TEST_NAL_POSITION + 1] = 0; - data[TEST_NAL_POSITION + 2] = 1; - data[TEST_NAL_POSITION + 3] = 5; - return data; - } - - private static void assertPrefixFlagsCleared(boolean[] flags) { - assertEquals(false, flags[0] || flags[1] || flags[2]); - } - - private static void assertUnescapeDoesNotModify(String input) { - assertUnescapeMatchesExpected(input, input); - } - - private static void assertUnescapeMatchesExpected(String input, String expectedOutput) { - byte[] bitstream = Util.getBytesFromHexString(input); - byte[] expectedOutputBitstream = Util.getBytesFromHexString(expectedOutput); - int count = NalUnitUtil.unescapeStream(bitstream, bitstream.length); - assertEquals(expectedOutputBitstream.length, count); - byte[] outputBitstream = new byte[count]; - System.arraycopy(bitstream, 0, outputBitstream, 0, count); - assertTrue(Arrays.equals(expectedOutputBitstream, outputBitstream)); - } - - private static void assertDiscardToSpsMatchesExpected(String input, String expectedOutput) { - byte[] bitstream = Util.getBytesFromHexString(input); - byte[] expectedOutputBitstream = Util.getBytesFromHexString(expectedOutput); - ByteBuffer buffer = ByteBuffer.wrap(bitstream); - buffer.position(buffer.limit()); - NalUnitUtil.discardToSps(buffer); - assertTrue(Arrays.equals(expectedOutputBitstream, - Arrays.copyOf(buffer.array(), buffer.position()))); - } - -} diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ParsableBitArrayTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ParsableBitArrayTest.java deleted file mode 100644 index d7b2b36740..0000000000 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ParsableBitArrayTest.java +++ /dev/null @@ -1,177 +0,0 @@ -/* - * 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.util; - -import android.test.MoreAsserts; -import junit.framework.TestCase; - -/** - * Tests for {@link ParsableBitArray}. - */ -public final class ParsableBitArrayTest extends TestCase { - - private static final byte[] TEST_DATA = new byte[] {0x3C, (byte) 0xD2, (byte) 0x5F, (byte) 0x01, - (byte) 0xFF, (byte) 0x14, (byte) 0x60, (byte) 0x99}; - - private ParsableBitArray testArray; - - @Override - public void setUp() { - testArray = new ParsableBitArray(TEST_DATA); - } - - public void testReadAllBytes() { - byte[] bytesRead = new byte[TEST_DATA.length]; - testArray.readBytes(bytesRead, 0, TEST_DATA.length); - MoreAsserts.assertEquals(TEST_DATA, bytesRead); - assertEquals(TEST_DATA.length * 8, testArray.getPosition()); - assertEquals(TEST_DATA.length, testArray.getBytePosition()); - } - - public void testReadBit() { - assertReadBitsToEnd(0); - } - - public void testReadBits() { - assertEquals(getTestDataBits(0, 5), testArray.readBits(5)); - assertEquals(getTestDataBits(5, 0), testArray.readBits(0)); - assertEquals(getTestDataBits(5, 3), testArray.readBits(3)); - assertEquals(getTestDataBits(8, 16), testArray.readBits(16)); - assertEquals(getTestDataBits(24, 3), testArray.readBits(3)); - assertEquals(getTestDataBits(27, 18), testArray.readBits(18)); - assertEquals(getTestDataBits(45, 5), testArray.readBits(5)); - assertEquals(getTestDataBits(50, 14), testArray.readBits(14)); - } - - public void testReadBitsToByteArray() { - byte[] result = new byte[TEST_DATA.length]; - // Test read within byte boundaries. - testArray.readBits(result, 0, 6); - assertEquals(TEST_DATA[0] & 0xFC, result[0]); - // Test read across byte boundaries. - testArray.readBits(result, 0, 8); - assertEquals(((TEST_DATA[0] & 0x03) << 6) | ((TEST_DATA[1] & 0xFC) >> 2), result[0]); - // Test reading across multiple bytes. - testArray.readBits(result, 1, 50); - for (int i = 1; i < 7; i++) { - assertEquals((byte) (((TEST_DATA[i] & 0x03) << 6) | ((TEST_DATA[i + 1] & 0xFC) >> 2)), - result[i]); - } - assertEquals((byte) (TEST_DATA[7] & 0x03) << 6, result[7]); - assertEquals(0, testArray.bitsLeft()); - // Test read last buffer byte across input data bytes. - testArray.setPosition(31); - result[3] = 0; - testArray.readBits(result, 3, 3); - assertEquals((byte) 0xE0, result[3]); - // Test read bits in the middle of a input data byte. - result[0] = 0; - assertEquals(34, testArray.getPosition()); - testArray.readBits(result, 0, 3); - assertEquals((byte) 0xE0, result[0]); - // Test read 0 bits. - testArray.setPosition(32); - result[1] = 0; - testArray.readBits(result, 1, 0); - assertEquals(0, result[1]); - // Test reading a number of bits divisible by 8. - testArray.setPosition(0); - testArray.readBits(result, 0, 16); - assertEquals(TEST_DATA[0], result[0]); - assertEquals(TEST_DATA[1], result[1]); - // Test least significant bits are unmodified. - result[1] = (byte) 0xFF; - testArray.readBits(result, 0, 9); - assertEquals(0x5F, result[0]); - assertEquals(0x7F, result[1]); - } - - public void testRead32BitsByteAligned() { - assertEquals(getTestDataBits(0, 32), testArray.readBits(32)); - assertEquals(getTestDataBits(32, 32), testArray.readBits(32)); - } - - public void testRead32BitsNonByteAligned() { - assertEquals(getTestDataBits(0, 5), testArray.readBits(5)); - assertEquals(getTestDataBits(5, 32), testArray.readBits(32)); - } - - public void testSkipBytes() { - testArray.skipBytes(2); - assertReadBitsToEnd(16); - } - - public void testSkipBitsByteAligned() { - testArray.skipBits(16); - assertReadBitsToEnd(16); - } - - public void testSkipBitsNonByteAligned() { - testArray.skipBits(5); - assertReadBitsToEnd(5); - } - - public void testSetPositionByteAligned() { - testArray.setPosition(16); - assertReadBitsToEnd(16); - } - - public void testSetPositionNonByteAligned() { - testArray.setPosition(5); - assertReadBitsToEnd(5); - } - - public void testByteAlignFromNonByteAligned() { - testArray.setPosition(11); - testArray.byteAlign(); - assertEquals(2, testArray.getBytePosition()); - assertEquals(16, testArray.getPosition()); - assertReadBitsToEnd(16); - } - - public void testByteAlignFromByteAligned() { - testArray.setPosition(16); - testArray.byteAlign(); // Should be a no-op. - assertEquals(2, testArray.getBytePosition()); - assertEquals(16, testArray.getPosition()); - assertReadBitsToEnd(16); - } - - private void assertReadBitsToEnd(int expectedStartPosition) { - int position = testArray.getPosition(); - assertEquals(expectedStartPosition, position); - for (int i = position; i < TEST_DATA.length * 8; i++) { - assertEquals(getTestDataBit(i), testArray.readBit()); - assertEquals(i + 1, testArray.getPosition()); - } - } - - private static int getTestDataBits(int bitPosition, int length) { - int result = 0; - for (int i = 0; i < length; i++) { - result = result << 1; - if (getTestDataBit(bitPosition++)) { - result |= 0x1; - } - } - return result; - } - - private static boolean getTestDataBit(int bitPosition) { - return (TEST_DATA[bitPosition / 8] & (0x80 >>> (bitPosition % 8))) != 0; - } - -} diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ParsableByteArrayTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ParsableByteArrayTest.java deleted file mode 100644 index 324d668c7a..0000000000 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ParsableByteArrayTest.java +++ /dev/null @@ -1,492 +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 java.nio.ByteBuffer; -import java.nio.charset.Charset; -import java.util.Arrays; -import junit.framework.TestCase; - -/** - * Tests for {@link ParsableByteArray}. - */ -public class ParsableByteArrayTest extends TestCase { - - private static final byte[] TEST_DATA = - new byte[] {0x0F, (byte) 0xFF, (byte) 0x42, (byte) 0x0F, 0x00, 0x00, 0x00, 0x00}; - - private static ParsableByteArray getTestDataArray() { - ParsableByteArray testArray = new ParsableByteArray(TEST_DATA.length); - System.arraycopy(TEST_DATA, 0, testArray.data, 0, TEST_DATA.length); - return testArray; - } - - public void testReadShort() { - testReadShort((short) -1); - testReadShort((short) 0); - testReadShort((short) 1); - testReadShort(Short.MIN_VALUE); - testReadShort(Short.MAX_VALUE); - } - - private static void testReadShort(short testValue) { - ParsableByteArray testArray = new ParsableByteArray( - ByteBuffer.allocate(4).putShort(testValue).array()); - int readValue = testArray.readShort(); - - // Assert that the value we read was the value we wrote. - assertEquals(testValue, readValue); - // And that the position advanced as expected. - assertEquals(2, testArray.getPosition()); - - // And that skipping back and reading gives the same results. - testArray.skipBytes(-2); - readValue = testArray.readShort(); - assertEquals(testValue, readValue); - assertEquals(2, testArray.getPosition()); - } - - public void testReadInt() { - testReadInt(0); - testReadInt(1); - testReadInt(-1); - testReadInt(Integer.MIN_VALUE); - testReadInt(Integer.MAX_VALUE); - } - - private static void testReadInt(int testValue) { - ParsableByteArray testArray = new ParsableByteArray( - ByteBuffer.allocate(4).putInt(testValue).array()); - int readValue = testArray.readInt(); - - // Assert that the value we read was the value we wrote. - assertEquals(testValue, readValue); - // And that the position advanced as expected. - assertEquals(4, testArray.getPosition()); - - // And that skipping back and reading gives the same results. - testArray.skipBytes(-4); - readValue = testArray.readInt(); - assertEquals(testValue, readValue); - assertEquals(4, testArray.getPosition()); - } - - public void testReadUnsignedInt() { - testReadUnsignedInt(0); - testReadUnsignedInt(1); - testReadUnsignedInt(Integer.MAX_VALUE); - testReadUnsignedInt(Integer.MAX_VALUE + 1L); - testReadUnsignedInt(0xFFFFFFFFL); - } - - private static void testReadUnsignedInt(long testValue) { - ParsableByteArray testArray = new ParsableByteArray( - Arrays.copyOfRange(ByteBuffer.allocate(8).putLong(testValue).array(), 4, 8)); - long readValue = testArray.readUnsignedInt(); - - // Assert that the value we read was the value we wrote. - assertEquals(testValue, readValue); - // And that the position advanced as expected. - assertEquals(4, testArray.getPosition()); - - // And that skipping back and reading gives the same results. - testArray.skipBytes(-4); - readValue = testArray.readUnsignedInt(); - assertEquals(testValue, readValue); - assertEquals(4, testArray.getPosition()); - } - - public void testReadUnsignedIntToInt() { - testReadUnsignedIntToInt(0); - testReadUnsignedIntToInt(1); - testReadUnsignedIntToInt(Integer.MAX_VALUE); - try { - testReadUnsignedIntToInt(-1); - fail(); - } catch (IllegalStateException e) { - // Expected. - } - try { - testReadUnsignedIntToInt(Integer.MIN_VALUE); - fail(); - } catch (IllegalStateException e) { - // Expected. - } - } - - private static void testReadUnsignedIntToInt(int testValue) { - ParsableByteArray testArray = new ParsableByteArray( - ByteBuffer.allocate(4).putInt(testValue).array()); - int readValue = testArray.readUnsignedIntToInt(); - - // Assert that the value we read was the value we wrote. - assertEquals(testValue, readValue); - // And that the position advanced as expected. - assertEquals(4, testArray.getPosition()); - - // And that skipping back and reading gives the same results. - testArray.skipBytes(-4); - readValue = testArray.readUnsignedIntToInt(); - assertEquals(testValue, readValue); - assertEquals(4, testArray.getPosition()); - } - - public void testReadUnsignedLongToLong() { - testReadUnsignedLongToLong(0); - testReadUnsignedLongToLong(1); - testReadUnsignedLongToLong(Long.MAX_VALUE); - try { - testReadUnsignedLongToLong(-1); - fail(); - } catch (IllegalStateException e) { - // Expected. - } - try { - testReadUnsignedLongToLong(Long.MIN_VALUE); - fail(); - } catch (IllegalStateException e) { - // Expected. - } - } - - private static void testReadUnsignedLongToLong(long testValue) { - ParsableByteArray testArray = new ParsableByteArray( - ByteBuffer.allocate(8).putLong(testValue).array()); - long readValue = testArray.readUnsignedLongToLong(); - - // Assert that the value we read was the value we wrote. - assertEquals(testValue, readValue); - // And that the position advanced as expected. - assertEquals(8, testArray.getPosition()); - - // And that skipping back and reading gives the same results. - testArray.skipBytes(-8); - readValue = testArray.readUnsignedLongToLong(); - assertEquals(testValue, readValue); - assertEquals(8, testArray.getPosition()); - } - - public void testReadLong() { - testReadLong(0); - testReadLong(1); - testReadLong(-1); - testReadLong(Long.MIN_VALUE); - testReadLong(Long.MAX_VALUE); - } - - private static void testReadLong(long testValue) { - ParsableByteArray testArray = new ParsableByteArray( - ByteBuffer.allocate(8).putLong(testValue).array()); - long readValue = testArray.readLong(); - - // Assert that the value we read was the value we wrote. - assertEquals(testValue, readValue); - // And that the position advanced as expected. - assertEquals(8, testArray.getPosition()); - - // And that skipping back and reading gives the same results. - testArray.skipBytes(-8); - readValue = testArray.readLong(); - assertEquals(testValue, readValue); - assertEquals(8, testArray.getPosition()); - } - - public void testReadingMovesPosition() { - ParsableByteArray parsableByteArray = getTestDataArray(); - - // Given an array at the start - assertEquals(0, parsableByteArray.getPosition()); - // When reading an integer, the position advances - parsableByteArray.readUnsignedInt(); - assertEquals(4, parsableByteArray.getPosition()); - } - - public void testOutOfBoundsThrows() { - ParsableByteArray parsableByteArray = getTestDataArray(); - - // Given an array at the end - parsableByteArray.readUnsignedLongToLong(); - assertEquals(TEST_DATA.length, parsableByteArray.getPosition()); - // Then reading more data throws. - try { - parsableByteArray.readUnsignedInt(); - fail(); - } catch (Exception e) { - // Expected. - } - } - - public void testModificationsAffectParsableArray() { - ParsableByteArray parsableByteArray = getTestDataArray(); - - // When modifying the wrapped byte array - byte[] data = parsableByteArray.data; - long readValue = parsableByteArray.readUnsignedInt(); - data[0] = (byte) (TEST_DATA[0] + 1); - parsableByteArray.setPosition(0); - // Then the parsed value changes. - assertFalse(parsableByteArray.readUnsignedInt() == readValue); - } - - public void testReadingUnsignedLongWithMsbSetThrows() { - ParsableByteArray parsableByteArray = getTestDataArray(); - - // Given an array with the most-significant bit set on the top byte - byte[] data = parsableByteArray.data; - data[0] = (byte) 0x80; - // Then reading an unsigned long throws. - try { - parsableByteArray.readUnsignedLongToLong(); - fail(); - } catch (Exception e) { - // Expected. - } - } - - public void testReadUnsignedFixedPoint1616() { - ParsableByteArray parsableByteArray = getTestDataArray(); - - // When reading the integer part of a 16.16 fixed point value - int value = parsableByteArray.readUnsignedFixedPoint1616(); - // Then the read value is equal to the array elements interpreted as a short. - assertEquals((0xFF & TEST_DATA[0]) << 8 | (TEST_DATA[1] & 0xFF), value); - assertEquals(4, parsableByteArray.getPosition()); - } - - public void testReadingBytesReturnsCopy() { - ParsableByteArray parsableByteArray = getTestDataArray(); - - // When reading all the bytes back - int length = parsableByteArray.limit(); - assertEquals(TEST_DATA.length, length); - byte[] copy = new byte[length]; - parsableByteArray.readBytes(copy, 0, length); - // Then the array elements are the same. - assertTrue(Arrays.equals(parsableByteArray.data, copy)); - } - - public void testReadLittleEndianLong() { - ParsableByteArray byteArray = new ParsableByteArray(new byte[] { - 0x01, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, (byte) 0xFF - }); - assertEquals(0xFF00000000000001L, byteArray.readLittleEndianLong()); - assertEquals(8, byteArray.getPosition()); - } - - public void testReadLittleEndianUnsignedInt() { - ParsableByteArray byteArray = new ParsableByteArray(new byte[] { - 0x10, 0x00, 0x00, (byte) 0xFF - }); - assertEquals(0xFF000010L, byteArray.readLittleEndianUnsignedInt()); - assertEquals(4, byteArray.getPosition()); - } - - public void testReadLittleEndianInt() { - ParsableByteArray byteArray = new ParsableByteArray(new byte[] { - 0x01, 0x00, 0x00, (byte) 0xFF - }); - assertEquals(0xFF000001, byteArray.readLittleEndianInt()); - assertEquals(4, byteArray.getPosition()); - } - - public void testReadLittleEndianUnsignedInt24() { - byte[] data = { 0x01, 0x02, (byte) 0xFF }; - ParsableByteArray byteArray = new ParsableByteArray(data); - assertEquals(0xFF0201, byteArray.readLittleEndianUnsignedInt24()); - assertEquals(3, byteArray.getPosition()); - } - - public void testReadLittleEndianUnsignedShort() { - ParsableByteArray byteArray = new ParsableByteArray(new byte[] { - 0x01, (byte) 0xFF, 0x02, (byte) 0xFF - }); - assertEquals(0xFF01, byteArray.readLittleEndianUnsignedShort()); - assertEquals(2, byteArray.getPosition()); - assertEquals(0xFF02, byteArray.readLittleEndianUnsignedShort()); - assertEquals(4, byteArray.getPosition()); - } - - public void testReadLittleEndianShort() { - ParsableByteArray byteArray = new ParsableByteArray(new byte[] { - 0x01, (byte) 0xFF, 0x02, (byte) 0xFF - }); - assertEquals((short) 0xFF01, byteArray.readLittleEndianShort()); - assertEquals(2, byteArray.getPosition()); - assertEquals((short) 0xFF02, byteArray.readLittleEndianShort()); - assertEquals(4, byteArray.getPosition()); - } - - public void testReadString() { - byte[] data = { - (byte) 0xC3, (byte) 0xA4, (byte) 0x20, - (byte) 0xC3, (byte) 0xB6, (byte) 0x20, - (byte) 0xC2, (byte) 0xAE, (byte) 0x20, - (byte) 0xCF, (byte) 0x80, (byte) 0x20, - (byte) 0xE2, (byte) 0x88, (byte) 0x9A, (byte) 0x20, - (byte) 0xC2, (byte) 0xB1, (byte) 0x20, - (byte) 0xE8, (byte) 0xB0, (byte) 0xA2, (byte) 0x20, - }; - ParsableByteArray byteArray = new ParsableByteArray(data); - assertEquals("ä ö ® π √ ± 谢 ", byteArray.readString(data.length)); - assertEquals(data.length, byteArray.getPosition()); - } - - public void testReadAsciiString() { - byte[] data = new byte[] {'t', 'e', 's', 't'}; - ParsableByteArray testArray = new ParsableByteArray(data); - assertEquals("test", testArray.readString(data.length, Charset.forName("US-ASCII"))); - assertEquals(data.length, testArray.getPosition()); - } - - public void testReadStringOutOfBoundsDoesNotMovePosition() { - byte[] data = { - (byte) 0xC3, (byte) 0xA4, (byte) 0x20 - }; - ParsableByteArray byteArray = new ParsableByteArray(data); - try { - byteArray.readString(data.length + 1); - fail(); - } catch (StringIndexOutOfBoundsException e) { - assertEquals(0, byteArray.getPosition()); - } - } - - public void testReadEmptyString() { - byte[] bytes = new byte[0]; - ParsableByteArray parser = new ParsableByteArray(bytes); - assertNull(parser.readLine()); - } - - public void testReadNullTerminatedStringWithLengths() { - byte[] bytes = new byte[] { - 'f', 'o', 'o', 0, 'b', 'a', 'r', 0 - }; - // Test with lengths that match NUL byte positions. - ParsableByteArray parser = new ParsableByteArray(bytes); - assertEquals("foo", parser.readNullTerminatedString(4)); - assertEquals(4, parser.getPosition()); - assertEquals("bar", parser.readNullTerminatedString(4)); - assertEquals(8, parser.getPosition()); - assertNull(parser.readNullTerminatedString()); - // Test with lengths that do not match NUL byte positions. - parser = new ParsableByteArray(bytes); - assertEquals("fo", parser.readNullTerminatedString(2)); - assertEquals(2, parser.getPosition()); - assertEquals("o", parser.readNullTerminatedString(2)); - assertEquals(4, parser.getPosition()); - assertEquals("bar", parser.readNullTerminatedString(3)); - assertEquals(7, parser.getPosition()); - assertEquals("", parser.readNullTerminatedString(1)); - assertEquals(8, parser.getPosition()); - assertNull(parser.readNullTerminatedString()); - // Test with limit at NUL - parser = new ParsableByteArray(bytes, 4); - assertEquals("foo", parser.readNullTerminatedString(4)); - assertEquals(4, parser.getPosition()); - assertNull(parser.readNullTerminatedString()); - // Test with limit before NUL - parser = new ParsableByteArray(bytes, 3); - assertEquals("foo", parser.readNullTerminatedString(3)); - assertEquals(3, parser.getPosition()); - assertNull(parser.readNullTerminatedString()); - } - - public void testReadNullTerminatedString() { - byte[] bytes = new byte[] { - 'f', 'o', 'o', 0, 'b', 'a', 'r', 0 - }; - // Test normal case. - ParsableByteArray parser = new ParsableByteArray(bytes); - assertEquals("foo", parser.readNullTerminatedString()); - assertEquals(4, parser.getPosition()); - assertEquals("bar", parser.readNullTerminatedString()); - assertEquals(8, parser.getPosition()); - assertNull(parser.readNullTerminatedString()); - // Test with limit at NUL. - parser = new ParsableByteArray(bytes, 4); - assertEquals("foo", parser.readNullTerminatedString()); - assertEquals(4, parser.getPosition()); - assertNull(parser.readNullTerminatedString()); - // Test with limit before NUL. - parser = new ParsableByteArray(bytes, 3); - assertEquals("foo", parser.readNullTerminatedString()); - assertEquals(3, parser.getPosition()); - assertNull(parser.readNullTerminatedString()); - } - - public void testReadNullTerminatedStringWithoutEndingNull() { - byte[] bytes = new byte[] { - 'f', 'o', 'o', 0, 'b', 'a', 'r' - }; - ParsableByteArray parser = new ParsableByteArray(bytes); - assertEquals("foo", parser.readNullTerminatedString()); - assertEquals("bar", parser.readNullTerminatedString()); - assertNull(parser.readNullTerminatedString()); - } - - public void testReadSingleLineWithoutEndingTrail() { - byte[] bytes = new byte[] { - 'f', 'o', 'o' - }; - ParsableByteArray parser = new ParsableByteArray(bytes); - assertEquals("foo", parser.readLine()); - assertNull(parser.readLine()); - } - - public void testReadSingleLineWithEndingLf() { - byte[] bytes = new byte[] { - 'f', 'o', 'o', '\n' - }; - ParsableByteArray parser = new ParsableByteArray(bytes); - assertEquals("foo", parser.readLine()); - assertNull(parser.readLine()); - } - - public void testReadTwoLinesWithCrFollowedByLf() { - byte[] bytes = new byte[] { - 'f', 'o', 'o', '\r', '\n', 'b', 'a', 'r' - }; - ParsableByteArray parser = new ParsableByteArray(bytes); - assertEquals("foo", parser.readLine()); - assertEquals("bar", parser.readLine()); - assertNull(parser.readLine()); - } - - public void testReadThreeLinesWithEmptyLine() { - byte[] bytes = new byte[] { - 'f', 'o', 'o', '\r', '\n', '\r', 'b', 'a', 'r' - }; - ParsableByteArray parser = new ParsableByteArray(bytes); - assertEquals("foo", parser.readLine()); - assertEquals("", parser.readLine()); - assertEquals("bar", parser.readLine()); - assertNull(parser.readLine()); - } - - public void testReadFourLinesWithLfFollowedByCr() { - byte[] bytes = new byte[] { - 'f', 'o', 'o', '\n', '\r', '\r', 'b', 'a', 'r', '\r', '\n' - }; - ParsableByteArray parser = new ParsableByteArray(bytes); - assertEquals("foo", parser.readLine()); - assertEquals("", parser.readLine()); - assertEquals("", parser.readLine()); - assertEquals("bar", parser.readLine()); - assertNull(parser.readLine()); - } - -} diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ParsableNalUnitBitArrayTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ParsableNalUnitBitArrayTest.java deleted file mode 100644 index 294d3d352a..0000000000 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ParsableNalUnitBitArrayTest.java +++ /dev/null @@ -1,114 +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 static com.google.android.exoplayer2.testutil.TestUtil.createByteArray; - -import junit.framework.TestCase; - -/** - * Tests for {@link ParsableNalUnitBitArray}. - */ -public final class ParsableNalUnitBitArrayTest extends TestCase { - - private static final byte[] NO_ESCAPING_TEST_DATA = createByteArray(0, 3, 0, 1, 3, 0, 0); - private static final byte[] ALL_ESCAPING_TEST_DATA = createByteArray(0, 0, 3, 0, 0, 3, 0, 0, 3); - private static final byte[] MIX_TEST_DATA = createByteArray(255, 0, 0, 3, 255, 0, 0, 127); - - public void testReadNoEscaping() { - ParsableNalUnitBitArray array = - new ParsableNalUnitBitArray(NO_ESCAPING_TEST_DATA, 0, NO_ESCAPING_TEST_DATA.length); - assertEquals(0x000300, array.readBits(24)); - assertEquals(0, array.readBits(7)); - assertTrue(array.readBit()); - assertEquals(0x030000, array.readBits(24)); - assertFalse(array.canReadBits(1)); - assertFalse(array.canReadBits(8)); - } - - public void testReadNoEscapingTruncated() { - ParsableNalUnitBitArray array = new ParsableNalUnitBitArray(NO_ESCAPING_TEST_DATA, 0, 4); - assertTrue(array.canReadBits(32)); - array.skipBits(32); - assertFalse(array.canReadBits(1)); - try { - array.readBit(); - fail(); - } catch (Exception e) { - // Expected. - } - } - - public void testReadAllEscaping() { - ParsableNalUnitBitArray array = - new ParsableNalUnitBitArray(ALL_ESCAPING_TEST_DATA, 0, ALL_ESCAPING_TEST_DATA.length); - assertTrue(array.canReadBits(48)); - assertFalse(array.canReadBits(49)); - assertEquals(0, array.readBits(15)); - assertFalse(array.readBit()); - assertEquals(0, array.readBits(17)); - assertEquals(0, array.readBits(15)); - } - - public void testReadMix() { - ParsableNalUnitBitArray array = - new ParsableNalUnitBitArray(MIX_TEST_DATA, 0, MIX_TEST_DATA.length); - assertTrue(array.canReadBits(56)); - assertFalse(array.canReadBits(57)); - assertEquals(127, array.readBits(7)); - assertEquals(2, array.readBits(2)); - assertEquals(3, array.readBits(17)); - assertEquals(126, array.readBits(7)); - assertEquals(127, array.readBits(23)); - assertFalse(array.canReadBits(1)); - } - - public void testReadExpGolomb() { - ParsableNalUnitBitArray array = new ParsableNalUnitBitArray(createByteArray(0x9E), 0, 1); - assertTrue(array.canReadExpGolombCodedNum()); - assertEquals(0, array.readUnsignedExpGolombCodedInt()); - assertEquals(6, array.readUnsignedExpGolombCodedInt()); - assertEquals(0, array.readUnsignedExpGolombCodedInt()); - assertFalse(array.canReadExpGolombCodedNum()); - try { - array.readUnsignedExpGolombCodedInt(); - fail(); - } catch (Exception e) { - // Expected. - } - } - - public void testReadExpGolombWithEscaping() { - ParsableNalUnitBitArray array = - new ParsableNalUnitBitArray(createByteArray(0, 0, 3, 128, 0), 0, 5); - assertFalse(array.canReadExpGolombCodedNum()); - array.skipBit(); - assertTrue(array.canReadExpGolombCodedNum()); - assertEquals(32767, array.readUnsignedExpGolombCodedInt()); - assertFalse(array.canReadBits(1)); - } - - public void testReset() { - ParsableNalUnitBitArray array = new ParsableNalUnitBitArray(createByteArray(0, 0), 0, 2); - assertFalse(array.canReadExpGolombCodedNum()); - assertTrue(array.canReadBits(16)); - assertFalse(array.canReadBits(17)); - array.reset(createByteArray(0, 0, 3, 0), 0, 4); - assertTrue(array.canReadBits(24)); - assertFalse(array.canReadBits(25)); - } - -} diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ReusableBufferedOutputStreamTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ReusableBufferedOutputStreamTest.java deleted file mode 100644 index beb9e44853..0000000000 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/util/ReusableBufferedOutputStreamTest.java +++ /dev/null @@ -1,46 +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.test.MoreAsserts; -import java.io.ByteArrayOutputStream; -import junit.framework.TestCase; - -/** - * Tests {@link ReusableBufferedOutputStream}. - */ -public class ReusableBufferedOutputStreamTest extends TestCase { - - private static final byte[] TEST_DATA_1 = "test data 1".getBytes(); - private static final byte[] TEST_DATA_2 = "2 test data".getBytes(); - - public void testReset() throws Exception { - ByteArrayOutputStream byteArrayOutputStream1 = new ByteArrayOutputStream(1000); - ReusableBufferedOutputStream outputStream = new ReusableBufferedOutputStream( - byteArrayOutputStream1, 1000); - outputStream.write(TEST_DATA_1); - outputStream.close(); - - ByteArrayOutputStream byteArrayOutputStream2 = new ByteArrayOutputStream(1000); - outputStream.reset(byteArrayOutputStream2); - outputStream.write(TEST_DATA_2); - outputStream.close(); - - MoreAsserts.assertEquals(TEST_DATA_1, byteArrayOutputStream1.toByteArray()); - MoreAsserts.assertEquals(TEST_DATA_2, byteArrayOutputStream2.toByteArray()); - } - -} diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/util/UriUtilTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/util/UriUtilTest.java deleted file mode 100644 index 1755c6f70d..0000000000 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/util/UriUtilTest.java +++ /dev/null @@ -1,98 +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 junit.framework.TestCase; - -/** - * Unit tests for {@link UriUtil}. - */ -public class UriUtilTest extends TestCase { - - /** - * Tests normal usage of {@link UriUtil#resolve(String, String)}. - *

      - * The test cases are taken from RFC-3986 5.4.1. - */ - public void testResolveNormal() { - String base = "http://a/b/c/d;p?q"; - - assertEquals("g:h", UriUtil.resolve(base, "g:h")); - assertEquals("http://a/b/c/g", UriUtil.resolve(base, "g")); - assertEquals("http://a/b/c/g/", UriUtil.resolve(base, "g/")); - assertEquals("http://a/g", UriUtil.resolve(base, "/g")); - assertEquals("http://g", UriUtil.resolve(base, "//g")); - assertEquals("http://a/b/c/d;p?y", UriUtil.resolve(base, "?y")); - assertEquals("http://a/b/c/g?y", UriUtil.resolve(base, "g?y")); - assertEquals("http://a/b/c/d;p?q#s", UriUtil.resolve(base, "#s")); - assertEquals("http://a/b/c/g#s", UriUtil.resolve(base, "g#s")); - assertEquals("http://a/b/c/g?y#s", UriUtil.resolve(base, "g?y#s")); - assertEquals("http://a/b/c/;x", UriUtil.resolve(base, ";x")); - assertEquals("http://a/b/c/g;x", UriUtil.resolve(base, "g;x")); - assertEquals("http://a/b/c/g;x?y#s", UriUtil.resolve(base, "g;x?y#s")); - assertEquals("http://a/b/c/d;p?q", UriUtil.resolve(base, "")); - assertEquals("http://a/b/c/", UriUtil.resolve(base, ".")); - assertEquals("http://a/b/c/", UriUtil.resolve(base, "./")); - assertEquals("http://a/b/", UriUtil.resolve(base, "..")); - assertEquals("http://a/b/", UriUtil.resolve(base, "../")); - assertEquals("http://a/b/g", UriUtil.resolve(base, "../g")); - assertEquals("http://a/", UriUtil.resolve(base, "../..")); - assertEquals("http://a/", UriUtil.resolve(base, "../../")); - assertEquals("http://a/g", UriUtil.resolve(base, "../../g")); - } - - /** - * Tests abnormal usage of {@link UriUtil#resolve(String, String)}. - *

      - * The test cases are taken from RFC-3986 5.4.2. - */ - public void testResolveAbnormal() { - String base = "http://a/b/c/d;p?q"; - - assertEquals("http://a/g", UriUtil.resolve(base, "../../../g")); - assertEquals("http://a/g", UriUtil.resolve(base, "../../../../g")); - - assertEquals("http://a/g", UriUtil.resolve(base, "/./g")); - assertEquals("http://a/g", UriUtil.resolve(base, "/../g")); - assertEquals("http://a/b/c/g.", UriUtil.resolve(base, "g.")); - assertEquals("http://a/b/c/.g", UriUtil.resolve(base, ".g")); - assertEquals("http://a/b/c/g..", UriUtil.resolve(base, "g..")); - assertEquals("http://a/b/c/..g", UriUtil.resolve(base, "..g")); - - assertEquals("http://a/b/g", UriUtil.resolve(base, "./../g")); - assertEquals("http://a/b/c/g/", UriUtil.resolve(base, "./g/.")); - assertEquals("http://a/b/c/g/h", UriUtil.resolve(base, "g/./h")); - assertEquals("http://a/b/c/h", UriUtil.resolve(base, "g/../h")); - assertEquals("http://a/b/c/g;x=1/y", UriUtil.resolve(base, "g;x=1/./y")); - assertEquals("http://a/b/c/y", UriUtil.resolve(base, "g;x=1/../y")); - - assertEquals("http://a/b/c/g?y/./x", UriUtil.resolve(base, "g?y/./x")); - assertEquals("http://a/b/c/g?y/../x", UriUtil.resolve(base, "g?y/../x")); - assertEquals("http://a/b/c/g#s/./x", UriUtil.resolve(base, "g#s/./x")); - assertEquals("http://a/b/c/g#s/../x", UriUtil.resolve(base, "g#s/../x")); - - assertEquals("http:g", UriUtil.resolve(base, "http:g")); - } - - /** - * Tests additional abnormal usage of {@link UriUtil#resolve(String, String)}. - */ - public void testResolveAbnormalAdditional() { - assertEquals("c:e", UriUtil.resolve("http://a/b", "c:d/../e")); - assertEquals("a:c", UriUtil.resolve("a:b", "../c")); - } - -} diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/util/UtilTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/util/UtilTest.java deleted file mode 100644 index 1d9aff0723..0000000000 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/util/UtilTest.java +++ /dev/null @@ -1,179 +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 com.google.android.exoplayer2.testutil.TestUtil; -import java.util.ArrayList; -import java.util.List; -import java.util.Random; -import junit.framework.TestCase; - -/** - * Unit tests for {@link Util}. - */ -public class UtilTest extends TestCase { - - public void testArrayBinarySearchFloor() { - long[] values = new long[0]; - assertEquals(-1, Util.binarySearchFloor(values, 0, false, false)); - assertEquals(0, Util.binarySearchFloor(values, 0, false, true)); - - values = new long[] {1, 3, 5}; - assertEquals(-1, Util.binarySearchFloor(values, 0, false, false)); - assertEquals(-1, Util.binarySearchFloor(values, 0, true, false)); - assertEquals(0, Util.binarySearchFloor(values, 0, false, true)); - assertEquals(0, Util.binarySearchFloor(values, 0, true, true)); - - assertEquals(-1, Util.binarySearchFloor(values, 1, false, false)); - assertEquals(0, Util.binarySearchFloor(values, 1, true, false)); - assertEquals(0, Util.binarySearchFloor(values, 1, false, true)); - assertEquals(0, Util.binarySearchFloor(values, 1, true, true)); - - assertEquals(1, Util.binarySearchFloor(values, 4, false, false)); - assertEquals(1, Util.binarySearchFloor(values, 4, true, false)); - - assertEquals(1, Util.binarySearchFloor(values, 5, false, false)); - assertEquals(2, Util.binarySearchFloor(values, 5, true, false)); - - assertEquals(2, Util.binarySearchFloor(values, 6, false, false)); - assertEquals(2, Util.binarySearchFloor(values, 6, true, false)); - } - - public void testListBinarySearchFloor() { - List values = new ArrayList<>(); - assertEquals(-1, Util.binarySearchFloor(values, 0, false, false)); - assertEquals(0, Util.binarySearchFloor(values, 0, false, true)); - - values.add(1); - values.add(3); - values.add(5); - assertEquals(-1, Util.binarySearchFloor(values, 0, false, false)); - assertEquals(-1, Util.binarySearchFloor(values, 0, true, false)); - assertEquals(0, Util.binarySearchFloor(values, 0, false, true)); - assertEquals(0, Util.binarySearchFloor(values, 0, true, true)); - - assertEquals(-1, Util.binarySearchFloor(values, 1, false, false)); - assertEquals(0, Util.binarySearchFloor(values, 1, true, false)); - assertEquals(0, Util.binarySearchFloor(values, 1, false, true)); - assertEquals(0, Util.binarySearchFloor(values, 1, true, true)); - - assertEquals(1, Util.binarySearchFloor(values, 4, false, false)); - assertEquals(1, Util.binarySearchFloor(values, 4, true, false)); - - assertEquals(1, Util.binarySearchFloor(values, 5, false, false)); - assertEquals(2, Util.binarySearchFloor(values, 5, true, false)); - - assertEquals(2, Util.binarySearchFloor(values, 6, false, false)); - assertEquals(2, Util.binarySearchFloor(values, 6, true, false)); - } - - public void testArrayBinarySearchCeil() { - long[] values = new long[0]; - assertEquals(0, Util.binarySearchCeil(values, 0, false, false)); - assertEquals(-1, Util.binarySearchCeil(values, 0, false, true)); - - values = new long[] {1, 3, 5}; - assertEquals(0, Util.binarySearchCeil(values, 0, false, false)); - assertEquals(0, Util.binarySearchCeil(values, 0, true, false)); - - assertEquals(1, Util.binarySearchCeil(values, 1, false, false)); - assertEquals(0, Util.binarySearchCeil(values, 1, true, false)); - - assertEquals(1, Util.binarySearchCeil(values, 2, false, false)); - assertEquals(1, Util.binarySearchCeil(values, 2, true, false)); - - assertEquals(3, Util.binarySearchCeil(values, 5, false, false)); - assertEquals(2, Util.binarySearchCeil(values, 5, true, false)); - assertEquals(2, Util.binarySearchCeil(values, 5, false, true)); - assertEquals(2, Util.binarySearchCeil(values, 5, true, true)); - - assertEquals(3, Util.binarySearchCeil(values, 6, false, false)); - assertEquals(3, Util.binarySearchCeil(values, 6, true, false)); - assertEquals(2, Util.binarySearchCeil(values, 6, false, true)); - assertEquals(2, Util.binarySearchCeil(values, 6, true, true)); - } - - public void testListBinarySearchCeil() { - List values = new ArrayList<>(); - assertEquals(0, Util.binarySearchCeil(values, 0, false, false)); - assertEquals(-1, Util.binarySearchCeil(values, 0, false, true)); - - values.add(1); - values.add(3); - values.add(5); - assertEquals(0, Util.binarySearchCeil(values, 0, false, false)); - assertEquals(0, Util.binarySearchCeil(values, 0, true, false)); - - assertEquals(1, Util.binarySearchCeil(values, 1, false, false)); - assertEquals(0, Util.binarySearchCeil(values, 1, true, false)); - - assertEquals(1, Util.binarySearchCeil(values, 2, false, false)); - assertEquals(1, Util.binarySearchCeil(values, 2, true, false)); - - assertEquals(3, Util.binarySearchCeil(values, 5, false, false)); - assertEquals(2, Util.binarySearchCeil(values, 5, true, false)); - assertEquals(2, Util.binarySearchCeil(values, 5, false, true)); - assertEquals(2, Util.binarySearchCeil(values, 5, true, true)); - - assertEquals(3, Util.binarySearchCeil(values, 6, false, false)); - assertEquals(3, Util.binarySearchCeil(values, 6, true, false)); - assertEquals(2, Util.binarySearchCeil(values, 6, false, true)); - assertEquals(2, Util.binarySearchCeil(values, 6, true, true)); - } - - public void testParseXsDuration() { - assertEquals(150279L, Util.parseXsDuration("PT150.279S")); - assertEquals(1500L, Util.parseXsDuration("PT1.500S")); - } - - public void testParseXsDateTime() throws Exception { - assertEquals(1403219262000L, Util.parseXsDateTime("2014-06-19T23:07:42")); - assertEquals(1407322800000L, Util.parseXsDateTime("2014-08-06T11:00:00Z")); - assertEquals(1407322800000L, Util.parseXsDateTime("2014-08-06T11:00:00,000Z")); - assertEquals(1411161535000L, Util.parseXsDateTime("2014-09-19T13:18:55-08:00")); - assertEquals(1411161535000L, Util.parseXsDateTime("2014-09-19T13:18:55-0800")); - assertEquals(1411161535000L, Util.parseXsDateTime("2014-09-19T13:18:55.000-0800")); - assertEquals(1411161535000L, Util.parseXsDateTime("2014-09-19T13:18:55.000-800")); - } - - public void testUnescapeInvalidFileName() { - assertNull(Util.unescapeFileName("%a")); - assertNull(Util.unescapeFileName("%xyz")); - } - - public void testEscapeUnescapeFileName() { - assertEscapeUnescapeFileName("just+a regular+fileName", "just+a regular+fileName"); - assertEscapeUnescapeFileName("key:value", "key%3avalue"); - assertEscapeUnescapeFileName("<>:\"/\\|?*%", "%3c%3e%3a%22%2f%5c%7c%3f%2a%25"); - - Random random = new Random(0); - for (int i = 0; i < 1000; i++) { - String string = TestUtil.buildTestString(1000, random); - assertEscapeUnescapeFileName(string); - } - } - - private static void assertEscapeUnescapeFileName(String fileName, String escapedFileName) { - assertEquals(escapedFileName, Util.escapeFileName(fileName)); - assertEquals(fileName, Util.unescapeFileName(escapedFileName)); - } - - private static void assertEscapeUnescapeFileName(String fileName) { - String escapedFileName = Util.escapeFileName(fileName); - assertEquals(fileName, Util.unescapeFileName(escapedFileName)); - } - -} From daafbb1f1ce0cae9a23c324a073ca38e02ed8a52 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 31 Aug 2017 10:02:43 -0700 Subject: [PATCH 0331/2472] Add missing Robolectric test path to codebase ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167151714 --- .../com/google/android/exoplayer2/CTest.java | 43 ++ .../google/android/exoplayer2/FormatTest.java | 159 ++++++ .../exoplayer2/util/AtomicFileTest.java | 97 ++++ .../exoplayer2/util/ColorParserTest.java | 103 ++++ .../exoplayer2/util/NalUnitUtilTest.java | 217 +++++++ .../exoplayer2/util/ParsableBitArrayTest.java | 198 +++++++ .../util/ParsableByteArrayTest.java | 530 ++++++++++++++++++ .../util/ParsableNalUnitBitArrayTest.java | 128 +++++ .../ReusableBufferedOutputStreamTest.java | 53 ++ .../android/exoplayer2/util/UriUtilTest.java | 109 ++++ .../android/exoplayer2/util/UtilTest.java | 200 +++++++ 11 files changed, 1837 insertions(+) create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/CTest.java create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/FormatTest.java create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/util/AtomicFileTest.java create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/util/ColorParserTest.java create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/util/NalUnitUtilTest.java create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/util/ParsableBitArrayTest.java create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/util/ParsableByteArrayTest.java create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/util/ParsableNalUnitBitArrayTest.java create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/util/ReusableBufferedOutputStreamTest.java create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/util/UriUtilTest.java create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/util/UtilTest.java diff --git a/library/core/src/test/java/com/google/android/exoplayer2/CTest.java b/library/core/src/test/java/com/google/android/exoplayer2/CTest.java new file mode 100644 index 0000000000..ff4756f5ed --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/CTest.java @@ -0,0 +1,43 @@ +/* + * 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 static com.google.common.truth.Truth.assertThat; + +import android.annotation.SuppressLint; +import android.media.MediaCodec; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Unit test for {@link C}. + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public class CTest { + + @SuppressLint("InlinedApi") + @Test + public void testConstants() { + // Sanity check that constant values match those defined by the platform. + assertThat(C.BUFFER_FLAG_KEY_FRAME).isEqualTo(MediaCodec.BUFFER_FLAG_KEY_FRAME); + assertThat(C.BUFFER_FLAG_END_OF_STREAM).isEqualTo(MediaCodec.BUFFER_FLAG_END_OF_STREAM); + assertThat(C.CRYPTO_MODE_AES_CTR).isEqualTo(MediaCodec.CRYPTO_MODE_AES_CTR); + } + +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/FormatTest.java b/library/core/src/test/java/com/google/android/exoplayer2/FormatTest.java new file mode 100644 index 0000000000..8e36edc105 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/FormatTest.java @@ -0,0 +1,159 @@ +/* + * 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 static com.google.android.exoplayer2.C.WIDEVINE_UUID; +import static com.google.android.exoplayer2.util.MimeTypes.VIDEO_MP4; +import static com.google.android.exoplayer2.util.MimeTypes.VIDEO_WEBM; +import static com.google.common.truth.Truth.assertThat; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.media.MediaFormat; +import android.os.Parcel; +import com.google.android.exoplayer2.drm.DrmInitData; +import com.google.android.exoplayer2.metadata.Metadata; +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; +import java.util.Collections; +import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Unit test for {@link Format}. + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class FormatTest { + + private static final List INIT_DATA; + static { + byte[] initData1 = new byte[] {1, 2, 3}; + byte[] initData2 = new byte[] {4, 5, 6}; + List initData = new ArrayList<>(); + initData.add(initData1); + initData.add(initData2); + INIT_DATA = Collections.unmodifiableList(initData); + } + + @Test + public void testParcelable() { + DrmInitData.SchemeData DRM_DATA_1 = new DrmInitData.SchemeData(WIDEVINE_UUID, "cenc", VIDEO_MP4, + TestUtil.buildTestData(128, 1 /* data seed */)); + DrmInitData.SchemeData DRM_DATA_2 = new DrmInitData.SchemeData(C.UUID_NIL, null, VIDEO_WEBM, + TestUtil.buildTestData(128, 1 /* data seed */)); + DrmInitData drmInitData = new DrmInitData(DRM_DATA_1, DRM_DATA_2); + byte[] projectionData = new byte[] {1, 2, 3}; + 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, 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); + parcel.setDataPosition(0); + + Format formatFromParcel = Format.CREATOR.createFromParcel(parcel); + assertThat(formatFromParcel).isEqualTo(formatToParcel); + + parcel.recycle(); + } + + @Test + public void testConversionToFrameworkMediaFormat() { + if (Util.SDK_INT < 16) { + // Test doesn't apply. + return; + } + + testConversionToFrameworkMediaFormatV16(Format.createVideoSampleFormat(null, "video/xyz", null, + 5000, 102400, 1280, 720, 30, INIT_DATA, null)); + testConversionToFrameworkMediaFormatV16(Format.createVideoSampleFormat(null, "video/xyz", null, + 5000, Format.NO_VALUE, 1280, 720, 30, null, null)); + testConversionToFrameworkMediaFormatV16(Format.createAudioSampleFormat(null, "audio/xyz", null, + 500, 128, 5, 44100, INIT_DATA, null, 0, null)); + testConversionToFrameworkMediaFormatV16(Format.createAudioSampleFormat(null, "audio/xyz", null, + 500, Format.NO_VALUE, 5, 44100, null, null, 0, null)); + testConversionToFrameworkMediaFormatV16(Format.createTextSampleFormat(null, "text/xyz", 0, + "eng")); + testConversionToFrameworkMediaFormatV16(Format.createTextSampleFormat(null, "text/xyz", 0, + null)); + } + + @SuppressLint("InlinedApi") + @TargetApi(16) + private static void testConversionToFrameworkMediaFormatV16(Format in) { + MediaFormat out = in.getFrameworkMediaFormatV16(); + assertThat(out.getString(MediaFormat.KEY_MIME)).isEqualTo(in.sampleMimeType); + assertOptionalV16(out, MediaFormat.KEY_LANGUAGE, in.language); + assertOptionalV16(out, MediaFormat.KEY_MAX_INPUT_SIZE, in.maxInputSize); + assertOptionalV16(out, MediaFormat.KEY_WIDTH, in.width); + assertOptionalV16(out, MediaFormat.KEY_HEIGHT, in.height); + assertOptionalV16(out, MediaFormat.KEY_CHANNEL_COUNT, in.channelCount); + assertOptionalV16(out, MediaFormat.KEY_SAMPLE_RATE, in.sampleRate); + assertOptionalV16(out, MediaFormat.KEY_FRAME_RATE, in.frameRate); + + for (int i = 0; i < in.initializationData.size(); i++) { + byte[] originalData = in.initializationData.get(i); + ByteBuffer frameworkBuffer = out.getByteBuffer("csd-" + i); + byte[] frameworkData = Arrays.copyOf(frameworkBuffer.array(), frameworkBuffer.limit()); + assertThat(frameworkData).isEqualTo(originalData); + } + } + + @TargetApi(16) + private static void assertOptionalV16(MediaFormat format, String key, String value) { + if (value == null) { + assertThat(format.containsKey(key)).isEqualTo(false); + } else { + assertThat(format.getString(key)).isEqualTo(value); + } + } + + @TargetApi(16) + private static void assertOptionalV16(MediaFormat format, String key, int value) { + if (value == Format.NO_VALUE) { + assertThat(format.containsKey(key)).isEqualTo(false); + } else { + assertThat(format.getInteger(key)).isEqualTo(value); + } + } + + @TargetApi(16) + private static void assertOptionalV16(MediaFormat format, String key, float value) { + if (value == Format.NO_VALUE) { + assertThat(format.containsKey(key)).isEqualTo(false); + } else { + assertThat(format.getFloat(key)).isEqualTo(value); + } + } + +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/AtomicFileTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/AtomicFileTest.java new file mode 100644 index 0000000000..dcf3d31eb3 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/AtomicFileTest.java @@ -0,0 +1,97 @@ +/* + * 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 static com.google.common.truth.Truth.assertThat; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +/** + * Tests {@link AtomicFile}. + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class AtomicFileTest { + + private File tempFolder; + private File file; + private AtomicFile atomicFile; + + @Before + public void setUp() throws Exception { + tempFolder = Util.createTempDirectory(RuntimeEnvironment.application, "ExoPlayerTest"); + file = new File(tempFolder, "atomicFile"); + atomicFile = new AtomicFile(file); + } + + @After + public void tearDown() throws Exception { + Util.recursiveDelete(tempFolder); + } + + @Test + public void testDelete() throws Exception { + assertThat(file.createNewFile()).isTrue(); + atomicFile.delete(); + assertThat(file.exists()).isFalse(); + } + + @Test + public void testWriteRead() throws Exception { + OutputStream output = atomicFile.startWrite(); + output.write(5); + atomicFile.endWrite(output); + output.close(); + + assertRead(); + + output = atomicFile.startWrite(); + output.write(5); + output.write(6); + output.close(); + + assertRead(); + + output = atomicFile.startWrite(); + output.write(6); + + assertRead(); + output.close(); + + output = atomicFile.startWrite(); + + assertRead(); + output.close(); + } + + private void assertRead() throws IOException { + InputStream input = atomicFile.openRead(); + assertThat(input.read()).isEqualTo(5); + assertThat(input.read()).isEqualTo(-1); + input.close(); + } + +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/ColorParserTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/ColorParserTest.java new file mode 100644 index 0000000000..13b126090c --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/ColorParserTest.java @@ -0,0 +1,103 @@ +/* + * 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 static android.graphics.Color.BLACK; +import static android.graphics.Color.RED; +import static android.graphics.Color.WHITE; +import static android.graphics.Color.argb; +import static android.graphics.Color.parseColor; +import static com.google.android.exoplayer2.util.ColorParser.parseTtmlColor; +import static com.google.common.truth.Truth.assertThat; + +import android.graphics.Color; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Unit test for ColorParser. + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class ColorParserTest { + + // Negative tests. + + @Test(expected = IllegalArgumentException.class) + public void testParseUnknownColor() { + ColorParser.parseTtmlColor("colorOfAnElectron"); + } + + @Test(expected = IllegalArgumentException.class) + public void testParseNull() { + ColorParser.parseTtmlColor(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testParseEmpty() { + ColorParser.parseTtmlColor(""); + } + + @Test(expected = IllegalArgumentException.class) + public void testRgbColorParsingRgbValuesNegative() { + ColorParser.parseTtmlColor("rgb(-4, 55, 209)"); + } + + // Positive tests. + + @Test + public void testHexCodeParsing() { + assertThat(parseTtmlColor("#FFFFFF")).isEqualTo(WHITE); + assertThat(parseTtmlColor("#FFFFFFFF")).isEqualTo(WHITE); + assertThat(parseTtmlColor("#123456")).isEqualTo(parseColor("#FF123456")); + // Hex colors in ColorParser are RGBA, where-as {@link Color#parseColor} takes ARGB. + assertThat(parseTtmlColor("#FFFFFF00")).isEqualTo(parseColor("#00FFFFFF")); + assertThat(parseTtmlColor("#12345678")).isEqualTo(parseColor("#78123456")); + } + + @Test + public void testRgbColorParsing() { + assertThat(parseTtmlColor("rgb(255,255,255)")).isEqualTo(WHITE); + // Spaces are ignored. + assertThat(parseTtmlColor(" rgb ( 255, 255, 255)")).isEqualTo(WHITE); + } + + @Test + public void testRgbColorParsingRgbValuesOutOfBounds() { + int outOfBounds = ColorParser.parseTtmlColor("rgb(999, 999, 999)"); + int color = Color.rgb(999, 999, 999); + // Behave like the framework does. + assertThat(outOfBounds).isEqualTo(color); + } + + @Test + public void testRgbaColorParsing() { + assertThat(parseTtmlColor("rgba(255,255,255,255)")).isEqualTo(WHITE); + assertThat(parseTtmlColor("rgba(255,255,255,255)")) + .isEqualTo(argb(255, 255, 255, 255)); + assertThat(parseTtmlColor("rgba(0, 0, 0, 255)")).isEqualTo(BLACK); + assertThat(parseTtmlColor("rgba(0, 0, 255, 0)")) + .isEqualTo(argb(0, 0, 0, 255)); + assertThat(parseTtmlColor("rgba(255, 0, 0, 255)")).isEqualTo(RED); + assertThat(parseTtmlColor("rgba(255, 0, 255, 0)")) + .isEqualTo(argb(0, 255, 0, 255)); + assertThat(parseTtmlColor("rgba(255, 0, 0, 205)")) + .isEqualTo(argb(205, 255, 0, 0)); + } + +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/NalUnitUtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/NalUnitUtilTest.java new file mode 100644 index 0000000000..ee77664cce --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/NalUnitUtilTest.java @@ -0,0 +1,217 @@ +/* + * 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 static com.google.android.exoplayer2.testutil.TestUtil.createByteArray; +import static com.google.common.truth.Truth.assertThat; + +import java.nio.ByteBuffer; +import java.util.Arrays; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Tests for {@link NalUnitUtil}. + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class NalUnitUtilTest { + + private static final int TEST_PARTIAL_NAL_POSITION = 4; + private static final int TEST_NAL_POSITION = 10; + private static final byte[] SPS_TEST_DATA = createByteArray(0x00, 0x00, 0x01, 0x67, 0x4D, 0x40, + 0x16, 0xEC, 0xA0, 0x50, 0x17, 0xFC, 0xB8, 0x08, 0x80, 0x00, 0x00, 0x03, 0x00, 0x80, 0x00, + 0x00, 0x0F, 0x47, 0x8B, 0x16, 0xCB); + private static final int SPS_TEST_DATA_OFFSET = 3; + + @Test + public void testFindNalUnit() { + byte[] data = buildTestData(); + + // Should find NAL unit. + int result = NalUnitUtil.findNalUnit(data, 0, data.length, null); + assertThat(result).isEqualTo(TEST_NAL_POSITION); + // Should find NAL unit whose prefix ends one byte before the limit. + result = NalUnitUtil.findNalUnit(data, 0, TEST_NAL_POSITION + 4, null); + assertThat(result).isEqualTo(TEST_NAL_POSITION); + // Shouldn't find NAL unit whose prefix ends at the limit (since the limit is exclusive). + result = NalUnitUtil.findNalUnit(data, 0, TEST_NAL_POSITION + 3, null); + assertThat(result).isEqualTo(TEST_NAL_POSITION + 3); + // Should find NAL unit whose prefix starts at the offset. + result = NalUnitUtil.findNalUnit(data, TEST_NAL_POSITION, data.length, null); + assertThat(result).isEqualTo(TEST_NAL_POSITION); + // Shouldn't find NAL unit whose prefix starts one byte past the offset. + result = NalUnitUtil.findNalUnit(data, TEST_NAL_POSITION + 1, data.length, null); + assertThat(result).isEqualTo(data.length); + } + + @Test + public void testFindNalUnitWithPrefix() { + byte[] data = buildTestData(); + + // First byte of NAL unit in data1, rest in data2. + boolean[] prefixFlags = new boolean[3]; + byte[] data1 = Arrays.copyOfRange(data, 0, TEST_NAL_POSITION + 1); + byte[] data2 = Arrays.copyOfRange(data, TEST_NAL_POSITION + 1, data.length); + int result = NalUnitUtil.findNalUnit(data1, 0, data1.length, prefixFlags); + assertThat(result).isEqualTo(data1.length); + result = NalUnitUtil.findNalUnit(data2, 0, data2.length, prefixFlags); + assertThat(result).isEqualTo(-1); + assertPrefixFlagsCleared(prefixFlags); + + // First three bytes of NAL unit in data1, rest in data2. + prefixFlags = new boolean[3]; + data1 = Arrays.copyOfRange(data, 0, TEST_NAL_POSITION + 3); + data2 = Arrays.copyOfRange(data, TEST_NAL_POSITION + 3, data.length); + result = NalUnitUtil.findNalUnit(data1, 0, data1.length, prefixFlags); + assertThat(result).isEqualTo(data1.length); + result = NalUnitUtil.findNalUnit(data2, 0, data2.length, prefixFlags); + assertThat(result).isEqualTo(-3); + assertPrefixFlagsCleared(prefixFlags); + + // First byte of NAL unit in data1, second byte in data2, rest in data3. + prefixFlags = new boolean[3]; + data1 = Arrays.copyOfRange(data, 0, TEST_NAL_POSITION + 1); + data2 = Arrays.copyOfRange(data, TEST_NAL_POSITION + 1, TEST_NAL_POSITION + 2); + byte[] data3 = Arrays.copyOfRange(data, TEST_NAL_POSITION + 2, data.length); + result = NalUnitUtil.findNalUnit(data1, 0, data1.length, prefixFlags); + assertThat(result).isEqualTo(data1.length); + result = NalUnitUtil.findNalUnit(data2, 0, data2.length, prefixFlags); + assertThat(result).isEqualTo(data2.length); + result = NalUnitUtil.findNalUnit(data3, 0, data3.length, prefixFlags); + assertThat(result).isEqualTo(-2); + assertPrefixFlagsCleared(prefixFlags); + + // NAL unit split with one byte in four arrays. + prefixFlags = new boolean[3]; + data1 = Arrays.copyOfRange(data, 0, TEST_NAL_POSITION + 1); + data2 = Arrays.copyOfRange(data, TEST_NAL_POSITION + 1, TEST_NAL_POSITION + 2); + data3 = Arrays.copyOfRange(data, TEST_NAL_POSITION + 2, TEST_NAL_POSITION + 3); + byte[] data4 = Arrays.copyOfRange(data, TEST_NAL_POSITION + 2, data.length); + result = NalUnitUtil.findNalUnit(data1, 0, data1.length, prefixFlags); + assertThat(result).isEqualTo(data1.length); + result = NalUnitUtil.findNalUnit(data2, 0, data2.length, prefixFlags); + assertThat(result).isEqualTo(data2.length); + result = NalUnitUtil.findNalUnit(data3, 0, data3.length, prefixFlags); + assertThat(result).isEqualTo(data3.length); + result = NalUnitUtil.findNalUnit(data4, 0, data4.length, prefixFlags); + assertThat(result).isEqualTo(-3); + assertPrefixFlagsCleared(prefixFlags); + + // NAL unit entirely in data2. data1 ends with partial prefix. + prefixFlags = new boolean[3]; + data1 = Arrays.copyOfRange(data, 0, TEST_PARTIAL_NAL_POSITION + 2); + data2 = Arrays.copyOfRange(data, TEST_PARTIAL_NAL_POSITION + 2, data.length); + result = NalUnitUtil.findNalUnit(data1, 0, data1.length, prefixFlags); + assertThat(result).isEqualTo(data1.length); + result = NalUnitUtil.findNalUnit(data2, 0, data2.length, prefixFlags); + assertThat(result).isEqualTo(4); + assertPrefixFlagsCleared(prefixFlags); + } + + @Test + public void testParseSpsNalUnit() { + NalUnitUtil.SpsData data = NalUnitUtil.parseSpsNalUnit(SPS_TEST_DATA, SPS_TEST_DATA_OFFSET, + SPS_TEST_DATA.length); + assertThat(data.width).isEqualTo(640); + assertThat(data.height).isEqualTo(360); + assertThat(data.deltaPicOrderAlwaysZeroFlag).isFalse(); + assertThat(data.frameMbsOnlyFlag).isTrue(); + assertThat(data.frameNumLength).isEqualTo(4); + assertThat(data.picOrderCntLsbLength).isEqualTo(6); + assertThat(data.seqParameterSetId).isEqualTo(0); + assertThat(data.pixelWidthAspectRatio).isEqualTo(1.0f); + assertThat(data.picOrderCountType).isEqualTo(0); + assertThat(data.separateColorPlaneFlag).isFalse(); + } + + @Test + public void testUnescapeDoesNotModifyBuffersWithoutStartCodes() { + assertUnescapeDoesNotModify(""); + assertUnescapeDoesNotModify("0000"); + assertUnescapeDoesNotModify("172BF38A3C"); + assertUnescapeDoesNotModify("000004"); + } + + @Test + public void testUnescapeModifiesBuffersWithStartCodes() { + assertUnescapeMatchesExpected("00000301", "000001"); + assertUnescapeMatchesExpected("0000030200000300", "000002000000"); + } + + @Test + public void testDiscardToSps() { + assertDiscardToSpsMatchesExpected("", ""); + assertDiscardToSpsMatchesExpected("00", ""); + assertDiscardToSpsMatchesExpected("FFFF000001", ""); + assertDiscardToSpsMatchesExpected("00000001", ""); + assertDiscardToSpsMatchesExpected("00000001FF67", ""); + assertDiscardToSpsMatchesExpected("00000001000167", ""); + assertDiscardToSpsMatchesExpected("0000000167", "0000000167"); + assertDiscardToSpsMatchesExpected("0000000167FF", "0000000167FF"); + assertDiscardToSpsMatchesExpected("0000000167FF", "0000000167FF"); + assertDiscardToSpsMatchesExpected("0000000167FF000000016700", "0000000167FF000000016700"); + assertDiscardToSpsMatchesExpected("000000000167FF", "0000000167FF"); + assertDiscardToSpsMatchesExpected("0001670000000167FF", "0000000167FF"); + assertDiscardToSpsMatchesExpected("FF00000001660000000167FF", "0000000167FF"); + } + + private static byte[] buildTestData() { + byte[] data = new byte[20]; + for (int i = 0; i < data.length; i++) { + data[i] = (byte) 0xFF; + } + // Insert an incomplete NAL unit start code. + data[TEST_PARTIAL_NAL_POSITION] = 0; + data[TEST_PARTIAL_NAL_POSITION + 1] = 0; + // Insert a complete NAL unit start code. + data[TEST_NAL_POSITION] = 0; + data[TEST_NAL_POSITION + 1] = 0; + data[TEST_NAL_POSITION + 2] = 1; + data[TEST_NAL_POSITION + 3] = 5; + return data; + } + + private static void assertPrefixFlagsCleared(boolean[] flags) { + assertThat(flags[0] || flags[1] || flags[2]).isEqualTo(false); + } + + private static void assertUnescapeDoesNotModify(String input) { + assertUnescapeMatchesExpected(input, input); + } + + private static void assertUnescapeMatchesExpected(String input, String expectedOutput) { + byte[] bitstream = Util.getBytesFromHexString(input); + byte[] expectedOutputBitstream = Util.getBytesFromHexString(expectedOutput); + int count = NalUnitUtil.unescapeStream(bitstream, bitstream.length); + assertThat(count).isEqualTo(expectedOutputBitstream.length); + byte[] outputBitstream = new byte[count]; + System.arraycopy(bitstream, 0, outputBitstream, 0, count); + assertThat(outputBitstream).isEqualTo(expectedOutputBitstream); + } + + private static void assertDiscardToSpsMatchesExpected(String input, String expectedOutput) { + byte[] bitstream = Util.getBytesFromHexString(input); + byte[] expectedOutputBitstream = Util.getBytesFromHexString(expectedOutput); + ByteBuffer buffer = ByteBuffer.wrap(bitstream); + buffer.position(buffer.limit()); + NalUnitUtil.discardToSps(buffer); + assertThat(Arrays.copyOf(buffer.array(), buffer.position())).isEqualTo(expectedOutputBitstream); + } + +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/ParsableBitArrayTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/ParsableBitArrayTest.java new file mode 100644 index 0000000000..0d864f407f --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/ParsableBitArrayTest.java @@ -0,0 +1,198 @@ +/* + * 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.util; + +import static com.google.common.truth.Truth.assertThat; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Tests for {@link ParsableBitArray}. + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class ParsableBitArrayTest { + + private static final byte[] TEST_DATA = new byte[] {0x3C, (byte) 0xD2, (byte) 0x5F, (byte) 0x01, + (byte) 0xFF, (byte) 0x14, (byte) 0x60, (byte) 0x99}; + + private ParsableBitArray testArray; + + @Before + public void setUp() { + testArray = new ParsableBitArray(TEST_DATA); + } + + @Test + public void testReadAllBytes() { + byte[] bytesRead = new byte[TEST_DATA.length]; + testArray.readBytes(bytesRead, 0, TEST_DATA.length); + assertThat(bytesRead).isEqualTo(TEST_DATA); + assertThat(testArray.getPosition()).isEqualTo(TEST_DATA.length * 8); + assertThat(testArray.getBytePosition()).isEqualTo(TEST_DATA.length); + } + + @Test + public void testReadBit() { + assertReadBitsToEnd(0); + } + + @Test + public void testReadBits() { + assertThat(testArray.readBits(5)).isEqualTo(getTestDataBits(0, 5)); + assertThat(testArray.readBits(0)).isEqualTo(getTestDataBits(5, 0)); + assertThat(testArray.readBits(3)).isEqualTo(getTestDataBits(5, 3)); + assertThat(testArray.readBits(16)).isEqualTo(getTestDataBits(8, 16)); + assertThat(testArray.readBits(3)).isEqualTo(getTestDataBits(24, 3)); + assertThat(testArray.readBits(18)).isEqualTo(getTestDataBits(27, 18)); + assertThat(testArray.readBits(5)).isEqualTo(getTestDataBits(45, 5)); + assertThat(testArray.readBits(14)).isEqualTo(getTestDataBits(50, 14)); + } + + @Test + public void testReadBitsToByteArray() { + byte[] result = new byte[TEST_DATA.length]; + // Test read within byte boundaries. + testArray.readBits(result, 0, 6); + assertThat(result[0]).isEqualTo((byte) (TEST_DATA[0] & 0xFC)); + // Test read across byte boundaries. + testArray.readBits(result, 0, 8); + assertThat(result[0]).isEqualTo( + (byte) (((TEST_DATA[0] & 0x03) << 6) | ((TEST_DATA[1] & 0xFC) >> 2))); + // Test reading across multiple bytes. + testArray.readBits(result, 1, 50); + for (int i = 1; i < 7; i++) { + assertThat(result[i]) + .isEqualTo((byte) (((TEST_DATA[i] & 0x03) << 6) | ((TEST_DATA[i + 1] & 0xFC) >> 2))); + } + assertThat(result[7]).isEqualTo((byte) ((TEST_DATA[7] & 0x03) << 6)); + assertThat(testArray.bitsLeft()).isEqualTo(0); + // Test read last buffer byte across input data bytes. + testArray.setPosition(31); + result[3] = 0; + testArray.readBits(result, 3, 3); + assertThat(result[3]).isEqualTo((byte) 0xE0); + // Test read bits in the middle of a input data byte. + result[0] = 0; + assertThat(testArray.getPosition()).isEqualTo(34); + testArray.readBits(result, 0, 3); + assertThat(result[0]).isEqualTo((byte) 0xE0); + // Test read 0 bits. + testArray.setPosition(32); + result[1] = 0; + testArray.readBits(result, 1, 0); + assertThat(result[1]).isEqualTo((byte) 0); + // Test reading a number of bits divisible by 8. + testArray.setPosition(0); + testArray.readBits(result, 0, 16); + assertThat(result[0]).isEqualTo(TEST_DATA[0]); + assertThat(result[1]).isEqualTo(TEST_DATA[1]); + // Test least significant bits are unmodified. + result[1] = (byte) 0xFF; + testArray.readBits(result, 0, 9); + assertThat(result[0]).isEqualTo((byte) 0x5F); + assertThat(result[1]).isEqualTo((byte) 0x7F); + } + + @Test + public void testRead32BitsByteAligned() { + assertThat(testArray.readBits(32)).isEqualTo(getTestDataBits(0, 32)); + assertThat(testArray.readBits(32)).isEqualTo(getTestDataBits(32, 32)); + } + + @Test + public void testRead32BitsNonByteAligned() { + assertThat(testArray.readBits(5)).isEqualTo(getTestDataBits(0, 5)); + assertThat(testArray.readBits(32)).isEqualTo(getTestDataBits(5, 32)); + } + + @Test + public void testSkipBytes() { + testArray.skipBytes(2); + assertReadBitsToEnd(16); + } + + @Test + public void testSkipBitsByteAligned() { + testArray.skipBits(16); + assertReadBitsToEnd(16); + } + + @Test + public void testSkipBitsNonByteAligned() { + testArray.skipBits(5); + assertReadBitsToEnd(5); + } + + @Test + public void testSetPositionByteAligned() { + testArray.setPosition(16); + assertReadBitsToEnd(16); + } + + @Test + public void testSetPositionNonByteAligned() { + testArray.setPosition(5); + assertReadBitsToEnd(5); + } + + @Test + public void testByteAlignFromNonByteAligned() { + testArray.setPosition(11); + testArray.byteAlign(); + assertThat(testArray.getBytePosition()).isEqualTo(2); + assertThat(testArray.getPosition()).isEqualTo(16); + assertReadBitsToEnd(16); + } + + @Test + public void testByteAlignFromByteAligned() { + testArray.setPosition(16); + testArray.byteAlign(); // Should be a no-op. + assertThat(testArray.getBytePosition()).isEqualTo(2); + assertThat(testArray.getPosition()).isEqualTo(16); + assertReadBitsToEnd(16); + } + + private void assertReadBitsToEnd(int expectedStartPosition) { + int position = testArray.getPosition(); + assertThat(position).isEqualTo(expectedStartPosition); + for (int i = position; i < TEST_DATA.length * 8; i++) { + assertThat(testArray.readBit()).isEqualTo(getTestDataBit(i)); + assertThat(testArray.getPosition()).isEqualTo(i + 1); + } + } + + private static int getTestDataBits(int bitPosition, int length) { + int result = 0; + for (int i = 0; i < length; i++) { + result = result << 1; + if (getTestDataBit(bitPosition++)) { + result |= 0x1; + } + } + return result; + } + + private static boolean getTestDataBit(int bitPosition) { + return (TEST_DATA[bitPosition / 8] & (0x80 >>> (bitPosition % 8))) != 0; + } + +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/ParsableByteArrayTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/ParsableByteArrayTest.java new file mode 100644 index 0000000000..504a58b4a8 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/ParsableByteArrayTest.java @@ -0,0 +1,530 @@ +/* + * 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 static com.google.common.truth.Truth.assertThat; +import static java.nio.charset.Charset.forName; +import static junit.framework.TestCase.fail; + +import java.nio.ByteBuffer; +import java.util.Arrays; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Tests for {@link ParsableByteArray}. + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class ParsableByteArrayTest { + + private static final byte[] TEST_DATA = + new byte[] {0x0F, (byte) 0xFF, (byte) 0x42, (byte) 0x0F, 0x00, 0x00, 0x00, 0x00}; + + private static ParsableByteArray getTestDataArray() { + ParsableByteArray testArray = new ParsableByteArray(TEST_DATA.length); + System.arraycopy(TEST_DATA, 0, testArray.data, 0, TEST_DATA.length); + return testArray; + } + + @Test + public void testReadShort() { + testReadShort((short) -1); + testReadShort((short) 0); + testReadShort((short) 1); + testReadShort(Short.MIN_VALUE); + testReadShort(Short.MAX_VALUE); + } + + private static void testReadShort(short testValue) { + ParsableByteArray testArray = new ParsableByteArray( + ByteBuffer.allocate(4).putShort(testValue).array()); + int readValue = testArray.readShort(); + + // Assert that the value we read was the value we wrote. + assertThat(readValue).isEqualTo(testValue); + // And that the position advanced as expected. + assertThat(testArray.getPosition()).isEqualTo(2); + + // And that skipping back and reading gives the same results. + testArray.skipBytes(-2); + readValue = testArray.readShort(); + assertThat(readValue).isEqualTo(testValue); + assertThat(testArray.getPosition()).isEqualTo(2); + } + + @Test + public void testReadInt() { + testReadInt(0); + testReadInt(1); + testReadInt(-1); + testReadInt(Integer.MIN_VALUE); + testReadInt(Integer.MAX_VALUE); + } + + private static void testReadInt(int testValue) { + ParsableByteArray testArray = new ParsableByteArray( + ByteBuffer.allocate(4).putInt(testValue).array()); + int readValue = testArray.readInt(); + + // Assert that the value we read was the value we wrote. + assertThat(readValue).isEqualTo(testValue); + // And that the position advanced as expected. + assertThat(testArray.getPosition()).isEqualTo(4); + + // And that skipping back and reading gives the same results. + testArray.skipBytes(-4); + readValue = testArray.readInt(); + assertThat(readValue).isEqualTo(testValue); + assertThat(testArray.getPosition()).isEqualTo(4); + } + + @Test + public void testReadUnsignedInt() { + testReadUnsignedInt(0); + testReadUnsignedInt(1); + testReadUnsignedInt(Integer.MAX_VALUE); + testReadUnsignedInt(Integer.MAX_VALUE + 1L); + testReadUnsignedInt(0xFFFFFFFFL); + } + + private static void testReadUnsignedInt(long testValue) { + ParsableByteArray testArray = new ParsableByteArray( + Arrays.copyOfRange(ByteBuffer.allocate(8).putLong(testValue).array(), 4, 8)); + long readValue = testArray.readUnsignedInt(); + + // Assert that the value we read was the value we wrote. + assertThat(readValue).isEqualTo(testValue); + // And that the position advanced as expected. + assertThat(testArray.getPosition()).isEqualTo(4); + + // And that skipping back and reading gives the same results. + testArray.skipBytes(-4); + readValue = testArray.readUnsignedInt(); + assertThat(readValue).isEqualTo(testValue); + assertThat(testArray.getPosition()).isEqualTo(4); + } + + @Test + public void testReadUnsignedIntToInt() { + testReadUnsignedIntToInt(0); + testReadUnsignedIntToInt(1); + testReadUnsignedIntToInt(Integer.MAX_VALUE); + try { + testReadUnsignedIntToInt(-1); + fail(); + } catch (IllegalStateException e) { + // Expected. + } + try { + testReadUnsignedIntToInt(Integer.MIN_VALUE); + fail(); + } catch (IllegalStateException e) { + // Expected. + } + } + + private static void testReadUnsignedIntToInt(int testValue) { + ParsableByteArray testArray = new ParsableByteArray( + ByteBuffer.allocate(4).putInt(testValue).array()); + int readValue = testArray.readUnsignedIntToInt(); + + // Assert that the value we read was the value we wrote. + assertThat(readValue).isEqualTo(testValue); + // And that the position advanced as expected. + assertThat(testArray.getPosition()).isEqualTo(4); + + // And that skipping back and reading gives the same results. + testArray.skipBytes(-4); + readValue = testArray.readUnsignedIntToInt(); + assertThat(readValue).isEqualTo(testValue); + assertThat(testArray.getPosition()).isEqualTo(4); + } + + @Test + public void testReadUnsignedLongToLong() { + testReadUnsignedLongToLong(0); + testReadUnsignedLongToLong(1); + testReadUnsignedLongToLong(Long.MAX_VALUE); + try { + testReadUnsignedLongToLong(-1); + fail(); + } catch (IllegalStateException e) { + // Expected. + } + try { + testReadUnsignedLongToLong(Long.MIN_VALUE); + fail(); + } catch (IllegalStateException e) { + // Expected. + } + } + + private static void testReadUnsignedLongToLong(long testValue) { + ParsableByteArray testArray = new ParsableByteArray( + ByteBuffer.allocate(8).putLong(testValue).array()); + long readValue = testArray.readUnsignedLongToLong(); + + // Assert that the value we read was the value we wrote. + assertThat(readValue).isEqualTo(testValue); + // And that the position advanced as expected. + assertThat(testArray.getPosition()).isEqualTo(8); + + // And that skipping back and reading gives the same results. + testArray.skipBytes(-8); + readValue = testArray.readUnsignedLongToLong(); + assertThat(readValue).isEqualTo(testValue); + assertThat(testArray.getPosition()).isEqualTo(8); + } + + @Test + public void testReadLong() { + testReadLong(0); + testReadLong(1); + testReadLong(-1); + testReadLong(Long.MIN_VALUE); + testReadLong(Long.MAX_VALUE); + } + + private static void testReadLong(long testValue) { + ParsableByteArray testArray = new ParsableByteArray( + ByteBuffer.allocate(8).putLong(testValue).array()); + long readValue = testArray.readLong(); + + // Assert that the value we read was the value we wrote. + assertThat(readValue).isEqualTo(testValue); + // And that the position advanced as expected. + assertThat(testArray.getPosition()).isEqualTo(8); + + // And that skipping back and reading gives the same results. + testArray.skipBytes(-8); + readValue = testArray.readLong(); + assertThat(readValue).isEqualTo(testValue); + assertThat(testArray.getPosition()).isEqualTo(8); + } + + @Test + public void testReadingMovesPosition() { + ParsableByteArray parsableByteArray = getTestDataArray(); + + // Given an array at the start + assertThat(parsableByteArray.getPosition()).isEqualTo(0); + // When reading an integer, the position advances + parsableByteArray.readUnsignedInt(); + assertThat(parsableByteArray.getPosition()).isEqualTo(4); + } + + @Test + public void testOutOfBoundsThrows() { + ParsableByteArray parsableByteArray = getTestDataArray(); + + // Given an array at the end + parsableByteArray.readUnsignedLongToLong(); + assertThat(parsableByteArray.getPosition()).isEqualTo(TEST_DATA.length); + // Then reading more data throws. + try { + parsableByteArray.readUnsignedInt(); + fail(); + } catch (Exception e) { + // Expected. + } + } + + @Test + public void testModificationsAffectParsableArray() { + ParsableByteArray parsableByteArray = getTestDataArray(); + + // When modifying the wrapped byte array + byte[] data = parsableByteArray.data; + long readValue = parsableByteArray.readUnsignedInt(); + data[0] = (byte) (TEST_DATA[0] + 1); + parsableByteArray.setPosition(0); + // Then the parsed value changes. + assertThat(parsableByteArray.readUnsignedInt()).isNotEqualTo(readValue); + } + + @Test + public void testReadingUnsignedLongWithMsbSetThrows() { + ParsableByteArray parsableByteArray = getTestDataArray(); + + // Given an array with the most-significant bit set on the top byte + byte[] data = parsableByteArray.data; + data[0] = (byte) 0x80; + // Then reading an unsigned long throws. + try { + parsableByteArray.readUnsignedLongToLong(); + fail(); + } catch (Exception e) { + // Expected. + } + } + + @Test + public void testReadUnsignedFixedPoint1616() { + ParsableByteArray parsableByteArray = getTestDataArray(); + + // When reading the integer part of a 16.16 fixed point value + int value = parsableByteArray.readUnsignedFixedPoint1616(); + // Then the read value is equal to the array elements interpreted as a short. + assertThat(value).isEqualTo((0xFF & TEST_DATA[0]) << 8 | (TEST_DATA[1] & 0xFF)); + assertThat(parsableByteArray.getPosition()).isEqualTo(4); + } + + @Test + public void testReadingBytesReturnsCopy() { + ParsableByteArray parsableByteArray = getTestDataArray(); + + // When reading all the bytes back + int length = parsableByteArray.limit(); + assertThat(length).isEqualTo(TEST_DATA.length); + byte[] copy = new byte[length]; + parsableByteArray.readBytes(copy, 0, length); + // Then the array elements are the same. + assertThat(copy).isEqualTo(parsableByteArray.data); + } + + @Test + public void testReadLittleEndianLong() { + ParsableByteArray byteArray = new ParsableByteArray(new byte[] { + 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, (byte) 0xFF + }); + assertThat(byteArray.readLittleEndianLong()).isEqualTo(0xFF00000000000001L); + assertThat(byteArray.getPosition()).isEqualTo(8); + } + + @Test + public void testReadLittleEndianUnsignedInt() { + ParsableByteArray byteArray = new ParsableByteArray(new byte[] { + 0x10, 0x00, 0x00, (byte) 0xFF + }); + assertThat(byteArray.readLittleEndianUnsignedInt()).isEqualTo(0xFF000010L); + assertThat(byteArray.getPosition()).isEqualTo(4); + } + + @Test + public void testReadLittleEndianInt() { + ParsableByteArray byteArray = new ParsableByteArray(new byte[] { + 0x01, 0x00, 0x00, (byte) 0xFF + }); + assertThat(byteArray.readLittleEndianInt()).isEqualTo(0xFF000001); + assertThat(byteArray.getPosition()).isEqualTo(4); + } + + @Test + public void testReadLittleEndianUnsignedInt24() { + byte[] data = { 0x01, 0x02, (byte) 0xFF }; + ParsableByteArray byteArray = new ParsableByteArray(data); + assertThat(byteArray.readLittleEndianUnsignedInt24()).isEqualTo(0xFF0201); + assertThat(byteArray.getPosition()).isEqualTo(3); + } + + @Test + public void testReadLittleEndianUnsignedShort() { + ParsableByteArray byteArray = new ParsableByteArray(new byte[] { + 0x01, (byte) 0xFF, 0x02, (byte) 0xFF + }); + assertThat(byteArray.readLittleEndianUnsignedShort()).isEqualTo(0xFF01); + assertThat(byteArray.getPosition()).isEqualTo(2); + assertThat(byteArray.readLittleEndianUnsignedShort()).isEqualTo(0xFF02); + assertThat(byteArray.getPosition()).isEqualTo(4); + } + + @Test + public void testReadLittleEndianShort() { + ParsableByteArray byteArray = new ParsableByteArray(new byte[] { + 0x01, (byte) 0xFF, 0x02, (byte) 0xFF + }); + assertThat(byteArray.readLittleEndianShort()).isEqualTo((short) 0xFF01); + assertThat(byteArray.getPosition()).isEqualTo(2); + assertThat(byteArray.readLittleEndianShort()).isEqualTo((short) 0xFF02); + assertThat(byteArray.getPosition()).isEqualTo(4); + } + + @Test + public void testReadString() { + byte[] data = { + (byte) 0xC3, (byte) 0xA4, (byte) 0x20, + (byte) 0xC3, (byte) 0xB6, (byte) 0x20, + (byte) 0xC2, (byte) 0xAE, (byte) 0x20, + (byte) 0xCF, (byte) 0x80, (byte) 0x20, + (byte) 0xE2, (byte) 0x88, (byte) 0x9A, (byte) 0x20, + (byte) 0xC2, (byte) 0xB1, (byte) 0x20, + (byte) 0xE8, (byte) 0xB0, (byte) 0xA2, (byte) 0x20, + }; + ParsableByteArray byteArray = new ParsableByteArray(data); + assertThat(byteArray.readString(data.length)).isEqualTo("ä ö ® π √ ± 谢 "); + assertThat(byteArray.getPosition()).isEqualTo(data.length); + } + + @Test + public void testReadAsciiString() { + byte[] data = new byte[] {'t', 'e', 's', 't'}; + ParsableByteArray testArray = new ParsableByteArray(data); + assertThat(testArray.readString(data.length, forName("US-ASCII"))).isEqualTo("test"); + assertThat(testArray.getPosition()).isEqualTo(data.length); + } + + @Test + public void testReadStringOutOfBoundsDoesNotMovePosition() { + byte[] data = { + (byte) 0xC3, (byte) 0xA4, (byte) 0x20 + }; + ParsableByteArray byteArray = new ParsableByteArray(data); + try { + byteArray.readString(data.length + 1); + fail(); + } catch (StringIndexOutOfBoundsException e) { + assertThat(byteArray.getPosition()).isEqualTo(0); + } + } + + @Test + public void testReadEmptyString() { + byte[] bytes = new byte[0]; + ParsableByteArray parser = new ParsableByteArray(bytes); + assertThat(parser.readLine()).isNull(); + } + + @Test + public void testReadNullTerminatedStringWithLengths() { + byte[] bytes = new byte[] { + 'f', 'o', 'o', 0, 'b', 'a', 'r', 0 + }; + // Test with lengths that match NUL byte positions. + ParsableByteArray parser = new ParsableByteArray(bytes); + assertThat(parser.readNullTerminatedString(4)).isEqualTo("foo"); + assertThat(parser.getPosition()).isEqualTo(4); + assertThat(parser.readNullTerminatedString(4)).isEqualTo("bar"); + assertThat(parser.getPosition()).isEqualTo(8); + assertThat(parser.readNullTerminatedString()).isNull(); + // Test with lengths that do not match NUL byte positions. + parser = new ParsableByteArray(bytes); + assertThat(parser.readNullTerminatedString(2)).isEqualTo("fo"); + assertThat(parser.getPosition()).isEqualTo(2); + assertThat(parser.readNullTerminatedString(2)).isEqualTo("o"); + assertThat(parser.getPosition()).isEqualTo(4); + assertThat(parser.readNullTerminatedString(3)).isEqualTo("bar"); + assertThat(parser.getPosition()).isEqualTo(7); + assertThat(parser.readNullTerminatedString(1)).isEqualTo(""); + assertThat(parser.getPosition()).isEqualTo(8); + assertThat(parser.readNullTerminatedString()).isNull(); + // Test with limit at NUL + parser = new ParsableByteArray(bytes, 4); + assertThat(parser.readNullTerminatedString(4)).isEqualTo("foo"); + assertThat(parser.getPosition()).isEqualTo(4); + assertThat(parser.readNullTerminatedString()).isNull(); + // Test with limit before NUL + parser = new ParsableByteArray(bytes, 3); + assertThat(parser.readNullTerminatedString(3)).isEqualTo("foo"); + assertThat(parser.getPosition()).isEqualTo(3); + assertThat(parser.readNullTerminatedString()).isNull(); + } + + @Test + public void testReadNullTerminatedString() { + byte[] bytes = new byte[] { + 'f', 'o', 'o', 0, 'b', 'a', 'r', 0 + }; + // Test normal case. + ParsableByteArray parser = new ParsableByteArray(bytes); + assertThat(parser.readNullTerminatedString()).isEqualTo("foo"); + assertThat(parser.getPosition()).isEqualTo(4); + assertThat(parser.readNullTerminatedString()).isEqualTo("bar"); + assertThat(parser.getPosition()).isEqualTo(8); + assertThat(parser.readNullTerminatedString()).isNull(); + // Test with limit at NUL. + parser = new ParsableByteArray(bytes, 4); + assertThat(parser.readNullTerminatedString()).isEqualTo("foo"); + assertThat(parser.getPosition()).isEqualTo(4); + assertThat(parser.readNullTerminatedString()).isNull(); + // Test with limit before NUL. + parser = new ParsableByteArray(bytes, 3); + assertThat(parser.readNullTerminatedString()).isEqualTo("foo"); + assertThat(parser.getPosition()).isEqualTo(3); + assertThat(parser.readNullTerminatedString()).isNull(); + } + + @Test + public void testReadNullTerminatedStringWithoutEndingNull() { + byte[] bytes = new byte[] { + 'f', 'o', 'o', 0, 'b', 'a', 'r' + }; + ParsableByteArray parser = new ParsableByteArray(bytes); + assertThat(parser.readNullTerminatedString()).isEqualTo("foo"); + assertThat(parser.readNullTerminatedString()).isEqualTo("bar"); + assertThat(parser.readNullTerminatedString()).isNull(); + } + + @Test + public void testReadSingleLineWithoutEndingTrail() { + byte[] bytes = new byte[] { + 'f', 'o', 'o' + }; + ParsableByteArray parser = new ParsableByteArray(bytes); + assertThat(parser.readLine()).isEqualTo("foo"); + assertThat(parser.readLine()).isNull(); + } + + @Test + public void testReadSingleLineWithEndingLf() { + byte[] bytes = new byte[] { + 'f', 'o', 'o', '\n' + }; + ParsableByteArray parser = new ParsableByteArray(bytes); + assertThat(parser.readLine()).isEqualTo("foo"); + assertThat(parser.readLine()).isNull(); + } + + @Test + public void testReadTwoLinesWithCrFollowedByLf() { + byte[] bytes = new byte[] { + 'f', 'o', 'o', '\r', '\n', 'b', 'a', 'r' + }; + ParsableByteArray parser = new ParsableByteArray(bytes); + assertThat(parser.readLine()).isEqualTo("foo"); + assertThat(parser.readLine()).isEqualTo("bar"); + assertThat(parser.readLine()).isNull(); + } + + @Test + public void testReadThreeLinesWithEmptyLine() { + byte[] bytes = new byte[] { + 'f', 'o', 'o', '\r', '\n', '\r', 'b', 'a', 'r' + }; + ParsableByteArray parser = new ParsableByteArray(bytes); + assertThat(parser.readLine()).isEqualTo("foo"); + assertThat(parser.readLine()).isEqualTo(""); + assertThat(parser.readLine()).isEqualTo("bar"); + assertThat(parser.readLine()).isNull(); + } + + @Test + public void testReadFourLinesWithLfFollowedByCr() { + byte[] bytes = new byte[] { + 'f', 'o', 'o', '\n', '\r', '\r', 'b', 'a', 'r', '\r', '\n' + }; + ParsableByteArray parser = new ParsableByteArray(bytes); + assertThat(parser.readLine()).isEqualTo("foo"); + assertThat(parser.readLine()).isEqualTo(""); + assertThat(parser.readLine()).isEqualTo(""); + assertThat(parser.readLine()).isEqualTo("bar"); + assertThat(parser.readLine()).isNull(); + } + +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/ParsableNalUnitBitArrayTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/ParsableNalUnitBitArrayTest.java new file mode 100644 index 0000000000..a3f38abcdb --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/ParsableNalUnitBitArrayTest.java @@ -0,0 +1,128 @@ +/* + * 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 static com.google.android.exoplayer2.testutil.TestUtil.createByteArray; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Tests for {@link ParsableNalUnitBitArray}. + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class ParsableNalUnitBitArrayTest { + + private static final byte[] NO_ESCAPING_TEST_DATA = createByteArray(0, 3, 0, 1, 3, 0, 0); + private static final byte[] ALL_ESCAPING_TEST_DATA = createByteArray(0, 0, 3, 0, 0, 3, 0, 0, 3); + private static final byte[] MIX_TEST_DATA = createByteArray(255, 0, 0, 3, 255, 0, 0, 127); + + @Test + public void testReadNoEscaping() { + ParsableNalUnitBitArray array = + new ParsableNalUnitBitArray(NO_ESCAPING_TEST_DATA, 0, NO_ESCAPING_TEST_DATA.length); + assertThat(array.readBits(24)).isEqualTo(0x000300); + assertThat(array.readBits(7)).isEqualTo(0); + assertThat(array.readBit()).isTrue(); + assertThat(array.readBits(24)).isEqualTo(0x030000); + assertThat(array.canReadBits(1)).isFalse(); + assertThat(array.canReadBits(8)).isFalse(); + } + + @Test + public void testReadNoEscapingTruncated() { + ParsableNalUnitBitArray array = new ParsableNalUnitBitArray(NO_ESCAPING_TEST_DATA, 0, 4); + assertThat(array.canReadBits(32)).isTrue(); + array.skipBits(32); + assertThat(array.canReadBits(1)).isFalse(); + try { + array.readBit(); + fail(); + } catch (Exception e) { + // Expected. + } + } + + @Test + public void testReadAllEscaping() { + ParsableNalUnitBitArray array = + new ParsableNalUnitBitArray(ALL_ESCAPING_TEST_DATA, 0, ALL_ESCAPING_TEST_DATA.length); + assertThat(array.canReadBits(48)).isTrue(); + assertThat(array.canReadBits(49)).isFalse(); + assertThat(array.readBits(15)).isEqualTo(0); + assertThat(array.readBit()).isFalse(); + assertThat(array.readBits(17)).isEqualTo(0); + assertThat(array.readBits(15)).isEqualTo(0); + } + + @Test + public void testReadMix() { + ParsableNalUnitBitArray array = + new ParsableNalUnitBitArray(MIX_TEST_DATA, 0, MIX_TEST_DATA.length); + assertThat(array.canReadBits(56)).isTrue(); + assertThat(array.canReadBits(57)).isFalse(); + assertThat(array.readBits(7)).isEqualTo(127); + assertThat(array.readBits(2)).isEqualTo(2); + assertThat(array.readBits(17)).isEqualTo(3); + assertThat(array.readBits(7)).isEqualTo(126); + assertThat(array.readBits(23)).isEqualTo(127); + assertThat(array.canReadBits(1)).isFalse(); + } + + @Test + public void testReadExpGolomb() { + ParsableNalUnitBitArray array = new ParsableNalUnitBitArray(createByteArray(0x9E), 0, 1); + assertThat(array.canReadExpGolombCodedNum()).isTrue(); + assertThat(array.readUnsignedExpGolombCodedInt()).isEqualTo(0); + assertThat(array.readUnsignedExpGolombCodedInt()).isEqualTo(6); + assertThat(array.readUnsignedExpGolombCodedInt()).isEqualTo(0); + assertThat(array.canReadExpGolombCodedNum()).isFalse(); + try { + array.readUnsignedExpGolombCodedInt(); + fail(); + } catch (Exception e) { + // Expected. + } + } + + @Test + public void testReadExpGolombWithEscaping() { + ParsableNalUnitBitArray array = + new ParsableNalUnitBitArray(createByteArray(0, 0, 3, 128, 0), 0, 5); + assertThat(array.canReadExpGolombCodedNum()).isFalse(); + array.skipBit(); + assertThat(array.canReadExpGolombCodedNum()).isTrue(); + assertThat(array.readUnsignedExpGolombCodedInt()).isEqualTo(32767); + assertThat(array.canReadBits(1)).isFalse(); + } + + @Test + public void testReset() { + ParsableNalUnitBitArray array = new ParsableNalUnitBitArray(createByteArray(0, 0), 0, 2); + assertThat(array.canReadExpGolombCodedNum()).isFalse(); + assertThat(array.canReadBits(16)).isTrue(); + assertThat(array.canReadBits(17)).isFalse(); + array.reset(createByteArray(0, 0, 3, 0), 0, 4); + assertThat(array.canReadBits(24)).isTrue(); + assertThat(array.canReadBits(25)).isFalse(); + } + +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/ReusableBufferedOutputStreamTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/ReusableBufferedOutputStreamTest.java new file mode 100644 index 0000000000..8e384bbb10 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/ReusableBufferedOutputStreamTest.java @@ -0,0 +1,53 @@ +/* + * 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 static com.google.common.truth.Truth.assertThat; + +import java.io.ByteArrayOutputStream; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Tests {@link ReusableBufferedOutputStream}. + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class ReusableBufferedOutputStreamTest { + + private static final byte[] TEST_DATA_1 = "test data 1".getBytes(); + private static final byte[] TEST_DATA_2 = "2 test data".getBytes(); + + @Test + public void testReset() throws Exception { + ByteArrayOutputStream byteArrayOutputStream1 = new ByteArrayOutputStream(1000); + ReusableBufferedOutputStream outputStream = new ReusableBufferedOutputStream( + byteArrayOutputStream1, 1000); + outputStream.write(TEST_DATA_1); + outputStream.close(); + + ByteArrayOutputStream byteArrayOutputStream2 = new ByteArrayOutputStream(1000); + outputStream.reset(byteArrayOutputStream2); + outputStream.write(TEST_DATA_2); + outputStream.close(); + + assertThat(byteArrayOutputStream1.toByteArray()).isEqualTo(TEST_DATA_1); + assertThat(byteArrayOutputStream2.toByteArray()).isEqualTo(TEST_DATA_2); + } + +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/UriUtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/UriUtilTest.java new file mode 100644 index 0000000000..52e7a722fb --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/UriUtilTest.java @@ -0,0 +1,109 @@ +/* + * 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 static com.google.android.exoplayer2.util.UriUtil.resolve; +import static com.google.common.truth.Truth.assertThat; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Unit tests for {@link UriUtil}. + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class UriUtilTest { + + /** + * Tests normal usage of {@link UriUtil#resolve(String, String)}. + *

      + * The test cases are taken from RFC-3986 5.4.1. + */ + @Test + public void testResolveNormal() { + String base = "http://a/b/c/d;p?q"; + + assertThat(resolve(base, "g:h")).isEqualTo("g:h"); + assertThat(resolve(base, "g")).isEqualTo("http://a/b/c/g"); + assertThat(resolve(base, "g/")).isEqualTo("http://a/b/c/g/"); + assertThat(resolve(base, "/g")).isEqualTo("http://a/g"); + assertThat(resolve(base, "//g")).isEqualTo("http://g"); + assertThat(resolve(base, "?y")).isEqualTo("http://a/b/c/d;p?y"); + assertThat(resolve(base, "g?y")).isEqualTo("http://a/b/c/g?y"); + assertThat(resolve(base, "#s")).isEqualTo("http://a/b/c/d;p?q#s"); + assertThat(resolve(base, "g#s")).isEqualTo("http://a/b/c/g#s"); + assertThat(resolve(base, "g?y#s")).isEqualTo("http://a/b/c/g?y#s"); + assertThat(resolve(base, ";x")).isEqualTo("http://a/b/c/;x"); + assertThat(resolve(base, "g;x")).isEqualTo("http://a/b/c/g;x"); + assertThat(resolve(base, "g;x?y#s")).isEqualTo("http://a/b/c/g;x?y#s"); + assertThat(resolve(base, "")).isEqualTo("http://a/b/c/d;p?q"); + assertThat(resolve(base, ".")).isEqualTo("http://a/b/c/"); + assertThat(resolve(base, "./")).isEqualTo("http://a/b/c/"); + assertThat(resolve(base, "..")).isEqualTo("http://a/b/"); + assertThat(resolve(base, "../")).isEqualTo("http://a/b/"); + assertThat(resolve(base, "../g")).isEqualTo("http://a/b/g"); + assertThat(resolve(base, "../..")).isEqualTo("http://a/"); + assertThat(resolve(base, "../../")).isEqualTo("http://a/"); + assertThat(resolve(base, "../../g")).isEqualTo("http://a/g"); + } + + /** + * Tests abnormal usage of {@link UriUtil#resolve(String, String)}. + *

      + * The test cases are taken from RFC-3986 5.4.2. + */ + @Test + public void testResolveAbnormal() { + String base = "http://a/b/c/d;p?q"; + + assertThat(resolve(base, "../../../g")).isEqualTo("http://a/g"); + assertThat(resolve(base, "../../../../g")).isEqualTo("http://a/g"); + + assertThat(resolve(base, "/./g")).isEqualTo("http://a/g"); + assertThat(resolve(base, "/../g")).isEqualTo("http://a/g"); + assertThat(resolve(base, "g.")).isEqualTo("http://a/b/c/g."); + assertThat(resolve(base, ".g")).isEqualTo("http://a/b/c/.g"); + assertThat(resolve(base, "g..")).isEqualTo("http://a/b/c/g.."); + assertThat(resolve(base, "..g")).isEqualTo("http://a/b/c/..g"); + + assertThat(resolve(base, "./../g")).isEqualTo("http://a/b/g"); + assertThat(resolve(base, "./g/.")).isEqualTo("http://a/b/c/g/"); + assertThat(resolve(base, "g/./h")).isEqualTo("http://a/b/c/g/h"); + assertThat(resolve(base, "g/../h")).isEqualTo("http://a/b/c/h"); + assertThat(resolve(base, "g;x=1/./y")).isEqualTo("http://a/b/c/g;x=1/y"); + assertThat(resolve(base, "g;x=1/../y")).isEqualTo("http://a/b/c/y"); + + assertThat(resolve(base, "g?y/./x")).isEqualTo("http://a/b/c/g?y/./x"); + assertThat(resolve(base, "g?y/../x")).isEqualTo("http://a/b/c/g?y/../x"); + assertThat(resolve(base, "g#s/./x")).isEqualTo("http://a/b/c/g#s/./x"); + assertThat(resolve(base, "g#s/../x")).isEqualTo("http://a/b/c/g#s/../x"); + + assertThat(resolve(base, "http:g")).isEqualTo("http:g"); + } + + /** + * Tests additional abnormal usage of {@link UriUtil#resolve(String, String)}. + */ + @Test + public void testResolveAbnormalAdditional() { + assertThat(resolve("http://a/b", "c:d/../e")).isEqualTo("c:e"); + assertThat(resolve("a:b", "../c")).isEqualTo("a:c"); + } + +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/UtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/UtilTest.java new file mode 100644 index 0000000000..70caff9bf1 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/UtilTest.java @@ -0,0 +1,200 @@ +/* + * 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 static com.google.android.exoplayer2.util.Util.binarySearchCeil; +import static com.google.android.exoplayer2.util.Util.binarySearchFloor; +import static com.google.android.exoplayer2.util.Util.escapeFileName; +import static com.google.android.exoplayer2.util.Util.parseXsDateTime; +import static com.google.android.exoplayer2.util.Util.parseXsDuration; +import static com.google.android.exoplayer2.util.Util.unescapeFileName; +import static com.google.common.truth.Truth.assertThat; + +import com.google.android.exoplayer2.testutil.TestUtil; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Unit tests for {@link Util}. + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public class UtilTest { + + @Test + public void testArrayBinarySearchFloor() { + long[] values = new long[0]; + assertThat(binarySearchFloor(values, 0, false, false)).isEqualTo(-1); + assertThat(binarySearchFloor(values, 0, false, true)).isEqualTo(0); + + values = new long[] {1, 3, 5}; + assertThat(binarySearchFloor(values, 0, false, false)).isEqualTo(-1); + assertThat(binarySearchFloor(values, 0, true, false)).isEqualTo(-1); + assertThat(binarySearchFloor(values, 0, false, true)).isEqualTo(0); + assertThat(binarySearchFloor(values, 0, true, true)).isEqualTo(0); + + assertThat(binarySearchFloor(values, 1, false, false)).isEqualTo(-1); + assertThat(binarySearchFloor(values, 1, true, false)).isEqualTo(0); + assertThat(binarySearchFloor(values, 1, false, true)).isEqualTo(0); + assertThat(binarySearchFloor(values, 1, true, true)).isEqualTo(0); + + assertThat(binarySearchFloor(values, 4, false, false)).isEqualTo(1); + assertThat(binarySearchFloor(values, 4, true, false)).isEqualTo(1); + + assertThat(binarySearchFloor(values, 5, false, false)).isEqualTo(1); + assertThat(binarySearchFloor(values, 5, true, false)).isEqualTo(2); + + assertThat(binarySearchFloor(values, 6, false, false)).isEqualTo(2); + assertThat(binarySearchFloor(values, 6, true, false)).isEqualTo(2); + } + + @Test + public void testListBinarySearchFloor() { + List values = new ArrayList<>(); + assertThat(binarySearchFloor(values, 0, false, false)).isEqualTo(-1); + assertThat(binarySearchFloor(values, 0, false, true)).isEqualTo(0); + + values.add(1); + values.add(3); + values.add(5); + assertThat(binarySearchFloor(values, 0, false, false)).isEqualTo(-1); + assertThat(binarySearchFloor(values, 0, true, false)).isEqualTo(-1); + assertThat(binarySearchFloor(values, 0, false, true)).isEqualTo(0); + assertThat(binarySearchFloor(values, 0, true, true)).isEqualTo(0); + + assertThat(binarySearchFloor(values, 1, false, false)).isEqualTo(-1); + assertThat(binarySearchFloor(values, 1, true, false)).isEqualTo(0); + assertThat(binarySearchFloor(values, 1, false, true)).isEqualTo(0); + assertThat(binarySearchFloor(values, 1, true, true)).isEqualTo(0); + + assertThat(binarySearchFloor(values, 4, false, false)).isEqualTo(1); + assertThat(binarySearchFloor(values, 4, true, false)).isEqualTo(1); + + assertThat(binarySearchFloor(values, 5, false, false)).isEqualTo(1); + assertThat(binarySearchFloor(values, 5, true, false)).isEqualTo(2); + + assertThat(binarySearchFloor(values, 6, false, false)).isEqualTo(2); + assertThat(binarySearchFloor(values, 6, true, false)).isEqualTo(2); + } + + @Test + public void testArrayBinarySearchCeil() { + long[] values = new long[0]; + assertThat(binarySearchCeil(values, 0, false, false)).isEqualTo(0); + assertThat(binarySearchCeil(values, 0, false, true)).isEqualTo(-1); + + values = new long[] {1, 3, 5}; + assertThat(binarySearchCeil(values, 0, false, false)).isEqualTo(0); + assertThat(binarySearchCeil(values, 0, true, false)).isEqualTo(0); + + assertThat(binarySearchCeil(values, 1, false, false)).isEqualTo(1); + assertThat(binarySearchCeil(values, 1, true, false)).isEqualTo(0); + + assertThat(binarySearchCeil(values, 2, false, false)).isEqualTo(1); + assertThat(binarySearchCeil(values, 2, true, false)).isEqualTo(1); + + assertThat(binarySearchCeil(values, 5, false, false)).isEqualTo(3); + assertThat(binarySearchCeil(values, 5, true, false)).isEqualTo(2); + assertThat(binarySearchCeil(values, 5, false, true)).isEqualTo(2); + assertThat(binarySearchCeil(values, 5, true, true)).isEqualTo(2); + + assertThat(binarySearchCeil(values, 6, false, false)).isEqualTo(3); + assertThat(binarySearchCeil(values, 6, true, false)).isEqualTo(3); + assertThat(binarySearchCeil(values, 6, false, true)).isEqualTo(2); + assertThat(binarySearchCeil(values, 6, true, true)).isEqualTo(2); + } + + @Test + public void testListBinarySearchCeil() { + List values = new ArrayList<>(); + assertThat(binarySearchCeil(values, 0, false, false)).isEqualTo(0); + assertThat(binarySearchCeil(values, 0, false, true)).isEqualTo(-1); + + values.add(1); + values.add(3); + values.add(5); + assertThat(binarySearchCeil(values, 0, false, false)).isEqualTo(0); + assertThat(binarySearchCeil(values, 0, true, false)).isEqualTo(0); + + assertThat(binarySearchCeil(values, 1, false, false)).isEqualTo(1); + assertThat(binarySearchCeil(values, 1, true, false)).isEqualTo(0); + + assertThat(binarySearchCeil(values, 2, false, false)).isEqualTo(1); + assertThat(binarySearchCeil(values, 2, true, false)).isEqualTo(1); + + assertThat(binarySearchCeil(values, 5, false, false)).isEqualTo(3); + assertThat(binarySearchCeil(values, 5, true, false)).isEqualTo(2); + assertThat(binarySearchCeil(values, 5, false, true)).isEqualTo(2); + assertThat(binarySearchCeil(values, 5, true, true)).isEqualTo(2); + + assertThat(binarySearchCeil(values, 6, false, false)).isEqualTo(3); + assertThat(binarySearchCeil(values, 6, true, false)).isEqualTo(3); + assertThat(binarySearchCeil(values, 6, false, true)).isEqualTo(2); + assertThat(binarySearchCeil(values, 6, true, true)).isEqualTo(2); + } + + @Test + public void testParseXsDuration() { + assertThat(parseXsDuration("PT150.279S")).isEqualTo(150279L); + assertThat(parseXsDuration("PT1.500S")).isEqualTo(1500L); + } + + @Test + public void testParseXsDateTime() throws Exception { + assertThat(parseXsDateTime("2014-06-19T23:07:42")).isEqualTo(1403219262000L); + assertThat(parseXsDateTime("2014-08-06T11:00:00Z")).isEqualTo(1407322800000L); + assertThat(parseXsDateTime("2014-08-06T11:00:00,000Z")).isEqualTo(1407322800000L); + assertThat(parseXsDateTime("2014-09-19T13:18:55-08:00")).isEqualTo(1411161535000L); + assertThat(parseXsDateTime("2014-09-19T13:18:55-0800")).isEqualTo(1411161535000L); + assertThat(parseXsDateTime("2014-09-19T13:18:55.000-0800")).isEqualTo(1411161535000L); + assertThat(parseXsDateTime("2014-09-19T13:18:55.000-800")).isEqualTo(1411161535000L); + } + + @Test + public void testUnescapeInvalidFileName() { + assertThat(Util.unescapeFileName("%a")).isNull(); + assertThat(Util.unescapeFileName("%xyz")).isNull(); + } + + @Test + public void testEscapeUnescapeFileName() { + assertEscapeUnescapeFileName("just+a regular+fileName", "just+a regular+fileName"); + assertEscapeUnescapeFileName("key:value", "key%3avalue"); + assertEscapeUnescapeFileName("<>:\"/\\|?*%", "%3c%3e%3a%22%2f%5c%7c%3f%2a%25"); + + Random random = new Random(0); + for (int i = 0; i < 1000; i++) { + String string = TestUtil.buildTestString(1000, random); + assertEscapeUnescapeFileName(string); + } + } + + private static void assertEscapeUnescapeFileName(String fileName, String escapedFileName) { + assertThat(escapeFileName(fileName)).isEqualTo(escapedFileName); + assertThat(unescapeFileName(escapedFileName)).isEqualTo(fileName); + } + + private static void assertEscapeUnescapeFileName(String fileName) { + String escapedFileName = Util.escapeFileName(fileName); + assertThat(unescapeFileName(escapedFileName)).isEqualTo(fileName); + } + +} From ca2bfbc56ea91169e2abdb56170340cf28e8e9f7 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 7 Apr 2017 12:10:08 +0100 Subject: [PATCH 0332/2472] DashManifestParser: Move schemeType up to DrmInitData ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167171407 --- .../exoplayer2/drm/DrmInitDataTest.java | 10 +-- .../drm/OfflineLicenseHelperTest.java | 2 +- .../drm/DefaultDrmSessionManager.java | 2 +- .../android/exoplayer2/drm/DrmInitData.java | 77 +++++++------------ .../extractor/mkv/MatroskaExtractor.java | 2 +- .../extractor/mp4/FragmentedMp4Extractor.java | 2 +- .../google/android/exoplayer2/FormatTest.java | 6 +- .../exoplayer2/source/dash/DashUtilTest.java | 2 +- .../dash/manifest/DashManifestParser.java | 62 ++++++++++----- .../manifest/SsManifestParser.java | 2 +- 10 files changed, 81 insertions(+), 86 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java index aa8cbfdb62..8a2c24beba 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java @@ -31,15 +31,15 @@ import junit.framework.TestCase; */ public class DrmInitDataTest extends TestCase { - private static final SchemeData DATA_1 = new SchemeData(WIDEVINE_UUID, "cbc1", VIDEO_MP4, + private static final SchemeData DATA_1 = new SchemeData(WIDEVINE_UUID, VIDEO_MP4, TestUtil.buildTestData(128, 1 /* data seed */)); - private static final SchemeData DATA_2 = new SchemeData(PLAYREADY_UUID, null, VIDEO_MP4, + private static final SchemeData DATA_2 = new SchemeData(PLAYREADY_UUID, VIDEO_MP4, TestUtil.buildTestData(128, 2 /* data seed */)); - private static final SchemeData DATA_1B = new SchemeData(WIDEVINE_UUID, "cbc1", VIDEO_MP4, + private static final SchemeData DATA_1B = new SchemeData(WIDEVINE_UUID, VIDEO_MP4, TestUtil.buildTestData(128, 1 /* data seed */)); - private static final SchemeData DATA_2B = new SchemeData(PLAYREADY_UUID, null, VIDEO_MP4, + private static final SchemeData DATA_2B = new SchemeData(PLAYREADY_UUID, VIDEO_MP4, TestUtil.buildTestData(128, 2 /* data seed */)); - private static final SchemeData DATA_UNIVERSAL = new SchemeData(C.UUID_NIL, null, VIDEO_MP4, + private static final SchemeData DATA_UNIVERSAL = new SchemeData(C.UUID_NIL, VIDEO_MP4, TestUtil.buildTestData(128, 3 /* data seed */)); public void testParcelable() { diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java index 43c867f435..821656475d 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java @@ -153,7 +153,7 @@ public class OfflineLicenseHelperTest extends InstrumentationTestCase { } private static DrmInitData newDrmInitData() { - return new DrmInitData(new SchemeData(C.WIDEVINE_UUID, "cenc", "mimeType", + return new DrmInitData(new SchemeData(C.WIDEVINE_UUID, "mimeType", new byte[] {1, 4, 7, 0, 3, 6})); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java index 25a73a67c1..9ea696e074 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java @@ -278,7 +278,7 @@ public class DefaultDrmSessionManager implements DrmSe // No data for this manager's scheme. return false; } - String schemeType = schemeData.type; + String schemeType = drmInitData.schemeType; if (schemeType == null || C.CENC_TYPE_cenc.equals(schemeType)) { // If there is no scheme information, assume patternless AES-CTR. return true; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java index d814ece31d..c8e76ec291 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java @@ -37,6 +37,11 @@ public final class DrmInitData implements Comparator, Parcelable { // Lazily initialized hashcode. private int hashCode; + /** + * The protection scheme type, or null if not applicable or unknown. + */ + @Nullable public final String schemeType; + /** * Number of {@link SchemeData}s. */ @@ -46,17 +51,19 @@ public final class DrmInitData implements Comparator, Parcelable { * @param schemeDatas Scheme initialization data for possibly multiple DRM schemes. */ public DrmInitData(List schemeDatas) { - this(false, schemeDatas.toArray(new SchemeData[schemeDatas.size()])); + this(null, false, schemeDatas.toArray(new SchemeData[schemeDatas.size()])); } /** * @param schemeDatas Scheme initialization data for possibly multiple DRM schemes. */ public DrmInitData(SchemeData... schemeDatas) { - this(true, schemeDatas); + this(null, true, schemeDatas); } - private DrmInitData(boolean cloneSchemeDatas, SchemeData... schemeDatas) { + private DrmInitData(@Nullable String schemeType, boolean cloneSchemeDatas, + SchemeData... schemeDatas) { + this.schemeType = schemeType; if (cloneSchemeDatas) { schemeDatas = schemeDatas.clone(); } @@ -74,6 +81,7 @@ public final class DrmInitData implements Comparator, Parcelable { } /* package */ DrmInitData(Parcel in) { + schemeType = in.readString(); schemeDatas = in.createTypedArray(SchemeData.CREATOR); schemeDataCount = schemeDatas.length; } @@ -104,36 +112,24 @@ public final class DrmInitData implements Comparator, Parcelable { } /** - * Returns a copy of the {@link DrmInitData} instance whose {@link SchemeData}s have been updated - * to have the specified scheme type. + * Returns a copy with the specified protection scheme type. * * @param schemeType A protection scheme type. May be null. - * @return A copy of the {@link DrmInitData} instance whose {@link SchemeData}s have been updated - * to have the specified scheme type. + * @return A copy with the specified protection scheme type. */ public DrmInitData copyWithSchemeType(@Nullable String schemeType) { - boolean isCopyRequired = false; - for (SchemeData schemeData : schemeDatas) { - if (!Util.areEqual(schemeData.type, schemeType)) { - isCopyRequired = true; - break; - } - } - if (isCopyRequired) { - SchemeData[] schemeDatas = new SchemeData[this.schemeDatas.length]; - for (int i = 0; i < schemeDatas.length; i++) { - schemeDatas[i] = this.schemeDatas[i].copyWithSchemeType(schemeType); - } - return new DrmInitData(schemeDatas); - } else { + if (Util.areEqual(this.schemeType, schemeType)) { return this; } + return new DrmInitData(schemeType, false, schemeDatas); } @Override public int hashCode() { if (hashCode == 0) { - hashCode = Arrays.hashCode(schemeDatas); + int result = (schemeType == null ? 0 : schemeType.hashCode()); + result = 31 * result + Arrays.hashCode(schemeDatas); + hashCode = result; } return hashCode; } @@ -146,7 +142,9 @@ public final class DrmInitData implements Comparator, Parcelable { if (obj == null || getClass() != obj.getClass()) { return false; } - return Arrays.equals(schemeDatas, ((DrmInitData) obj).schemeDatas); + DrmInitData other = (DrmInitData) obj; + return Util.areEqual(schemeType, other.schemeType) + && Arrays.equals(schemeDatas, other.schemeDatas); } @Override @@ -164,6 +162,7 @@ public final class DrmInitData implements Comparator, Parcelable { @Override public void writeToParcel(Parcel dest, int flags) { + dest.writeString(schemeType); dest.writeTypedArray(schemeDatas, 0); } @@ -195,10 +194,6 @@ public final class DrmInitData implements Comparator, Parcelable { * applies to all schemes). */ private final UUID uuid; - /** - * The protection scheme type, or null if not applicable or unknown. - */ - @Nullable public final String type; /** * The mimeType of {@link #data}. */ @@ -215,26 +210,22 @@ public final class DrmInitData implements Comparator, Parcelable { /** * @param uuid The {@link UUID} of the DRM scheme, or {@link C#UUID_NIL} if the data is * universal (i.e. applies to all schemes). - * @param type The type of the protection scheme, or null if not applicable or unknown. * @param mimeType The mimeType of the initialization data. * @param data The initialization data. */ - public SchemeData(UUID uuid, @Nullable String type, String mimeType, byte[] data) { - this(uuid, type, mimeType, data, false); + public SchemeData(UUID uuid, String mimeType, byte[] data) { + this(uuid, mimeType, data, false); } /** * @param uuid The {@link UUID} of the DRM scheme, or {@link C#UUID_NIL} if the data is * universal (i.e. applies to all schemes). - * @param type The type of the protection scheme, or null if not applicable or unknown. * @param mimeType The mimeType of the initialization data. * @param data The initialization data. * @param requiresSecureDecryption Whether secure decryption is required. */ - public SchemeData(UUID uuid, @Nullable String type, String mimeType, byte[] data, - boolean requiresSecureDecryption) { + public SchemeData(UUID uuid, String mimeType, byte[] data, boolean requiresSecureDecryption) { this.uuid = Assertions.checkNotNull(uuid); - this.type = type; this.mimeType = Assertions.checkNotNull(mimeType); this.data = Assertions.checkNotNull(data); this.requiresSecureDecryption = requiresSecureDecryption; @@ -242,7 +233,6 @@ public final class DrmInitData implements Comparator, Parcelable { /* package */ SchemeData(Parcel in) { uuid = new UUID(in.readLong(), in.readLong()); - type = in.readString(); mimeType = in.readString(); data = in.createByteArray(); requiresSecureDecryption = in.readByte() != 0; @@ -258,19 +248,6 @@ public final class DrmInitData implements Comparator, Parcelable { return C.UUID_NIL.equals(uuid) || schemeUuid.equals(uuid); } - /** - * Returns a copy of the {@link SchemeData} instance with the given scheme type. - * - * @param type A protection scheme type. - * @return A copy of the {@link SchemeData} instance with the given scheme type. - */ - public SchemeData copyWithSchemeType(String type) { - if (Util.areEqual(this.type, type)) { - return this; - } - return new SchemeData(uuid, type, mimeType, data, requiresSecureDecryption); - } - @Override public boolean equals(Object obj) { if (!(obj instanceof SchemeData)) { @@ -281,14 +258,13 @@ public final class DrmInitData implements Comparator, Parcelable { } SchemeData other = (SchemeData) obj; return mimeType.equals(other.mimeType) && Util.areEqual(uuid, other.uuid) - && Util.areEqual(type, other.type) && Arrays.equals(data, other.data); + && Arrays.equals(data, other.data); } @Override public int hashCode() { if (hashCode == 0) { int result = uuid.hashCode(); - result = 31 * result + (type == null ? 0 : type.hashCode()); result = 31 * result + mimeType.hashCode(); result = 31 * result + Arrays.hashCode(data); hashCode = result; @@ -307,7 +283,6 @@ public final class DrmInitData implements Comparator, Parcelable { public void writeToParcel(Parcel dest, int flags) { dest.writeLong(uuid.getMostSignificantBits()); dest.writeLong(uuid.getLeastSignificantBits()); - dest.writeString(type); dest.writeString(mimeType); dest.writeByteArray(data); dest.writeByte((byte) (requiresSecureDecryption ? 1 : 0)); diff --git a/library/core/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 index 6c4eb033ce..a5fe6ae35b 100644 --- a/library/core/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 @@ -628,7 +628,7 @@ public final class MatroskaExtractor implements Extractor { if (currentTrack.cryptoData == null) { throw new ParserException("Encrypted Track found but ContentEncKeyID was not found"); } - currentTrack.drmInitData = new DrmInitData(new SchemeData(C.UUID_NIL, null, + currentTrack.drmInitData = new DrmInitData(new SchemeData(C.UUID_NIL, MimeTypes.VIDEO_WEBM, currentTrack.cryptoData.encryptionKey)); } break; diff --git a/library/core/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 index c3f2a9fb38..d9ab47546a 100644 --- a/library/core/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 @@ -1275,7 +1275,7 @@ public final class FragmentedMp4Extractor implements Extractor { if (uuid == null) { Log.w(TAG, "Skipped pssh atom (failed to extract uuid)"); } else { - schemeDatas.add(new SchemeData(uuid, null, MimeTypes.VIDEO_MP4, psshData)); + schemeDatas.add(new SchemeData(uuid, MimeTypes.VIDEO_MP4, psshData)); } } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/FormatTest.java b/library/core/src/test/java/com/google/android/exoplayer2/FormatTest.java index 8e36edc105..33e1a673bd 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/FormatTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/FormatTest.java @@ -60,11 +60,11 @@ public final class FormatTest { @Test public void testParcelable() { - DrmInitData.SchemeData DRM_DATA_1 = new DrmInitData.SchemeData(WIDEVINE_UUID, "cenc", VIDEO_MP4, + DrmInitData.SchemeData drmData1 = new DrmInitData.SchemeData(WIDEVINE_UUID, VIDEO_MP4, TestUtil.buildTestData(128, 1 /* data seed */)); - DrmInitData.SchemeData DRM_DATA_2 = new DrmInitData.SchemeData(C.UUID_NIL, null, VIDEO_WEBM, + DrmInitData.SchemeData drmData2 = new DrmInitData.SchemeData(C.UUID_NIL, VIDEO_WEBM, TestUtil.buildTestData(128, 1 /* data seed */)); - DrmInitData drmInitData = new DrmInitData(DRM_DATA_1, DRM_DATA_2); + DrmInitData drmInitData = new DrmInitData(drmData1, drmData2); byte[] projectionData = new byte[] {1, 2, 3}; Metadata metadata = new Metadata( new TextInformationFrame("id1", "description1", "value1"), 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 index 7a1c78b2e8..3db8c6b2f9 100644 --- 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 @@ -75,7 +75,7 @@ public final class DashUtilTest extends TestCase { } private static DrmInitData newDrmInitData() { - return new DrmInitData(new SchemeData(C.WIDEVINE_UUID, null, "mimeType", + return new DrmInitData(new SchemeData(C.WIDEVINE_UUID, "mimeType", new byte[] {1, 4, 7, 0, 3, 6})); } diff --git a/library/dash/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 index 2f4724c258..c973db79d7 100644 --- a/library/dash/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 @@ -238,6 +238,7 @@ public class DashManifestParser extends DefaultHandler int audioChannels = Format.NO_VALUE; int audioSamplingRate = parseInt(xpp, "audioSamplingRate", Format.NO_VALUE); String language = xpp.getAttributeValue(null, "lang"); + String drmSchemeType = null; ArrayList drmSchemeDatas = new ArrayList<>(); ArrayList inbandEventStreams = new ArrayList<>(); ArrayList accessibilityDescriptors = new ArrayList<>(); @@ -254,9 +255,12 @@ public class DashManifestParser extends DefaultHandler seenFirstBaseUrl = true; } } else if (XmlPullParserUtil.isStartTag(xpp, "ContentProtection")) { - SchemeData contentProtection = parseContentProtection(xpp); - if (contentProtection != null) { - drmSchemeDatas.add(contentProtection); + Pair contentProtection = parseContentProtection(xpp); + if (contentProtection.first != null) { + drmSchemeType = contentProtection.first; + } + if (contentProtection.second != null) { + drmSchemeDatas.add(contentProtection.second); } } else if (XmlPullParserUtil.isStartTag(xpp, "ContentComponent")) { language = checkLanguageConsistency(language, xpp.getAttributeValue(null, "lang")); @@ -293,7 +297,7 @@ public class DashManifestParser extends DefaultHandler List representations = new ArrayList<>(representationInfos.size()); for (int i = 0; i < representationInfos.size(); i++) { representations.add(buildRepresentation(representationInfos.get(i), contentId, - drmSchemeDatas, inbandEventStreams)); + drmSchemeType, drmSchemeDatas, inbandEventStreams)); } return buildAdaptationSet(id, contentType, representations, accessibilityDescriptors, @@ -311,9 +315,9 @@ public class DashManifestParser extends DefaultHandler String contentType = xpp.getAttributeValue(null, "contentType"); return TextUtils.isEmpty(contentType) ? C.TRACK_TYPE_UNKNOWN : MimeTypes.BASE_TYPE_AUDIO.equals(contentType) ? C.TRACK_TYPE_AUDIO - : MimeTypes.BASE_TYPE_VIDEO.equals(contentType) ? C.TRACK_TYPE_VIDEO - : MimeTypes.BASE_TYPE_TEXT.equals(contentType) ? C.TRACK_TYPE_TEXT - : C.TRACK_TYPE_UNKNOWN; + : MimeTypes.BASE_TYPE_VIDEO.equals(contentType) ? C.TRACK_TYPE_VIDEO + : MimeTypes.BASE_TYPE_TEXT.equals(contentType) ? C.TRACK_TYPE_TEXT + : C.TRACK_TYPE_UNKNOWN; } protected int getContentType(Format format) { @@ -336,19 +340,20 @@ public class DashManifestParser extends DefaultHandler * @param xpp The parser from which to read. * @throws XmlPullParserException If an error occurs parsing the element. * @throws IOException If an error occurs reading the element. - * @return {@link SchemeData} parsed from the ContentProtection element, or null if the element is - * unsupported. + * @return The scheme type and/or {@link SchemeData} parsed from the ContentProtection element. + * Either or both may be null, depending on the ContentProtection element being parsed. */ - protected SchemeData parseContentProtection(XmlPullParser xpp) throws XmlPullParserException, - IOException { + protected Pair parseContentProtection(XmlPullParser xpp) + throws XmlPullParserException, IOException { String schemeIdUri = xpp.getAttributeValue(null, "schemeIdUri"); boolean isPlayReady = "urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95".equals(schemeIdUri); - String schemeType = xpp.getAttributeValue(null, "value"); + String schemeType = null; byte[] data = null; UUID uuid = null; boolean requiresSecureDecoder = false; if ("urn:mpeg:dash:mp4protection:2011".equals(schemeIdUri)) { + schemeType = xpp.getAttributeValue(null, "value"); String defaultKid = xpp.getAttributeValue(null, "cenc:default_KID"); if (defaultKid != null) { UUID keyId = UUID.fromString(defaultKid); @@ -379,8 +384,9 @@ public class DashManifestParser extends DefaultHandler requiresSecureDecoder = robustnessLevel != null && robustnessLevel.startsWith("HW"); } } while (!XmlPullParserUtil.isEndTag(xpp, "ContentProtection")); - return data != null - ? new SchemeData(uuid, schemeType, MimeTypes.VIDEO_MP4, data, requiresSecureDecoder) : null; + SchemeData schemeData = data != null + ? new SchemeData(uuid, MimeTypes.VIDEO_MP4, data, requiresSecureDecoder) : null; + return Pair.create(schemeType, schemeData); } /** @@ -432,6 +438,7 @@ public class DashManifestParser extends DefaultHandler float frameRate = parseFrameRate(xpp, adaptationSetFrameRate); int audioChannels = adaptationSetAudioChannels; int audioSamplingRate = parseInt(xpp, "audioSamplingRate", adaptationSetAudioSamplingRate); + String drmSchemeType = null; ArrayList drmSchemeDatas = new ArrayList<>(); ArrayList inbandEventStreams = new ArrayList<>(); @@ -452,9 +459,12 @@ public class DashManifestParser extends DefaultHandler } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTemplate")) { segmentBase = parseSegmentTemplate(xpp, (SegmentTemplate) segmentBase); } else if (XmlPullParserUtil.isStartTag(xpp, "ContentProtection")) { - SchemeData contentProtection = parseContentProtection(xpp); - if (contentProtection != null) { - drmSchemeDatas.add(contentProtection); + Pair contentProtection = parseContentProtection(xpp); + if (contentProtection.first != null) { + drmSchemeType = contentProtection.first; + } + if (contentProtection.second != null) { + drmSchemeDatas.add(contentProtection.second); } } else if (XmlPullParserUtil.isStartTag(xpp, "InbandEventStream")) { inbandEventStreams.add(parseDescriptor(xpp, "InbandEventStream")); @@ -466,7 +476,8 @@ public class DashManifestParser extends DefaultHandler adaptationSetAccessibilityDescriptors, codecs); segmentBase = segmentBase != null ? segmentBase : new SingleSegmentBase(); - return new RepresentationInfo(format, baseUrl, segmentBase, drmSchemeDatas, inbandEventStreams); + return new RepresentationInfo(format, baseUrl, segmentBase, drmSchemeType, drmSchemeDatas, + inbandEventStreams); } protected Format buildFormat(String id, String containerMimeType, int width, int height, @@ -499,13 +510,19 @@ public class DashManifestParser extends DefaultHandler } protected Representation buildRepresentation(RepresentationInfo representationInfo, - String contentId, ArrayList extraDrmSchemeDatas, + String contentId, String extraDrmSchemeType, ArrayList extraDrmSchemeDatas, ArrayList extraInbandEventStreams) { Format format = representationInfo.format; + String drmSchemeType = representationInfo.drmSchemeType != null + ? representationInfo.drmSchemeType : extraDrmSchemeType; ArrayList drmSchemeDatas = representationInfo.drmSchemeDatas; drmSchemeDatas.addAll(extraDrmSchemeDatas); if (!drmSchemeDatas.isEmpty()) { - format = format.copyWithDrmInitData(new DrmInitData(drmSchemeDatas)); + DrmInitData drmInitData = new DrmInitData(drmSchemeDatas); + if (drmSchemeType != null) { + drmInitData = drmInitData.copyWithSchemeType(drmSchemeType); + } + format = format.copyWithDrmInitData(drmInitData); } ArrayList inbandEventStremas = representationInfo.inbandEventStreams; inbandEventStremas.addAll(extraInbandEventStreams); @@ -946,14 +963,17 @@ public class DashManifestParser extends DefaultHandler public final Format format; public final String baseUrl; public final SegmentBase segmentBase; + public final String drmSchemeType; public final ArrayList drmSchemeDatas; public final ArrayList inbandEventStreams; public RepresentationInfo(Format format, String baseUrl, SegmentBase segmentBase, - ArrayList drmSchemeDatas, ArrayList inbandEventStreams) { + String drmSchemeType, ArrayList drmSchemeDatas, + ArrayList inbandEventStreams) { this.format = format; this.baseUrl = baseUrl; this.segmentBase = segmentBase; + this.drmSchemeType = drmSchemeType; this.drmSchemeDatas = drmSchemeDatas; this.inbandEventStreams = inbandEventStreams; } diff --git a/library/smoothstreaming/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 index 5784cc7bc6..3ca5f8d997 100644 --- a/library/smoothstreaming/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 @@ -375,7 +375,7 @@ public class SsManifestParser implements ParsingLoadable.Parser { StreamElement[] streamElementArray = new StreamElement[streamElements.size()]; streamElements.toArray(streamElementArray); if (protectionElement != null) { - DrmInitData drmInitData = new DrmInitData(new SchemeData(protectionElement.uuid, null, + DrmInitData drmInitData = new DrmInitData(new SchemeData(protectionElement.uuid, MimeTypes.VIDEO_MP4, protectionElement.data)); for (StreamElement streamElement : streamElementArray) { for (int i = 0; i < streamElement.formats.length; i++) { From 7804c2b07961390d7a91d788ad2883e5dfbbfdfd Mon Sep 17 00:00:00 2001 From: anjalibh Date: Fri, 1 Sep 2017 18:02:20 -0700 Subject: [PATCH 0333/2472] HDR 10 bits: Use a separate sampler for U and V dithering. Using the same sampler introduced some minor horizontal scratches. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167347995 --- extensions/vp9/src/main/jni/vpx_jni.cc | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/extensions/vp9/src/main/jni/vpx_jni.cc b/extensions/vp9/src/main/jni/vpx_jni.cc index d02d524713..f0b93b1dc2 100644 --- a/extensions/vp9/src/main/jni/vpx_jni.cc +++ b/extensions/vp9/src/main/jni/vpx_jni.cc @@ -197,12 +197,12 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) { const int32_t uvHeight = (img->d_h + 1) / 2; const uint64_t yLength = img->stride[VPX_PLANE_Y] * img->d_h; const uint64_t uvLength = img->stride[VPX_PLANE_U] * uvHeight; - int sample = 0; if (img->fmt == VPX_IMG_FMT_I42016) { // HBD planar 420. // Note: The stride for BT2020 is twice of what we use so this is wasting // memory. The long term goal however is to upload half-float/short so // it's not important to optimize the stride at this time. // Y + int sampleY = 0; for (int y = 0; y < img->d_h; y++) { const uint16_t* srcBase = reinterpret_cast( img->planes[VPX_PLANE_Y] + img->stride[VPX_PLANE_Y] * y); @@ -210,12 +210,14 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) { for (int x = 0; x < img->d_w; x++) { // Lightweight dither. Carryover the remainder of each 10->8 bit // conversion to the next pixel. - sample += *srcBase++; - *destBase++ = sample >> 2; - sample = sample & 3; // Remainder. + sampleY += *srcBase++; + *destBase++ = sampleY >> 2; + sampleY = sampleY & 3; // Remainder. } } // UV + int sampleU = 0; + int sampleV = 0; const int32_t uvWidth = (img->d_w + 1) / 2; for (int y = 0; y < uvHeight; y++) { const uint16_t* srcUBase = reinterpret_cast( @@ -228,11 +230,12 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) { for (int x = 0; x < uvWidth; x++) { // Lightweight dither. Carryover the remainder of each 10->8 bit // conversion to the next pixel. - sample += *srcUBase++; - *destUBase++ = sample >> 2; - sample = (*srcVBase++) + (sample & 3); // srcV + previousRemainder. - *destVBase++ = sample >> 2; - sample = sample & 3; // Remainder. + sampleU += *srcUBase++; + *destUBase++ = sampleU >> 2; + sampleU = sampleU & 3; // Remainder. + sampleV += *srcVBase++; + *destVBase++ = sampleV >> 2; + sampleV = sampleV & 3; // Remainder. } } } else { From ba0bd729192f6f99e80cc11d647e3ddc41977dda Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 4 Sep 2017 02:11:00 -0700 Subject: [PATCH 0334/2472] Fix typo ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167474040 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index aff473c488..bd261e75ff 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,7 @@ depend on them as you would on any other local module, for example: ```gradle compile project(':exoplayer-library-core') compile project(':exoplayer-library-dash') -compile project(':exoplayer-library-ui) +compile project(':exoplayer-library-ui') ``` ## Developing ExoPlayer ## From 74b8c45e6dd9c13ed9a32a1aad1ce795535b2ac2 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 4 Sep 2017 11:34:57 +0100 Subject: [PATCH 0335/2472] Minor cleanup to AspectRatioFrameLayout --- .../exoplayer2/ui/AspectRatioFrameLayout.java | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java index 3367a46374..037519b7a4 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java @@ -32,7 +32,8 @@ public final class AspectRatioFrameLayout extends FrameLayout { * Resize modes for {@link AspectRatioFrameLayout}. */ @Retention(RetentionPolicy.SOURCE) - @IntDef({RESIZE_MODE_FIT, RESIZE_MODE_FIXED_WIDTH, RESIZE_MODE_FIXED_HEIGHT, RESIZE_MODE_FILL, RESIZE_MODE_ZOOM}) + @IntDef({RESIZE_MODE_FIT, RESIZE_MODE_FIXED_WIDTH, RESIZE_MODE_FIXED_HEIGHT, RESIZE_MODE_FILL, + RESIZE_MODE_ZOOM}) public @interface ResizeMode {} /** @@ -52,7 +53,7 @@ public final class AspectRatioFrameLayout extends FrameLayout { */ public static final int RESIZE_MODE_FILL = 3; /** - * Either height or width is increased to obtain the desired aspect ratio. + * Either the width or height is increased to obtain the desired aspect ratio. */ public static final int RESIZE_MODE_ZOOM = 4; @@ -89,7 +90,7 @@ public final class AspectRatioFrameLayout extends FrameLayout { } /** - * Set the aspect ratio that this view should satisfy. + * Sets the aspect ratio that this view should satisfy. * * @param widthHeightRatio The width to height ratio. */ @@ -101,12 +102,10 @@ public final class AspectRatioFrameLayout extends FrameLayout { } /** - * Gets the resize mode. - * - * @return The resize mode. + * Returns the resize mode. */ - public int getResizeMode() { - return this.resizeMode; + public @ResizeMode int getResizeMode() { + return resizeMode; } /** @@ -146,7 +145,7 @@ public final class AspectRatioFrameLayout extends FrameLayout { width = (int) (height * videoAspectRatio); break; case RESIZE_MODE_ZOOM: - if (videoAspectRatio > viewAspectRatio) { + if (aspectDeformation > 0) { width = (int) (height * videoAspectRatio); } else { height = (int) (width / videoAspectRatio); From ab1e4df11aefe71c8069730ad5a9300733440f25 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 4 Sep 2017 05:32:54 -0700 Subject: [PATCH 0336/2472] Update moe eqiuvalence ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167488837 --- .../android/exoplayer2/video/DummySurface.java | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java index 8551f2541d..a1820ed7a1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java @@ -43,7 +43,6 @@ import static android.opengl.GLES20.glGenTextures; import android.annotation.TargetApi; import android.content.Context; -import android.content.pm.PackageManager; import android.graphics.SurfaceTexture; import android.graphics.SurfaceTexture.OnFrameAvailableListener; import android.opengl.EGL14; @@ -153,16 +152,9 @@ public final class DummySurface extends Surface { * * @param context Any {@link Context}. */ - @SuppressWarnings("unused") + @SuppressWarnings("unused") // Context may be needed in the future for better targeting. private static boolean deviceNeedsSecureDummySurfaceWorkaround(Context context) { - return (Util.SDK_INT == 24 && "samsung".equals(Util.MANUFACTURER)) - || (Util.SDK_INT >= 24 && Util.SDK_INT < 26 - && !hasVrModeHighPerformanceSystemFeatureV24(context.getPackageManager())); - } - - @TargetApi(24) - private static boolean hasVrModeHighPerformanceSystemFeatureV24(PackageManager packageManager) { - return packageManager.hasSystemFeature(PackageManager.FEATURE_VR_MODE_HIGH_PERFORMANCE); + return Util.SDK_INT == 24 && "samsung".equals(Util.MANUFACTURER); } private static class DummySurfaceThread extends HandlerThread implements OnFrameAvailableListener, From bab2ce817e76431243aab61cb40425d163c3141d Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 4 Sep 2017 06:47:03 -0700 Subject: [PATCH 0337/2472] Allow EXIF tracks to be exposed ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167493800 --- .../main/java/com/google/android/exoplayer2/util/MimeTypes.java | 1 + 1 file changed, 1 insertion(+) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java b/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java index 1c8bb62a75..2daf16d3d2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java @@ -85,6 +85,7 @@ public final class MimeTypes { 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"; + public static final String APPLICATION_EXIF = BASE_TYPE_APPLICATION + "/x-exif"; private MimeTypes() {} From 0d86f4475c4b30e5a1a0ac55a5479ca680fc2096 Mon Sep 17 00:00:00 2001 From: hoangtc Date: Mon, 4 Sep 2017 06:55:07 -0700 Subject: [PATCH 0338/2472] Remove the resampling to 16bit step from FlacDecoder. Currently FlacDecoder/FlacExtractor always perform resampling to 16bit. In some case (with 24bit audio), this might lower the audio quality if the system supports 24bit audio. Since AudioTrack implementation supports resampling, we will remove the resampling step, and return an output with the same bits-per-sample as the original stream. User can choose to re-sample to 16bit in AudioTrack if necessary. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167494350 --- extensions/flac/README.md | 2 +- .../exoplayer2/ext/flac/FlacExtractor.java | 20 ++- extensions/flac/src/main/jni/flac_parser.cc | 124 ++++++------------ .../flac/src/main/jni/include/flac_parser.h | 4 +- .../exoplayer2/util/FlacStreamInfo.java | 2 +- 5 files changed, 57 insertions(+), 95 deletions(-) diff --git a/extensions/flac/README.md b/extensions/flac/README.md index cd0f2efe47..fda5f0085d 100644 --- a/extensions/flac/README.md +++ b/extensions/flac/README.md @@ -38,7 +38,7 @@ NDK_PATH="" ``` cd "${FLAC_EXT_PATH}/jni" && \ -curl http://downloads.xiph.org/releases/flac/flac-1.3.1.tar.xz | tar xJ && \ +curl https://ftp.osuosl.org/pub/xiph/releases/flac/flac-1.3.1.tar.xz | tar xJ && \ mv flac-1.3.1 flac ``` diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java index 7b71b5c743..a2f141a712 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.ext.flac; +import static com.google.android.exoplayer2.util.Util.getPcmEncoding; + import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.Extractor; @@ -122,10 +124,20 @@ public final class FlacExtractor implements Extractor { } }); - - Format mediaFormat = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_RAW, null, - streamInfo.bitRate(), Format.NO_VALUE, streamInfo.channels, streamInfo.sampleRate, - C.ENCODING_PCM_16BIT, null, null, 0, null); + Format mediaFormat = + Format.createAudioSampleFormat( + null, + MimeTypes.AUDIO_RAW, + null, + streamInfo.bitRate(), + Format.NO_VALUE, + streamInfo.channels, + streamInfo.sampleRate, + getPcmEncoding(streamInfo.bitsPerSample), + null, + null, + 0, + null); trackOutput.format(mediaFormat); outputBuffer = new ParsableByteArray(streamInfo.maxDecodedFrameSize()); diff --git a/extensions/flac/src/main/jni/flac_parser.cc b/extensions/flac/src/main/jni/flac_parser.cc index 6c6e57f5f7..b9918e7871 100644 --- a/extensions/flac/src/main/jni/flac_parser.cc +++ b/extensions/flac/src/main/jni/flac_parser.cc @@ -42,6 +42,9 @@ #define CHECK(x) \ if (!(x)) ALOGE("Check failed: %s ", #x) +const int endian = 1; +#define isBigEndian() (*(reinterpret_cast(&endian)) == 0) + // The FLAC parser calls our C++ static callbacks using C calling conventions, // inside FLAC__stream_decoder_process_until_end_of_metadata // and FLAC__stream_decoder_process_single. @@ -180,85 +183,42 @@ void FLACParser::errorCallback(FLAC__StreamDecoderErrorStatus status) { mErrorStatus = status; } -// Copy samples from FLAC native 32-bit non-interleaved to 16-bit interleaved. +// Copy samples from FLAC native 32-bit non-interleaved to +// correct bit-depth (non-zero padded), interleaved. // These are candidates for optimization if needed. - -static void copyMono8(int16_t *dst, const int *const *src, unsigned nSamples, - unsigned /* nChannels */) { - for (unsigned i = 0; i < nSamples; ++i) { - *dst++ = src[0][i] << 8; - } -} - -static void copyStereo8(int16_t *dst, const int *const *src, unsigned nSamples, - unsigned /* nChannels */) { - for (unsigned i = 0; i < nSamples; ++i) { - *dst++ = src[0][i] << 8; - *dst++ = src[1][i] << 8; - } -} - -static void copyMultiCh8(int16_t *dst, const int *const *src, unsigned nSamples, - unsigned nChannels) { +static void copyToByteArrayBigEndian(int8_t *dst, const int *const *src, + unsigned bytesPerSample, unsigned nSamples, + unsigned nChannels) { for (unsigned i = 0; i < nSamples; ++i) { for (unsigned c = 0; c < nChannels; ++c) { - *dst++ = src[c][i] << 8; + // point to the first byte of the source address + // and then skip the first few bytes (most significant bytes) + // depending on the bit depth + const int8_t *byteSrc = + reinterpret_cast(&src[c][i]) + 4 - bytesPerSample; + memcpy(dst, byteSrc, bytesPerSample); + dst = dst + bytesPerSample; } } } -static void copyMono16(int16_t *dst, const int *const *src, unsigned nSamples, - unsigned /* nChannels */) { +static void copyToByteArrayLittleEndian(int8_t *dst, const int *const *src, + unsigned bytesPerSample, + unsigned nSamples, unsigned nChannels) { for (unsigned i = 0; i < nSamples; ++i) { - *dst++ = src[0][i]; + for (unsigned c = 0; c < nChannels; ++c) { + // with little endian, the most significant bytes will be at the end + // copy the bytes in little endian will remove the most significant byte + // so we are good here. + memcpy(dst, &(src[c][i]), bytesPerSample); + dst = dst + bytesPerSample; + } } } -static void copyStereo16(int16_t *dst, const int *const *src, unsigned nSamples, +static void copyTrespass(int8_t * /* dst */, const int *const * /* src */, + unsigned /* bytesPerSample */, unsigned /* nSamples */, unsigned /* nChannels */) { - for (unsigned i = 0; i < nSamples; ++i) { - *dst++ = src[0][i]; - *dst++ = src[1][i]; - } -} - -static void copyMultiCh16(int16_t *dst, const int *const *src, - unsigned nSamples, unsigned nChannels) { - for (unsigned i = 0; i < nSamples; ++i) { - for (unsigned c = 0; c < nChannels; ++c) { - *dst++ = src[c][i]; - } - } -} - -// 24-bit versions should do dithering or noise-shaping, here or in AudioFlinger - -static void copyMono24(int16_t *dst, const int *const *src, unsigned nSamples, - unsigned /* nChannels */) { - for (unsigned i = 0; i < nSamples; ++i) { - *dst++ = src[0][i] >> 8; - } -} - -static void copyStereo24(int16_t *dst, const int *const *src, unsigned nSamples, - unsigned /* nChannels */) { - for (unsigned i = 0; i < nSamples; ++i) { - *dst++ = src[0][i] >> 8; - *dst++ = src[1][i] >> 8; - } -} - -static void copyMultiCh24(int16_t *dst, const int *const *src, - unsigned nSamples, unsigned nChannels) { - for (unsigned i = 0; i < nSamples; ++i) { - for (unsigned c = 0; c < nChannels; ++c) { - *dst++ = src[c][i] >> 8; - } - } -} - -static void copyTrespass(int16_t * /* dst */, const int *const * /* src */, - unsigned /* nSamples */, unsigned /* nChannels */) { TRESPASS(); } @@ -340,6 +300,7 @@ bool FLACParser::decodeMetadata() { case 8: case 16: case 24: + case 32: break; default: ALOGE("unsupported bits per sample %u", getBitsPerSample()); @@ -363,23 +324,11 @@ bool FLACParser::decodeMetadata() { ALOGE("unsupported sample rate %u", getSampleRate()); return false; } - // configure the appropriate copy function, defaulting to trespass - static const struct { - unsigned mChannels; - unsigned mBitsPerSample; - void (*mCopy)(int16_t *dst, const int *const *src, unsigned nSamples, - unsigned nChannels); - } table[] = { - {1, 8, copyMono8}, {2, 8, copyStereo8}, {8, 8, copyMultiCh8}, - {1, 16, copyMono16}, {2, 16, copyStereo16}, {8, 16, copyMultiCh16}, - {1, 24, copyMono24}, {2, 24, copyStereo24}, {8, 24, copyMultiCh24}, - }; - for (unsigned i = 0; i < sizeof(table) / sizeof(table[0]); ++i) { - if (table[i].mChannels >= getChannels() && - table[i].mBitsPerSample == getBitsPerSample()) { - mCopy = table[i].mCopy; - break; - } + // configure the appropriate copy function based on device endianness. + if (isBigEndian()) { + mCopy = copyToByteArrayBigEndian; + } else { + mCopy = copyToByteArrayLittleEndian; } } else { ALOGE("missing STREAMINFO"); @@ -424,7 +373,8 @@ size_t FLACParser::readBuffer(void *output, size_t output_size) { return -1; } - size_t bufferSize = blocksize * getChannels() * sizeof(int16_t); + unsigned bytesPerSample = getBitsPerSample() >> 3; + size_t bufferSize = blocksize * getChannels() * bytesPerSample; if (bufferSize > output_size) { ALOGE( "FLACParser::readBuffer not enough space in output buffer " @@ -434,8 +384,8 @@ size_t FLACParser::readBuffer(void *output, size_t output_size) { } // copy PCM from FLAC write buffer to our media buffer, with interleaving. - (*mCopy)(reinterpret_cast(output), mWriteBuffer, blocksize, - getChannels()); + (*mCopy)(reinterpret_cast(output), mWriteBuffer, bytesPerSample, + blocksize, getChannels()); // fill in buffer metadata CHECK(mWriteHeader.number_type == FLAC__FRAME_NUMBER_TYPE_SAMPLE_NUMBER); diff --git a/extensions/flac/src/main/jni/include/flac_parser.h b/extensions/flac/src/main/jni/include/flac_parser.h index 8c302adb36..8a769b66d4 100644 --- a/extensions/flac/src/main/jni/include/flac_parser.h +++ b/extensions/flac/src/main/jni/include/flac_parser.h @@ -86,8 +86,8 @@ class FLACParser { private: DataSource *mDataSource; - void (*mCopy)(int16_t *dst, const int *const *src, unsigned nSamples, - unsigned nChannels); + void (*mCopy)(int8_t *dst, const int *const *src, unsigned bytesPerSample, + unsigned nSamples, unsigned nChannels); // handle to underlying libFLAC parser FLAC__StreamDecoder *mDecoder; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamInfo.java index 6382f1130e..b08f4a31e3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/FlacStreamInfo.java @@ -65,7 +65,7 @@ public final class FlacStreamInfo { } public int maxDecodedFrameSize() { - return maxBlockSize * channels * 2; + return maxBlockSize * channels * (bitsPerSample / 8); } public int bitRate() { From c9f31a41cd45bfe0a522ee77dfbe1af8ffd97442 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 4 Sep 2017 07:28:24 -0700 Subject: [PATCH 0339/2472] Adding missing license header in IMA build.gradle ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167496569 --- extensions/ima/build.gradle | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/extensions/ima/build.gradle b/extensions/ima/build.gradle index c084ec6bf8..cb69e92990 100644 --- a/extensions/ima/build.gradle +++ b/extensions/ima/build.gradle @@ -1,3 +1,16 @@ +// 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 from: '../../constants.gradle' apply plugin: 'com.android.library' @@ -6,7 +19,7 @@ android { buildToolsVersion project.ext.buildToolsVersion defaultConfig { - minSdkVersion 14 + minSdkVersion project.ext.minSdkVersion targetSdkVersion project.ext.targetSdkVersion testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } From 472df08f0837ebbdcd0b1c7e8269ad816b13269b Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 4 Sep 2017 08:55:20 -0700 Subject: [PATCH 0340/2472] Additional secure DummySurface device exclusions Merge: https://github.com/google/ExoPlayer/pull/3225 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167502127 --- .../google/android/exoplayer2/video/DummySurface.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java index a1820ed7a1..d623ea33ea 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java @@ -43,6 +43,7 @@ import static android.opengl.GLES20.glGenTextures; import android.annotation.TargetApi; import android.content.Context; +import android.content.pm.PackageManager; import android.graphics.SurfaceTexture; import android.graphics.SurfaceTexture.OnFrameAvailableListener; import android.opengl.EGL14; @@ -152,9 +153,15 @@ public final class DummySurface extends Surface { * * @param context Any {@link Context}. */ - @SuppressWarnings("unused") // Context may be needed in the future for better targeting. private static boolean deviceNeedsSecureDummySurfaceWorkaround(Context context) { - return Util.SDK_INT == 24 && "samsung".equals(Util.MANUFACTURER); + return (Util.SDK_INT == 24 && "samsung".equals(Util.MANUFACTURER)) + || (Util.SDK_INT < 26 + && !hasVrModeHighPerformanceSystemFeatureV24(context.getPackageManager())); + } + + @TargetApi(24) + private static boolean hasVrModeHighPerformanceSystemFeatureV24(PackageManager packageManager) { + return packageManager.hasSystemFeature(PackageManager.FEATURE_VR_MODE_HIGH_PERFORMANCE); } private static class DummySurfaceThread extends HandlerThread implements OnFrameAvailableListener, From a0df5bb50a817bf99c335be2479ab9293ec735bf Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 4 Sep 2017 09:26:59 -0700 Subject: [PATCH 0341/2472] Be robust against unexpected EOS in WebvttCueParser Issue: #3228 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167504122 --- .../text/webvtt/WebvttCueParser.java | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/library/core/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 index 30c9c8737e..54af4dbf63 100644 --- a/library/core/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 @@ -21,6 +21,7 @@ import android.text.Layout.Alignment; import android.text.Spannable; import android.text.SpannableStringBuilder; import android.text.Spanned; +import android.text.TextUtils; import android.text.style.AbsoluteSizeSpan; import android.text.style.AlignmentSpan; import android.text.style.BackgroundColorSpan; @@ -92,19 +93,24 @@ import java.util.regex.Pattern; /* package */ boolean parseCue(ParsableByteArray webvttData, WebvttCue.Builder builder, List styles) { String firstLine = webvttData.readLine(); + if (firstLine == null) { + return false; + } Matcher cueHeaderMatcher = WebvttCueParser.CUE_HEADER_PATTERN.matcher(firstLine); if (cueHeaderMatcher.matches()) { // We have found the timestamps in the first line. No id present. return parseCue(null, cueHeaderMatcher, webvttData, builder, textBuilder, styles); - } else { - // The first line is not the timestamps, but could be the cue id. - String secondLine = webvttData.readLine(); - cueHeaderMatcher = WebvttCueParser.CUE_HEADER_PATTERN.matcher(secondLine); - if (cueHeaderMatcher.matches()) { - // We can do the rest of the parsing, including the id. - return parseCue(firstLine.trim(), cueHeaderMatcher, webvttData, builder, textBuilder, - styles); - } + } + // The first line is not the timestamps, but could be the cue id. + String secondLine = webvttData.readLine(); + if (secondLine == null) { + return false; + } + cueHeaderMatcher = WebvttCueParser.CUE_HEADER_PATTERN.matcher(secondLine); + if (cueHeaderMatcher.matches()) { + // We can do the rest of the parsing, including the id. + return parseCue(firstLine.trim(), cueHeaderMatcher, webvttData, builder, textBuilder, + styles); } return false; } @@ -233,7 +239,7 @@ import java.util.regex.Pattern; // Parse the cue text. textBuilder.setLength(0); String line; - while ((line = webvttData.readLine()) != null && !line.isEmpty()) { + while (!TextUtils.isEmpty(line = webvttData.readLine())) { if (textBuilder.length() > 0) { textBuilder.append("\n"); } From bec5e6e2b2b59290963f7a22a6d005c8dac467e5 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 4 Sep 2017 09:57:56 -0700 Subject: [PATCH 0342/2472] Rewrite logic for enabling secure DummySurface Issue: #3215 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167505797 --- .../exoplayer2/video/DummySurface.java | 35 ++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java index d623ea33ea..20fe862dd2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java @@ -90,12 +90,7 @@ public final class DummySurface extends Surface { */ public static synchronized boolean isSecureSupported(Context context) { if (!secureSupportedInitialized) { - if (Util.SDK_INT >= 17) { - EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY); - String extensions = EGL14.eglQueryString(display, EGL10.EGL_EXTENSIONS); - secureSupported = extensions != null && extensions.contains("EGL_EXT_protected_content") - && !deviceNeedsSecureDummySurfaceWorkaround(context); - } + secureSupported = Util.SDK_INT >= 24 && enableSecureDummySurfaceV24(context); secureSupportedInitialized = true; } return secureSupported; @@ -148,20 +143,28 @@ public final class DummySurface extends Surface { } /** - * Returns whether the device is known to advertise secure surface textures but not implement them - * correctly. + * Returns whether use of secure dummy surfaces should be enabled. * * @param context Any {@link Context}. */ - private static boolean deviceNeedsSecureDummySurfaceWorkaround(Context context) { - return (Util.SDK_INT == 24 && "samsung".equals(Util.MANUFACTURER)) - || (Util.SDK_INT < 26 - && !hasVrModeHighPerformanceSystemFeatureV24(context.getPackageManager())); - } - @TargetApi(24) - private static boolean hasVrModeHighPerformanceSystemFeatureV24(PackageManager packageManager) { - return packageManager.hasSystemFeature(PackageManager.FEATURE_VR_MODE_HIGH_PERFORMANCE); + private static boolean enableSecureDummySurfaceV24(Context context) { + EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY); + String eglExtensions = EGL14.eglQueryString(display, EGL10.EGL_EXTENSIONS); + if (eglExtensions == null || !eglExtensions.contains("EGL_EXT_protected_content")) { + // EGL_EXT_protected_content is required to enable secure dummy surfaces. + return false; + } + if (Util.SDK_INT == 24 && "samsung".equals(Util.MANUFACTURER)) { + // Samsung devices running API level 24 are known to be broken [Internal: b/37197802]. + return false; + } + if (Util.SDK_INT < 26 && !context.getPackageManager().hasSystemFeature( + PackageManager.FEATURE_VR_MODE_HIGH_PERFORMANCE)) { + // Pre API level 26 devices were not well tested unless they supported VR mode. + return false; + } + return true; } private static class DummySurfaceThread extends HandlerThread implements OnFrameAvailableListener, From 264f1c903def8a4f8ec7b65d52478470fc8a6358 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 5 Sep 2017 05:23:22 -0700 Subject: [PATCH 0343/2472] Fix bug in FakeChunkSource. In order to retrieve the data set, the track selection index was used, but the data set is actually indexed by track group indices. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167566049 --- .../google/android/exoplayer2/testutil/FakeChunkSource.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeChunkSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeChunkSource.java index b8f25bfbce..41fda178d7 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeChunkSource.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeChunkSource.java @@ -92,7 +92,8 @@ public final class FakeChunkSource implements ChunkSource { Format selectedFormat = trackSelection.getSelectedFormat(); long startTimeUs = dataSet.getStartTime(chunkIndex); long endTimeUs = startTimeUs + dataSet.getChunkDuration(chunkIndex); - String uri = dataSet.getUri(trackSelection.getSelectedIndex()); + int trackGroupIndex = trackSelection.getIndexInTrackGroup(trackSelection.getSelectedIndex()); + String uri = dataSet.getUri(trackGroupIndex); Segment fakeDataChunk = dataSet.getData(uri).getSegments().get(chunkIndex); DataSpec dataSpec = new DataSpec(Uri.parse(uri), fakeDataChunk.byteOffset, fakeDataChunk.length, null); From 2f4a3fe1f3a95ed9a8029c6e3d21e537852bc271 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 5 Sep 2017 05:50:43 -0700 Subject: [PATCH 0344/2472] Add postDelayed operation to Clock interface. The default implementation is just calling through to handler.postDelayed, while the fake clock uses its internal time value to trigger the handler calls at the correct time. This is useful to apply a fake clock in situations where a handler is used to post delayed messages. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167567914 --- .../google/android/exoplayer2/util/Clock.java | 12 +++++++ .../android/exoplayer2/util/SystemClock.java | 7 ++++ .../exoplayer2/testutil/FakeClock.java | 34 ++++++++++++++++++- 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Clock.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Clock.java index f8d5759c2c..9619ed53ea 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Clock.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Clock.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.util; +import android.os.Handler; + /** * An interface through which system clocks can be read. The {@link #DEFAULT} implementation * must be used for all non-test cases. @@ -36,4 +38,14 @@ public interface Clock { */ void sleep(long sleepTimeMs); + /** + * Post a {@link Runnable} on a {@link Handler} thread with a delay measured by this clock. + * @see Handler#postDelayed(Runnable, long) + * + * @param handler The {@link Handler} to post the {@code runnable} on. + * @param runnable A {@link Runnable} to be posted. + * @param delayMs The delay in milliseconds as measured by this clock. + */ + void postDelayed(Handler handler, Runnable runnable, long delayMs); + } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/SystemClock.java b/library/core/src/main/java/com/google/android/exoplayer2/util/SystemClock.java index 1f937b721b..272c3f43ec 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/SystemClock.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/SystemClock.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.util; +import android.os.Handler; + /** * The standard implementation of {@link Clock}. */ @@ -30,4 +32,9 @@ package com.google.android.exoplayer2.util; android.os.SystemClock.sleep(sleepTimeMs); } + @Override + public void postDelayed(Handler handler, Runnable runnable, long delayMs) { + handler.postDelayed(runnable, delayMs); + } + } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeClock.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeClock.java index 36ce4b5c3e..843e5858d8 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeClock.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeClock.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.testutil; +import android.os.Handler; import com.google.android.exoplayer2.util.Clock; import java.util.ArrayList; import java.util.List; @@ -26,6 +27,7 @@ public final class FakeClock implements Clock { private long currentTimeMs; private final List wakeUpTimes; + private final List handlerPosts; /** * Create {@link FakeClock} with an arbitrary initial timestamp. @@ -35,6 +37,7 @@ public final class FakeClock implements Clock { public FakeClock(long initialTimeMs) { this.currentTimeMs = initialTimeMs; this.wakeUpTimes = new ArrayList<>(); + this.handlerPosts = new ArrayList<>(); } /** @@ -50,10 +53,16 @@ public final class FakeClock implements Clock { break; } } + for (int i = handlerPosts.size() - 1; i >= 0; i--) { + if (handlerPosts.get(i).postTime <= currentTimeMs) { + HandlerPostData postData = handlerPosts.remove(i); + postData.handler.post(postData.runnable); + } + } } @Override - public long elapsedRealtime() { + public synchronized long elapsedRealtime() { return currentTimeMs; } @@ -74,5 +83,28 @@ public final class FakeClock implements Clock { wakeUpTimes.remove(wakeUpTimeMs); } + @Override + public synchronized void postDelayed(Handler handler, Runnable runnable, long delayMs) { + if (delayMs <= 0) { + handler.post(runnable); + } else { + handlerPosts.add(new HandlerPostData(currentTimeMs + delayMs, handler, runnable)); + } + } + + private static final class HandlerPostData { + + public final long postTime; + public final Handler handler; + public final Runnable runnable; + + public HandlerPostData(long postTime, Handler handler, Runnable runnable) { + this.postTime = postTime; + this.handler = handler; + this.runnable = runnable; + } + + } + } From a90cef0cb5e581757b43264e923e06653cda183e Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 5 Sep 2017 08:03:07 -0700 Subject: [PATCH 0345/2472] Upgrade gradle plugin / wrapper ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167579719 --- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index d5cc64baa1..8ec24a6e82 100644 --- a/build.gradle +++ b/build.gradle @@ -19,7 +19,7 @@ buildscript { } } dependencies { - classpath 'com.android.tools.build:gradle:2.3.3' + classpath 'com.android.tools.build:gradle:3.0.0-beta4' classpath 'com.novoda:bintray-release:0.5.0' } // Workaround for the following test coverage issue. Remove when fixed: diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index fc42154505..32ec7e3327 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Wed Jul 12 10:31:13 BST 2017 +#Tue Sep 05 13:43:42 BST 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.0.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip From 0183a83047360f3bf2c60b2180ce356c75b454da Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 5 Sep 2017 08:17:44 -0700 Subject: [PATCH 0346/2472] Don't use MediaCodec.setOutputSurface on Nexus 7 with qcom decoder Issue: #3236 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167581198 --- .../video/MediaCodecVideoRenderer.java | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index f70d74e413..d72619dbe1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -78,6 +78,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { private Format[] streamFormats; private CodecMaxValues codecMaxValues; + private boolean codecNeedsSetOutputSurfaceWorkaround; private Surface surface; private Surface dummySurface; @@ -360,7 +361,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { int state = getState(); if (state == STATE_ENABLED || state == STATE_STARTED) { MediaCodec codec = getCodec(); - if (Util.SDK_INT >= 23 && codec != null && surface != null) { + if (Util.SDK_INT >= 23 && codec != null && surface != null + && !codecNeedsSetOutputSurfaceWorkaround) { setOutputSurfaceV23(codec, surface); } else { releaseCodec(); @@ -431,6 +433,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { protected void onCodecInitialized(String name, long initializedTimestampMs, long initializationDurationMs) { eventDispatcher.decoderInitialized(name, initializedTimestampMs, initializationDurationMs); + codecNeedsSetOutputSurfaceWorkaround = codecNeedsSetOutputSurfaceWorkaround(name); } @Override @@ -969,6 +972,18 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { return Util.SDK_INT <= 22 && "foster".equals(Util.DEVICE) && "NVIDIA".equals(Util.MANUFACTURER); } + /** + * Returns whether the device is known to implement {@link MediaCodec#setOutputSurface(Surface)} + * incorrectly. + *

      + * If true is returned then we fall back to releasing and re-instantiating the codec instead. + */ + private static boolean codecNeedsSetOutputSurfaceWorkaround(String name) { + // Work around https://github.com/google/ExoPlayer/issues/3236 + return ("deb".equals(Util.DEVICE) || "flo".equals(Util.DEVICE)) + && "OMX.qcom.video.decoder.avc".equals(name); + } + /** * Returns whether a codec with suitable {@link CodecMaxValues} will support adaptation between * two {@link Format}s. From 8413dab9dec518a3311762747084fac83d6e03c4 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 5 Sep 2017 08:32:56 -0700 Subject: [PATCH 0347/2472] Use clock in action schedule to handle delays. This allows to use a fake clock and an action schedule with timed delays together. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167582860 --- .../exoplayer2/testutil/ActionSchedule.java | 33 ++++++++++++++----- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java index c9ae02c957..28e62f3057 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java @@ -36,6 +36,7 @@ import com.google.android.exoplayer2.testutil.Action.Stop; import com.google.android.exoplayer2.testutil.Action.WaitForPositionDiscontinuity; import com.google.android.exoplayer2.testutil.Action.WaitForTimelineChanged; import com.google.android.exoplayer2.trackselection.MappingTrackSelector; +import com.google.android.exoplayer2.util.Clock; /** * Schedules a sequence of {@link Action}s for execution during a test. @@ -70,17 +71,27 @@ public final class ActionSchedule { public static final class Builder { private final String tag; + private final Clock clock; private final ActionNode rootNode; - private long currentDelayMs; + private long currentDelayMs; private ActionNode previousNode; /** * @param tag A tag to use for logging. */ public Builder(String tag) { + this(tag, Clock.DEFAULT); + } + + /** + * @param tag A tag to use for logging. + * @param clock A clock to use for measuring delays. + */ + public Builder(String tag, Clock clock) { this.tag = tag; - rootNode = new ActionNode(new RootAction(tag), 0); + this.clock = clock; + rootNode = new ActionNode(new RootAction(tag), clock, 0); previousNode = rootNode; } @@ -102,7 +113,7 @@ public final class ActionSchedule { * @return The builder, for convenience. */ public Builder apply(Action action) { - return appendActionNode(new ActionNode(action, currentDelayMs)); + return appendActionNode(new ActionNode(action, clock, currentDelayMs)); } /** @@ -113,7 +124,7 @@ public final class ActionSchedule { * @return The builder, for convenience. */ public Builder repeat(Action action, long intervalMs) { - return appendActionNode(new ActionNode(action, currentDelayMs, intervalMs)); + return appendActionNode(new ActionNode(action, clock, currentDelayMs, intervalMs)); } /** @@ -274,6 +285,7 @@ public final class ActionSchedule { /* package */ static final class ActionNode implements Runnable { private final Action action; + private final Clock clock; private final long delayMs; private final long repeatIntervalMs; @@ -286,20 +298,23 @@ public final class ActionSchedule { /** * @param action The wrapped action. + * @param clock The clock to use for measuring the delay. * @param delayMs The delay between the node being scheduled and the action being executed. */ - public ActionNode(Action action, long delayMs) { - this(action, delayMs, C.TIME_UNSET); + public ActionNode(Action action, Clock clock, long delayMs) { + this(action, clock, delayMs, C.TIME_UNSET); } /** * @param action The wrapped action. + * @param clock The clock to use for measuring the delay. * @param delayMs The delay between the node being scheduled and the action being executed. * @param repeatIntervalMs The interval between one execution and the next repetition. If set to * {@link C#TIME_UNSET}, the action is executed once only. */ - public ActionNode(Action action, long delayMs, long repeatIntervalMs) { + public ActionNode(Action action, Clock clock, long delayMs, long repeatIntervalMs) { this.action = action; + this.clock = clock; this.delayMs = delayMs; this.repeatIntervalMs = repeatIntervalMs; } @@ -328,14 +343,14 @@ public final class ActionSchedule { this.trackSelector = trackSelector; this.surface = surface; this.mainHandler = mainHandler; - mainHandler.postDelayed(this, delayMs); + clock.postDelayed(mainHandler, this, delayMs); } @Override public void run() { action.doActionAndScheduleNext(player, trackSelector, surface, mainHandler, next); if (repeatIntervalMs != C.TIME_UNSET) { - mainHandler.postDelayed(this, repeatIntervalMs); + clock.postDelayed(mainHandler, this, repeatIntervalMs); } } From e16610a82c696b6121301c792c7f04d5e36a37c7 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 5 Sep 2017 08:47:02 -0700 Subject: [PATCH 0348/2472] Fix import formatting ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167584287 --- .../exoplayer2/ext/mediasession/DefaultPlaybackController.java | 1 - .../exoplayer2/ext/mediasession/MediaSessionConnector.java | 1 - .../exoplayer2/ext/mediasession/RepeatModeActionProvider.java | 3 ++- .../exoplayer2/ext/mediasession/TimelineQueueNavigator.java | 1 - 4 files changed, 2 insertions(+), 4 deletions(-) diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/DefaultPlaybackController.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/DefaultPlaybackController.java index 95ebcde095..dcb6f8ca10 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/DefaultPlaybackController.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/DefaultPlaybackController.java @@ -18,7 +18,6 @@ package com.google.android.exoplayer2.ext.mediasession; import android.os.Bundle; import android.os.ResultReceiver; import android.support.v4.media.session.PlaybackStateCompat; - import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.util.RepeatModeUtil; diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java index 4c7ad123f3..7304d9cdb6 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java @@ -40,7 +40,6 @@ import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.util.ErrorMessageProvider; import com.google.android.exoplayer2.util.RepeatModeUtil; - import java.util.Collections; import java.util.HashMap; import java.util.List; diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java index b4cb3c73d0..1db5889e00 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/RepeatModeActionProvider.java @@ -1,4 +1,3 @@ -package com.google.android.exoplayer2.ext.mediasession; /* * Copyright (c) 2017 The Android Open Source Project * @@ -14,6 +13,8 @@ package com.google.android.exoplayer2.ext.mediasession; * See the License for the specific language governing permissions and * limitations under the License. */ +package com.google.android.exoplayer2.ext.mediasession; + import android.content.Context; import android.os.Bundle; import android.support.v4.media.session.PlaybackStateCompat; diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java index 0484c0b641..1b9bd3ecd9 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/TimelineQueueNavigator.java @@ -25,7 +25,6 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.util.Util; - import java.util.ArrayList; import java.util.Collections; import java.util.List; From b62eab63a4d13945d07431be3d30628392646260 Mon Sep 17 00:00:00 2001 From: zhihuichen Date: Tue, 5 Sep 2017 11:06:03 -0700 Subject: [PATCH 0349/2472] Implement multi session to support DRM key rotation. Spec: https://storage.googleapis.com/wvdocs/Widevine_DRM_Android_Using_Key_Rotation.pdf 1. Implement multisession to support drm key rotation 2. Put MediaDrmEventListener back to manager since this is a per mediaDrm thing. 3. It seems diffrenciate between single/multi session is unnecessary. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167602965 --- .../exoplayer2/demo/PlayerActivity.java | 9 +- .../demo/SampleChooserActivity.java | 63 ++++-- .../exoplayer2/drm/DefaultDrmSession.java | 194 ++++++++---------- .../drm/DefaultDrmSessionManager.java | 185 +++++++++++++++-- 4 files changed, 301 insertions(+), 150 deletions(-) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index b2750a93bb..c2c4df9ea8 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -89,6 +89,7 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi public static final String DRM_SCHEME_UUID_EXTRA = "drm_scheme_uuid"; public static final String DRM_LICENSE_URL = "drm_license_url"; public static final String DRM_KEY_REQUEST_PROPERTIES = "drm_key_request_properties"; + public static final String DRM_MULTI_SESSION = "drm_multi_session"; public static final String PREFER_EXTENSION_DECODERS = "prefer_extension_decoders"; public static final String ACTION_VIEW = "com.google.android.exoplayer.demo.action.VIEW"; @@ -264,13 +265,14 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi if (drmSchemeUuid != null) { String drmLicenseUrl = intent.getStringExtra(DRM_LICENSE_URL); String[] keyRequestPropertiesArray = intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES); + boolean multiSession = intent.getBooleanExtra(DRM_MULTI_SESSION, false); int errorStringId = R.string.error_drm_unknown; if (Util.SDK_INT < 18) { errorStringId = R.string.error_drm_not_supported; } else { try { drmSessionManager = buildDrmSessionManagerV18(drmSchemeUuid, drmLicenseUrl, - keyRequestPropertiesArray); + keyRequestPropertiesArray, multiSession); } catch (UnsupportedDrmException e) { errorStringId = e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME ? R.string.error_drm_unsupported_scheme : R.string.error_drm_unknown; @@ -379,7 +381,8 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi } private DrmSessionManager buildDrmSessionManagerV18(UUID uuid, - String licenseUrl, String[] keyRequestPropertiesArray) throws UnsupportedDrmException { + String licenseUrl, String[] keyRequestPropertiesArray, boolean multiSession) + throws UnsupportedDrmException { HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(licenseUrl, buildHttpDataSourceFactory(false)); if (keyRequestPropertiesArray != null) { @@ -389,7 +392,7 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi } } return new DefaultDrmSessionManager<>(uuid, FrameworkMediaDrm.newInstance(uuid), drmCallback, - null, mainHandler, eventLogger); + null, mainHandler, eventLogger, multiSession); } private void releasePlayer() { diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java index c0edb1d1b8..1f84b1f29c 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java @@ -182,6 +182,7 @@ public class SampleChooserActivity extends Activity { UUID drmUuid = null; String drmLicenseUrl = null; String[] drmKeyRequestProperties = null; + boolean drmMultiSession = false; boolean preferExtensionDecoders = false; ArrayList playlistSamples = null; String adTagUri = null; @@ -220,6 +221,9 @@ public class SampleChooserActivity extends Activity { reader.endObject(); drmKeyRequestProperties = drmKeyRequestPropertiesList.toArray(new String[0]); break; + case "drm_multi_session": + drmMultiSession = reader.nextBoolean(); + break; case "prefer_extension_decoders": Assertions.checkState(!insidePlaylist, "Invalid attribute on nested item: prefer_extension_decoders"); @@ -242,15 +246,16 @@ public class SampleChooserActivity extends Activity { } } reader.endObject(); - + DrmInfo drmInfo = drmUuid == null ? null : new DrmInfo(drmUuid, drmLicenseUrl, + drmKeyRequestProperties, drmMultiSession); if (playlistSamples != null) { UriSample[] playlistSamplesArray = playlistSamples.toArray( new UriSample[playlistSamples.size()]); - return new PlaylistSample(sampleName, drmUuid, drmLicenseUrl, drmKeyRequestProperties, - preferExtensionDecoders, playlistSamplesArray); + return new PlaylistSample(sampleName, preferExtensionDecoders, drmInfo, + playlistSamplesArray); } else { - return new UriSample(sampleName, drmUuid, drmLicenseUrl, drmKeyRequestProperties, - preferExtensionDecoders, uri, extension, adTagUri); + return new UriSample(sampleName, preferExtensionDecoders, drmInfo, uri, extension, + adTagUri); } } @@ -372,31 +377,47 @@ public class SampleChooserActivity extends Activity { } - private abstract static class Sample { - - public final String name; - public final boolean preferExtensionDecoders; + private static final class DrmInfo { public final UUID drmSchemeUuid; public final String drmLicenseUrl; public final String[] drmKeyRequestProperties; + public final boolean drmMultiSession; - public Sample(String name, UUID drmSchemeUuid, String drmLicenseUrl, - String[] drmKeyRequestProperties, boolean preferExtensionDecoders) { - this.name = name; + public DrmInfo(UUID drmSchemeUuid, String drmLicenseUrl, + String[] drmKeyRequestProperties, boolean drmMultiSession) { this.drmSchemeUuid = drmSchemeUuid; this.drmLicenseUrl = drmLicenseUrl; this.drmKeyRequestProperties = drmKeyRequestProperties; + this.drmMultiSession = drmMultiSession; + } + + public void updateIntent(Intent intent) { + Assertions.checkNotNull(intent); + intent.putExtra(PlayerActivity.DRM_SCHEME_UUID_EXTRA, drmSchemeUuid.toString()); + intent.putExtra(PlayerActivity.DRM_LICENSE_URL, drmLicenseUrl); + intent.putExtra(PlayerActivity.DRM_KEY_REQUEST_PROPERTIES, drmKeyRequestProperties); + intent.putExtra(PlayerActivity.DRM_MULTI_SESSION, drmMultiSession); + } + } + + private abstract static class Sample { + public final String name; + public final boolean preferExtensionDecoders; + public final DrmInfo drmInfo; + + public Sample(String name, boolean preferExtensionDecoders, DrmInfo drmInfo) { + this.name = name; this.preferExtensionDecoders = preferExtensionDecoders; + this.drmInfo = drmInfo; } public Intent buildIntent(Context context) { Intent intent = new Intent(context, PlayerActivity.class); intent.putExtra(PlayerActivity.PREFER_EXTENSION_DECODERS, preferExtensionDecoders); - if (drmSchemeUuid != null) { - intent.putExtra(PlayerActivity.DRM_SCHEME_UUID_EXTRA, drmSchemeUuid.toString()); - intent.putExtra(PlayerActivity.DRM_LICENSE_URL, drmLicenseUrl); - intent.putExtra(PlayerActivity.DRM_KEY_REQUEST_PROPERTIES, drmKeyRequestProperties); + if (drmInfo != null) { + drmInfo.updateIntent(intent); } + return intent; } @@ -408,10 +429,9 @@ public class SampleChooserActivity extends Activity { public final String extension; public final String adTagUri; - public UriSample(String name, UUID drmSchemeUuid, String drmLicenseUrl, - String[] drmKeyRequestProperties, boolean preferExtensionDecoders, String uri, + public UriSample(String name, boolean preferExtensionDecoders, DrmInfo drmInfo, String uri, String extension, String adTagUri) { - super(name, drmSchemeUuid, drmLicenseUrl, drmKeyRequestProperties, preferExtensionDecoders); + super(name, preferExtensionDecoders, drmInfo); this.uri = uri; this.extension = extension; this.adTagUri = adTagUri; @@ -432,10 +452,9 @@ public class SampleChooserActivity extends Activity { public final UriSample[] children; - public PlaylistSample(String name, UUID drmSchemeUuid, String drmLicenseUrl, - String[] drmKeyRequestProperties, boolean preferExtensionDecoders, + public PlaylistSample(String name, boolean preferExtensionDecoders, DrmInfo drmInfo, UriSample... children) { - super(name, drmSchemeUuid, drmLicenseUrl, drmKeyRequestProperties, preferExtensionDecoders); + super(name, preferExtensionDecoders, drmInfo); this.children = children; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java index b4dab7b971..d6776c8ed0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java @@ -27,29 +27,36 @@ import android.os.Message; import android.util.Log; import android.util.Pair; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; import com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest; -import com.google.android.exoplayer2.drm.ExoMediaDrm.OnEventListener; import com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest; -import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil; -import com.google.android.exoplayer2.util.MimeTypes; -import com.google.android.exoplayer2.util.Util; +import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.UUID; +import java.util.concurrent.atomic.AtomicBoolean; /** * A {@link DrmSession} that supports playbacks using {@link MediaDrm}. */ @TargetApi(18) /* package */ class DefaultDrmSession implements DrmSession { - private static final String TAG = "DefaultDrmSession"; - private static final String CENC_SCHEME_MIME_TYPE = "cenc"; + /** + * Listener of {@link DefaultDrmSession} events. + */ + public interface EventListener { + + /** + * Called each time provision is completed. + */ + void onProvisionCompleted(); + + } + + private static final String TAG = "DefaultDrmSession"; private static final int MSG_PROVISION = 0; private static final int MSG_KEYS = 1; - private static final int MAX_LICENSE_DURATION_TO_RENEW = 60; private final Handler eventHandler; @@ -58,7 +65,6 @@ import java.util.UUID; private final HashMap optionalKeyRequestParameters; /* package */ final MediaDrmCallback callback; /* package */ final UUID uuid; - /* package */ MediaDrmHandler mediaDrmHandler; /* package */ PostResponseHandler postResponseHandler; private HandlerThread requestHandlerThread; private Handler postRequestHandler; @@ -66,13 +72,14 @@ import java.util.UUID; @DefaultDrmSessionManager.Mode private final int mode; private int openCount; - private boolean provisioningInProgress; + private final AtomicBoolean provisioningInProgress; + private final EventListener sessionEventListener; @DrmSession.State private int state; private T mediaCrypto; private DrmSessionException lastException; - private final byte[] schemeInitData; - private final String schemeMimeType; + private final byte[] initData; + private final String mimeType; private byte[] sessionId; private byte[] offlineLicenseKeySetId; @@ -90,11 +97,12 @@ import java.util.UUID; * @param eventHandler The handler to post listener events. * @param eventListener The DRM session manager event listener. */ - public DefaultDrmSession(UUID uuid, ExoMediaDrm mediaDrm, DrmInitData initData, + public DefaultDrmSession(UUID uuid, ExoMediaDrm mediaDrm, byte[] initData, String mimeType, @DefaultDrmSessionManager.Mode int mode, byte[] offlineLicenseKeySetId, HashMap optionalKeyRequestParameters, MediaDrmCallback callback, Looper playbackLooper, Handler eventHandler, - DefaultDrmSessionManager.EventListener eventListener) { + DefaultDrmSessionManager.EventListener eventListener, AtomicBoolean provisioningInProgress, + EventListener sessionEventListener) { this.uuid = uuid; this.mediaDrm = mediaDrm; this.mode = mode; @@ -104,44 +112,22 @@ import java.util.UUID; this.eventHandler = eventHandler; this.eventListener = eventListener; + this.provisioningInProgress = provisioningInProgress; + this.sessionEventListener = sessionEventListener; state = STATE_OPENING; - mediaDrmHandler = new MediaDrmHandler(playbackLooper); - mediaDrm.setOnEventListener(new MediaDrmEventListener()); postResponseHandler = new PostResponseHandler(playbackLooper); requestHandlerThread = new HandlerThread("DrmRequestHandler"); requestHandlerThread.start(); postRequestHandler = new PostRequestHandler(requestHandlerThread.getLooper()); - // Parse init data. - byte[] schemeInitData = null; - String schemeMimeType = null; if (offlineLicenseKeySetId == null) { - SchemeData data = getSchemeData(initData, uuid); - if (data == null) { - onError(new IllegalStateException("Media does not support uuid: " + uuid)); - } else { - schemeInitData = data.data; - schemeMimeType = data.mimeType; - if (Util.SDK_INT < 21) { - // Prior to L the Widevine CDM required data to be extracted from the PSSH atom. - byte[] psshData = PsshAtomUtil.parseSchemeSpecificData(schemeInitData, uuid); - if (psshData == null) { - // Extraction failed. schemeData isn't a Widevine PSSH atom, so leave it unchanged. - } else { - schemeInitData = psshData; - } - } - if (Util.SDK_INT < 26 && C.CLEARKEY_UUID.equals(uuid) - && (MimeTypes.VIDEO_MP4.equals(schemeMimeType) - || MimeTypes.AUDIO_MP4.equals(schemeMimeType))) { - // Prior to API level 26 the ClearKey CDM only accepted "cenc" as the scheme for MP4. - schemeMimeType = CENC_SCHEME_MIME_TYPE; - } - } + this.initData = initData; + this.mimeType = mimeType; + } else { + this.initData = null; + this.mimeType = null; } - this.schemeInitData = schemeInitData; - this.schemeMimeType = schemeMimeType; } // Life cycle. @@ -163,9 +149,6 @@ import java.util.UUID; public boolean release() { if (--openCount == 0) { state = STATE_RELEASED; - provisioningInProgress = false; - mediaDrmHandler.removeCallbacksAndMessages(null); - mediaDrmHandler = null; postResponseHandler.removeCallbacksAndMessages(null); postRequestHandler.removeCallbacksAndMessages(null); postRequestHandler = null; @@ -182,6 +165,14 @@ import java.util.UUID; return false; } + public boolean canReuse(byte[] initData) { + return Arrays.equals(this.initData, initData); + } + + public boolean hasSessionId(byte[] sessionId) { + return Arrays.equals(this.sessionId, sessionId); + } + // DrmSession Implementation. @Override @@ -245,21 +236,15 @@ import java.util.UUID; } private void postProvisionRequest() { - if (provisioningInProgress) { + if (provisioningInProgress.getAndSet(true)) { return; } - provisioningInProgress = true; ProvisionRequest request = mediaDrm.getProvisionRequest(); postRequestHandler.obtainMessage(MSG_PROVISION, request).sendToTarget(); } private void onProvisionResponse(Object response) { - provisioningInProgress = false; - if (state != STATE_OPENING && !isOpen()) { - // This event is stale. - return; - } - + provisioningInProgress.set(false); if (response instanceof Exception) { onError((Exception) response); return; @@ -267,11 +252,24 @@ import java.util.UUID; try { mediaDrm.provideProvisionResponse((byte[]) response); - if (openInternal(false)) { - doLicense(); - } } catch (DeniedByServerException e) { onError(e); + return; + } + + if (sessionEventListener != null) { + sessionEventListener.onProvisionCompleted(); + } + } + + public void onProvisionCompleted() { + if (state != STATE_OPENING && !isOpen()) { + // This event is stale. + return; + } + + if (openInternal(false)) { + doLicense(); } } @@ -322,6 +320,8 @@ import java.util.UUID; postKeyRequest(MediaDrm.KEY_TYPE_RELEASE); } break; + default: + break; } } @@ -347,7 +347,7 @@ import java.util.UUID; private void postKeyRequest(int type) { byte[] scope = type == MediaDrm.KEY_TYPE_RELEASE ? offlineLicenseKeySetId : sessionId; try { - KeyRequest request = mediaDrm.getKeyRequest(scope, schemeInitData, schemeMimeType, type, + KeyRequest request = mediaDrm.getKeyRequest(scope, initData, mimeType, type, optionalKeyRequestParameters); postRequestHandler.obtainMessage(MSG_KEYS, request).sendToTarget(); } catch (Exception e) { @@ -433,46 +433,27 @@ import java.util.UUID; return state == STATE_OPENED || state == STATE_OPENED_WITH_KEYS; } - @SuppressLint("HandlerLeak") - private class MediaDrmHandler extends Handler { - - public MediaDrmHandler(Looper looper) { - super(looper); + @SuppressWarnings("deprecation") + public void onMediaDrmEvent(int what) { + if (!isOpen()) { + return; } - - @SuppressWarnings("deprecation") - @Override - public void handleMessage(Message msg) { - if (!isOpen()) { - return; - } - switch (msg.what) { - case MediaDrm.EVENT_KEY_REQUIRED: - doLicense(); - break; - case MediaDrm.EVENT_KEY_EXPIRED: - // When an already expired key is loaded MediaDrm sends this event immediately. Ignore - // this event if the state isn't STATE_OPENED_WITH_KEYS yet which means we're still - // waiting for key response. - onKeysExpired(); - break; - case MediaDrm.EVENT_PROVISION_REQUIRED: - state = STATE_OPENED; - postProvisionRequest(); - break; - } - } - - } - - private class MediaDrmEventListener implements OnEventListener { - - @Override - public void onEvent(ExoMediaDrm md, byte[] sessionId, int event, int extra, - byte[] data) { - if (mode == DefaultDrmSessionManager.MODE_PLAYBACK) { - mediaDrmHandler.sendEmptyMessage(event); - } + switch (what) { + case MediaDrm.EVENT_KEY_REQUIRED: + doLicense(); + break; + case MediaDrm.EVENT_KEY_EXPIRED: + // When an already expired key is loaded MediaDrm sends this event immediately. Ignore + // this event if the state isn't STATE_OPENED_WITH_KEYS yet which means we're still + // waiting for key response. + onKeysExpired(); + break; + case MediaDrm.EVENT_PROVISION_REQUIRED: + state = STATE_OPENED; + postProvisionRequest(); + break; + default: + break; } } @@ -493,6 +474,9 @@ import java.util.UUID; case MSG_KEYS: onKeyResponse(msg.obj); break; + default: + break; + } } @@ -527,20 +511,4 @@ import java.util.UUID; } - /** - * Extracts {@link SchemeData} suitable for the given DRM scheme {@link UUID}. - * - * @param drmInitData The {@link DrmInitData} from which to extract the {@link SchemeData}. - * @param uuid The UUID of the scheme. - * @return The extracted {@link SchemeData}, or null if no suitable data is present. - */ - public static SchemeData getSchemeData(DrmInitData drmInitData, UUID uuid) { - SchemeData schemeData = drmInitData.get(uuid); - if (schemeData == null && C.CLEARKEY_UUID.equals(uuid)) { - // If present, the Common PSSH box should be used for ClearKey. - schemeData = drmInitData.get(C.COMMON_PSSH_UUID); - } - return schemeData; - } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java index 9ea696e074..67931d106b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java @@ -15,27 +15,36 @@ */ package com.google.android.exoplayer2.drm; +import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.media.MediaDrm; import android.os.Handler; import android.os.Looper; +import android.os.Message; import android.support.annotation.IntDef; import android.support.annotation.NonNull; import android.text.TextUtils; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; +import com.google.android.exoplayer2.drm.ExoMediaDrm.OnEventListener; +import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.UUID; +import java.util.concurrent.atomic.AtomicBoolean; /** * A {@link DrmSessionManager} that supports playbacks using {@link MediaDrm}. */ @TargetApi(18) -public class DefaultDrmSessionManager implements DrmSessionManager { +public class DefaultDrmSessionManager implements DrmSessionManager, + DefaultDrmSession.EventListener { /** * Listener of {@link DefaultDrmSessionManager} events. @@ -70,6 +79,7 @@ public class DefaultDrmSessionManager implements DrmSe * The key to use when passing CustomData to a PlayReady instance in an optional parameter map. */ public static final String PLAYREADY_CUSTOM_DATA_KEY = "PRCustomData"; + private static final String CENC_SCHEME_MIME_TYPE = "cenc"; /** Determines the action to be done after a session acquired. */ @Retention(RetentionPolicy.SOURCE) @@ -93,14 +103,17 @@ public class DefaultDrmSessionManager implements DrmSe private final EventListener eventListener; private final ExoMediaDrm mediaDrm; private final HashMap optionalKeyRequestParameters; - private final MediaDrmCallback callback; private final UUID uuid; + private final boolean multiSession; private Looper playbackLooper; private int mode; private byte[] offlineLicenseKeySetId; - private DefaultDrmSession session; + + private final List> sessions; + private final AtomicBoolean provisioningInProgress; + /* package */ MediaDrmHandler mediaDrmHandler; /** * Instantiates a new instance using the Widevine scheme. @@ -163,7 +176,7 @@ public class DefaultDrmSessionManager implements DrmSe UUID uuid, MediaDrmCallback callback, HashMap optionalKeyRequestParameters, Handler eventHandler, EventListener eventListener) throws UnsupportedDrmException { return new DefaultDrmSessionManager<>(uuid, FrameworkMediaDrm.newInstance(uuid), callback, - optionalKeyRequestParameters, eventHandler, eventListener); + optionalKeyRequestParameters, eventHandler, eventListener, false); } /** @@ -179,7 +192,27 @@ public class DefaultDrmSessionManager implements DrmSe public DefaultDrmSessionManager(UUID uuid, ExoMediaDrm mediaDrm, MediaDrmCallback callback, HashMap optionalKeyRequestParameters, Handler eventHandler, EventListener eventListener) { + this(uuid, mediaDrm, callback, optionalKeyRequestParameters, eventHandler, eventListener, + false); + } + + /** + * @param uuid The UUID of the drm scheme. + * @param mediaDrm An underlying {@link ExoMediaDrm} for use by the manager. + * @param callback Performs key and provisioning requests. + * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument + * to {@link MediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null. + * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be + * null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + * @param multiSession A boolean that specify whether multiple key session support is enabled. + * Default is false. + */ + public DefaultDrmSessionManager(UUID uuid, ExoMediaDrm mediaDrm, MediaDrmCallback callback, + HashMap optionalKeyRequestParameters, Handler eventHandler, + EventListener eventListener, boolean multiSession) { Assertions.checkNotNull(uuid); + Assertions.checkNotNull(mediaDrm); Assertions.checkArgument(!C.COMMON_PSSH_UUID.equals(uuid), "Use C.CLEARKEY_UUID instead"); this.uuid = uuid; this.mediaDrm = mediaDrm; @@ -187,7 +220,13 @@ public class DefaultDrmSessionManager implements DrmSe this.optionalKeyRequestParameters = optionalKeyRequestParameters; this.eventHandler = eventHandler; this.eventListener = eventListener; + this.multiSession = multiSession; mode = MODE_PLAYBACK; + sessions = new ArrayList<>(); + provisioningInProgress = new AtomicBoolean(false); + if (multiSession) { + mediaDrm.setPropertyString("sessionSharing", "enable"); + } } /** @@ -261,7 +300,7 @@ public class DefaultDrmSessionManager implements DrmSe * @param offlineLicenseKeySetId The key set id of the license to be used with the given mode. */ public void setMode(@Mode int mode, byte[] offlineLicenseKeySetId) { - Assertions.checkState(session == null); + Assertions.checkState(sessions.isEmpty()); if (mode == MODE_QUERY || mode == MODE_RELEASE) { Assertions.checkNotNull(offlineLicenseKeySetId); } @@ -273,7 +312,7 @@ public class DefaultDrmSessionManager implements DrmSe @Override public boolean canAcquireSession(@NonNull DrmInitData drmInitData) { - SchemeData schemeData = DefaultDrmSession.getSchemeData(drmInitData, uuid); + SchemeData schemeData = getSchemeData(drmInitData, uuid); if (schemeData == null) { // No data for this manager's scheme. return false; @@ -294,22 +333,144 @@ public class DefaultDrmSessionManager implements DrmSe @Override public DrmSession acquireSession(Looper playbackLooper, DrmInitData drmInitData) { Assertions.checkState(this.playbackLooper == null || this.playbackLooper == playbackLooper); - if (session == null) { + if (sessions.isEmpty()) { this.playbackLooper = playbackLooper; - session = new DefaultDrmSession(uuid, mediaDrm, drmInitData, mode, offlineLicenseKeySetId, - optionalKeyRequestParameters, callback, playbackLooper, eventHandler, eventListener); + mediaDrmHandler = new MediaDrmHandler(playbackLooper); + mediaDrm.setOnEventListener(new MediaDrmEventListener()); } + DefaultDrmSession session = null; + byte[] initData = null; + String mimeType = null; + + if (offlineLicenseKeySetId == null) { + SchemeData data = getSchemeData(drmInitData, uuid); + if (data == null) { + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onDrmSessionManagerError(new IllegalStateException( + "Media does not support uuid: " + uuid)); + } + }); + } + } else { + initData = getSchemeInitData(data, uuid); + mimeType = getSchemeMimeType(data, uuid); + } + } + + for (DefaultDrmSession s : sessions) { + if (!multiSession || s.canReuse(initData)) { + session = s; + break; + } + } + + if (session == null) { + session = new DefaultDrmSession(uuid, mediaDrm, initData, mimeType, mode, + offlineLicenseKeySetId, optionalKeyRequestParameters, callback, playbackLooper, + eventHandler, eventListener, provisioningInProgress, this); + sessions.add(session); + } session.acquire(); return session; } @Override public void releaseSession(DrmSession session) { - Assertions.checkState(session == this.session); - if (this.session.release()) { - this.session = null; + DefaultDrmSession drmSession = (DefaultDrmSession) session; + if (drmSession.release()) { + sessions.remove(drmSession); + } + + if (sessions.isEmpty()) { + mediaDrm.setOnEventListener(null); + mediaDrmHandler.removeCallbacksAndMessages(null); + mediaDrmHandler = null; + playbackLooper = null; } } + @Override + public void onProvisionCompleted() { + for (DefaultDrmSession session : sessions) { + session.onProvisionCompleted(); + } + } + + /** + * Extracts {@link SchemeData} suitable for the given DRM scheme {@link UUID}. + * + * @param drmInitData The {@link DrmInitData} from which to extract the {@link SchemeData}. + * @param uuid The UUID. + * @return The extracted {@link SchemeData}, or null if no suitable data is present. + */ + private static SchemeData getSchemeData(DrmInitData drmInitData, UUID uuid) { + SchemeData schemeData = drmInitData.get(uuid); + if (schemeData == null && C.CLEARKEY_UUID.equals(uuid)) { + // If present, the Common PSSH box should be used for ClearKey. + schemeData = drmInitData.get(C.COMMON_PSSH_UUID); + } + return schemeData; + } + + private static byte[] getSchemeInitData(SchemeData data, UUID uuid) { + byte[] schemeInitData = data.data; + if (Util.SDK_INT < 21) { + // Prior to L the Widevine CDM required data to be extracted from the PSSH atom. + byte[] psshData = PsshAtomUtil.parseSchemeSpecificData(schemeInitData, uuid); + if (psshData == null) { + // Extraction failed. schemeData isn't a Widevine PSSH atom, so leave it unchanged. + } else { + schemeInitData = psshData; + } + } + return schemeInitData; + } + + private static String getSchemeMimeType(SchemeData data, UUID uuid) { + String schemeMimeType = data.mimeType; + if (Util.SDK_INT < 26 && C.CLEARKEY_UUID.equals(uuid) + && (MimeTypes.VIDEO_MP4.equals(schemeMimeType) + || MimeTypes.AUDIO_MP4.equals(schemeMimeType))) { + // Prior to API level 26 the ClearKey CDM only accepted "cenc" as the scheme for MP4. + schemeMimeType = CENC_SCHEME_MIME_TYPE; + } + return schemeMimeType; + } + + @SuppressLint("HandlerLeak") + private class MediaDrmHandler extends Handler { + + public MediaDrmHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + byte[] sessionId = (byte[]) msg.obj; + for (DefaultDrmSession session : sessions) { + if (session.hasSessionId(sessionId)) { + session.onMediaDrmEvent(msg.what); + return; + } + } + } + + } + + private class MediaDrmEventListener implements OnEventListener { + + @Override + public void onEvent(ExoMediaDrm md, byte[] sessionId, int event, int extra, + byte[] data) { + if (mode == DefaultDrmSessionManager.MODE_PLAYBACK) { + mediaDrmHandler.obtainMessage(event, sessionId).sendToTarget(); + } + } + + } + } From fb023da529d9d86ec2f7c921dae7ba084fa50e5c Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 5 Sep 2017 13:06:02 -0700 Subject: [PATCH 0350/2472] Fix attr inheritance in SimpleExoPlayerView When creating PlaybackControlView inside SimpleExoPlayerView, we want certain attributes to be passed through. This lets you set control attributes on the SimpleExoPlayerView directly. We don't want all attributes to be propagated though; only our own custom ones. Not sure if there's a cleaner way to do this. Pragmatically this solution seems ... ok :)? ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167619801 --- .../android/exoplayer2/ui/PlaybackControlView.java | 10 +++++++--- .../android/exoplayer2/ui/SimpleExoPlayerView.java | 8 ++++---- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java index c89feaebf5..9bbb2fa27b 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java @@ -298,16 +298,20 @@ public class PlaybackControlView extends FrameLayout { } public PlaybackControlView(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); + this(context, attrs, defStyleAttr, attrs); + } + public PlaybackControlView(Context context, AttributeSet attrs, int defStyleAttr, + AttributeSet playbackAttrs) { + super(context, attrs, defStyleAttr); int controllerLayoutId = R.layout.exo_playback_control_view; rewindMs = DEFAULT_REWIND_MS; fastForwardMs = DEFAULT_FAST_FORWARD_MS; showTimeoutMs = DEFAULT_SHOW_TIMEOUT_MS; repeatToggleModes = DEFAULT_REPEAT_TOGGLE_MODES; showShuffleButton = false; - if (attrs != null) { - TypedArray a = context.getTheme().obtainStyledAttributes(attrs, + if (playbackAttrs != null) { + TypedArray a = context.getTheme().obtainStyledAttributes(playbackAttrs, R.styleable.PlaybackControlView, 0, 0); try { rewindMs = a.getInt(R.styleable.PlaybackControlView_rewind_increment, rewindMs); diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index 5b6e11c5e4..488411550c 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -240,7 +240,7 @@ public final class SimpleExoPlayerView extends FrameLayout { controller = null; componentListener = null; overlayFrameLayout = null; - ImageView logo = new ImageView(context, attrs); + ImageView logo = new ImageView(context); if (Util.SDK_INT >= 23) { configureEditModeLogoV23(getResources(), logo); } else { @@ -330,9 +330,9 @@ public final class SimpleExoPlayerView extends FrameLayout { if (customController != null) { this.controller = customController; } else if (controllerPlaceholder != null) { - // Note: rewindMs and fastForwardMs are passed via attrs, so we don't need to make explicit - // calls to set them. - this.controller = new PlaybackControlView(context, attrs); + // Propagate attrs as playbackAttrs so that PlaybackControlView's custom attributes are + // transferred, but standard FrameLayout attributes (e.g. background) are not. + this.controller = new PlaybackControlView(context, null, 0, attrs); controller.setLayoutParams(controllerPlaceholder.getLayoutParams()); ViewGroup parent = ((ViewGroup) controllerPlaceholder.getParent()); int controllerIndex = parent.indexOfChild(controllerPlaceholder); From 8ef6a2e7bd4f10c4e1600b67bfc94a1ab00865b7 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 6 Sep 2017 00:14:55 -0700 Subject: [PATCH 0351/2472] Clear gapless playback metadata for clipped media Also pass an unresolved end point to ClippingMediaPeriod. This removes some assertions checking timestamps in the ClippingMediaPeriod, but makes it possible to identify when the end point is at the end of the media. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167683358 --- .../android/exoplayer2/source/ClippingMediaPeriod.java | 9 ++++++++- .../android/exoplayer2/source/ClippingMediaSource.java | 5 +---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java index a8c33b4625..89af07a3f0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java @@ -270,7 +270,14 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb return C.RESULT_BUFFER_READ; } int result = stream.readData(formatHolder, buffer, requireFormat); - // TODO: Clear gapless playback metadata if a format was read (if applicable). + if (result == C.RESULT_FORMAT_READ) { + // Clear gapless playback metadata if the start/end points don't match the media. + Format format = formatHolder.format; + int encoderDelay = startUs != 0 ? 0 : format.encoderDelay; + int encoderPadding = endUs != C.TIME_END_OF_SOURCE ? 0 : format.encoderPadding; + formatHolder.format = format.copyWithGaplessInfo(encoderDelay, encoderPadding); + return C.RESULT_FORMAT_READ; + } if (endUs != C.TIME_END_OF_SOURCE && ((result == C.RESULT_BUFFER_READ && buffer.timeUs >= endUs) || (result == C.RESULT_NOTHING_READ && mediaPeriod.getBufferedPositionUs() == C.TIME_END_OF_SOURCE))) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java index 2387b43d5e..21de83524a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java @@ -98,7 +98,7 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste ClippingMediaPeriod mediaPeriod = new ClippingMediaPeriod( mediaSource.createPeriod(id, allocator), enableInitialDiscontinuity); mediaPeriods.add(mediaPeriod); - mediaPeriod.setClipping(clippingTimeline.startUs, clippingTimeline.endUs); + mediaPeriod.setClipping(startUs, endUs); return mediaPeriod; } @@ -119,9 +119,6 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste public void onSourceInfoRefreshed(Timeline timeline, Object manifest) { clippingTimeline = new ClippingTimeline(timeline, startUs, endUs); sourceListener.onSourceInfoRefreshed(clippingTimeline, manifest); - long startUs = clippingTimeline.startUs; - long endUs = clippingTimeline.endUs == C.TIME_UNSET ? C.TIME_END_OF_SOURCE - : clippingTimeline.endUs; int count = mediaPeriods.size(); for (int i = 0; i < count; i++) { mediaPeriods.get(i).setClipping(startUs, endUs); From 17232f58a339809279c0d7f8432a8014ce72309b Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 6 Sep 2017 03:51:04 -0700 Subject: [PATCH 0352/2472] Fix position reporting during ads when period has non-zero window offset. Reporting incorrect positions for ad playbacks was causing IMA to think the ad wasn't playing, when in fact it was. Issue: #3180 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167702032 --- .../exoplayer2/ext/ima/ImaAdsLoader.java | 4 +++- .../android/exoplayer2/ExoPlayerImpl.java | 17 ++++++++++++----- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index c27ef17b87..87033173de 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -430,7 +430,9 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, } else if (!playingAd) { return VideoProgressUpdate.VIDEO_TIME_NOT_READY; } else { - return new VideoProgressUpdate(player.getCurrentPosition(), player.getDuration()); + long adDuration = player.getDuration(); + return adDuration == C.TIME_UNSET ? VideoProgressUpdate.VIDEO_TIME_NOT_READY + : new VideoProgressUpdate(player.getCurrentPosition(), adDuration); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index 7bf0cd5a02..e574bfc1ee 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -349,8 +349,7 @@ import java.util.concurrent.CopyOnWriteArraySet; if (timeline.isEmpty() || pendingSeekAcks > 0) { return maskingWindowPositionMs; } else { - timeline.getPeriod(playbackInfo.periodId.periodIndex, period); - return period.getPositionInWindowMs() + C.usToMs(playbackInfo.positionUs); + return playbackInfoPositionUsToWindowPositionMs(playbackInfo.positionUs); } } @@ -360,8 +359,7 @@ import java.util.concurrent.CopyOnWriteArraySet; if (timeline.isEmpty() || pendingSeekAcks > 0) { return maskingWindowPositionMs; } else { - timeline.getPeriod(playbackInfo.periodId.periodIndex, period); - return period.getPositionInWindowMs() + C.usToMs(playbackInfo.bufferedPositionUs); + return playbackInfoPositionUsToWindowPositionMs(playbackInfo.bufferedPositionUs); } } @@ -388,7 +386,7 @@ import java.util.concurrent.CopyOnWriteArraySet; @Override public boolean isPlayingAd() { - return pendingSeekAcks == 0 && playbackInfo.periodId.adGroupIndex != C.INDEX_UNSET; + return pendingSeekAcks == 0 && playbackInfo.periodId.isAd(); } @Override @@ -542,4 +540,13 @@ import java.util.concurrent.CopyOnWriteArraySet; } } + private long playbackInfoPositionUsToWindowPositionMs(long positionUs) { + long positionMs = C.usToMs(positionUs); + if (!playbackInfo.periodId.isAd()) { + timeline.getPeriod(playbackInfo.periodId.periodIndex, period); + positionMs += period.getPositionInWindowMs(); + } + return positionMs; + } + } From 013379fd3ee299ab6f24e8d42686af13249d54c2 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 6 Sep 2017 05:40:33 -0700 Subject: [PATCH 0353/2472] Workaround for SurfaceView not being hidden properly This appears to be fixed in Oreo, but given how harmless the workaround is we can probably just apply it on all API levels to be sure. Issue: #3160 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167709070 --- .../android/exoplayer2/ui/SimpleExoPlayerView.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index 488411550c..8fa264cad1 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -428,6 +428,15 @@ public final class SimpleExoPlayerView extends FrameLayout { } } + @Override + public void setVisibility(int visibility) { + super.setVisibility(visibility); + if (surfaceView instanceof SurfaceView) { + // Work around https://github.com/google/ExoPlayer/issues/3160 + surfaceView.setVisibility(visibility); + } + } + /** * Sets the resize mode. * From e7992513d3494c273cba15e97a9292527f46f668 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 6 Sep 2017 05:58:56 -0700 Subject: [PATCH 0354/2472] Remove references to MediaDrm from DefaultDrmSession classes Everything should go through the ExoMediaDrm layer. We still need to abstract away the android.media exception classes, but this is left as future work. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167710213 --- .../exoplayer2/drm/DefaultDrmSession.java | 23 +++++------ .../drm/DefaultDrmSessionManager.java | 19 +++++---- .../android/exoplayer2/drm/ExoMediaDrm.java | 39 ++++++++++++++++++- .../exoplayer2/drm/FrameworkMediaDrm.java | 4 +- 4 files changed, 58 insertions(+), 27 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java index d6776c8ed0..d9550c756c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java @@ -18,7 +18,6 @@ package com.google.android.exoplayer2.drm; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.media.DeniedByServerException; -import android.media.MediaDrm; import android.media.NotProvisionedException; import android.os.Handler; import android.os.HandlerThread; @@ -36,7 +35,7 @@ import java.util.UUID; import java.util.concurrent.atomic.AtomicBoolean; /** - * A {@link DrmSession} that supports playbacks using {@link MediaDrm}. + * A {@link DrmSession} that supports playbacks using {@link ExoMediaDrm}. */ @TargetApi(18) /* package */ class DefaultDrmSession implements DrmSession { @@ -227,8 +226,6 @@ import java.util.concurrent.atomic.AtomicBoolean; onError(e); } } catch (Exception e) { - // MediaCryptoException - // ResourceBusyException only available on 19+ onError(e); } @@ -278,7 +275,7 @@ import java.util.concurrent.atomic.AtomicBoolean; case DefaultDrmSessionManager.MODE_PLAYBACK: case DefaultDrmSessionManager.MODE_QUERY: if (offlineLicenseKeySetId == null) { - postKeyRequest(MediaDrm.KEY_TYPE_STREAMING); + postKeyRequest(ExoMediaDrm.KEY_TYPE_STREAMING); } else { if (restoreKeys()) { long licenseDurationRemainingSec = getLicenseDurationRemainingSec(); @@ -286,7 +283,7 @@ import java.util.concurrent.atomic.AtomicBoolean; && licenseDurationRemainingSec <= MAX_LICENSE_DURATION_TO_RENEW) { Log.d(TAG, "Offline license has expired or will expire soon. " + "Remaining seconds: " + licenseDurationRemainingSec); - postKeyRequest(MediaDrm.KEY_TYPE_OFFLINE); + postKeyRequest(ExoMediaDrm.KEY_TYPE_OFFLINE); } else if (licenseDurationRemainingSec <= 0) { onError(new KeysExpiredException()); } else { @@ -305,11 +302,11 @@ import java.util.concurrent.atomic.AtomicBoolean; break; case DefaultDrmSessionManager.MODE_DOWNLOAD: if (offlineLicenseKeySetId == null) { - postKeyRequest(MediaDrm.KEY_TYPE_OFFLINE); + postKeyRequest(ExoMediaDrm.KEY_TYPE_OFFLINE); } else { // Renew if (restoreKeys()) { - postKeyRequest(MediaDrm.KEY_TYPE_OFFLINE); + postKeyRequest(ExoMediaDrm.KEY_TYPE_OFFLINE); } } break; @@ -317,7 +314,7 @@ import java.util.concurrent.atomic.AtomicBoolean; // It's not necessary to restore the key (and open a session to do that) before releasing it // but this serves as a good sanity/fast-failure check. if (restoreKeys()) { - postKeyRequest(MediaDrm.KEY_TYPE_RELEASE); + postKeyRequest(ExoMediaDrm.KEY_TYPE_RELEASE); } break; default: @@ -345,7 +342,7 @@ import java.util.concurrent.atomic.AtomicBoolean; } private void postKeyRequest(int type) { - byte[] scope = type == MediaDrm.KEY_TYPE_RELEASE ? offlineLicenseKeySetId : sessionId; + byte[] scope = type == ExoMediaDrm.KEY_TYPE_RELEASE ? offlineLicenseKeySetId : sessionId; try { KeyRequest request = mediaDrm.getKeyRequest(scope, initData, mimeType, type, optionalKeyRequestParameters); @@ -439,16 +436,16 @@ import java.util.concurrent.atomic.AtomicBoolean; return; } switch (what) { - case MediaDrm.EVENT_KEY_REQUIRED: + case ExoMediaDrm.EVENT_KEY_REQUIRED: doLicense(); break; - case MediaDrm.EVENT_KEY_EXPIRED: + case ExoMediaDrm.EVENT_KEY_EXPIRED: // When an already expired key is loaded MediaDrm sends this event immediately. Ignore // this event if the state isn't STATE_OPENED_WITH_KEYS yet which means we're still // waiting for key response. onKeysExpired(); break; - case MediaDrm.EVENT_PROVISION_REQUIRED: + case ExoMediaDrm.EVENT_PROVISION_REQUIRED: state = STATE_OPENED; postProvisionRequest(); break; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java index 67931d106b..029b41fde8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.drm; import android.annotation.SuppressLint; import android.annotation.TargetApi; -import android.media.MediaDrm; import android.os.Handler; import android.os.Looper; import android.os.Message; @@ -40,7 +39,7 @@ import java.util.UUID; import java.util.concurrent.atomic.AtomicBoolean; /** - * A {@link DrmSessionManager} that supports playbacks using {@link MediaDrm}. + * A {@link DrmSessionManager} that supports playbacks using {@link ExoMediaDrm}. */ @TargetApi(18) public class DefaultDrmSessionManager implements DrmSessionManager, @@ -120,7 +119,7 @@ public class DefaultDrmSessionManager implements DrmSe * * @param callback Performs key and provisioning requests. * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument - * to {@link MediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null. + * to {@link ExoMediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null. * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be * null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. @@ -166,7 +165,7 @@ public class DefaultDrmSessionManager implements DrmSe * @param uuid The UUID of the drm scheme. * @param callback Performs key and provisioning requests. * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument - * to {@link MediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null. + * to {@link ExoMediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null. * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be * null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. @@ -184,7 +183,7 @@ public class DefaultDrmSessionManager implements DrmSe * @param mediaDrm An underlying {@link ExoMediaDrm} for use by the manager. * @param callback Performs key and provisioning requests. * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument - * to {@link MediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null. + * to {@link ExoMediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null. * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be * null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. @@ -201,7 +200,7 @@ public class DefaultDrmSessionManager implements DrmSe * @param mediaDrm An underlying {@link ExoMediaDrm} for use by the manager. * @param callback Performs key and provisioning requests. * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument - * to {@link MediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null. + * to {@link ExoMediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null. * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be * null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. @@ -230,7 +229,7 @@ public class DefaultDrmSessionManager implements DrmSe } /** - * Provides access to {@link MediaDrm#getPropertyString(String)}. + * Provides access to {@link ExoMediaDrm#getPropertyString(String)}. *

      * This method may be called when the manager is in any state. * @@ -242,7 +241,7 @@ public class DefaultDrmSessionManager implements DrmSe } /** - * Provides access to {@link MediaDrm#setPropertyString(String, String)}. + * Provides access to {@link ExoMediaDrm#setPropertyString(String, String)}. *

      * This method may be called when the manager is in any state. * @@ -254,7 +253,7 @@ public class DefaultDrmSessionManager implements DrmSe } /** - * Provides access to {@link MediaDrm#getPropertyByteArray(String)}. + * Provides access to {@link ExoMediaDrm#getPropertyByteArray(String)}. *

      * This method may be called when the manager is in any state. * @@ -266,7 +265,7 @@ public class DefaultDrmSessionManager implements DrmSe } /** - * Provides access to {@link MediaDrm#setPropertyByteArray(String, byte[])}. + * Provides access to {@link ExoMediaDrm#setPropertyByteArray(String, byte[])}. *

      * This method may be called when the manager is in any state. * diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java index 63387f19e1..25b065c543 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java @@ -15,19 +15,54 @@ */ package com.google.android.exoplayer2.drm; +import android.annotation.TargetApi; import android.media.DeniedByServerException; import android.media.MediaCryptoException; import android.media.MediaDrm; +import android.media.MediaDrmException; import android.media.NotProvisionedException; -import android.media.ResourceBusyException; import java.util.HashMap; import java.util.Map; +import java.util.UUID; /** * Used to obtain keys for decrypting protected media streams. See {@link android.media.MediaDrm}. */ +@TargetApi(18) public interface ExoMediaDrm { + /** + * @see MediaDrm#EVENT_KEY_REQUIRED + */ + @SuppressWarnings("InlinedApi") + int EVENT_KEY_REQUIRED = MediaDrm.EVENT_KEY_REQUIRED; + /** + * @see MediaDrm#EVENT_KEY_EXPIRED + */ + @SuppressWarnings("InlinedApi") + int EVENT_KEY_EXPIRED = MediaDrm.EVENT_KEY_EXPIRED; + /** + * @see MediaDrm#EVENT_PROVISION_REQUIRED + */ + @SuppressWarnings("InlinedApi") + int EVENT_PROVISION_REQUIRED = MediaDrm.EVENT_PROVISION_REQUIRED; + + /** + * @see MediaDrm#KEY_TYPE_STREAMING + */ + @SuppressWarnings("InlinedApi") + int KEY_TYPE_STREAMING = MediaDrm.KEY_TYPE_STREAMING; + /** + * @see MediaDrm#KEY_TYPE_OFFLINE + */ + @SuppressWarnings("InlinedApi") + int KEY_TYPE_OFFLINE = MediaDrm.KEY_TYPE_OFFLINE; + /** + * @see MediaDrm#KEY_TYPE_RELEASE + */ + @SuppressWarnings("InlinedApi") + int KEY_TYPE_RELEASE = MediaDrm.KEY_TYPE_RELEASE; + /** * @see android.media.MediaDrm.OnEventListener */ @@ -69,7 +104,7 @@ public interface ExoMediaDrm { /** * @see MediaDrm#openSession() */ - byte[] openSession() throws NotProvisionedException, ResourceBusyException; + byte[] openSession() throws MediaDrmException; /** * @see MediaDrm#closeSession(byte[]) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java index d664cb69a9..2edd6ff254 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java @@ -20,8 +20,8 @@ import android.media.DeniedByServerException; import android.media.MediaCrypto; import android.media.MediaCryptoException; import android.media.MediaDrm; +import android.media.MediaDrmException; import android.media.NotProvisionedException; -import android.media.ResourceBusyException; import android.media.UnsupportedSchemeException; import android.support.annotation.NonNull; import com.google.android.exoplayer2.C; @@ -79,7 +79,7 @@ public final class FrameworkMediaDrm implements ExoMediaDrm Date: Wed, 6 Sep 2017 06:10:02 -0700 Subject: [PATCH 0355/2472] Add full stops ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167711267 --- .../android/exoplayer2/extractor/mp4/AtomParsers.java | 2 +- .../android/exoplayer2/mediacodec/MediaCodecUtil.java | 6 +++--- .../android/exoplayer2/metadata/MetadataRenderer.java | 4 ++-- .../android/exoplayer2/video/MediaCodecVideoRenderer.java | 2 +- .../google/android/exoplayer2/ui/SimpleExoPlayerView.java | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/library/core/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 index 450e0682e6..c754c4b566 100644 --- a/library/core/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 @@ -1147,7 +1147,7 @@ import java.util.List; } /** - * Parses the proj box from sv3d box, as specified by https://github.com/google/spatial-media + * Parses the proj box from sv3d box, as specified by https://github.com/google/spatial-media. */ private static byte[] parseProjFromParent(ParsableByteArray parent, int position, int size) { int childPosition = position + Atom.HEADER_SIZE; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java index 1073e8d9c1..2cd336e9ef 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java @@ -283,13 +283,13 @@ public final class MediaCodecUtil { return false; } - // Work around https://github.com/google/ExoPlayer/issues/398 + // 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 and - // https://github.com/google/ExoPlayer/issues/3171 + // https://github.com/google/ExoPlayer/issues/3171. if (Util.SDK_INT < 18 && "OMX.MTK.AUDIO.DECODER.AAC".equals(name) && ("a70".equals(Util.DEVICE) || ("Xiaomi".equals(Util.MANUFACTURER) && Util.DEVICE.startsWith("HM")))) { @@ -325,7 +325,7 @@ public final class MediaCodecUtil { return false; } - // Work around https://github.com/google/ExoPlayer/issues/548 + // Work around https://github.com/google/ExoPlayer/issues/548. // 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) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java index 869e9306a5..7d36d87a9e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/MetadataRenderer.java @@ -40,8 +40,8 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { private static final int MSG_INVOKE_RENDERER = 0; // TODO: Holding multiple pending metadata objects is temporary mitigation against - // https://github.com/google/ExoPlayer/issues/1874 - // It should be removed once this issue has been addressed. + // https://github.com/google/ExoPlayer/issues/1874. It should be removed once this issue has been + // addressed. private static final int MAX_PENDING_METADATA_COUNT = 5; private final MetadataDecoderFactory decoderFactory; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index d72619dbe1..4c1f4c0eb2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -979,7 +979,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { * If true is returned then we fall back to releasing and re-instantiating the codec instead. */ private static boolean codecNeedsSetOutputSurfaceWorkaround(String name) { - // Work around https://github.com/google/ExoPlayer/issues/3236 + // Work around https://github.com/google/ExoPlayer/issues/3236. return ("deb".equals(Util.DEVICE) || "flo".equals(Util.DEVICE)) && "OMX.qcom.video.decoder.avc".equals(name); } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index 8fa264cad1..9bc4bb5b87 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -432,7 +432,7 @@ public final class SimpleExoPlayerView extends FrameLayout { public void setVisibility(int visibility) { super.setVisibility(visibility); if (surfaceView instanceof SurfaceView) { - // Work around https://github.com/google/ExoPlayer/issues/3160 + // Work around https://github.com/google/ExoPlayer/issues/3160. surfaceView.setVisibility(visibility); } } From c6fa034eba88fe1a7fc62aae3288006ff4f3ad6d Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 6 Sep 2017 06:17:09 -0700 Subject: [PATCH 0356/2472] DecryptionException cleanup + add missing header ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167711928 --- .../exoplayer2/drm/DecryptionException.java | 33 ++++++++++++++----- .../android/exoplayer2/drm/DrmSession.java | 4 ++- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DecryptionException.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DecryptionException.java index 6916b972b2..81cfc26393 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DecryptionException.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DecryptionException.java @@ -1,20 +1,37 @@ +/* + * 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.drm; /** - * An exception when doing drm decryption using the In-App Drm + * Thrown when a non-platform component fails to decrypt data. */ public class DecryptionException extends Exception { - private final int errorCode; + /** + * A component specific error code. + */ + public final int errorCode; + + /** + * @param errorCode A component specific error code. + * @param message The detail message. + */ public DecryptionException(int errorCode, String message) { super(message); this.errorCode = errorCode; } - /** - * Get error code - */ - public int getErrorCode() { - return errorCode; - } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java index 0c17b102fd..a3ae1d8b71 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java @@ -28,7 +28,9 @@ import java.util.Map; @TargetApi(16) public interface DrmSession { - /** Wraps the throwable which is the cause of the error state. */ + /** + * Wraps the throwable which is the cause of the error state. + */ class DrmSessionException extends Exception { public DrmSessionException(Throwable cause) { From 7d4190f3c84a223d189d5f0689778b1b047da886 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 6 Sep 2017 07:22:00 -0700 Subject: [PATCH 0357/2472] Regroup final/non-final vars ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167717715 --- .../exoplayer2/drm/DefaultDrmSession.java | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java index d9550c756c..4f187264a6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java @@ -58,30 +58,30 @@ import java.util.concurrent.atomic.AtomicBoolean; private static final int MSG_KEYS = 1; private static final int MAX_LICENSE_DURATION_TO_RENEW = 60; - private final Handler eventHandler; - private final DefaultDrmSessionManager.EventListener eventListener; private final ExoMediaDrm mediaDrm; - private final HashMap optionalKeyRequestParameters; - /* package */ final MediaDrmCallback callback; - /* package */ final UUID uuid; - /* package */ PostResponseHandler postResponseHandler; - private HandlerThread requestHandlerThread; - private Handler postRequestHandler; - - @DefaultDrmSessionManager.Mode - private final int mode; - private int openCount; - private final AtomicBoolean provisioningInProgress; - private final EventListener sessionEventListener; - @DrmSession.State - private int state; - private T mediaCrypto; - private DrmSessionException lastException; private final byte[] initData; private final String mimeType; + private final @DefaultDrmSessionManager.Mode int mode; + private final HashMap optionalKeyRequestParameters; + private final Handler eventHandler; + private final DefaultDrmSessionManager.EventListener eventListener; + private final AtomicBoolean provisioningInProgress; + private final EventListener sessionEventListener; + + /* package */ final MediaDrmCallback callback; + /* package */ final UUID uuid; + + private @DrmSession.State int state; + private int openCount; + private HandlerThread requestHandlerThread; + private Handler postRequestHandler; + private T mediaCrypto; + private DrmSessionException lastException; private byte[] sessionId; private byte[] offlineLicenseKeySetId; + /* package */ PostResponseHandler postResponseHandler; + /** * Instantiates a new DRM session. * From 4869a2506960899a94215df1fbf536e6542ac984 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 6 Sep 2017 07:26:23 -0700 Subject: [PATCH 0358/2472] Pick up rtmpClient fix Issue: #3156 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167718081 --- extensions/rtmp/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/rtmp/build.gradle b/extensions/rtmp/build.gradle index c832cb82e9..7687f03e32 100644 --- a/extensions/rtmp/build.gradle +++ b/extensions/rtmp/build.gradle @@ -26,7 +26,7 @@ android { dependencies { compile project(modulePrefix + 'library-core') - compile 'net.butterflytv.utils:rtmp-client:0.2.8' + compile 'net.butterflytv.utils:rtmp-client:3.0.0' } ext { From 7c3fe19d3f5e0a08d855d4564c5c09d9e41d2ba2 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 7 Sep 2017 01:45:56 -0700 Subject: [PATCH 0359/2472] Migrate remaining tests to Robolectric Remaining instrumentation tests either use android.os.Handler or rely on assets. In the latter case, the tests are difficult to migrate due to differences between the internal and external build systems, and configuration needed in Android Studio. In addition, SimpleCacheSpanTest remains as an instrumentation test because it fails due to a problem with string encoding on the internal build (and two other tests in its package are kept with it because they depend on it). This test removes a dependency from testutils on Mockito, as a different version of Mockito needs to be used for instrumentation tests vs Robolectric tests, yet both sets of tests need to rely on testutils. Mockito setup is now done directly in the tests that need it. Move OggTestData to testutils so it can be used from both instrumentation and Robolectric tests. It may be possible to simplify assertions further using Truth but this is left for possible later changes. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167831435 --- .../drm/OfflineLicenseHelperTest.java | 14 +- .../extractor/ogg/OggExtractorTest.java | 13 +- .../extractor/ogg/OggPacketTest.java | 43 +++-- .../exoplayer2/extractor/ogg/OggTestFile.java | 3 +- .../extractor/ogg/VorbisBitArrayTest.java | 139 -------------- .../extractor/ogg/VorbisReaderTest.java | 104 ---------- .../extractor/ogg/VorbisUtilTest.java | 126 ------------ .../exoplayer2/text/ttml/TtmlStyleTest.java | 132 ------------- .../cache/CachedRegionTrackerTest.java | 15 +- .../exoplayer2/drm/DrmInitDataTest.java | 82 ++++---- .../extractor/DefaultExtractorInputTest.java | 140 ++++++++------ .../exoplayer2/extractor/ExtractorTest.java | 20 +- .../extractor/mkv/DefaultEbmlReaderTest.java | 31 ++- .../extractor/mkv/VarintReaderTest.java | 30 ++- .../extractor/mp3/XingSeekerTest.java | 53 ++++-- .../extractor/mp4/AtomParsersTest.java | 20 +- .../extractor/mp4/PsshAtomUtilTest.java | 31 ++- .../ogg/DefaultOggSeekerUtilMethodsTest.java | 78 +++++--- .../extractor/ogg/OggPageHeaderTest.java | 54 ++++-- .../extractor/ogg/VorbisBitArrayTest.java | 155 +++++++++++++++ .../extractor/ogg/VorbisReaderTest.java | 117 ++++++++++++ .../extractor/ogg/VorbisUtilTest.java | 146 ++++++++++++++ .../extractor/ts/SectionReaderTest.java | 53 ++++-- .../emsg/EventMessageDecoderTest.java | 25 ++- .../metadata/emsg/EventMessageTest.java | 14 +- .../metadata/id3/ChapterFrameTest.java | 14 +- .../metadata/id3/ChapterTocFrameTest.java | 14 +- .../metadata/id3/Id3DecoderTest.java | 135 +++++++------ .../scte35/SpliceInfoDecoderTest.java | 78 ++++---- .../ProgressiveDownloadActionTest.java | 143 ++++++++++++++ .../exoplayer2/source/SampleQueueTest.java | 146 ++++++++------ .../exoplayer2/source/ShuffleOrderTest.java | 43 +++-- .../text/ttml/TtmlRenderUtilTest.java | 62 ++++-- .../exoplayer2/text/ttml/TtmlStyleTest.java | 155 +++++++++++++++ .../exoplayer2/text/webvtt/CssParserTest.java | 115 ++++++----- .../text/webvtt/Mp4WebvttDecoderTest.java | 38 ++-- .../text/webvtt/WebvttCueParserTest.java | 180 ++++++++++-------- .../text/webvtt/WebvttSubtitleTest.java | 76 +++++--- .../MappingTrackSelectorTest.java | 38 ++-- .../upstream/ByteArrayDataSourceTest.java | 42 ++-- .../upstream/DataSchemeDataSourceTest.java | 46 +++-- .../upstream/DataSourceAsserts.java | 50 +++++ .../upstream/DataSourceInputStreamTest.java | 51 +++-- .../upstream/cache/CacheAsserts.java | 115 +++++++++++ .../upstream/cache/CacheDataSourceTest.java | 74 ++++--- .../upstream/cache/CacheDataSourceTest2.java | 37 ++-- .../upstream/cache/CacheUtilTest.java | 68 +++++-- .../upstream/cache/SimpleCacheTest.java | 105 +++++----- .../crypto/AesFlushingCipherTest.java | 63 +++--- .../exoplayer2/source/dash/MockitoUtil.java | 38 ++++ .../dash/offline/DashDownloaderTest.java | 3 +- .../exoplayer2/testutil/OggTestData.java | 10 +- .../android/exoplayer2/testutil/TestUtil.java | 9 - 53 files changed, 2266 insertions(+), 1320 deletions(-) delete mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/VorbisBitArrayTest.java delete mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/VorbisReaderTest.java delete mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/VorbisUtilTest.java delete mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/text/ttml/TtmlStyleTest.java rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java (61%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/extractor/DefaultExtractorInputTest.java (78%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/extractor/ExtractorTest.java (60%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReaderTest.java (92%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/extractor/mkv/VarintReaderTest.java (92%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/extractor/mp3/XingSeekerTest.java (67%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/extractor/mp4/AtomParsersTest.java (79%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtilTest.java (56%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerUtilMethodsTest.java (72%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/extractor/ogg/OggPageHeaderTest.java (59%) create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/VorbisBitArrayTest.java create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/VorbisReaderTest.java create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/VorbisUtilTest.java rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/extractor/ts/SectionReaderTest.java (81%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoderTest.java (70%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/metadata/emsg/EventMessageTest.java (74%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/metadata/id3/ChapterFrameTest.java (75%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrameTest.java (76%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/metadata/id3/Id3DecoderTest.java (66%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoderTest.java (73%) create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/offline/ProgressiveDownloadActionTest.java rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/source/SampleQueueTest.java (86%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/source/ShuffleOrderTest.java (78%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtilTest.java (59%) create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlStyleTest.java rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/text/webvtt/CssParserTest.java (61%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoderTest.java (82%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParserTest.java (50%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/text/webvtt/WebvttSubtitleTest.java (78%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/trackselection/MappingTrackSelectorTest.java (87%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/upstream/ByteArrayDataSourceTest.java (82%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/upstream/DataSchemeDataSourceTest.java (61%) create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSourceAsserts.java rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/upstream/DataSourceInputStreamTest.java (65%) create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheAsserts.java rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java (77%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest2.java (86%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/upstream/cache/CacheUtilTest.java (83%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java (69%) rename library/core/src/{androidTest => test}/java/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipherTest.java (77%) create mode 100644 library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/MockitoUtil.java rename library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/TestData.java => testutils/src/main/java/com/google/android/exoplayer2/testutil/OggTestData.java (99%) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java index 821656475d..35bfbe613a 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java @@ -23,10 +23,10 @@ import android.test.MoreAsserts; import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; -import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.upstream.HttpDataSource; import java.util.HashMap; import org.mockito.Mock; +import org.mockito.MockitoAnnotations; /** * Tests {@link OfflineLicenseHelper}. @@ -40,7 +40,7 @@ public class OfflineLicenseHelperTest extends InstrumentationTestCase { @Override protected void setUp() throws Exception { - TestUtil.setUpMockito(this); + setUpMockito(this); when(mediaDrm.openSession()).thenReturn(new byte[] {1, 2, 3}); offlineLicenseHelper = new OfflineLicenseHelper<>(mediaDrm, mediaDrmCallback, null); } @@ -157,4 +157,14 @@ public class OfflineLicenseHelperTest extends InstrumentationTestCase { new byte[] {1, 4, 7, 0, 3, 6})); } + /** + * Sets up Mockito for an instrumentation test. + */ + private static void setUpMockito(InstrumentationTestCase instrumentationTestCase) { + // Workaround for https://code.google.com/p/dexmaker/issues/detail?id=2. + System.setProperty("dexmaker.dexcache", + instrumentationTestCase.getInstrumentation().getTargetContext().getCacheDir().getPath()); + MockitoAnnotations.initMocks(instrumentationTestCase); + } + } diff --git a/library/core/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 index 3be23422cc..b1ebdf3261 100644 --- a/library/core/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 @@ -20,6 +20,7 @@ import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.testutil.ExtractorAsserts; import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; import com.google.android.exoplayer2.testutil.FakeExtractorInput; +import com.google.android.exoplayer2.testutil.OggTestData; import com.google.android.exoplayer2.testutil.TestUtil; import java.io.IOException; @@ -56,7 +57,7 @@ public final class OggExtractorTest extends InstrumentationTestCase { public void testSniffVorbis() throws Exception { byte[] data = TestUtil.joinByteArrays( - TestData.buildOggHeader(0x02, 0, 1000, 1), + OggTestData.buildOggHeader(0x02, 0, 1000, 1), TestUtil.createByteArray(7), // Laces new byte[] {0x01, 'v', 'o', 'r', 'b', 'i', 's'}); assertTrue(sniff(data)); @@ -64,7 +65,7 @@ public final class OggExtractorTest extends InstrumentationTestCase { public void testSniffFlac() throws Exception { byte[] data = TestUtil.joinByteArrays( - TestData.buildOggHeader(0x02, 0, 1000, 1), + OggTestData.buildOggHeader(0x02, 0, 1000, 1), TestUtil.createByteArray(5), // Laces new byte[] {0x7F, 'F', 'L', 'A', 'C'}); assertTrue(sniff(data)); @@ -72,26 +73,26 @@ public final class OggExtractorTest extends InstrumentationTestCase { public void testSniffFailsOpusFile() throws Exception { byte[] data = TestUtil.joinByteArrays( - TestData.buildOggHeader(0x02, 0, 1000, 0x00), + OggTestData.buildOggHeader(0x02, 0, 1000, 0x00), new byte[] {'O', 'p', 'u', 's'}); assertFalse(sniff(data)); } public void testSniffFailsInvalidOggHeader() throws Exception { - byte[] data = TestData.buildOggHeader(0x00, 0, 1000, 0x00); + byte[] data = OggTestData.buildOggHeader(0x00, 0, 1000, 0x00); assertFalse(sniff(data)); } public void testSniffInvalidHeader() throws Exception { byte[] data = TestUtil.joinByteArrays( - TestData.buildOggHeader(0x02, 0, 1000, 1), + OggTestData.buildOggHeader(0x02, 0, 1000, 1), TestUtil.createByteArray(7), // Laces new byte[] {0x7F, 'X', 'o', 'r', 'b', 'i', 's'}); assertFalse(sniff(data)); } public void testSniffFailsEOF() throws Exception { - byte[] data = TestData.buildOggHeader(0x02, 0, 1000, 0x00); + byte[] data = OggTestData.buildOggHeader(0x02, 0, 1000, 0x00); assertFalse(sniff(data)); } diff --git a/library/core/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 index 991d31ff03..186b842bab 100644 --- a/library/core/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 @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor.ogg; import android.test.InstrumentationTestCase; import android.test.MoreAsserts; import com.google.android.exoplayer2.testutil.FakeExtractorInput; +import com.google.android.exoplayer2.testutil.OggTestData; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.util.ParsableByteArray; import java.io.IOException; @@ -47,20 +48,20 @@ public final class OggPacketTest extends InstrumentationTestCase { byte[] thirdPacket = TestUtil.buildTestData(256, random); byte[] fourthPacket = TestUtil.buildTestData(271, random); - FakeExtractorInput input = TestData.createInput( + FakeExtractorInput input = OggTestData.createInput( TestUtil.joinByteArrays( // First page with a single packet. - TestData.buildOggHeader(0x02, 0, 1000, 0x01), + OggTestData.buildOggHeader(0x02, 0, 1000, 0x01), TestUtil.createByteArray(0x08), // Laces firstPacket, // Second page with a single packet. - TestData.buildOggHeader(0x00, 16, 1001, 0x02), + OggTestData.buildOggHeader(0x00, 16, 1001, 0x02), TestUtil.createByteArray(0xFF, 0x11), // Laces secondPacket, // Third page with zero packets. - TestData.buildOggHeader(0x00, 16, 1002, 0x00), + OggTestData.buildOggHeader(0x00, 16, 1002, 0x00), // Fourth page with two packets. - TestData.buildOggHeader(0x04, 128, 1003, 0x04), + OggTestData.buildOggHeader(0x04, 128, 1003, 0x04), TestUtil.createByteArray(0xFF, 0x01, 0xFF, 0x10), // Laces thirdPacket, fourthPacket), true); @@ -107,9 +108,9 @@ public final class OggPacketTest extends InstrumentationTestCase { byte[] firstPacket = TestUtil.buildTestData(255, random); byte[] secondPacket = TestUtil.buildTestData(8, random); - FakeExtractorInput input = TestData.createInput( + FakeExtractorInput input = OggTestData.createInput( TestUtil.joinByteArrays( - TestData.buildOggHeader(0x06, 0, 1000, 0x04), + OggTestData.buildOggHeader(0x06, 0, 1000, 0x04), TestUtil.createByteArray(0xFF, 0x00, 0x00, 0x08), // Laces. firstPacket, secondPacket), true); @@ -122,14 +123,14 @@ public final class OggPacketTest extends InstrumentationTestCase { public void testReadContinuedPacketOverTwoPages() throws Exception { byte[] firstPacket = TestUtil.buildTestData(518); - FakeExtractorInput input = TestData.createInput( + FakeExtractorInput input = OggTestData.createInput( TestUtil.joinByteArrays( // First page. - TestData.buildOggHeader(0x02, 0, 1000, 0x02), + OggTestData.buildOggHeader(0x02, 0, 1000, 0x02), TestUtil.createByteArray(0xFF, 0xFF), // Laces. Arrays.copyOf(firstPacket, 510), // Second page (continued packet). - TestData.buildOggHeader(0x05, 10, 1001, 0x01), + OggTestData.buildOggHeader(0x05, 10, 1001, 0x01), TestUtil.createByteArray(0x08), // Laces. Arrays.copyOfRange(firstPacket, 510, 510 + 8)), true); @@ -144,22 +145,22 @@ public final class OggPacketTest extends InstrumentationTestCase { public void testReadContinuedPacketOverFourPages() throws Exception { byte[] firstPacket = TestUtil.buildTestData(1028); - FakeExtractorInput input = TestData.createInput( + FakeExtractorInput input = OggTestData.createInput( TestUtil.joinByteArrays( // First page. - TestData.buildOggHeader(0x02, 0, 1000, 0x02), + OggTestData.buildOggHeader(0x02, 0, 1000, 0x02), TestUtil.createByteArray(0xFF, 0xFF), // Laces. Arrays.copyOf(firstPacket, 510), // Second page (continued packet). - TestData.buildOggHeader(0x01, 10, 1001, 0x01), + OggTestData.buildOggHeader(0x01, 10, 1001, 0x01), TestUtil.createByteArray(0xFF), // Laces. Arrays.copyOfRange(firstPacket, 510, 510 + 255), // Third page (continued packet). - TestData.buildOggHeader(0x01, 10, 1002, 0x01), + OggTestData.buildOggHeader(0x01, 10, 1002, 0x01), TestUtil.createByteArray(0xFF), // Laces. Arrays.copyOfRange(firstPacket, 510 + 255, 510 + 255 + 255), // Fourth page (continued packet). - TestData.buildOggHeader(0x05, 10, 1003, 0x01), + OggTestData.buildOggHeader(0x05, 10, 1003, 0x01), TestUtil.createByteArray(0x08), // Laces. Arrays.copyOfRange(firstPacket, 510 + 255 + 255, 510 + 255 + 255 + 8)), true); @@ -174,10 +175,10 @@ public final class OggPacketTest extends InstrumentationTestCase { public void testReadDiscardContinuedPacketAtStart() throws Exception { byte[] pageBody = TestUtil.buildTestData(256 + 8); - FakeExtractorInput input = TestData.createInput( + FakeExtractorInput input = OggTestData.createInput( TestUtil.joinByteArrays( // Page with a continued packet at start. - TestData.buildOggHeader(0x01, 10, 1001, 0x03), + OggTestData.buildOggHeader(0x01, 10, 1001, 0x03), TestUtil.createByteArray(255, 1, 8), // Laces. pageBody), true); @@ -191,15 +192,15 @@ public final class OggPacketTest extends InstrumentationTestCase { byte[] secondPacket = TestUtil.buildTestData(8, random); byte[] thirdPacket = TestUtil.buildTestData(8, random); - FakeExtractorInput input = TestData.createInput( + FakeExtractorInput input = OggTestData.createInput( TestUtil.joinByteArrays( - TestData.buildOggHeader(0x02, 0, 1000, 0x01), + OggTestData.buildOggHeader(0x02, 0, 1000, 0x01), TestUtil.createByteArray(0x08), // Laces. firstPacket, - TestData.buildOggHeader(0x04, 0, 1001, 0x03), + OggTestData.buildOggHeader(0x04, 0, 1001, 0x03), TestUtil.createByteArray(0x08, 0x00, 0x00), // Laces. secondPacket, - TestData.buildOggHeader(0x04, 0, 1002, 0x03), + OggTestData.buildOggHeader(0x04, 0, 1002, 0x03), TestUtil.createByteArray(0x08, 0x00, 0x00), // Laces. thirdPacket), true); diff --git a/library/core/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 index d5d187ee7c..6d839a8355 100644 --- a/library/core/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 @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor.ogg; +import com.google.android.exoplayer2.testutil.OggTestData; import com.google.android.exoplayer2.testutil.TestUtil; import java.util.ArrayList; import java.util.Random; @@ -68,7 +69,7 @@ import junit.framework.Assert; } granule += random.nextInt(MAX_GRANULES_IN_PAGE - 1) + 1; int pageSegmentCount = random.nextInt(MAX_SEGMENT_COUNT); - byte[] header = TestData.buildOggHeader(headerType, granule, 0, pageSegmentCount); + byte[] header = OggTestData.buildOggHeader(headerType, granule, 0, pageSegmentCount); fileData.add(header); fileSize += header.length; diff --git a/library/core/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 deleted file mode 100644 index a24cb1599b..0000000000 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/VorbisBitArrayTest.java +++ /dev/null @@ -1,139 +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.ogg; - -import com.google.android.exoplayer2.testutil.TestUtil; -import junit.framework.TestCase; - -/** - * Unit test for {@link VorbisBitArray}. - */ -public final class VorbisBitArrayTest extends TestCase { - - public void testReadBit() { - VorbisBitArray bitArray = new VorbisBitArray(TestUtil.createByteArray(0x5c, 0x50)); - assertFalse(bitArray.readBit()); - assertFalse(bitArray.readBit()); - assertTrue(bitArray.readBit()); - assertTrue(bitArray.readBit()); - assertTrue(bitArray.readBit()); - assertFalse(bitArray.readBit()); - assertTrue(bitArray.readBit()); - assertFalse(bitArray.readBit()); - assertFalse(bitArray.readBit()); - assertFalse(bitArray.readBit()); - assertFalse(bitArray.readBit()); - assertFalse(bitArray.readBit()); - assertTrue(bitArray.readBit()); - assertFalse(bitArray.readBit()); - assertTrue(bitArray.readBit()); - assertFalse(bitArray.readBit()); - } - - public void testSkipBits() { - VorbisBitArray bitArray = new VorbisBitArray(TestUtil.createByteArray(0xF0, 0x0F)); - bitArray.skipBits(10); - assertEquals(10, bitArray.getPosition()); - assertTrue(bitArray.readBit()); - assertTrue(bitArray.readBit()); - assertFalse(bitArray.readBit()); - bitArray.skipBits(1); - assertEquals(14, bitArray.getPosition()); - assertFalse(bitArray.readBit()); - assertFalse(bitArray.readBit()); - } - - public void testGetPosition() throws Exception { - VorbisBitArray bitArray = new VorbisBitArray(TestUtil.createByteArray(0xF0, 0x0F)); - assertEquals(0, bitArray.getPosition()); - bitArray.readBit(); - assertEquals(1, bitArray.getPosition()); - bitArray.readBit(); - bitArray.readBit(); - bitArray.skipBits(4); - assertEquals(7, bitArray.getPosition()); - } - - public void testSetPosition() throws Exception { - VorbisBitArray bitArray = new VorbisBitArray(TestUtil.createByteArray(0xF0, 0x0F)); - assertEquals(0, bitArray.getPosition()); - bitArray.setPosition(4); - assertEquals(4, bitArray.getPosition()); - bitArray.setPosition(15); - assertFalse(bitArray.readBit()); - } - - public void testReadInt32() { - VorbisBitArray bitArray = new VorbisBitArray(TestUtil.createByteArray(0xF0, 0x0F, 0xF0, 0x0F)); - assertEquals(0x0FF00FF0, bitArray.readBits(32)); - bitArray = new VorbisBitArray(TestUtil.createByteArray(0x0F, 0xF0, 0x0F, 0xF0)); - assertEquals(0xF00FF00F, bitArray.readBits(32)); - } - - public void testReadBits() throws Exception { - VorbisBitArray bitArray = new VorbisBitArray(TestUtil.createByteArray(0x03, 0x22)); - assertEquals(3, bitArray.readBits(2)); - bitArray.skipBits(6); - assertEquals(2, bitArray.readBits(2)); - bitArray.skipBits(2); - assertEquals(2, bitArray.readBits(2)); - bitArray.reset(); - assertEquals(0x2203, bitArray.readBits(16)); - } - - public void testRead4BitsBeyondBoundary() throws Exception { - VorbisBitArray bitArray = new VorbisBitArray(TestUtil.createByteArray(0x2e, 0x10)); - assertEquals(0x2e, bitArray.readBits(7)); - assertEquals(7, bitArray.getPosition()); - assertEquals(0x0, bitArray.readBits(4)); - } - - public void testReadBitsBeyondByteBoundaries() throws Exception { - VorbisBitArray bitArray = new VorbisBitArray(TestUtil.createByteArray(0xFF, 0x0F, 0xFF, 0x0F)); - assertEquals(0x0FFF0FFF, bitArray.readBits(32)); - - bitArray.reset(); - bitArray.skipBits(4); - assertEquals(0xF0FF, bitArray.readBits(16)); - - bitArray.reset(); - bitArray.skipBits(6); - assertEquals(0xc3F, bitArray.readBits(12)); - - bitArray.reset(); - bitArray.skipBits(6); - assertTrue(bitArray.readBit()); - assertTrue(bitArray.readBit()); - assertEquals(24, bitArray.bitsLeft()); - - bitArray.reset(); - bitArray.skipBits(10); - assertEquals(3, bitArray.readBits(5)); - assertEquals(15, bitArray.getPosition()); - } - - public void testReadBitsIllegalLengths() throws Exception { - VorbisBitArray bitArray = new VorbisBitArray(TestUtil.createByteArray(0x03, 0x22, 0x30)); - - // reading zero bits gets 0 without advancing position - // (like a zero-bit read is defined to yield zer0) - assertEquals(0, bitArray.readBits(0)); - assertEquals(0, bitArray.getPosition()); - bitArray.readBit(); - assertEquals(1, bitArray.getPosition()); - } - -} diff --git a/library/core/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 deleted file mode 100644 index c3165b34f6..0000000000 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/VorbisReaderTest.java +++ /dev/null @@ -1,104 +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.ogg; - -import com.google.android.exoplayer2.extractor.ExtractorInput; -import com.google.android.exoplayer2.extractor.ogg.VorbisReader.VorbisSetup; -import com.google.android.exoplayer2.testutil.FakeExtractorInput; -import com.google.android.exoplayer2.testutil.FakeExtractorInput.SimulatedIOException; -import com.google.android.exoplayer2.util.ParsableByteArray; -import java.io.IOException; -import junit.framework.TestCase; - -/** - * Unit test for {@link VorbisReader}. - */ -public final class VorbisReaderTest extends TestCase { - - public void testReadBits() throws Exception { - assertEquals(0, VorbisReader.readBits((byte) 0x00, 2, 2)); - assertEquals(1, VorbisReader.readBits((byte) 0x02, 1, 1)); - assertEquals(15, VorbisReader.readBits((byte) 0xF0, 4, 4)); - assertEquals(1, VorbisReader.readBits((byte) 0x80, 1, 7)); - } - - public void testAppendNumberOfSamples() throws Exception { - ParsableByteArray buffer = new ParsableByteArray(4); - buffer.setLimit(0); - VorbisReader.appendNumberOfSamples(buffer, 0x01234567); - assertEquals(4, buffer.limit()); - assertEquals(0x67, buffer.data[0]); - assertEquals(0x45, buffer.data[1]); - assertEquals(0x23, buffer.data[2]); - assertEquals(0x01, buffer.data[3]); - } - - public void testReadSetupHeadersWithIOExceptions() throws IOException, InterruptedException { - byte[] data = TestData.getVorbisHeaderPages(); - ExtractorInput input = new FakeExtractorInput.Builder().setData(data).setSimulateIOErrors(true) - .setSimulateUnknownLength(true).setSimulatePartialReads(true).build(); - - VorbisReader reader = new VorbisReader(); - VorbisReader.VorbisSetup vorbisSetup = readSetupHeaders(reader, input); - - assertNotNull(vorbisSetup.idHeader); - assertNotNull(vorbisSetup.commentHeader); - assertNotNull(vorbisSetup.setupHeaderData); - assertNotNull(vorbisSetup.modes); - - assertEquals(45, vorbisSetup.commentHeader.length); - assertEquals(30, vorbisSetup.idHeader.data.length); - assertEquals(3597, vorbisSetup.setupHeaderData.length); - - assertEquals(-1, vorbisSetup.idHeader.bitrateMax); - assertEquals(-1, vorbisSetup.idHeader.bitrateMin); - assertEquals(66666, vorbisSetup.idHeader.bitrateNominal); - assertEquals(512, vorbisSetup.idHeader.blockSize0); - assertEquals(1024, vorbisSetup.idHeader.blockSize1); - assertEquals(2, vorbisSetup.idHeader.channels); - assertTrue(vorbisSetup.idHeader.framingFlag); - assertEquals(22050, vorbisSetup.idHeader.sampleRate); - assertEquals(0, vorbisSetup.idHeader.version); - - assertEquals("Xiph.Org libVorbis I 20030909", vorbisSetup.commentHeader.vendor); - assertEquals(1, vorbisSetup.iLogModes); - - assertEquals(data[data.length - 1], - vorbisSetup.setupHeaderData[vorbisSetup.setupHeaderData.length - 1]); - - assertFalse(vorbisSetup.modes[0].blockFlag); - assertTrue(vorbisSetup.modes[1].blockFlag); - } - - private static VorbisSetup readSetupHeaders(VorbisReader reader, ExtractorInput input) - throws IOException, InterruptedException { - OggPacket oggPacket = new OggPacket(); - while (true) { - try { - if (!oggPacket.populate(input)) { - fail(); - } - VorbisSetup vorbisSetup = reader.readSetupHeaders(oggPacket.getPayload()); - if (vorbisSetup != null) { - return vorbisSetup; - } - } catch (SimulatedIOException e) { - // Ignore. - } - } - } - -} diff --git a/library/core/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 deleted file mode 100644 index ddbfee8446..0000000000 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/VorbisUtilTest.java +++ /dev/null @@ -1,126 +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.ogg; - -import com.google.android.exoplayer2.ParserException; -import com.google.android.exoplayer2.util.ParsableByteArray; -import junit.framework.TestCase; - -/** - * Unit test for {@link VorbisUtil}. - */ -public final class VorbisUtilTest extends TestCase { - - public void testILog() throws Exception { - assertEquals(0, VorbisUtil.iLog(0)); - assertEquals(1, VorbisUtil.iLog(1)); - assertEquals(2, VorbisUtil.iLog(2)); - assertEquals(2, VorbisUtil.iLog(3)); - assertEquals(3, VorbisUtil.iLog(4)); - assertEquals(3, VorbisUtil.iLog(5)); - assertEquals(4, VorbisUtil.iLog(8)); - assertEquals(0, VorbisUtil.iLog(-1)); - assertEquals(0, VorbisUtil.iLog(-122)); - } - - public void testReadIdHeader() throws Exception { - byte[] data = TestData.getIdentificationHeaderData(); - ParsableByteArray headerData = new ParsableByteArray(data, data.length); - VorbisUtil.VorbisIdHeader vorbisIdHeader = - VorbisUtil.readVorbisIdentificationHeader(headerData); - - assertEquals(22050, vorbisIdHeader.sampleRate); - assertEquals(0, vorbisIdHeader.version); - assertTrue(vorbisIdHeader.framingFlag); - assertEquals(2, vorbisIdHeader.channels); - assertEquals(512, vorbisIdHeader.blockSize0); - assertEquals(1024, vorbisIdHeader.blockSize1); - assertEquals(-1, vorbisIdHeader.bitrateMax); - assertEquals(-1, vorbisIdHeader.bitrateMin); - assertEquals(66666, vorbisIdHeader.bitrateNominal); - assertEquals(66666, vorbisIdHeader.getApproximateBitrate()); - } - - public void testReadCommentHeader() throws ParserException { - byte[] data = TestData.getCommentHeaderDataUTF8(); - ParsableByteArray headerData = new ParsableByteArray(data, data.length); - VorbisUtil.CommentHeader commentHeader = VorbisUtil.readVorbisCommentHeader(headerData); - - assertEquals("Xiph.Org libVorbis I 20120203 (Omnipresent)", commentHeader.vendor); - assertEquals(3, commentHeader.comments.length); - assertEquals("ALBUM=äö", commentHeader.comments[0]); - assertEquals("TITLE=A sample song", commentHeader.comments[1]); - assertEquals("ARTIST=Google", commentHeader.comments[2]); - } - - public void testReadVorbisModes() throws ParserException { - byte[] data = TestData.getSetupHeaderData(); - ParsableByteArray headerData = new ParsableByteArray(data, data.length); - VorbisUtil.Mode[] modes = VorbisUtil.readVorbisModes(headerData, 2); - - assertEquals(2, modes.length); - assertEquals(false, modes[0].blockFlag); - assertEquals(0, modes[0].mapping); - assertEquals(0, modes[0].transformType); - assertEquals(0, modes[0].windowType); - assertEquals(true, modes[1].blockFlag); - assertEquals(1, modes[1].mapping); - assertEquals(0, modes[1].transformType); - assertEquals(0, modes[1].windowType); - } - - public void testVerifyVorbisHeaderCapturePattern() throws ParserException { - ParsableByteArray header = new ParsableByteArray( - new byte[] {0x01, 'v', 'o', 'r', 'b', 'i', 's'}); - assertEquals(true, VorbisUtil.verifyVorbisHeaderCapturePattern(0x01, header, false)); - } - - public void testVerifyVorbisHeaderCapturePatternInvalidHeader() { - ParsableByteArray header = new ParsableByteArray( - new byte[] {0x01, 'v', 'o', 'r', 'b', 'i', 's'}); - try { - VorbisUtil.verifyVorbisHeaderCapturePattern(0x99, header, false); - fail(); - } catch (ParserException e) { - assertEquals("expected header type 99", e.getMessage()); - } - } - - public void testVerifyVorbisHeaderCapturePatternInvalidHeaderQuite() throws ParserException { - ParsableByteArray header = new ParsableByteArray( - new byte[] {0x01, 'v', 'o', 'r', 'b', 'i', 's'}); - assertFalse(VorbisUtil.verifyVorbisHeaderCapturePattern(0x99, header, true)); - } - - public void testVerifyVorbisHeaderCapturePatternInvalidPattern() { - ParsableByteArray header = new ParsableByteArray( - new byte[] {0x01, 'x', 'v', 'o', 'r', 'b', 'i', 's'}); - try { - VorbisUtil.verifyVorbisHeaderCapturePattern(0x01, header, false); - fail(); - } catch (ParserException e) { - assertEquals("expected characters 'vorbis'", e.getMessage()); - } - } - - public void testVerifyVorbisHeaderCapturePatternQuiteInvalidPatternQuite() - throws ParserException { - ParsableByteArray header = new ParsableByteArray( - new byte[] {0x01, 'x', 'v', 'o', 'r', 'b', 'i', 's'}); - assertFalse(VorbisUtil.verifyVorbisHeaderCapturePattern(0x01, header, true)); - } - -} diff --git a/library/core/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 deleted file mode 100644 index 1690371a47..0000000000 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/ttml/TtmlStyleTest.java +++ /dev/null @@ -1,132 +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.ttml; - -import android.graphics.Color; -import android.test.InstrumentationTestCase; - -/** - * Unit test for {@link TtmlStyle}. - */ -public final class TtmlStyleTest extends InstrumentationTestCase { - - private static final String FONT_FAMILY = "serif"; - private static final String ID = "id"; - public static final int FOREGROUND_COLOR = Color.WHITE; - public static final int BACKGROUND_COLOR = Color.BLACK; - private TtmlStyle style; - - @Override - public void setUp() throws Exception { - super.setUp(); - style = new TtmlStyle(); - } - - public void testInheritStyle() { - style.inherit(createAncestorStyle()); - assertNull("id must not be inherited", style.getId()); - assertTrue(style.isUnderline()); - assertTrue(style.isLinethrough()); - assertEquals(TtmlStyle.STYLE_BOLD_ITALIC, style.getStyle()); - assertEquals(FONT_FAMILY, style.getFontFamily()); - assertEquals(Color.WHITE, style.getFontColor()); - assertFalse("do not inherit backgroundColor", style.hasBackgroundColor()); - } - - public void testChainStyle() { - style.chain(createAncestorStyle()); - assertNull("id must not be inherited", style.getId()); - assertTrue(style.isUnderline()); - assertTrue(style.isLinethrough()); - assertEquals(TtmlStyle.STYLE_BOLD_ITALIC, style.getStyle()); - assertEquals(FONT_FAMILY, style.getFontFamily()); - assertEquals(FOREGROUND_COLOR, style.getFontColor()); - // do inherit backgroundColor when chaining - assertEquals("do not inherit backgroundColor when chaining", - BACKGROUND_COLOR, style.getBackgroundColor()); - } - - private TtmlStyle createAncestorStyle() { - TtmlStyle ancestor = new TtmlStyle(); - ancestor.setId(ID); - ancestor.setItalic(true); - ancestor.setBold(true); - ancestor.setBackgroundColor(BACKGROUND_COLOR); - ancestor.setFontColor(FOREGROUND_COLOR); - ancestor.setLinethrough(true); - ancestor.setUnderline(true); - ancestor.setFontFamily(FONT_FAMILY); - return ancestor; - } - - public void testStyle() { - assertEquals(TtmlStyle.UNSPECIFIED, style.getStyle()); - style.setItalic(true); - assertEquals(TtmlStyle.STYLE_ITALIC, style.getStyle()); - style.setBold(true); - assertEquals(TtmlStyle.STYLE_BOLD_ITALIC, style.getStyle()); - style.setItalic(false); - assertEquals(TtmlStyle.STYLE_BOLD, style.getStyle()); - style.setBold(false); - assertEquals(TtmlStyle.STYLE_NORMAL, style.getStyle()); - } - - public void testLinethrough() { - assertFalse(style.isLinethrough()); - style.setLinethrough(true); - assertTrue(style.isLinethrough()); - style.setLinethrough(false); - assertFalse(style.isLinethrough()); - } - - public void testUnderline() { - assertFalse(style.isUnderline()); - style.setUnderline(true); - assertTrue(style.isUnderline()); - style.setUnderline(false); - assertFalse(style.isUnderline()); - } - - public void testFontFamily() { - assertNull(style.getFontFamily()); - style.setFontFamily(FONT_FAMILY); - assertEquals(FONT_FAMILY, style.getFontFamily()); - style.setFontFamily(null); - assertNull(style.getFontFamily()); - } - - public void testColor() { - assertFalse(style.hasFontColor()); - style.setFontColor(Color.BLACK); - assertEquals(Color.BLACK, style.getFontColor()); - assertTrue(style.hasFontColor()); - } - - public void testBackgroundColor() { - assertFalse(style.hasBackgroundColor()); - style.setBackgroundColor(Color.BLACK); - assertEquals(Color.BLACK, style.getBackgroundColor()); - assertTrue(style.hasBackgroundColor()); - } - - public void testId() { - assertNull(style.getId()); - style.setId(ID); - assertEquals(ID, style.getId()); - style.setId(null); - assertNull(style.getId()); - } -} diff --git a/library/core/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 index f2e199578c..472b5c724b 100644 --- a/library/core/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 @@ -17,11 +17,11 @@ package com.google.android.exoplayer2.upstream.cache; import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.extractor.ChunkIndex; -import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.util.Util; import java.io.File; import java.io.IOException; import org.mockito.Mock; +import org.mockito.MockitoAnnotations; /** * Tests for {@link CachedRegionTracker}. @@ -46,10 +46,9 @@ public final class CachedRegionTrackerTest extends InstrumentationTestCase { @Override protected void setUp() throws Exception { - TestUtil.setUpMockito(this); + setUpMockito(this); tracker = new CachedRegionTracker(cache, CACHE_KEY, CHUNK_INDEX); - cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest"); index = new CachedContentIndex(cacheDir); } @@ -124,4 +123,14 @@ public final class CachedRegionTrackerTest extends InstrumentationTestCase { return SimpleCacheSpanTest.createCacheSpan(index, cacheDir, CACHE_KEY, position, length, 0); } + /** + * Sets up Mockito for an instrumentation test. + */ + private static void setUpMockito(InstrumentationTestCase instrumentationTestCase) { + // Workaround for https://code.google.com/p/dexmaker/issues/detail?id=2. + System.setProperty("dexmaker.dexcache", + instrumentationTestCase.getInstrumentation().getTargetContext().getCacheDir().getPath()); + MockitoAnnotations.initMocks(instrumentationTestCase); + } + } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java b/library/core/src/test/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java similarity index 61% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java index 8a2c24beba..b6c068c218 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java @@ -16,20 +16,27 @@ package com.google.android.exoplayer2.drm; import static com.google.android.exoplayer2.C.PLAYREADY_UUID; +import static com.google.android.exoplayer2.C.UUID_NIL; import static com.google.android.exoplayer2.C.WIDEVINE_UUID; import static com.google.android.exoplayer2.util.MimeTypes.VIDEO_MP4; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; import android.os.Parcel; -import android.test.MoreAsserts; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; import com.google.android.exoplayer2.testutil.TestUtil; -import junit.framework.TestCase; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; /** * Unit test for {@link DrmInitData}. */ -public class DrmInitDataTest extends TestCase { +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public class DrmInitDataTest { private static final SchemeData DATA_1 = new SchemeData(WIDEVINE_UUID, VIDEO_MP4, TestUtil.buildTestData(128, 1 /* data seed */)); @@ -42,6 +49,7 @@ public class DrmInitDataTest extends TestCase { private static final SchemeData DATA_UNIVERSAL = new SchemeData(C.UUID_NIL, VIDEO_MP4, TestUtil.buildTestData(128, 3 /* data seed */)); + @Test public void testParcelable() { DrmInitData drmInitDataToParcel = new DrmInitData(DATA_1, DATA_2); @@ -50,69 +58,72 @@ public class DrmInitDataTest extends TestCase { parcel.setDataPosition(0); DrmInitData drmInitDataFromParcel = DrmInitData.CREATOR.createFromParcel(parcel); - assertEquals(drmInitDataToParcel, drmInitDataFromParcel); + assertThat(drmInitDataFromParcel).isEqualTo(drmInitDataToParcel); parcel.recycle(); } + @Test public void testEquals() { DrmInitData drmInitData = new DrmInitData(DATA_1, DATA_2); // Basic non-referential equality test. DrmInitData testInitData = new DrmInitData(DATA_1, DATA_2); - assertEquals(drmInitData, testInitData); - assertEquals(drmInitData.hashCode(), testInitData.hashCode()); + assertThat(testInitData).isEqualTo(drmInitData); + assertThat(testInitData.hashCode()).isEqualTo(drmInitData.hashCode()); // Basic non-referential equality test with non-referential scheme data. testInitData = new DrmInitData(DATA_1B, DATA_2B); - assertEquals(drmInitData, testInitData); - assertEquals(drmInitData.hashCode(), testInitData.hashCode()); + assertThat(testInitData).isEqualTo(drmInitData); + assertThat(testInitData.hashCode()).isEqualTo(drmInitData.hashCode()); // Passing the scheme data in reverse order shouldn't affect equality. testInitData = new DrmInitData(DATA_2, DATA_1); - assertEquals(drmInitData, testInitData); - assertEquals(drmInitData.hashCode(), testInitData.hashCode()); + assertThat(testInitData).isEqualTo(drmInitData); + assertThat(testInitData.hashCode()).isEqualTo(drmInitData.hashCode()); // Ditto. testInitData = new DrmInitData(DATA_2B, DATA_1B); - assertEquals(drmInitData, testInitData); - assertEquals(drmInitData.hashCode(), testInitData.hashCode()); + assertThat(testInitData).isEqualTo(drmInitData); + assertThat(testInitData.hashCode()).isEqualTo(drmInitData.hashCode()); // Different number of tuples should affect equality. testInitData = new DrmInitData(DATA_1); - MoreAsserts.assertNotEqual(drmInitData, testInitData); + assertThat(drmInitData).isNotEqualTo(testInitData); // Different data in one of the tuples should affect equality. testInitData = new DrmInitData(DATA_1, DATA_UNIVERSAL); - MoreAsserts.assertNotEqual(drmInitData, testInitData); + assertThat(testInitData).isNotEqualTo(drmInitData); } + @Test public void testGet() { // Basic matching. DrmInitData testInitData = new DrmInitData(DATA_1, DATA_2); - assertEquals(DATA_1, testInitData.get(WIDEVINE_UUID)); - assertEquals(DATA_2, testInitData.get(PLAYREADY_UUID)); - assertNull(testInitData.get(C.UUID_NIL)); + assertThat(testInitData.get(WIDEVINE_UUID)).isEqualTo(DATA_1); + assertThat(testInitData.get(PLAYREADY_UUID)).isEqualTo(DATA_2); + assertThat(testInitData.get(UUID_NIL)).isNull(); // Basic matching including universal data. testInitData = new DrmInitData(DATA_1, DATA_2, DATA_UNIVERSAL); - assertEquals(DATA_1, testInitData.get(WIDEVINE_UUID)); - assertEquals(DATA_2, testInitData.get(PLAYREADY_UUID)); - assertEquals(DATA_UNIVERSAL, testInitData.get(C.UUID_NIL)); + assertThat(testInitData.get(WIDEVINE_UUID)).isEqualTo(DATA_1); + assertThat(testInitData.get(PLAYREADY_UUID)).isEqualTo(DATA_2); + assertThat(testInitData.get(UUID_NIL)).isEqualTo(DATA_UNIVERSAL); // Passing the scheme data in reverse order shouldn't affect equality. testInitData = new DrmInitData(DATA_UNIVERSAL, DATA_2, DATA_1); - assertEquals(DATA_1, testInitData.get(WIDEVINE_UUID)); - assertEquals(DATA_2, testInitData.get(PLAYREADY_UUID)); - assertEquals(DATA_UNIVERSAL, testInitData.get(C.UUID_NIL)); + assertThat(testInitData.get(WIDEVINE_UUID)).isEqualTo(DATA_1); + assertThat(testInitData.get(PLAYREADY_UUID)).isEqualTo(DATA_2); + assertThat(testInitData.get(UUID_NIL)).isEqualTo(DATA_UNIVERSAL); // Universal data should be returned in the absence of a specific match. testInitData = new DrmInitData(DATA_1, DATA_UNIVERSAL); - assertEquals(DATA_1, testInitData.get(WIDEVINE_UUID)); - assertEquals(DATA_UNIVERSAL, testInitData.get(PLAYREADY_UUID)); - assertEquals(DATA_UNIVERSAL, testInitData.get(C.UUID_NIL)); + assertThat(testInitData.get(WIDEVINE_UUID)).isEqualTo(DATA_1); + assertThat(testInitData.get(PLAYREADY_UUID)).isEqualTo(DATA_UNIVERSAL); + assertThat(testInitData.get(UUID_NIL)).isEqualTo(DATA_UNIVERSAL); } + @Test public void testDuplicateSchemeDataRejected() { try { new DrmInitData(DATA_1, DATA_1); @@ -136,18 +147,19 @@ public class DrmInitDataTest extends TestCase { } } + @Test public void testSchemeDataMatches() { - assertTrue(DATA_1.matches(WIDEVINE_UUID)); - assertFalse(DATA_1.matches(PLAYREADY_UUID)); - assertFalse(DATA_2.matches(C.UUID_NIL)); + assertThat(DATA_1.matches(WIDEVINE_UUID)).isTrue(); + assertThat(DATA_1.matches(PLAYREADY_UUID)).isFalse(); + assertThat(DATA_2.matches(UUID_NIL)).isFalse(); - assertFalse(DATA_2.matches(WIDEVINE_UUID)); - assertTrue(DATA_2.matches(PLAYREADY_UUID)); - assertFalse(DATA_2.matches(C.UUID_NIL)); + assertThat(DATA_2.matches(WIDEVINE_UUID)).isFalse(); + assertThat(DATA_2.matches(PLAYREADY_UUID)).isTrue(); + assertThat(DATA_2.matches(UUID_NIL)).isFalse(); - assertTrue(DATA_UNIVERSAL.matches(WIDEVINE_UUID)); - assertTrue(DATA_UNIVERSAL.matches(PLAYREADY_UUID)); - assertTrue(DATA_UNIVERSAL.matches(C.UUID_NIL)); + assertThat(DATA_UNIVERSAL.matches(WIDEVINE_UUID)).isTrue(); + assertThat(DATA_UNIVERSAL.matches(PLAYREADY_UUID)).isTrue(); + assertThat(DATA_UNIVERSAL.matches(UUID_NIL)).isTrue(); } } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/DefaultExtractorInputTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/DefaultExtractorInputTest.java similarity index 78% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/DefaultExtractorInputTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/extractor/DefaultExtractorInputTest.java index 6abd116086..8e27c4f7ca 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/DefaultExtractorInputTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/DefaultExtractorInputTest.java @@ -15,6 +15,12 @@ */ package com.google.android.exoplayer2.extractor; +import static com.google.android.exoplayer2.C.RESULT_END_OF_INPUT; +import static com.google.common.truth.Truth.assertThat; +import static java.util.Arrays.copyOf; +import static java.util.Arrays.copyOfRange; +import static org.junit.Assert.fail; + import android.net.Uri; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.testutil.FakeDataSource; @@ -22,42 +28,50 @@ import com.google.android.exoplayer2.upstream.DataSpec; import java.io.EOFException; import java.io.IOException; import java.util.Arrays; -import junit.framework.TestCase; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; /** * Test for {@link DefaultExtractorInput}. */ -public class DefaultExtractorInputTest extends TestCase { +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public class DefaultExtractorInputTest { private static final String TEST_URI = "http://www.google.com"; private static final byte[] TEST_DATA = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8}; private static final int LARGE_TEST_DATA_LENGTH = 8192; + @Test public void testInitialPosition() throws Exception { FakeDataSource testDataSource = buildDataSource(); DefaultExtractorInput input = new DefaultExtractorInput(testDataSource, 123, C.LENGTH_UNSET); - assertEquals(123, input.getPosition()); + assertThat(input.getPosition()).isEqualTo(123); } + @Test public void testRead() throws Exception { DefaultExtractorInput input = createDefaultExtractorInput(); byte[] target = new byte[TEST_DATA.length]; // We expect to perform three reads of three bytes, as setup in buildTestDataSource. int bytesRead = 0; bytesRead += input.read(target, 0, TEST_DATA.length); - assertEquals(3, bytesRead); + assertThat(bytesRead).isEqualTo(3); bytesRead += input.read(target, 3, TEST_DATA.length); - assertEquals(6, bytesRead); + assertThat(bytesRead).isEqualTo(6); bytesRead += input.read(target, 6, TEST_DATA.length); - assertEquals(9, bytesRead); + assertThat(bytesRead).isEqualTo(9); // Check the read data is correct. - assertTrue(Arrays.equals(TEST_DATA, target)); + assertThat(Arrays.equals(TEST_DATA, target)).isTrue(); // Check we're now indicated that the end of input is reached. int expectedEndOfInput = input.read(target, 0, TEST_DATA.length); - assertEquals(C.RESULT_END_OF_INPUT, expectedEndOfInput); + assertThat(expectedEndOfInput).isEqualTo(RESULT_END_OF_INPUT); } + @Test public void testReadPeeked() throws Exception { DefaultExtractorInput input = createDefaultExtractorInput(); byte[] target = new byte[TEST_DATA.length]; @@ -65,12 +79,13 @@ public class DefaultExtractorInputTest extends TestCase { input.advancePeekPosition(TEST_DATA.length); int bytesRead = input.read(target, 0, TEST_DATA.length); - assertEquals(TEST_DATA.length, bytesRead); + assertThat(bytesRead).isEqualTo(TEST_DATA.length); // Check the read data is correct. - assertTrue(Arrays.equals(TEST_DATA, target)); + assertThat(Arrays.equals(TEST_DATA, target)).isTrue(); } + @Test public void testReadMoreDataPeeked() throws Exception { DefaultExtractorInput input = createDefaultExtractorInput(); byte[] target = new byte[TEST_DATA.length]; @@ -78,22 +93,23 @@ public class DefaultExtractorInputTest extends TestCase { input.advancePeekPosition(TEST_DATA.length); int bytesRead = input.read(target, 0, TEST_DATA.length + 1); - assertEquals(TEST_DATA.length, bytesRead); + assertThat(bytesRead).isEqualTo(TEST_DATA.length); // Check the read data is correct. - assertTrue(Arrays.equals(TEST_DATA, target)); + assertThat(Arrays.equals(TEST_DATA, target)).isTrue(); } + @Test public void testReadFullyOnce() throws Exception { DefaultExtractorInput input = createDefaultExtractorInput(); byte[] target = new byte[TEST_DATA.length]; input.readFully(target, 0, TEST_DATA.length); // Check that we read the whole of TEST_DATA. - assertTrue(Arrays.equals(TEST_DATA, target)); - assertEquals(TEST_DATA.length, input.getPosition()); + assertThat(Arrays.equals(TEST_DATA, target)).isTrue(); + assertThat(input.getPosition()).isEqualTo(TEST_DATA.length); // Check that we see end of input if we read again with allowEndOfInput set. boolean result = input.readFully(target, 0, 1, true); - assertFalse(result); + assertThat(result).isFalse(); // Check that we fail with EOFException we read again with allowEndOfInput unset. try { input.readFully(target, 0, 1); @@ -103,19 +119,21 @@ public class DefaultExtractorInputTest extends TestCase { } } + @Test public void testReadFullyTwice() throws Exception { // Read TEST_DATA in two parts. DefaultExtractorInput input = createDefaultExtractorInput(); byte[] target = new byte[5]; input.readFully(target, 0, 5); - assertTrue(Arrays.equals(Arrays.copyOf(TEST_DATA, 5), target)); - assertEquals(5, input.getPosition()); + assertThat(Arrays.equals(copyOf(TEST_DATA, 5), target)).isTrue(); + assertThat(input.getPosition()).isEqualTo(5); target = new byte[4]; input.readFully(target, 0, 4); - assertTrue(Arrays.equals(Arrays.copyOfRange(TEST_DATA, 5, 9), target)); - assertEquals(5 + 4, input.getPosition()); + assertThat(Arrays.equals(copyOfRange(TEST_DATA, 5, 9), target)).isTrue(); + assertThat(input.getPosition()).isEqualTo(5 + 4); } + @Test public void testReadFullyTooMuch() throws Exception { // Read more than TEST_DATA. Should fail with an EOFException. Position should not update. DefaultExtractorInput input = createDefaultExtractorInput(); @@ -126,7 +144,7 @@ public class DefaultExtractorInputTest extends TestCase { } catch (EOFException e) { // Expected. } - assertEquals(0, input.getPosition()); + assertThat(input.getPosition()).isEqualTo(0); // Read more than TEST_DATA with allowEndOfInput set. Should fail with an EOFException because // the end of input isn't encountered immediately. Position should not update. @@ -138,9 +156,10 @@ public class DefaultExtractorInputTest extends TestCase { } catch (EOFException e) { // Expected. } - assertEquals(0, input.getPosition()); + assertThat(input.getPosition()).isEqualTo(0); } + @Test public void testReadFullyWithFailingDataSource() throws Exception { FakeDataSource testDataSource = buildFailingDataSource(); DefaultExtractorInput input = new DefaultExtractorInput(testDataSource, 0, C.LENGTH_UNSET); @@ -152,9 +171,10 @@ public class DefaultExtractorInputTest extends TestCase { // Expected. } // The position should not have advanced. - assertEquals(0, input.getPosition()); + assertThat(input.getPosition()).isEqualTo(0); } + @Test public void testReadFullyHalfPeeked() throws Exception { DefaultExtractorInput input = createDefaultExtractorInput(); byte[] target = new byte[TEST_DATA.length]; @@ -164,22 +184,24 @@ public class DefaultExtractorInputTest extends TestCase { input.readFully(target, 0, TEST_DATA.length); // Check the read data is correct. - assertTrue(Arrays.equals(TEST_DATA, target)); - assertEquals(TEST_DATA.length, input.getPosition()); + assertThat(Arrays.equals(TEST_DATA, target)).isTrue(); + assertThat(input.getPosition()).isEqualTo(TEST_DATA.length); } + @Test public void testSkip() throws Exception { FakeDataSource testDataSource = buildDataSource(); DefaultExtractorInput input = new DefaultExtractorInput(testDataSource, 0, C.LENGTH_UNSET); // We expect to perform three skips of three bytes, as setup in buildTestDataSource. for (int i = 0; i < 3; i++) { - assertEquals(3, input.skip(TEST_DATA.length)); + assertThat(input.skip(TEST_DATA.length)).isEqualTo(3); } // Check we're now indicated that the end of input is reached. int expectedEndOfInput = input.skip(TEST_DATA.length); - assertEquals(C.RESULT_END_OF_INPUT, expectedEndOfInput); + assertThat(expectedEndOfInput).isEqualTo(RESULT_END_OF_INPUT); } + @Test public void testLargeSkip() throws Exception { FakeDataSource testDataSource = buildLargeDataSource(); DefaultExtractorInput input = new DefaultExtractorInput(testDataSource, 0, C.LENGTH_UNSET); @@ -190,14 +212,15 @@ public class DefaultExtractorInputTest extends TestCase { } } + @Test public void testSkipFullyOnce() throws Exception { // Skip TEST_DATA. DefaultExtractorInput input = createDefaultExtractorInput(); input.skipFully(TEST_DATA.length); - assertEquals(TEST_DATA.length, input.getPosition()); + assertThat(input.getPosition()).isEqualTo(TEST_DATA.length); // Check that we see end of input if we skip again with allowEndOfInput set. boolean result = input.skipFully(1, true); - assertFalse(result); + assertThat(result).isFalse(); // Check that we fail with EOFException we skip again. try { input.skipFully(1); @@ -207,15 +230,17 @@ public class DefaultExtractorInputTest extends TestCase { } } + @Test public void testSkipFullyTwice() throws Exception { // Skip TEST_DATA in two parts. DefaultExtractorInput input = createDefaultExtractorInput(); input.skipFully(5); - assertEquals(5, input.getPosition()); + assertThat(input.getPosition()).isEqualTo(5); input.skipFully(4); - assertEquals(5 + 4, input.getPosition()); + assertThat(input.getPosition()).isEqualTo(5 + 4); } + @Test public void testSkipFullyTwicePeeked() throws Exception { // Skip TEST_DATA. DefaultExtractorInput input = createDefaultExtractorInput(); @@ -224,12 +249,13 @@ public class DefaultExtractorInputTest extends TestCase { int halfLength = TEST_DATA.length / 2; input.skipFully(halfLength); - assertEquals(halfLength, input.getPosition()); + assertThat(input.getPosition()).isEqualTo(halfLength); input.skipFully(TEST_DATA.length - halfLength); - assertEquals(TEST_DATA.length, input.getPosition()); + assertThat(input.getPosition()).isEqualTo(TEST_DATA.length); } + @Test public void testSkipFullyTooMuch() throws Exception { // Skip more than TEST_DATA. Should fail with an EOFException. Position should not update. DefaultExtractorInput input = createDefaultExtractorInput(); @@ -239,7 +265,7 @@ public class DefaultExtractorInputTest extends TestCase { } catch (EOFException e) { // Expected. } - assertEquals(0, input.getPosition()); + assertThat(input.getPosition()).isEqualTo(0); // Skip more than TEST_DATA with allowEndOfInput set. Should fail with an EOFException because // the end of input isn't encountered immediately. Position should not update. @@ -250,9 +276,10 @@ public class DefaultExtractorInputTest extends TestCase { } catch (EOFException e) { // Expected. } - assertEquals(0, input.getPosition()); + assertThat(input.getPosition()).isEqualTo(0); } + @Test public void testSkipFullyWithFailingDataSource() throws Exception { FakeDataSource testDataSource = buildFailingDataSource(); DefaultExtractorInput input = new DefaultExtractorInput(testDataSource, 0, C.LENGTH_UNSET); @@ -263,9 +290,10 @@ public class DefaultExtractorInputTest extends TestCase { // Expected. } // The position should not have advanced. - assertEquals(0, input.getPosition()); + assertThat(input.getPosition()).isEqualTo(0); } + @Test public void testSkipFullyLarge() throws Exception { // Tests skipping an amount of data that's larger than any internal scratch space. int largeSkipSize = 1024 * 1024; @@ -275,7 +303,7 @@ public class DefaultExtractorInputTest extends TestCase { DefaultExtractorInput input = new DefaultExtractorInput(testDataSource, 0, C.LENGTH_UNSET); input.skipFully(largeSkipSize); - assertEquals(largeSkipSize, input.getPosition()); + assertThat(input.getPosition()).isEqualTo(largeSkipSize); // Check that we fail with EOFException we skip again. try { input.skipFully(1); @@ -285,22 +313,23 @@ public class DefaultExtractorInputTest extends TestCase { } } + @Test public void testPeekFully() throws Exception { DefaultExtractorInput input = createDefaultExtractorInput(); byte[] target = new byte[TEST_DATA.length]; input.peekFully(target, 0, TEST_DATA.length); // Check that we read the whole of TEST_DATA. - assertTrue(Arrays.equals(TEST_DATA, target)); - assertEquals(0, input.getPosition()); - assertEquals(TEST_DATA.length, input.getPeekPosition()); + assertThat(Arrays.equals(TEST_DATA, target)).isTrue(); + assertThat(input.getPosition()).isEqualTo(0); + assertThat(input.getPeekPosition()).isEqualTo(TEST_DATA.length); // Check that we can read again from the buffer byte[] target2 = new byte[TEST_DATA.length]; input.readFully(target2, 0, TEST_DATA.length); - assertTrue(Arrays.equals(TEST_DATA, target2)); - assertEquals(TEST_DATA.length, input.getPosition()); - assertEquals(TEST_DATA.length, input.getPeekPosition()); + assertThat(Arrays.equals(TEST_DATA, target2)).isTrue(); + assertThat(input.getPosition()).isEqualTo(TEST_DATA.length); + assertThat(input.getPeekPosition()).isEqualTo(TEST_DATA.length); // Check that we fail with EOFException if we peek again try { @@ -311,20 +340,21 @@ public class DefaultExtractorInputTest extends TestCase { } } + @Test public void testResetPeekPosition() throws Exception { DefaultExtractorInput input = createDefaultExtractorInput(); byte[] target = new byte[TEST_DATA.length]; input.peekFully(target, 0, TEST_DATA.length); // Check that we read the whole of TEST_DATA. - assertTrue(Arrays.equals(TEST_DATA, target)); - assertEquals(0, input.getPosition()); + assertThat(Arrays.equals(TEST_DATA, target)).isTrue(); + assertThat(input.getPosition()).isEqualTo(0); // Check that we can peek again after resetting. input.resetPeekPosition(); byte[] target2 = new byte[TEST_DATA.length]; input.peekFully(target2, 0, TEST_DATA.length); - assertTrue(Arrays.equals(TEST_DATA, target2)); + assertThat(Arrays.equals(TEST_DATA, target2)).isTrue(); // Check that we fail with EOFException if we peek past the end of the input. try { @@ -335,40 +365,43 @@ public class DefaultExtractorInputTest extends TestCase { } } + @Test public void testPeekFullyAtEndOfStreamWithAllowEndOfInputSucceeds() throws Exception { DefaultExtractorInput input = createDefaultExtractorInput(); byte[] target = new byte[TEST_DATA.length]; // Check peeking up to the end of input succeeds. - assertTrue(input.peekFully(target, 0, TEST_DATA.length, true)); + assertThat(input.peekFully(target, 0, TEST_DATA.length, true)).isTrue(); // Check peeking at the end of input with allowEndOfInput signals the end of input. - assertFalse(input.peekFully(target, 0, 1, true)); + assertThat(input.peekFully(target, 0, 1, true)).isFalse(); } + @Test public void testPeekFullyAtEndThenReadEndOfInput() throws Exception { DefaultExtractorInput input = createDefaultExtractorInput(); byte[] target = new byte[TEST_DATA.length]; // Peek up to the end of the input. - assertTrue(input.peekFully(target, 0, TEST_DATA.length, false)); + assertThat(input.peekFully(target, 0, TEST_DATA.length, false)).isTrue(); // Peek the end of the input. - assertFalse(input.peekFully(target, 0, 1, true)); + assertThat(input.peekFully(target, 0, 1, true)).isFalse(); // Read up to the end of the input. - assertTrue(input.readFully(target, 0, TEST_DATA.length, false)); + assertThat(input.readFully(target, 0, TEST_DATA.length, false)).isTrue(); // Read the end of the input. - assertFalse(input.readFully(target, 0, 1, true)); + assertThat(input.readFully(target, 0, 1, true)).isFalse(); } + @Test public void testPeekFullyAcrossEndOfInputWithAllowEndOfInputFails() throws Exception { DefaultExtractorInput input = createDefaultExtractorInput(); byte[] target = new byte[TEST_DATA.length]; // Check peeking before the end of input with allowEndOfInput succeeds. - assertTrue(input.peekFully(target, 0, TEST_DATA.length - 1, true)); + assertThat(input.peekFully(target, 0, TEST_DATA.length - 1, true)).isTrue(); // Check peeking across the end of input with allowEndOfInput throws. try { @@ -379,12 +412,13 @@ public class DefaultExtractorInputTest extends TestCase { } } + @Test public void testResetAndPeekFullyPastEndOfStreamWithAllowEndOfInputFails() throws Exception { DefaultExtractorInput input = createDefaultExtractorInput(); byte[] target = new byte[TEST_DATA.length]; // Check peeking up to the end of input succeeds. - assertTrue(input.peekFully(target, 0, TEST_DATA.length, true)); + assertThat(input.peekFully(target, 0, TEST_DATA.length, true)).isTrue(); input.resetPeekPosition(); try { // Check peeking one more byte throws. diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ExtractorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ExtractorTest.java similarity index 60% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ExtractorTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/extractor/ExtractorTest.java index 250ae8c513..fc31a7be73 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ExtractorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ExtractorTest.java @@ -15,20 +15,28 @@ */ package com.google.android.exoplayer2.extractor; +import static com.google.common.truth.Truth.assertThat; + import com.google.android.exoplayer2.C; -import junit.framework.TestCase; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; /** * Unit test for {@link Extractor}. */ -public class ExtractorTest extends TestCase { +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class ExtractorTest { - public static void testConstants() { + @Test + public void testConstants() { // Sanity check that constant values match those defined by {@link C}. - assertEquals(C.RESULT_END_OF_INPUT, Extractor.RESULT_END_OF_INPUT); + assertThat(Extractor.RESULT_END_OF_INPUT).isEqualTo(C.RESULT_END_OF_INPUT); // Sanity check that the other constant values don't overlap. - assertTrue(C.RESULT_END_OF_INPUT != Extractor.RESULT_CONTINUE); - assertTrue(C.RESULT_END_OF_INPUT != Extractor.RESULT_SEEK); + assertThat(C.RESULT_END_OF_INPUT != Extractor.RESULT_CONTINUE).isTrue(); + assertThat(C.RESULT_END_OF_INPUT != Extractor.RESULT_SEEK).isTrue(); } } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReaderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReaderTest.java similarity index 92% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReaderTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReaderTest.java index acc62f41f9..708ffde080 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReaderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mkv/DefaultEbmlReaderTest.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.extractor.mkv; +import static com.google.common.truth.Truth.assertThat; + import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.testutil.FakeExtractorInput; import com.google.android.exoplayer2.testutil.TestUtil; @@ -22,13 +24,19 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import junit.framework.TestCase; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; /** * Tests {@link DefaultEbmlReader}. */ -public class DefaultEbmlReaderTest extends TestCase { +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public class DefaultEbmlReaderTest { + @Test public void testMasterElement() throws IOException, InterruptedException { ExtractorInput input = createTestInput(0x1A, 0x45, 0xDF, 0xA3, 0x84, 0x42, 0x85, 0x81, 0x01); TestOutput expected = new TestOutput(); @@ -38,6 +46,7 @@ public class DefaultEbmlReaderTest extends TestCase { assertEvents(input, expected.events); } + @Test public void testMasterElementEmpty() throws IOException, InterruptedException { ExtractorInput input = createTestInput(0x18, 0x53, 0x80, 0x67, 0x80); TestOutput expected = new TestOutput(); @@ -46,6 +55,7 @@ public class DefaultEbmlReaderTest extends TestCase { assertEvents(input, expected.events); } + @Test public void testUnsignedIntegerElement() throws IOException, InterruptedException { // 0xFE is chosen because for signed integers it should be interpreted as -2 ExtractorInput input = createTestInput(0x42, 0xF7, 0x81, 0xFE); @@ -54,6 +64,7 @@ public class DefaultEbmlReaderTest extends TestCase { assertEvents(input, expected.events); } + @Test public void testUnsignedIntegerElementLarge() throws IOException, InterruptedException { ExtractorInput input = createTestInput(0x42, 0xF7, 0x88, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF); @@ -62,6 +73,7 @@ public class DefaultEbmlReaderTest extends TestCase { assertEvents(input, expected.events); } + @Test public void testUnsignedIntegerElementTooLargeBecomesNegative() throws IOException, InterruptedException { ExtractorInput input = @@ -71,6 +83,7 @@ public class DefaultEbmlReaderTest extends TestCase { assertEvents(input, expected.events); } + @Test public void testStringElement() throws IOException, InterruptedException { ExtractorInput input = createTestInput(0x42, 0x82, 0x86, 0x41, 0x62, 0x63, 0x31, 0x32, 0x33); TestOutput expected = new TestOutput(); @@ -78,6 +91,7 @@ public class DefaultEbmlReaderTest extends TestCase { assertEvents(input, expected.events); } + @Test public void testStringElementEmpty() throws IOException, InterruptedException { ExtractorInput input = createTestInput(0x42, 0x82, 0x80); TestOutput expected = new TestOutput(); @@ -85,6 +99,7 @@ public class DefaultEbmlReaderTest extends TestCase { assertEvents(input, expected.events); } + @Test public void testFloatElementFourBytes() throws IOException, InterruptedException { ExtractorInput input = createTestInput(0x44, 0x89, 0x84, 0x3F, 0x80, 0x00, 0x00); @@ -93,6 +108,7 @@ public class DefaultEbmlReaderTest extends TestCase { assertEvents(input, expected.events); } + @Test public void testFloatElementEightBytes() throws IOException, InterruptedException { ExtractorInput input = createTestInput(0x44, 0x89, 0x88, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00); @@ -101,6 +117,7 @@ public class DefaultEbmlReaderTest extends TestCase { assertEvents(input, expected.events); } + @Test public void testBinaryElement() throws IOException, InterruptedException { ExtractorInput input = createTestInput(0xA3, 0x88, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08); @@ -118,16 +135,16 @@ public class DefaultEbmlReaderTest extends TestCase { // We expect the number of successful reads to equal the number of expected events. for (int i = 0; i < expectedEvents.size(); i++) { - assertTrue(reader.read(input)); + assertThat(reader.read(input)).isTrue(); } // The next read should be unsuccessful. - assertFalse(reader.read(input)); + assertThat(reader.read(input)).isFalse(); // Check that we really did get to the end of input. - assertFalse(input.readFully(new byte[1], 0, 1, true)); + assertThat(input.readFully(new byte[1], 0, 1, true)).isFalse(); - assertEquals(expectedEvents.size(), output.events.size()); + assertThat(output.events).hasSize(expectedEvents.size()); for (int i = 0; i < expectedEvents.size(); i++) { - assertEquals(expectedEvents.get(i), output.events.get(i)); + assertThat(output.events.get(i)).isEqualTo(expectedEvents.get(i)); } } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mkv/VarintReaderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mkv/VarintReaderTest.java similarity index 92% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mkv/VarintReaderTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/extractor/mkv/VarintReaderTest.java index 3eb2c10a30..bda93db812 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mkv/VarintReaderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mkv/VarintReaderTest.java @@ -15,18 +15,28 @@ */ package com.google.android.exoplayer2.extractor.mkv; +import static com.google.android.exoplayer2.C.RESULT_END_OF_INPUT; +import static com.google.android.exoplayer2.C.RESULT_MAX_LENGTH_EXCEEDED; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.testutil.FakeExtractorInput; import com.google.android.exoplayer2.testutil.FakeExtractorInput.SimulatedIOException; import java.io.EOFException; import java.io.IOException; -import junit.framework.TestCase; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; /** * Tests for {@link VarintReader}. */ -public final class VarintReaderTest extends TestCase { +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class VarintReaderTest { private static final byte MAX_BYTE = (byte) 0xFF; @@ -78,6 +88,7 @@ public final class VarintReaderTest extends TestCase { private static final long VALUE_8_BYTE_MAX = 0xFFFFFFFFFFFFFFL; private static final long VALUE_8_BYTE_MAX_WITH_MASK = 0x1FFFFFFFFFFFFFFL; + @Test public void testReadVarintEndOfInputAtStart() throws IOException, InterruptedException { VarintReader reader = new VarintReader(); // Build an input with no data. @@ -86,7 +97,7 @@ public final class VarintReaderTest extends TestCase { .build(); // End of input allowed. long result = reader.readUnsignedVarint(input, true, false, 8); - assertEquals(C.RESULT_END_OF_INPUT, result); + assertThat(result).isEqualTo(RESULT_END_OF_INPUT); // End of input not allowed. try { reader.readUnsignedVarint(input, false, false, 8); @@ -96,6 +107,7 @@ public final class VarintReaderTest extends TestCase { } } + @Test public void testReadVarintExceedsMaximumAllowedLength() throws IOException, InterruptedException { VarintReader reader = new VarintReader(); ExtractorInput input = new FakeExtractorInput.Builder() @@ -103,9 +115,10 @@ public final class VarintReaderTest extends TestCase { .setSimulateUnknownLength(true) .build(); long result = reader.readUnsignedVarint(input, false, true, 4); - assertEquals(C.RESULT_MAX_LENGTH_EXCEEDED, result); + assertThat(result).isEqualTo(RESULT_MAX_LENGTH_EXCEEDED); } + @Test public void testReadVarint() throws IOException, InterruptedException { VarintReader reader = new VarintReader(); testReadVarint(reader, true, DATA_1_BYTE_0, 1, 0); @@ -142,6 +155,7 @@ public final class VarintReaderTest extends TestCase { testReadVarint(reader, false, DATA_8_BYTE_MAX, 8, VALUE_8_BYTE_MAX_WITH_MASK); } + @Test public void testReadVarintFlaky() throws IOException, InterruptedException { VarintReader reader = new VarintReader(); testReadVarintFlaky(reader, true, DATA_1_BYTE_0, 1, 0); @@ -185,8 +199,8 @@ public final class VarintReaderTest extends TestCase { .setSimulateUnknownLength(true) .build(); long result = reader.readUnsignedVarint(input, false, removeMask, 8); - assertEquals(expectedLength, input.getPosition()); - assertEquals(expectedValue, result); + assertThat(input.getPosition()).isEqualTo(expectedLength); + assertThat(result).isEqualTo(expectedValue); } private static void testReadVarintFlaky(VarintReader reader, boolean removeMask, byte[] data, @@ -209,8 +223,8 @@ public final class VarintReaderTest extends TestCase { // Expected. } } - assertEquals(expectedLength, input.getPosition()); - assertEquals(expectedValue, result); + assertThat(input.getPosition()).isEqualTo(expectedLength); + assertThat(result).isEqualTo(expectedValue); } } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp3/XingSeekerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp3/XingSeekerTest.java similarity index 67% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp3/XingSeekerTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/extractor/mp3/XingSeekerTest.java index 18775b17f4..b43949b7c2 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp3/XingSeekerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp3/XingSeekerTest.java @@ -15,16 +15,24 @@ */ package com.google.android.exoplayer2.extractor.mp3; -import android.test.InstrumentationTestCase; +import static com.google.common.truth.Truth.assertThat; + import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.MpegAudioHeader; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; /** * Tests for {@link XingSeeker}. */ -public final class XingSeekerTest extends InstrumentationTestCase { +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class XingSeekerTest { // Xing header/payload from http://storage.googleapis.com/exoplayer-test-media-0/play.mp3. private static final int XING_FRAME_HEADER_DATA = 0xFFFB3000; @@ -51,7 +59,7 @@ public final class XingSeekerTest extends InstrumentationTestCase { private XingSeeker seekerWithInputLength; private int xingFrameSize; - @Override + @Before public void setUp() throws Exception { MpegAudioHeader xingFrameHeader = new MpegAudioHeader(); MpegAudioHeader.populateHeader(XING_FRAME_HEADER_DATA, xingFrameHeader); @@ -62,42 +70,49 @@ public final class XingSeekerTest extends InstrumentationTestCase { xingFrameSize = xingFrameHeader.frameSize; } + @Test public void testGetTimeUsBeforeFirstAudioFrame() { - assertEquals(0, seeker.getTimeUs(-1)); - assertEquals(0, seekerWithInputLength.getTimeUs(-1)); + assertThat(seeker.getTimeUs(-1)).isEqualTo(0); + assertThat(seekerWithInputLength.getTimeUs(-1)).isEqualTo(0); } + @Test public void testGetTimeUsAtFirstAudioFrame() { - assertEquals(0, seeker.getTimeUs(XING_FRAME_POSITION + xingFrameSize)); - assertEquals(0, seekerWithInputLength.getTimeUs(XING_FRAME_POSITION + xingFrameSize)); + assertThat(seeker.getTimeUs(XING_FRAME_POSITION + xingFrameSize)).isEqualTo(0); + assertThat(seekerWithInputLength.getTimeUs(XING_FRAME_POSITION + xingFrameSize)).isEqualTo(0); } + @Test public void testGetTimeUsAtEndOfStream() { - assertEquals(STREAM_DURATION_US, - seeker.getTimeUs(XING_FRAME_POSITION + xingFrameSize + STREAM_SIZE_BYTES)); - assertEquals(STREAM_DURATION_US, - seekerWithInputLength.getTimeUs(XING_FRAME_POSITION + xingFrameSize + STREAM_SIZE_BYTES)); + assertThat(seeker.getTimeUs(XING_FRAME_POSITION + xingFrameSize + STREAM_SIZE_BYTES)) + .isEqualTo(STREAM_DURATION_US); + assertThat( + seekerWithInputLength.getTimeUs(XING_FRAME_POSITION + xingFrameSize + STREAM_SIZE_BYTES)) + .isEqualTo(STREAM_DURATION_US); } + @Test public void testGetPositionAtStartOfStream() { - assertEquals(XING_FRAME_POSITION + xingFrameSize, seeker.getPosition(0)); - assertEquals(XING_FRAME_POSITION + xingFrameSize, seekerWithInputLength.getPosition(0)); + assertThat(seeker.getPosition(0)).isEqualTo(XING_FRAME_POSITION + xingFrameSize); + assertThat(seekerWithInputLength.getPosition(0)).isEqualTo(XING_FRAME_POSITION + xingFrameSize); } + @Test public void testGetPositionAtEndOfStream() { - assertEquals(XING_FRAME_POSITION + STREAM_SIZE_BYTES - 1, - seeker.getPosition(STREAM_DURATION_US)); - assertEquals(XING_FRAME_POSITION + STREAM_SIZE_BYTES - 1, - seekerWithInputLength.getPosition(STREAM_DURATION_US)); + assertThat(seeker.getPosition(STREAM_DURATION_US)) + .isEqualTo(XING_FRAME_POSITION + STREAM_SIZE_BYTES - 1); + assertThat(seekerWithInputLength.getPosition(STREAM_DURATION_US)) + .isEqualTo(XING_FRAME_POSITION + STREAM_SIZE_BYTES - 1); } + @Test public void testGetTimeForAllPositions() { for (int offset = xingFrameSize; offset < STREAM_SIZE_BYTES; offset++) { int position = XING_FRAME_POSITION + offset; long timeUs = seeker.getTimeUs(position); - assertEquals(position, seeker.getPosition(timeUs)); + assertThat(seeker.getPosition(timeUs)).isEqualTo(position); timeUs = seekerWithInputLength.getTimeUs(position); - assertEquals(position, seekerWithInputLength.getPosition(timeUs)); + assertThat(seekerWithInputLength.getPosition(timeUs)).isEqualTo(position); } } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/AtomParsersTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/AtomParsersTest.java similarity index 79% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/AtomParsersTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/AtomParsersTest.java index d0213337b8..0c69e0b176 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/AtomParsersTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/AtomParsersTest.java @@ -15,14 +15,21 @@ */ package com.google.android.exoplayer2.extractor.mp4; +import static com.google.common.truth.Truth.assertThat; + import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; -import junit.framework.TestCase; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; /** * Tests for {@link AtomParsers}. */ -public final class AtomParsersTest extends TestCase { +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class AtomParsersTest { private static final String ATOM_HEADER = "000000000000000000000000"; private static final String SAMPLE_COUNT = "00000004"; @@ -33,24 +40,27 @@ public final class AtomParsersTest extends TestCase { private static final byte[] SIXTEEN_BIT_STZ2 = Util.getBytesFromHexString(ATOM_HEADER + "00000010" + SAMPLE_COUNT + "0001000200030004"); + @Test public void testStz2Parsing4BitFieldSize() { verifyParsing(new Atom.LeafAtom(Atom.TYPE_stsz, new ParsableByteArray(FOUR_BIT_STZ2))); } + @Test public void testStz2Parsing8BitFieldSize() { verifyParsing(new Atom.LeafAtom(Atom.TYPE_stsz, new ParsableByteArray(EIGHT_BIT_STZ2))); } + @Test public void testStz2Parsing16BitFieldSize() { verifyParsing(new Atom.LeafAtom(Atom.TYPE_stsz, new ParsableByteArray(SIXTEEN_BIT_STZ2))); } private void verifyParsing(Atom.LeafAtom stz2Atom) { AtomParsers.Stz2SampleSizeBox box = new AtomParsers.Stz2SampleSizeBox(stz2Atom); - assertEquals(4, box.getSampleCount()); - assertFalse(box.isFixedSampleSize()); + assertThat(box.getSampleCount()).isEqualTo(4); + assertThat(box.isFixedSampleSize()).isFalse(); for (int i = 0; i < box.getSampleCount(); i++) { - assertEquals(i + 1, box.readNextSampleSize()); + assertThat(box.readNextSampleSize()).isEqualTo(i + 1); } } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtilTest.java similarity index 56% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtilTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtilTest.java index 5ac3979746..4d7931cc02 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtilTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/PsshAtomUtilTest.java @@ -15,33 +15,44 @@ */ package com.google.android.exoplayer2.extractor.mp4; -import android.test.MoreAsserts; +import static com.google.android.exoplayer2.C.WIDEVINE_UUID; +import static com.google.android.exoplayer2.extractor.mp4.Atom.TYPE_pssh; +import static com.google.android.exoplayer2.extractor.mp4.Atom.parseFullAtomFlags; +import static com.google.android.exoplayer2.extractor.mp4.Atom.parseFullAtomVersion; +import static com.google.common.truth.Truth.assertThat; + import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.ParsableByteArray; import java.util.UUID; -import junit.framework.TestCase; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; /** * Tests for {@link PsshAtomUtil}. */ -public class PsshAtomUtilTest extends TestCase { +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class PsshAtomUtilTest { + @Test public void testBuildPsshAtom() { byte[] schemeData = new byte[]{0, 1, 2, 3, 4, 5}; byte[] psshAtom = PsshAtomUtil.buildPsshAtom(C.WIDEVINE_UUID, schemeData); // Read the PSSH atom back and assert its content is as expected. ParsableByteArray parsablePsshAtom = new ParsableByteArray(psshAtom); - assertEquals(psshAtom.length, parsablePsshAtom.readUnsignedIntToInt()); // length - assertEquals(Atom.TYPE_pssh, parsablePsshAtom.readInt()); // type + assertThat(parsablePsshAtom.readUnsignedIntToInt()).isEqualTo(psshAtom.length); // length + assertThat(parsablePsshAtom.readInt()).isEqualTo(TYPE_pssh); // type int fullAtomInt = parsablePsshAtom.readInt(); // version + flags - assertEquals(0, Atom.parseFullAtomVersion(fullAtomInt)); - assertEquals(0, Atom.parseFullAtomFlags(fullAtomInt)); + assertThat(parseFullAtomVersion(fullAtomInt)).isEqualTo(0); + assertThat(parseFullAtomFlags(fullAtomInt)).isEqualTo(0); UUID systemId = new UUID(parsablePsshAtom.readLong(), parsablePsshAtom.readLong()); - assertEquals(C.WIDEVINE_UUID, systemId); - assertEquals(schemeData.length, parsablePsshAtom.readUnsignedIntToInt()); + assertThat(systemId).isEqualTo(WIDEVINE_UUID); + assertThat(parsablePsshAtom.readUnsignedIntToInt()).isEqualTo(schemeData.length); byte[] psshSchemeData = new byte[schemeData.length]; parsablePsshAtom.readBytes(psshSchemeData, 0, schemeData.length); - MoreAsserts.assertEquals(schemeData, psshSchemeData); + assertThat(psshSchemeData).isEqualTo(schemeData); } } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerUtilMethodsTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerUtilMethodsTest.java similarity index 72% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerUtilMethodsTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerUtilMethodsTest.java index 1acc208c29..a3f7e9a548 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerUtilMethodsTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/DefaultOggSeekerUtilMethodsTest.java @@ -15,54 +15,67 @@ */ package com.google.android.exoplayer2.extractor.ogg; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.testutil.FakeExtractorInput; +import com.google.android.exoplayer2.testutil.OggTestData; import com.google.android.exoplayer2.testutil.TestUtil; import java.io.EOFException; import java.io.IOException; import java.util.Random; -import junit.framework.TestCase; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; /** * Unit test for {@link DefaultOggSeeker} utility methods. */ -public class DefaultOggSeekerUtilMethodsTest extends TestCase { +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class DefaultOggSeekerUtilMethodsTest { private final Random random = new Random(0); + @Test public void testSkipToNextPage() throws Exception { - FakeExtractorInput extractorInput = TestData.createInput( + FakeExtractorInput extractorInput = OggTestData.createInput( TestUtil.joinByteArrays( TestUtil.buildTestData(4000, random), new byte[] {'O', 'g', 'g', 'S'}, TestUtil.buildTestData(4000, random) ), false); skipToNextPage(extractorInput); - assertEquals(4000, extractorInput.getPosition()); + assertThat(extractorInput.getPosition()).isEqualTo(4000); } + @Test public void testSkipToNextPageOverlap() throws Exception { - FakeExtractorInput extractorInput = TestData.createInput( + FakeExtractorInput extractorInput = OggTestData.createInput( TestUtil.joinByteArrays( TestUtil.buildTestData(2046, random), new byte[] {'O', 'g', 'g', 'S'}, TestUtil.buildTestData(4000, random) ), false); skipToNextPage(extractorInput); - assertEquals(2046, extractorInput.getPosition()); + assertThat(extractorInput.getPosition()).isEqualTo(2046); } + @Test public void testSkipToNextPageInputShorterThanPeekLength() throws Exception { - FakeExtractorInput extractorInput = TestData.createInput( + FakeExtractorInput extractorInput = OggTestData.createInput( TestUtil.joinByteArrays( new byte[] {'x', 'O', 'g', 'g', 'S'} ), false); skipToNextPage(extractorInput); - assertEquals(1, extractorInput.getPosition()); + assertThat(extractorInput.getPosition()).isEqualTo(1); } + @Test public void testSkipToNextPageNoMatch() throws Exception { - FakeExtractorInput extractorInput = TestData.createInput( + FakeExtractorInput extractorInput = OggTestData.createInput( new byte[] {'g', 'g', 'S', 'O', 'g', 'g'}, false); try { skipToNextPage(extractorInput); @@ -84,16 +97,17 @@ public class DefaultOggSeekerUtilMethodsTest extends TestCase { } } + @Test public void testSkipToPageOfGranule() throws IOException, InterruptedException { byte[] packet = TestUtil.buildTestData(3 * 254, random); byte[] data = TestUtil.joinByteArrays( - TestData.buildOggHeader(0x01, 20000, 1000, 0x03), + OggTestData.buildOggHeader(0x01, 20000, 1000, 0x03), TestUtil.createByteArray(254, 254, 254), // Laces. packet, - TestData.buildOggHeader(0x04, 40000, 1001, 0x03), + OggTestData.buildOggHeader(0x04, 40000, 1001, 0x03), TestUtil.createByteArray(254, 254, 254), // Laces. packet, - TestData.buildOggHeader(0x04, 60000, 1002, 0x03), + OggTestData.buildOggHeader(0x04, 60000, 1002, 0x03), TestUtil.createByteArray(254, 254, 254), // Laces. packet); FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build(); @@ -101,44 +115,46 @@ public class DefaultOggSeekerUtilMethodsTest extends TestCase { // expect to be granule of the previous page returned as elapsedSamples skipToPageOfGranule(input, 54000, 40000); // expect to be at the start of the third page - assertEquals(2 * (30 + (3 * 254)), input.getPosition()); + assertThat(input.getPosition()).isEqualTo(2 * (30 + (3 * 254))); } + @Test public void testSkipToPageOfGranulePreciseMatch() throws IOException, InterruptedException { byte[] packet = TestUtil.buildTestData(3 * 254, random); byte[] data = TestUtil.joinByteArrays( - TestData.buildOggHeader(0x01, 20000, 1000, 0x03), + OggTestData.buildOggHeader(0x01, 20000, 1000, 0x03), TestUtil.createByteArray(254, 254, 254), // Laces. packet, - TestData.buildOggHeader(0x04, 40000, 1001, 0x03), + OggTestData.buildOggHeader(0x04, 40000, 1001, 0x03), TestUtil.createByteArray(254, 254, 254), // Laces. packet, - TestData.buildOggHeader(0x04, 60000, 1002, 0x03), + OggTestData.buildOggHeader(0x04, 60000, 1002, 0x03), TestUtil.createByteArray(254, 254, 254), // Laces. packet); FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build(); skipToPageOfGranule(input, 40000, 20000); // expect to be at the start of the second page - assertEquals((30 + (3 * 254)), input.getPosition()); + assertThat(input.getPosition()).isEqualTo(30 + (3 * 254)); } + @Test public void testSkipToPageOfGranuleAfterTargetPage() throws IOException, InterruptedException { byte[] packet = TestUtil.buildTestData(3 * 254, random); byte[] data = TestUtil.joinByteArrays( - TestData.buildOggHeader(0x01, 20000, 1000, 0x03), + OggTestData.buildOggHeader(0x01, 20000, 1000, 0x03), TestUtil.createByteArray(254, 254, 254), // Laces. packet, - TestData.buildOggHeader(0x04, 40000, 1001, 0x03), + OggTestData.buildOggHeader(0x04, 40000, 1001, 0x03), TestUtil.createByteArray(254, 254, 254), // Laces. packet, - TestData.buildOggHeader(0x04, 60000, 1002, 0x03), + OggTestData.buildOggHeader(0x04, 60000, 1002, 0x03), TestUtil.createByteArray(254, 254, 254), // Laces. packet); FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build(); skipToPageOfGranule(input, 10000, -1); - assertEquals(0, input.getPosition()); + assertThat(input.getPosition()).isEqualTo(0); } private void skipToPageOfGranule(ExtractorInput input, long granule, @@ -146,7 +162,8 @@ public class DefaultOggSeekerUtilMethodsTest extends TestCase { DefaultOggSeeker oggSeeker = new DefaultOggSeeker(0, input.getLength(), new FlacReader(), 1, 2); while (true) { try { - assertEquals(elapsedSamplesExpected, oggSeeker.skipToPageOfGranule(input, granule, -1)); + assertThat(oggSeeker.skipToPageOfGranule(input, granule, -1)) + .isEqualTo(elapsedSamplesExpected); return; } catch (FakeExtractorInput.SimulatedIOException e) { input.resetPeekPosition(); @@ -154,24 +171,26 @@ public class DefaultOggSeekerUtilMethodsTest extends TestCase { } } + @Test public void testReadGranuleOfLastPage() throws IOException, InterruptedException { - FakeExtractorInput input = TestData.createInput(TestUtil.joinByteArrays( + FakeExtractorInput input = OggTestData.createInput(TestUtil.joinByteArrays( TestUtil.buildTestData(100, random), - TestData.buildOggHeader(0x00, 20000, 66, 3), + OggTestData.buildOggHeader(0x00, 20000, 66, 3), TestUtil.createByteArray(254, 254, 254), // laces TestUtil.buildTestData(3 * 254, random), - TestData.buildOggHeader(0x00, 40000, 67, 3), + OggTestData.buildOggHeader(0x00, 40000, 67, 3), TestUtil.createByteArray(254, 254, 254), // laces TestUtil.buildTestData(3 * 254, random), - TestData.buildOggHeader(0x05, 60000, 68, 3), + OggTestData.buildOggHeader(0x05, 60000, 68, 3), TestUtil.createByteArray(254, 254, 254), // laces TestUtil.buildTestData(3 * 254, random) ), false); assertReadGranuleOfLastPage(input, 60000); } + @Test public void testReadGranuleOfLastPageAfterLastHeader() throws IOException, InterruptedException { - FakeExtractorInput input = TestData.createInput(TestUtil.buildTestData(100, random), false); + FakeExtractorInput input = OggTestData.createInput(TestUtil.buildTestData(100, random), false); try { assertReadGranuleOfLastPage(input, 60000); fail(); @@ -180,9 +199,10 @@ public class DefaultOggSeekerUtilMethodsTest extends TestCase { } } + @Test public void testReadGranuleOfLastPageWithUnboundedLength() throws IOException, InterruptedException { - FakeExtractorInput input = TestData.createInput(new byte[0], true); + FakeExtractorInput input = OggTestData.createInput(new byte[0], true); try { assertReadGranuleOfLastPage(input, 60000); fail(); @@ -196,7 +216,7 @@ public class DefaultOggSeekerUtilMethodsTest extends TestCase { DefaultOggSeeker oggSeeker = new DefaultOggSeeker(0, input.getLength(), new FlacReader(), 1, 2); while (true) { try { - assertEquals(expected, oggSeeker.readGranuleOfLastPage(input)); + assertThat(oggSeeker.readGranuleOfLastPage(input)).isEqualTo(expected); break; } catch (FakeExtractorInput.SimulatedIOException e) { // ignored diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/OggPageHeaderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggPageHeaderTest.java similarity index 59% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/OggPageHeaderTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggPageHeaderTest.java index 9d39eba174..c8bcffde3c 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/OggPageHeaderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggPageHeaderTest.java @@ -15,67 +15,79 @@ */ package com.google.android.exoplayer2.extractor.ogg; +import static com.google.common.truth.Truth.assertThat; + import com.google.android.exoplayer2.testutil.FakeExtractorInput; import com.google.android.exoplayer2.testutil.FakeExtractorInput.SimulatedIOException; +import com.google.android.exoplayer2.testutil.OggTestData; import com.google.android.exoplayer2.testutil.TestUtil; import java.io.IOException; -import junit.framework.TestCase; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; /** * Unit test for {@link OggPageHeader}. */ -public final class OggPageHeaderTest extends TestCase { +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class OggPageHeaderTest { + @Test public void testPopulatePageHeader() throws IOException, InterruptedException { - FakeExtractorInput input = TestData.createInput(TestUtil.joinByteArrays( - TestData.buildOggHeader(0x01, 123456, 4, 2), + FakeExtractorInput input = OggTestData.createInput(TestUtil.joinByteArrays( + OggTestData.buildOggHeader(0x01, 123456, 4, 2), TestUtil.createByteArray(2, 2) ), true); OggPageHeader header = new OggPageHeader(); populatePageHeader(input, header, false); - assertEquals(0x01, header.type); - assertEquals(27 + 2, header.headerSize); - assertEquals(4, header.bodySize); - assertEquals(2, header.pageSegmentCount); - assertEquals(123456, header.granulePosition); - assertEquals(4, header.pageSequenceNumber); - assertEquals(0x1000, header.streamSerialNumber); - assertEquals(0x100000, header.pageChecksum); - assertEquals(0, header.revision); + assertThat(header.type).isEqualTo(0x01); + assertThat(header.headerSize).isEqualTo(27 + 2); + assertThat(header.bodySize).isEqualTo(4); + assertThat(header.pageSegmentCount).isEqualTo(2); + assertThat(header.granulePosition).isEqualTo(123456); + assertThat(header.pageSequenceNumber).isEqualTo(4); + assertThat(header.streamSerialNumber).isEqualTo(0x1000); + assertThat(header.pageChecksum).isEqualTo(0x100000); + assertThat(header.revision).isEqualTo(0); } + @Test public void testPopulatePageHeaderQuiteOnExceptionLessThan27Bytes() throws IOException, InterruptedException { - FakeExtractorInput input = TestData.createInput(TestUtil.createByteArray(2, 2), false); + FakeExtractorInput input = OggTestData.createInput(TestUtil.createByteArray(2, 2), false); OggPageHeader header = new OggPageHeader(); - assertFalse(populatePageHeader(input, header, true)); + assertThat(populatePageHeader(input, header, true)).isFalse(); } + @Test public void testPopulatePageHeaderQuiteOnExceptionNotOgg() throws IOException, InterruptedException { byte[] headerBytes = TestUtil.joinByteArrays( - TestData.buildOggHeader(0x01, 123456, 4, 2), + OggTestData.buildOggHeader(0x01, 123456, 4, 2), TestUtil.createByteArray(2, 2) ); // change from 'O' to 'o' headerBytes[0] = 'o'; - FakeExtractorInput input = TestData.createInput(headerBytes, false); + FakeExtractorInput input = OggTestData.createInput(headerBytes, false); OggPageHeader header = new OggPageHeader(); - assertFalse(populatePageHeader(input, header, true)); + assertThat(populatePageHeader(input, header, true)).isFalse(); } + @Test public void testPopulatePageHeaderQuiteOnExceptionWrongRevision() throws IOException, InterruptedException { byte[] headerBytes = TestUtil.joinByteArrays( - TestData.buildOggHeader(0x01, 123456, 4, 2), + OggTestData.buildOggHeader(0x01, 123456, 4, 2), TestUtil.createByteArray(2, 2) ); // change revision from 0 to 1 headerBytes[4] = 0x01; - FakeExtractorInput input = TestData.createInput(headerBytes, false); + FakeExtractorInput input = OggTestData.createInput(headerBytes, false); OggPageHeader header = new OggPageHeader(); - assertFalse(populatePageHeader(input, header, true)); + assertThat(populatePageHeader(input, header, true)).isFalse(); } private boolean populatePageHeader(FakeExtractorInput input, OggPageHeader header, diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/VorbisBitArrayTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/VorbisBitArrayTest.java new file mode 100644 index 0000000000..08b9b12a18 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/VorbisBitArrayTest.java @@ -0,0 +1,155 @@ +/* + * 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.ogg; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.android.exoplayer2.testutil.TestUtil; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Unit test for {@link VorbisBitArray}. + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class VorbisBitArrayTest { + + @Test + public void testReadBit() { + VorbisBitArray bitArray = new VorbisBitArray(TestUtil.createByteArray(0x5c, 0x50)); + assertThat(bitArray.readBit()).isFalse(); + assertThat(bitArray.readBit()).isFalse(); + assertThat(bitArray.readBit()).isTrue(); + assertThat(bitArray.readBit()).isTrue(); + assertThat(bitArray.readBit()).isTrue(); + assertThat(bitArray.readBit()).isFalse(); + assertThat(bitArray.readBit()).isTrue(); + assertThat(bitArray.readBit()).isFalse(); + assertThat(bitArray.readBit()).isFalse(); + assertThat(bitArray.readBit()).isFalse(); + assertThat(bitArray.readBit()).isFalse(); + assertThat(bitArray.readBit()).isFalse(); + assertThat(bitArray.readBit()).isTrue(); + assertThat(bitArray.readBit()).isFalse(); + assertThat(bitArray.readBit()).isTrue(); + assertThat(bitArray.readBit()).isFalse(); + } + + @Test + public void testSkipBits() { + VorbisBitArray bitArray = new VorbisBitArray(TestUtil.createByteArray(0xF0, 0x0F)); + bitArray.skipBits(10); + assertThat(bitArray.getPosition()).isEqualTo(10); + assertThat(bitArray.readBit()).isTrue(); + assertThat(bitArray.readBit()).isTrue(); + assertThat(bitArray.readBit()).isFalse(); + bitArray.skipBits(1); + assertThat(bitArray.getPosition()).isEqualTo(14); + assertThat(bitArray.readBit()).isFalse(); + assertThat(bitArray.readBit()).isFalse(); + } + + @Test + public void testGetPosition() throws Exception { + VorbisBitArray bitArray = new VorbisBitArray(TestUtil.createByteArray(0xF0, 0x0F)); + assertThat(bitArray.getPosition()).isEqualTo(0); + bitArray.readBit(); + assertThat(bitArray.getPosition()).isEqualTo(1); + bitArray.readBit(); + bitArray.readBit(); + bitArray.skipBits(4); + assertThat(bitArray.getPosition()).isEqualTo(7); + } + + @Test + public void testSetPosition() throws Exception { + VorbisBitArray bitArray = new VorbisBitArray(TestUtil.createByteArray(0xF0, 0x0F)); + assertThat(bitArray.getPosition()).isEqualTo(0); + bitArray.setPosition(4); + assertThat(bitArray.getPosition()).isEqualTo(4); + bitArray.setPosition(15); + assertThat(bitArray.readBit()).isFalse(); + } + + @Test + public void testReadInt32() { + VorbisBitArray bitArray = new VorbisBitArray(TestUtil.createByteArray(0xF0, 0x0F, 0xF0, 0x0F)); + assertThat(bitArray.readBits(32)).isEqualTo(0x0FF00FF0); + bitArray = new VorbisBitArray(TestUtil.createByteArray(0x0F, 0xF0, 0x0F, 0xF0)); + assertThat(bitArray.readBits(32)).isEqualTo(0xF00FF00F); + } + + @Test + public void testReadBits() throws Exception { + VorbisBitArray bitArray = new VorbisBitArray(TestUtil.createByteArray(0x03, 0x22)); + assertThat(bitArray.readBits(2)).isEqualTo(3); + bitArray.skipBits(6); + assertThat(bitArray.readBits(2)).isEqualTo(2); + bitArray.skipBits(2); + assertThat(bitArray.readBits(2)).isEqualTo(2); + bitArray.reset(); + assertThat(bitArray.readBits(16)).isEqualTo(0x2203); + } + + @Test + public void testRead4BitsBeyondBoundary() throws Exception { + VorbisBitArray bitArray = new VorbisBitArray(TestUtil.createByteArray(0x2e, 0x10)); + assertThat(bitArray.readBits(7)).isEqualTo(0x2e); + assertThat(bitArray.getPosition()).isEqualTo(7); + assertThat(bitArray.readBits(4)).isEqualTo(0x0); + } + + @Test + public void testReadBitsBeyondByteBoundaries() throws Exception { + VorbisBitArray bitArray = new VorbisBitArray(TestUtil.createByteArray(0xFF, 0x0F, 0xFF, 0x0F)); + assertThat(bitArray.readBits(32)).isEqualTo(0x0FFF0FFF); + + bitArray.reset(); + bitArray.skipBits(4); + assertThat(bitArray.readBits(16)).isEqualTo(0xF0FF); + + bitArray.reset(); + bitArray.skipBits(6); + assertThat(bitArray.readBits(12)).isEqualTo(0xc3F); + + bitArray.reset(); + bitArray.skipBits(6); + assertThat(bitArray.readBit()).isTrue(); + assertThat(bitArray.readBit()).isTrue(); + assertThat(bitArray.bitsLeft()).isEqualTo(24); + + bitArray.reset(); + bitArray.skipBits(10); + assertThat(bitArray.readBits(5)).isEqualTo(3); + assertThat(bitArray.getPosition()).isEqualTo(15); + } + + @Test + public void testReadBitsIllegalLengths() throws Exception { + VorbisBitArray bitArray = new VorbisBitArray(TestUtil.createByteArray(0x03, 0x22, 0x30)); + + // reading zero bits gets 0 without advancing position + // (like a zero-bit read is defined to yield zer0) + assertThat(bitArray.readBits(0)).isEqualTo(0); + assertThat(bitArray.getPosition()).isEqualTo(0); + bitArray.readBit(); + assertThat(bitArray.getPosition()).isEqualTo(1); + } + +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/VorbisReaderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/VorbisReaderTest.java new file mode 100644 index 0000000000..20a76e83e0 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/VorbisReaderTest.java @@ -0,0 +1,117 @@ +/* + * 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.ogg; + +import static com.google.android.exoplayer2.extractor.ogg.VorbisReader.readBits; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + +import com.google.android.exoplayer2.extractor.ExtractorInput; +import com.google.android.exoplayer2.extractor.ogg.VorbisReader.VorbisSetup; +import com.google.android.exoplayer2.testutil.FakeExtractorInput; +import com.google.android.exoplayer2.testutil.FakeExtractorInput.SimulatedIOException; +import com.google.android.exoplayer2.testutil.OggTestData; +import com.google.android.exoplayer2.util.ParsableByteArray; +import java.io.IOException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Unit test for {@link VorbisReader}. + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class VorbisReaderTest { + + @Test + public void testReadBits() throws Exception { + assertThat(readBits((byte) 0x00, 2, 2)).isEqualTo(0); + assertThat(readBits((byte) 0x02, 1, 1)).isEqualTo(1); + assertThat(readBits((byte) 0xF0, 4, 4)).isEqualTo(15); + assertThat(readBits((byte) 0x80, 1, 7)).isEqualTo(1); + } + + @Test + public void testAppendNumberOfSamples() throws Exception { + ParsableByteArray buffer = new ParsableByteArray(4); + buffer.setLimit(0); + VorbisReader.appendNumberOfSamples(buffer, 0x01234567); + assertThat(buffer.limit()).isEqualTo(4); + assertThat(buffer.data[0]).isEqualTo(0x67); + assertThat(buffer.data[1]).isEqualTo(0x45); + assertThat(buffer.data[2]).isEqualTo(0x23); + assertThat(buffer.data[3]).isEqualTo(0x01); + } + + @Test + public void testReadSetupHeadersWithIOExceptions() throws IOException, InterruptedException { + byte[] data = OggTestData.getVorbisHeaderPages(); + ExtractorInput input = new FakeExtractorInput.Builder().setData(data).setSimulateIOErrors(true) + .setSimulateUnknownLength(true).setSimulatePartialReads(true).build(); + + VorbisReader reader = new VorbisReader(); + VorbisReader.VorbisSetup vorbisSetup = readSetupHeaders(reader, input); + + assertThat(vorbisSetup.idHeader).isNotNull(); + assertThat(vorbisSetup.commentHeader).isNotNull(); + assertThat(vorbisSetup.setupHeaderData).isNotNull(); + assertThat(vorbisSetup.modes).isNotNull(); + + assertThat(vorbisSetup.commentHeader.length).isEqualTo(45); + assertThat(vorbisSetup.idHeader.data).hasLength(30); + assertThat(vorbisSetup.setupHeaderData).hasLength(3597); + + assertThat(vorbisSetup.idHeader.bitrateMax).isEqualTo(-1); + assertThat(vorbisSetup.idHeader.bitrateMin).isEqualTo(-1); + assertThat(vorbisSetup.idHeader.bitrateNominal).isEqualTo(66666); + assertThat(vorbisSetup.idHeader.blockSize0).isEqualTo(512); + assertThat(vorbisSetup.idHeader.blockSize1).isEqualTo(1024); + assertThat(vorbisSetup.idHeader.channels).isEqualTo(2); + assertThat(vorbisSetup.idHeader.framingFlag).isTrue(); + assertThat(vorbisSetup.idHeader.sampleRate).isEqualTo(22050); + assertThat(vorbisSetup.idHeader.version).isEqualTo(0); + + assertThat(vorbisSetup.commentHeader.vendor).isEqualTo("Xiph.Org libVorbis I 20030909"); + assertThat(vorbisSetup.iLogModes).isEqualTo(1); + + assertThat(vorbisSetup.setupHeaderData[vorbisSetup.setupHeaderData.length - 1]) + .isEqualTo(data[data.length - 1]); + + assertThat(vorbisSetup.modes[0].blockFlag).isFalse(); + assertThat(vorbisSetup.modes[1].blockFlag).isTrue(); + } + + private static VorbisSetup readSetupHeaders(VorbisReader reader, ExtractorInput input) + throws IOException, InterruptedException { + OggPacket oggPacket = new OggPacket(); + while (true) { + try { + if (!oggPacket.populate(input)) { + fail(); + } + VorbisSetup vorbisSetup = reader.readSetupHeaders(oggPacket.getPayload()); + if (vorbisSetup != null) { + return vorbisSetup; + } + } catch (SimulatedIOException e) { + // Ignore. + } + } + } + +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/VorbisUtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/VorbisUtilTest.java new file mode 100644 index 0000000000..bdc573f218 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ogg/VorbisUtilTest.java @@ -0,0 +1,146 @@ +/* + * 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.ogg; + +import static com.google.android.exoplayer2.extractor.ogg.VorbisUtil.iLog; +import static com.google.android.exoplayer2.extractor.ogg.VorbisUtil.verifyVorbisHeaderCapturePattern; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + +import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.testutil.OggTestData; +import com.google.android.exoplayer2.util.ParsableByteArray; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Unit test for {@link VorbisUtil}. + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class VorbisUtilTest { + + @Test + public void testILog() throws Exception { + assertThat(iLog(0)).isEqualTo(0); + assertThat(iLog(1)).isEqualTo(1); + assertThat(iLog(2)).isEqualTo(2); + assertThat(iLog(3)).isEqualTo(2); + assertThat(iLog(4)).isEqualTo(3); + assertThat(iLog(5)).isEqualTo(3); + assertThat(iLog(8)).isEqualTo(4); + assertThat(iLog(-1)).isEqualTo(0); + assertThat(iLog(-122)).isEqualTo(0); + } + + @Test + public void testReadIdHeader() throws Exception { + byte[] data = OggTestData.getIdentificationHeaderData(); + ParsableByteArray headerData = new ParsableByteArray(data, data.length); + VorbisUtil.VorbisIdHeader vorbisIdHeader = + VorbisUtil.readVorbisIdentificationHeader(headerData); + + assertThat(vorbisIdHeader.sampleRate).isEqualTo(22050); + assertThat(vorbisIdHeader.version).isEqualTo(0); + assertThat(vorbisIdHeader.framingFlag).isTrue(); + assertThat(vorbisIdHeader.channels).isEqualTo(2); + assertThat(vorbisIdHeader.blockSize0).isEqualTo(512); + assertThat(vorbisIdHeader.blockSize1).isEqualTo(1024); + assertThat(vorbisIdHeader.bitrateMax).isEqualTo(-1); + assertThat(vorbisIdHeader.bitrateMin).isEqualTo(-1); + assertThat(vorbisIdHeader.bitrateNominal).isEqualTo(66666); + assertThat(vorbisIdHeader.getApproximateBitrate()).isEqualTo(66666); + } + + @Test + public void testReadCommentHeader() throws ParserException { + byte[] data = OggTestData.getCommentHeaderDataUTF8(); + ParsableByteArray headerData = new ParsableByteArray(data, data.length); + VorbisUtil.CommentHeader commentHeader = VorbisUtil.readVorbisCommentHeader(headerData); + + assertThat(commentHeader.vendor).isEqualTo("Xiph.Org libVorbis I 20120203 (Omnipresent)"); + assertThat(commentHeader.comments).hasLength(3); + assertThat(commentHeader.comments[0]).isEqualTo("ALBUM=äö"); + assertThat(commentHeader.comments[1]).isEqualTo("TITLE=A sample song"); + assertThat(commentHeader.comments[2]).isEqualTo("ARTIST=Google"); + } + + @Test + public void testReadVorbisModes() throws ParserException { + byte[] data = OggTestData.getSetupHeaderData(); + ParsableByteArray headerData = new ParsableByteArray(data, data.length); + VorbisUtil.Mode[] modes = VorbisUtil.readVorbisModes(headerData, 2); + + assertThat(modes).hasLength(2); + assertThat(modes[0].blockFlag).isFalse(); + assertThat(modes[0].mapping).isEqualTo(0); + assertThat(modes[0].transformType).isEqualTo(0); + assertThat(modes[0].windowType).isEqualTo(0); + assertThat(modes[1].blockFlag).isTrue(); + assertThat(modes[1].mapping).isEqualTo(1); + assertThat(modes[1].transformType).isEqualTo(0); + assertThat(modes[1].windowType).isEqualTo(0); + } + + @Test + public void testVerifyVorbisHeaderCapturePattern() throws ParserException { + ParsableByteArray header = new ParsableByteArray( + new byte[] {0x01, 'v', 'o', 'r', 'b', 'i', 's'}); + assertThat(verifyVorbisHeaderCapturePattern(0x01, header, false)).isTrue(); + } + + @Test + public void testVerifyVorbisHeaderCapturePatternInvalidHeader() { + ParsableByteArray header = new ParsableByteArray( + new byte[] {0x01, 'v', 'o', 'r', 'b', 'i', 's'}); + try { + VorbisUtil.verifyVorbisHeaderCapturePattern(0x99, header, false); + fail(); + } catch (ParserException e) { + assertThat(e.getMessage()).isEqualTo("expected header type 99"); + } + } + + @Test + public void testVerifyVorbisHeaderCapturePatternInvalidHeaderQuite() throws ParserException { + ParsableByteArray header = new ParsableByteArray( + new byte[] {0x01, 'v', 'o', 'r', 'b', 'i', 's'}); + assertThat(verifyVorbisHeaderCapturePattern(0x99, header, true)).isFalse(); + } + + @Test + public void testVerifyVorbisHeaderCapturePatternInvalidPattern() { + ParsableByteArray header = new ParsableByteArray( + new byte[] {0x01, 'x', 'v', 'o', 'r', 'b', 'i', 's'}); + try { + VorbisUtil.verifyVorbisHeaderCapturePattern(0x01, header, false); + fail(); + } catch (ParserException e) { + assertThat(e.getMessage()).isEqualTo("expected characters 'vorbis'"); + } + } + + @Test + public void testVerifyVorbisHeaderCapturePatternQuiteInvalidPatternQuite() + throws ParserException { + ParsableByteArray header = new ParsableByteArray( + new byte[] {0x01, 'x', 'v', 'o', 'r', 'b', 'i', 's'}); + assertThat(verifyVorbisHeaderCapturePattern(0x01, header, true)).isFalse(); + } + +} diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/SectionReaderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/SectionReaderTest.java similarity index 81% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/SectionReaderTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/SectionReaderTest.java index c4d9de3100..56668d5124 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ts/SectionReaderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/SectionReaderTest.java @@ -15,26 +15,35 @@ */ package com.google.android.exoplayer2.extractor.ts; +import static com.google.common.truth.Truth.assertThat; +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; + import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.testutil.FakeExtractorOutput; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.TimestampAdjuster; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.List; -import junit.framework.TestCase; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; /** * Test for {@link SectionReader}. */ -public class SectionReaderTest extends TestCase { +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class SectionReaderTest { private byte[] packetPayload; private CustomSectionPayloadReader payloadReader; private SectionReader reader; - @Override + @Before public void setUp() { packetPayload = new byte[512]; Arrays.fill(packetPayload, (byte) 0xFF); @@ -44,27 +53,30 @@ public class SectionReaderTest extends TestCase { new TsPayloadReader.TrackIdGenerator(0, 1)); } + @Test public void testSingleOnePacketSection() { packetPayload[0] = 3; insertTableSection(4, (byte) 99, 3); reader.consume(new ParsableByteArray(packetPayload), true); - assertEquals(Collections.singletonList(99), payloadReader.parsedTableIds); + assertThat(payloadReader.parsedTableIds).isEqualTo(singletonList(99)); } + @Test public void testHeaderSplitAcrossPackets() { packetPayload[0] = 3; // The first packet includes a pointer_field. insertTableSection(4, (byte) 100, 3); // This section header spreads across both packets. ParsableByteArray firstPacket = new ParsableByteArray(packetPayload, 5); reader.consume(firstPacket, true); - assertEquals(Collections.emptyList(), payloadReader.parsedTableIds); + assertThat(payloadReader.parsedTableIds).isEmpty(); ParsableByteArray secondPacket = new ParsableByteArray(packetPayload); secondPacket.setPosition(5); reader.consume(secondPacket, false); - assertEquals(Collections.singletonList(100), payloadReader.parsedTableIds); + assertThat(payloadReader.parsedTableIds).isEqualTo(singletonList(100)); } + @Test public void testFiveSectionsInTwoPackets() { packetPayload[0] = 0; // The first packet includes a pointer_field. insertTableSection(1, (byte) 101, 10); @@ -76,14 +88,15 @@ public class SectionReaderTest extends TestCase { ParsableByteArray firstPacket = new ParsableByteArray(packetPayload, 40); reader.consume(firstPacket, true); - assertEquals(Arrays.asList(101, 102, 103), payloadReader.parsedTableIds); + assertThat(payloadReader.parsedTableIds).isEqualTo(asList(101, 102, 103)); ParsableByteArray secondPacket = new ParsableByteArray(packetPayload); secondPacket.setPosition(40); reader.consume(secondPacket, true); - assertEquals(Arrays.asList(101, 102, 103, 104, 105), payloadReader.parsedTableIds); + assertThat(payloadReader.parsedTableIds).isEqualTo(asList(101, 102, 103, 104, 105)); } + @Test public void testLongSectionAcrossFourPackets() { packetPayload[0] = 13; // The first packet includes a pointer_field. insertTableSection(1, (byte) 106, 10); // First section. Should be skipped. @@ -95,24 +108,25 @@ public class SectionReaderTest extends TestCase { ParsableByteArray firstPacket = new ParsableByteArray(packetPayload, 100); reader.consume(firstPacket, true); - assertEquals(Collections.emptyList(), payloadReader.parsedTableIds); + assertThat(payloadReader.parsedTableIds).isEmpty(); ParsableByteArray secondPacket = new ParsableByteArray(packetPayload, 200); secondPacket.setPosition(100); reader.consume(secondPacket, false); - assertEquals(Collections.emptyList(), payloadReader.parsedTableIds); + assertThat(payloadReader.parsedTableIds).isEmpty(); ParsableByteArray thirdPacket = new ParsableByteArray(packetPayload, 300); thirdPacket.setPosition(200); reader.consume(thirdPacket, false); - assertEquals(Collections.emptyList(), payloadReader.parsedTableIds); + assertThat(payloadReader.parsedTableIds).isEmpty(); ParsableByteArray fourthPacket = new ParsableByteArray(packetPayload); fourthPacket.setPosition(300); reader.consume(fourthPacket, true); - assertEquals(Arrays.asList(107, 108), payloadReader.parsedTableIds); + assertThat(payloadReader.parsedTableIds).isEqualTo(asList(107, 108)); } + @Test public void testSeek() { packetPayload[0] = 13; // The first packet includes a pointer_field. insertTableSection(1, (byte) 109, 10); // First section. Should be skipped. @@ -124,26 +138,27 @@ public class SectionReaderTest extends TestCase { ParsableByteArray firstPacket = new ParsableByteArray(packetPayload, 100); reader.consume(firstPacket, true); - assertEquals(Collections.emptyList(), payloadReader.parsedTableIds); + assertThat(payloadReader.parsedTableIds).isEmpty(); ParsableByteArray secondPacket = new ParsableByteArray(packetPayload, 200); secondPacket.setPosition(100); reader.consume(secondPacket, false); - assertEquals(Collections.emptyList(), payloadReader.parsedTableIds); + assertThat(payloadReader.parsedTableIds).isEmpty(); ParsableByteArray thirdPacket = new ParsableByteArray(packetPayload, 300); thirdPacket.setPosition(200); reader.consume(thirdPacket, false); - assertEquals(Collections.emptyList(), payloadReader.parsedTableIds); + assertThat(payloadReader.parsedTableIds).isEmpty(); reader.seek(); ParsableByteArray fourthPacket = new ParsableByteArray(packetPayload); fourthPacket.setPosition(300); reader.consume(fourthPacket, true); - assertEquals(Collections.singletonList(111), payloadReader.parsedTableIds); + assertThat(payloadReader.parsedTableIds).isEqualTo(singletonList(111)); } + @Test public void testCrcChecks() { byte[] correctCrcPat = new byte[] { (byte) 0x0, (byte) 0x0, (byte) 0xb0, (byte) 0xd, (byte) 0x0, (byte) 0x1, (byte) 0xc1, @@ -153,9 +168,9 @@ public class SectionReaderTest extends TestCase { // Crc field is incorrect, and should not be passed to the payload reader. incorrectCrcPat[16]--; reader.consume(new ParsableByteArray(correctCrcPat), true); - assertEquals(Collections.singletonList(0), payloadReader.parsedTableIds); + assertThat(payloadReader.parsedTableIds).isEqualTo(singletonList(0)); reader.consume(new ParsableByteArray(incorrectCrcPat), true); - assertEquals(Collections.singletonList(0), payloadReader.parsedTableIds); + assertThat(payloadReader.parsedTableIds).isEqualTo(singletonList(0)); } // Internal methods. diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoderTest.java similarity index 70% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoderTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoderTest.java index b33dfd1067..1ce0ccb93d 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoderTest.java @@ -15,17 +15,24 @@ */ package com.google.android.exoplayer2.metadata.emsg; -import android.test.MoreAsserts; +import static com.google.common.truth.Truth.assertThat; + import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataInputBuffer; import java.nio.ByteBuffer; -import junit.framework.TestCase; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; /** * Test for {@link EventMessageDecoder}. */ -public final class EventMessageDecoderTest extends TestCase { +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class EventMessageDecoderTest { + @Test public void testDecodeEventMessage() { byte[] rawEmsgBody = new byte[] { 117, 114, 110, 58, 116, 101, 115, 116, 0, // scheme_id_uri = "urn:test" @@ -39,13 +46,13 @@ public final class EventMessageDecoderTest extends TestCase { MetadataInputBuffer buffer = new MetadataInputBuffer(); buffer.data = ByteBuffer.allocate(rawEmsgBody.length).put(rawEmsgBody); Metadata metadata = decoder.decode(buffer); - assertEquals(1, metadata.length()); + assertThat(metadata.length()).isEqualTo(1); EventMessage eventMessage = (EventMessage) metadata.get(0); - assertEquals("urn:test", eventMessage.schemeIdUri); - assertEquals("123", eventMessage.value); - assertEquals(3000, eventMessage.durationMs); - assertEquals(1000403, eventMessage.id); - MoreAsserts.assertEquals(new byte[] {0, 1, 2, 3, 4}, eventMessage.messageData); + assertThat(eventMessage.schemeIdUri).isEqualTo("urn:test"); + assertThat(eventMessage.value).isEqualTo("123"); + assertThat(eventMessage.durationMs).isEqualTo(3000); + assertThat(eventMessage.id).isEqualTo(1000403); + assertThat(eventMessage.messageData).isEqualTo(new byte[]{0, 1, 2, 3, 4}); } } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/metadata/emsg/EventMessageTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageTest.java similarity index 74% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/metadata/emsg/EventMessageTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageTest.java index baafb6b18b..b48a071d0d 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/metadata/emsg/EventMessageTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageTest.java @@ -15,14 +15,22 @@ */ package com.google.android.exoplayer2.metadata.emsg; +import static com.google.common.truth.Truth.assertThat; + import android.os.Parcel; -import junit.framework.TestCase; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; /** * Test for {@link EventMessage}. */ -public final class EventMessageTest extends TestCase { +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class EventMessageTest { + @Test public void testEventMessageParcelable() { EventMessage eventMessage = new EventMessage("urn:test", "123", 3000, 1000403, new byte[] {0, 1, 2, 3, 4}); @@ -33,7 +41,7 @@ public final class EventMessageTest extends TestCase { parcel.setDataPosition(0); EventMessage fromParcelEventMessage = EventMessage.CREATOR.createFromParcel(parcel); // Assert equals. - assertEquals(eventMessage, fromParcelEventMessage); + assertThat(fromParcelEventMessage).isEqualTo(eventMessage); } } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/metadata/id3/ChapterFrameTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/id3/ChapterFrameTest.java similarity index 75% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/metadata/id3/ChapterFrameTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/metadata/id3/ChapterFrameTest.java index 182ae6f1c9..a42b71731a 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/metadata/id3/ChapterFrameTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/id3/ChapterFrameTest.java @@ -15,14 +15,22 @@ */ package com.google.android.exoplayer2.metadata.id3; +import static com.google.common.truth.Truth.assertThat; + import android.os.Parcel; -import junit.framework.TestCase; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; /** * Test for {@link ChapterFrame}. */ -public final class ChapterFrameTest extends TestCase { +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class ChapterFrameTest { + @Test public void testParcelable() { Id3Frame[] subFrames = new Id3Frame[] { new TextInformationFrame("TIT2", null, "title"), @@ -35,7 +43,7 @@ public final class ChapterFrameTest extends TestCase { parcel.setDataPosition(0); ChapterFrame chapterFrameFromParcel = ChapterFrame.CREATOR.createFromParcel(parcel); - assertEquals(chapterFrameToParcel, chapterFrameFromParcel); + assertThat(chapterFrameFromParcel).isEqualTo(chapterFrameToParcel); parcel.recycle(); } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrameTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrameTest.java similarity index 76% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrameTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrameTest.java index 9641de7669..9636b04e51 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrameTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/id3/ChapterTocFrameTest.java @@ -15,14 +15,22 @@ */ package com.google.android.exoplayer2.metadata.id3; +import static com.google.common.truth.Truth.assertThat; + import android.os.Parcel; -import junit.framework.TestCase; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; /** * Test for {@link ChapterTocFrame}. */ -public final class ChapterTocFrameTest extends TestCase { +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class ChapterTocFrameTest { + @Test public void testParcelable() { String[] children = new String[] {"child0", "child1"}; Id3Frame[] subFrames = new Id3Frame[] { @@ -37,7 +45,7 @@ public final class ChapterTocFrameTest extends TestCase { parcel.setDataPosition(0); ChapterTocFrame chapterTocFrameFromParcel = ChapterTocFrame.CREATOR.createFromParcel(parcel); - assertEquals(chapterTocFrameToParcel, chapterTocFrameFromParcel); + assertThat(chapterTocFrameFromParcel).isEqualTo(chapterTocFrameToParcel); parcel.recycle(); } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/metadata/id3/Id3DecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/id3/Id3DecoderTest.java similarity index 66% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/metadata/id3/Id3DecoderTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/metadata/id3/Id3DecoderTest.java index 6b39ed1645..06ce330146 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/metadata/id3/Id3DecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/id3/Id3DecoderTest.java @@ -15,182 +15,197 @@ */ package com.google.android.exoplayer2.metadata.id3; -import android.test.MoreAsserts; +import static com.google.common.truth.Truth.assertThat; + +import com.google.android.exoplayer2.C; 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; +import java.nio.charset.Charset; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; /** * Test for {@link Id3Decoder}. */ -public final class Id3DecoderTest extends TestCase { +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class Id3DecoderTest { 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; + @Test 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()); + assertThat(metadata.length()).isEqualTo(1); TextInformationFrame textInformationFrame = (TextInformationFrame) metadata.get(0); - assertEquals("TXXX", textInformationFrame.id); - assertEquals("", textInformationFrame.description); - assertEquals("mdialog_VINDICO1527664_start", textInformationFrame.value); + assertThat(textInformationFrame.id).isEqualTo("TXXX"); + assertThat(textInformationFrame.description).isEmpty(); + assertThat(textInformationFrame.value).isEqualTo("mdialog_VINDICO1527664_start"); // Test empty. rawId3 = buildSingleFrameTag("TXXX", new byte[0]); metadata = decoder.decode(rawId3, rawId3.length); - assertEquals(0, metadata.length()); + assertThat(metadata.length()).isEqualTo(0); // Test encoding byte only. rawId3 = buildSingleFrameTag("TXXX", new byte[] {ID3_TEXT_ENCODING_UTF_8}); metadata = decoder.decode(rawId3, rawId3.length); - assertEquals(1, metadata.length()); + assertThat(metadata.length()).isEqualTo(1); textInformationFrame = (TextInformationFrame) metadata.get(0); - assertEquals("TXXX", textInformationFrame.id); - assertEquals("", textInformationFrame.description); - assertEquals("", textInformationFrame.value); + assertThat(textInformationFrame.id).isEqualTo("TXXX"); + assertThat(textInformationFrame.description).isEmpty(); + assertThat(textInformationFrame.value).isEmpty(); } + @Test 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()); + assertThat(metadata.length()).isEqualTo(1); TextInformationFrame textInformationFrame = (TextInformationFrame) metadata.get(0); - assertEquals("TIT2", textInformationFrame.id); - assertNull(textInformationFrame.description); - assertEquals("Hello World", textInformationFrame.value); + assertThat(textInformationFrame.id).isEqualTo("TIT2"); + assertThat(textInformationFrame.description).isNull(); + assertThat(textInformationFrame.value).isEqualTo("Hello World"); // Test empty. rawId3 = buildSingleFrameTag("TIT2", new byte[0]); metadata = decoder.decode(rawId3, rawId3.length); - assertEquals(0, metadata.length()); + assertThat(metadata.length()).isEqualTo(0); // Test encoding byte only. rawId3 = buildSingleFrameTag("TIT2", new byte[] {ID3_TEXT_ENCODING_UTF_8}); metadata = decoder.decode(rawId3, rawId3.length); - assertEquals(1, metadata.length()); + assertThat(metadata.length()).isEqualTo(1); textInformationFrame = (TextInformationFrame) metadata.get(0); - assertEquals("TIT2", textInformationFrame.id); - assertEquals(null, textInformationFrame.description); - assertEquals("", textInformationFrame.value); + assertThat(textInformationFrame.id).isEqualTo("TIT2"); + assertThat(textInformationFrame.description).isNull(); + assertThat(textInformationFrame.value).isEmpty(); } + @Test 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()); + assertThat(metadata.length()).isEqualTo(1); UrlLinkFrame urlLinkFrame = (UrlLinkFrame) metadata.get(0); - assertEquals("WXXX", urlLinkFrame.id); - assertEquals("test", urlLinkFrame.description); - assertEquals("https://test.com/abc?def", urlLinkFrame.url); + assertThat(urlLinkFrame.id).isEqualTo("WXXX"); + assertThat(urlLinkFrame.description).isEqualTo("test"); + assertThat(urlLinkFrame.url).isEqualTo("https://test.com/abc?def"); // Test empty. rawId3 = buildSingleFrameTag("WXXX", new byte[0]); metadata = decoder.decode(rawId3, rawId3.length); - assertEquals(0, metadata.length()); + assertThat(metadata.length()).isEqualTo(0); // Test encoding byte only. rawId3 = buildSingleFrameTag("WXXX", new byte[] {ID3_TEXT_ENCODING_UTF_8}); metadata = decoder.decode(rawId3, rawId3.length); - assertEquals(1, metadata.length()); + assertThat(metadata.length()).isEqualTo(1); urlLinkFrame = (UrlLinkFrame) metadata.get(0); - assertEquals("WXXX", urlLinkFrame.id); - assertEquals("", urlLinkFrame.description); - assertEquals("", urlLinkFrame.url); + assertThat(urlLinkFrame.id).isEqualTo("WXXX"); + assertThat(urlLinkFrame.description).isEmpty(); + assertThat(urlLinkFrame.url).isEmpty(); } + @Test 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()); + assertThat(metadata.length()).isEqualTo(1); UrlLinkFrame urlLinkFrame = (UrlLinkFrame) metadata.get(0); - assertEquals("WCOM", urlLinkFrame.id); - assertEquals(null, urlLinkFrame.description); - assertEquals("https://test.com/abc?def", urlLinkFrame.url); + assertThat(urlLinkFrame.id).isEqualTo("WCOM"); + assertThat(urlLinkFrame.description).isNull(); + assertThat(urlLinkFrame.url).isEqualTo("https://test.com/abc?def"); // Test empty. rawId3 = buildSingleFrameTag("WCOM", new byte[0]); metadata = decoder.decode(rawId3, rawId3.length); - assertEquals(1, metadata.length()); + assertThat(metadata.length()).isEqualTo(1); urlLinkFrame = (UrlLinkFrame) metadata.get(0); - assertEquals("WCOM", urlLinkFrame.id); - assertEquals(null, urlLinkFrame.description); - assertEquals("", urlLinkFrame.url); + assertThat(urlLinkFrame.id).isEqualTo("WCOM"); + assertThat(urlLinkFrame.description).isNull(); + assertThat(urlLinkFrame.url).isEmpty(); } + @Test 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()); + assertThat(metadata.length()).isEqualTo(1); PrivFrame privFrame = (PrivFrame) metadata.get(0); - assertEquals("test", privFrame.owner); - MoreAsserts.assertEquals(new byte[] {1, 2, 3, 4}, privFrame.privateData); + assertThat(privFrame.owner).isEqualTo("test"); + assertThat(privFrame.privateData).isEqualTo(new byte[]{1, 2, 3, 4}); // Test empty. rawId3 = buildSingleFrameTag("PRIV", new byte[0]); metadata = decoder.decode(rawId3, rawId3.length); - assertEquals(1, metadata.length()); + assertThat(metadata.length()).isEqualTo(1); privFrame = (PrivFrame) metadata.get(0); - assertEquals("", privFrame.owner); - MoreAsserts.assertEquals(new byte[0], privFrame.privateData); + assertThat(privFrame.owner).isEmpty(); + assertThat(privFrame.privateData).isEqualTo(new byte[0]); } + @Test 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()); + assertThat(metadata.length()).isEqualTo(1); 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); + assertThat(apicFrame.mimeType).isEqualTo("image/jpeg"); + assertThat(apicFrame.pictureType).isEqualTo(16); + assertThat(apicFrame.description).isEqualTo("Hello World"); + assertThat(apicFrame.pictureData).hasLength(10); + assertThat(apicFrame.pictureData).isEqualTo(new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}); } + @Test 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()); + assertThat(metadata.length()).isEqualTo(1); CommentFrame commentFrame = (CommentFrame) metadata.get(0); - assertEquals("eng", commentFrame.language); - assertEquals("description", commentFrame.description); - assertEquals("text", commentFrame.text); + assertThat(commentFrame.language).isEqualTo("eng"); + assertThat(commentFrame.description).isEqualTo("description"); + assertThat(commentFrame.text).isEqualTo("text"); // Test empty. rawId3 = buildSingleFrameTag("COMM", new byte[0]); metadata = decoder.decode(rawId3, rawId3.length); - assertEquals(0, metadata.length()); + assertThat(metadata.length()).isEqualTo(0); // 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()); + assertThat(metadata.length()).isEqualTo(1); commentFrame = (CommentFrame) metadata.get(0); - assertEquals("eng", commentFrame.language); - assertEquals("", commentFrame.description); - assertEquals("", commentFrame.text); + assertThat(commentFrame.language).isEqualTo("eng"); + assertThat(commentFrame.description).isEmpty(); + assertThat(commentFrame.text).isEmpty(); } private static byte[] buildSingleFrameTag(String frameId, byte[] frameData) { - byte[] frameIdBytes = frameId.getBytes(); + byte[] frameIdBytes = frameId.getBytes(Charset.forName(C.UTF8_NAME)); Assertions.checkState(frameIdBytes.length == 4); byte[] tagData = new byte[TAG_HEADER.length + FRAME_HEADER_LENGTH + frameData.length]; diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoderTest.java similarity index 73% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoderTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoderTest.java index c50ff06699..15cb9b23c5 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoderTest.java @@ -15,29 +15,38 @@ */ package com.google.android.exoplayer2.metadata.scte35; -import com.google.android.exoplayer2.C; +import static com.google.android.exoplayer2.C.TIME_UNSET; +import static com.google.common.truth.Truth.assertThat; + import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataDecoderException; import com.google.android.exoplayer2.metadata.MetadataInputBuffer; import com.google.android.exoplayer2.util.TimestampAdjuster; import java.nio.ByteBuffer; import java.util.List; -import junit.framework.TestCase; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; /** * Test for {@link SpliceInfoDecoder}. */ -public final class SpliceInfoDecoderTest extends TestCase { +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class SpliceInfoDecoderTest { private SpliceInfoDecoder decoder; private MetadataInputBuffer inputBuffer; - @Override + @Before public void setUp() { decoder = new SpliceInfoDecoder(); inputBuffer = new MetadataInputBuffer(); } + @Test public void testWrappedAroundTimeSignalCommand() throws MetadataDecoderException { byte[] rawTimeSignalSection = new byte[] { 0, // table_id. @@ -59,11 +68,12 @@ public final class SpliceInfoDecoderTest extends TestCase { // The playback position is 57:15:58.43 approximately. // With this offset, the playback position pts before wrapping is 0x451ebf851. Metadata metadata = feedInputBuffer(rawTimeSignalSection, 0x3000000000L, -0x50000L); - assertEquals(1, metadata.length()); - assertEquals(removePtsConversionPrecisionError(0x3001000000L, inputBuffer.subsampleOffsetUs), - ((TimeSignalCommand) metadata.get(0)).playbackPositionUs); + assertThat(metadata.length()).isEqualTo(1); + assertThat(((TimeSignalCommand) metadata.get(0)).playbackPositionUs) + .isEqualTo(removePtsConversionPrecisionError(0x3001000000L, inputBuffer.subsampleOffsetUs)); } + @Test public void test2SpliceInsertCommands() throws MetadataDecoderException { byte[] rawSpliceInsertCommand1 = new byte[] { 0, // table_id. @@ -91,18 +101,18 @@ public final class SpliceInfoDecoderTest extends TestCase { 0x00, 0x00, 0x00, 0x00}; // CRC_32 (ignored, check happens at extraction). Metadata metadata = feedInputBuffer(rawSpliceInsertCommand1, 2000000, 3000000); - assertEquals(1, metadata.length()); + assertThat(metadata.length()).isEqualTo(1); SpliceInsertCommand command = (SpliceInsertCommand) metadata.get(0); - assertEquals(66, command.spliceEventId); - assertFalse(command.spliceEventCancelIndicator); - assertFalse(command.outOfNetworkIndicator); - assertTrue(command.programSpliceFlag); - assertFalse(command.spliceImmediateFlag); - assertEquals(3000000, command.programSplicePlaybackPositionUs); - assertEquals(C.TIME_UNSET, command.breakDuration); - assertEquals(16, command.uniqueProgramId); - assertEquals(1, command.availNum); - assertEquals(2, command.availsExpected); + assertThat(command.spliceEventId).isEqualTo(66); + assertThat(command.spliceEventCancelIndicator).isFalse(); + assertThat(command.outOfNetworkIndicator).isFalse(); + assertThat(command.programSpliceFlag).isTrue(); + assertThat(command.spliceImmediateFlag).isFalse(); + assertThat(command.programSplicePlaybackPositionUs).isEqualTo(3000000); + assertThat(command.breakDuration).isEqualTo(TIME_UNSET); + assertThat(command.uniqueProgramId).isEqualTo(16); + assertThat(command.availNum).isEqualTo(1); + assertThat(command.availsExpected).isEqualTo(2); byte[] rawSpliceInsertCommand2 = new byte[] { 0, // table_id. @@ -137,24 +147,24 @@ public final class SpliceInfoDecoderTest extends TestCase { // By changing the subsample offset we force adjuster reconstruction. long subsampleOffset = 1000011; metadata = feedInputBuffer(rawSpliceInsertCommand2, 1000000, subsampleOffset); - assertEquals(1, metadata.length()); + assertThat(metadata.length()).isEqualTo(1); command = (SpliceInsertCommand) metadata.get(0); - assertEquals(0xffffffffL, command.spliceEventId); - assertFalse(command.spliceEventCancelIndicator); - assertFalse(command.outOfNetworkIndicator); - assertFalse(command.programSpliceFlag); - assertFalse(command.spliceImmediateFlag); - assertEquals(C.TIME_UNSET, command.programSplicePlaybackPositionUs); - assertEquals(C.TIME_UNSET, command.breakDuration); + assertThat(command.spliceEventId).isEqualTo(0xffffffffL); + assertThat(command.spliceEventCancelIndicator).isFalse(); + assertThat(command.outOfNetworkIndicator).isFalse(); + assertThat(command.programSpliceFlag).isFalse(); + assertThat(command.spliceImmediateFlag).isFalse(); + assertThat(command.programSplicePlaybackPositionUs).isEqualTo(TIME_UNSET); + assertThat(command.breakDuration).isEqualTo(TIME_UNSET); List componentSplices = command.componentSpliceList; - assertEquals(2, componentSplices.size()); - assertEquals(16, componentSplices.get(0).componentTag); - assertEquals(1000000, componentSplices.get(0).componentSplicePlaybackPositionUs); - assertEquals(17, componentSplices.get(1).componentTag); - assertEquals(C.TIME_UNSET, componentSplices.get(1).componentSplicePts); - assertEquals(32, command.uniqueProgramId); - assertEquals(1, command.availNum); - assertEquals(2, command.availsExpected); + assertThat(componentSplices).hasSize(2); + assertThat(componentSplices.get(0).componentTag).isEqualTo(16); + assertThat(componentSplices.get(0).componentSplicePlaybackPositionUs).isEqualTo(1000000); + assertThat(componentSplices.get(1).componentTag).isEqualTo(17); + assertThat(componentSplices.get(1).componentSplicePts).isEqualTo(TIME_UNSET); + assertThat(command.uniqueProgramId).isEqualTo(32); + assertThat(command.availNum).isEqualTo(1); + assertThat(command.availsExpected).isEqualTo(2); } private Metadata feedInputBuffer(byte[] data, long timeUs, long subsampleOffset) diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/ProgressiveDownloadActionTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/ProgressiveDownloadActionTest.java new file mode 100644 index 0000000000..95dd218092 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/ProgressiveDownloadActionTest.java @@ -0,0 +1,143 @@ +/* + * 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.offline; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.android.exoplayer2.upstream.DummyDataSource; +import com.google.android.exoplayer2.upstream.cache.Cache; +import com.google.android.exoplayer2.util.ClosedSource; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Unit tests for {@link ProgressiveDownloadAction}. + */ +@ClosedSource(reason = "Not ready yet") +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public class ProgressiveDownloadActionTest { + + @Test + public void testDownloadActionIsNotRemoveAction() throws Exception { + ProgressiveDownloadAction action = new ProgressiveDownloadAction("uri", null, false); + assertThat(action.isRemoveAction()).isFalse(); + } + + @Test + public void testRemoveActionIsRemoveAction() throws Exception { + ProgressiveDownloadAction action2 = new ProgressiveDownloadAction("uri", null, true); + assertThat(action2.isRemoveAction()).isTrue(); + } + + @Test + public void testCreateDownloader() throws Exception { + MockitoAnnotations.initMocks(this); + ProgressiveDownloadAction action = new ProgressiveDownloadAction("uri", null, false); + DownloaderConstructorHelper constructorHelper = new DownloaderConstructorHelper( + Mockito.mock(Cache.class), DummyDataSource.FACTORY); + assertThat(action.createDownloader(constructorHelper)).isNotNull(); + } + + @Test + public void testSameUriCacheKeyDifferentAction_IsSameMedia() throws Exception { + ProgressiveDownloadAction action1 = new ProgressiveDownloadAction("uri", null, true); + ProgressiveDownloadAction action2 = new ProgressiveDownloadAction("uri", null, false); + assertThat(action1.isSameMedia(action2)).isTrue(); + } + + @Test + public void testNullCacheKeyDifferentUriAction_IsNotSameMedia() throws Exception { + ProgressiveDownloadAction action3 = new ProgressiveDownloadAction("uri2", null, true); + ProgressiveDownloadAction action4 = new ProgressiveDownloadAction("uri", null, false); + assertThat(action3.isSameMedia(action4)).isFalse(); + } + + @Test + public void testSameCacheKeyDifferentUriAction_IsSameMedia() throws Exception { + ProgressiveDownloadAction action5 = new ProgressiveDownloadAction("uri2", "key", true); + ProgressiveDownloadAction action6 = new ProgressiveDownloadAction("uri", "key", false); + assertThat(action5.isSameMedia(action6)).isTrue(); + } + + @Test + public void testSameUriDifferentCacheKeyAction_IsNotSameMedia() throws Exception { + ProgressiveDownloadAction action7 = new ProgressiveDownloadAction("uri", "key", true); + ProgressiveDownloadAction action8 = new ProgressiveDownloadAction("uri", "key2", false); + assertThat(action7.isSameMedia(action8)).isFalse(); + } + + @Test + public void testEquals() throws Exception { + ProgressiveDownloadAction action1 = new ProgressiveDownloadAction("uri", null, true); + assertThat(action1.equals(action1)).isTrue(); + + ProgressiveDownloadAction action2 = new ProgressiveDownloadAction("uri", null, true); + ProgressiveDownloadAction action3 = new ProgressiveDownloadAction("uri", null, true); + assertThat(action2.equals(action3)).isTrue(); + + ProgressiveDownloadAction action4 = new ProgressiveDownloadAction("uri", null, true); + ProgressiveDownloadAction action5 = new ProgressiveDownloadAction("uri", null, false); + assertThat(action4.equals(action5)).isFalse(); + + ProgressiveDownloadAction action6 = new ProgressiveDownloadAction("uri", null, true); + ProgressiveDownloadAction action7 = new ProgressiveDownloadAction("uri", "key", true); + assertThat(action6.equals(action7)).isFalse(); + + ProgressiveDownloadAction action8 = new ProgressiveDownloadAction("uri", "key2", true); + ProgressiveDownloadAction action9 = new ProgressiveDownloadAction("uri", "key", true); + assertThat(action8.equals(action9)).isFalse(); + + ProgressiveDownloadAction action10 = new ProgressiveDownloadAction("uri", null, true); + ProgressiveDownloadAction action11 = new ProgressiveDownloadAction("uri2", null, true); + assertThat(action10.equals(action11)).isFalse(); + } + + @Test + public void testSerializerGetType() throws Exception { + ProgressiveDownloadAction action = new ProgressiveDownloadAction("uri", null, false); + assertThat(action.getType()).isNotNull(); + } + + @Test + public void testSerializerWriteRead() throws Exception { + doTestSerializationRoundTrip(new ProgressiveDownloadAction("uri1", null, false)); + doTestSerializationRoundTrip(new ProgressiveDownloadAction("uri2", "key", true)); + } + + private static void doTestSerializationRoundTrip(ProgressiveDownloadAction action1) + throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + DataOutputStream output = new DataOutputStream(out); + action1.writeToStream(output); + + ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); + DataInputStream input = new DataInputStream(in); + DownloadAction action2 = ProgressiveDownloadAction.DESERIALIZER.readFromStream(input); + + assertThat(action2).isEqualTo(action1); + } + +} diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/SampleQueueTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java similarity index 86% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/source/SampleQueueTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java index 77e61e39a9..49983fae30 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/SampleQueueTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java @@ -15,7 +15,14 @@ */ package com.google.android.exoplayer2.source; -import android.test.MoreAsserts; +import static com.google.android.exoplayer2.C.RESULT_BUFFER_READ; +import static com.google.android.exoplayer2.C.RESULT_FORMAT_READ; +import static com.google.android.exoplayer2.C.RESULT_NOTHING_READ; +import static com.google.android.exoplayer2.source.SampleQueue.ADVANCE_FAILED; +import static com.google.common.truth.Truth.assertThat; +import static java.lang.Long.MIN_VALUE; +import static java.util.Arrays.copyOfRange; + import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; @@ -24,13 +31,19 @@ import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DefaultAllocator; import com.google.android.exoplayer2.util.ParsableByteArray; -import java.util.Arrays; -import junit.framework.TestCase; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; /** * Test for {@link SampleQueue}. */ -public class SampleQueueTest extends TestCase { +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class SampleQueueTest { private static final int ALLOCATION_SIZE = 16; @@ -75,24 +88,23 @@ public class SampleQueueTest extends TestCase { private FormatHolder formatHolder; private DecoderInputBuffer inputBuffer; - @Override + @Before public void setUp() throws Exception { - super.setUp(); allocator = new DefaultAllocator(false, ALLOCATION_SIZE); sampleQueue = new SampleQueue(allocator); formatHolder = new FormatHolder(); inputBuffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL); } - @Override + @After public void tearDown() throws Exception { - super.tearDown(); allocator = null; sampleQueue = null; formatHolder = null; inputBuffer = null; } + @Test public void testResetReleasesAllocations() { writeTestData(); assertAllocationCount(10); @@ -100,10 +112,12 @@ public class SampleQueueTest extends TestCase { assertAllocationCount(0); } + @Test public void testReadWithoutWrite() { assertNoSamplesToRead(null); } + @Test public void testReadFormatDeduplicated() { sampleQueue.format(TEST_FORMAT_1); assertReadFormat(false, TEST_FORMAT_1); @@ -115,6 +129,7 @@ public class SampleQueueTest extends TestCase { assertNoSamplesToRead(TEST_FORMAT_1); } + @Test public void testReadSingleSamples() { sampleQueue.sampleData(new ParsableByteArray(TEST_DATA), ALLOCATION_SIZE); @@ -173,9 +188,10 @@ public class SampleQueueTest extends TestCase { assertAllocationCount(0); } + @Test public void testReadMultiSamples() { writeTestData(); - assertEquals(LAST_SAMPLE_TIMESTAMP, sampleQueue.getLargestQueuedTimestampUs()); + assertThat(sampleQueue.getLargestQueuedTimestampUs()).isEqualTo(LAST_SAMPLE_TIMESTAMP); assertAllocationCount(10); assertReadTestData(); assertAllocationCount(10); @@ -183,6 +199,7 @@ public class SampleQueueTest extends TestCase { assertAllocationCount(0); } + @Test public void testReadMultiSamplesTwice() { writeTestData(); writeTestData(); @@ -194,19 +211,21 @@ public class SampleQueueTest extends TestCase { assertAllocationCount(0); } + @Test public void testReadMultiWithRewind() { writeTestData(); assertReadTestData(); - assertEquals(8, sampleQueue.getReadIndex()); + assertThat(sampleQueue.getReadIndex()).isEqualTo(8); assertAllocationCount(10); // Rewind. sampleQueue.rewind(); assertAllocationCount(10); // Read again. - assertEquals(0, sampleQueue.getReadIndex()); + assertThat(sampleQueue.getReadIndex()).isEqualTo(0); assertReadTestData(); } + @Test public void testRewindAfterDiscard() { writeTestData(); assertReadTestData(); @@ -216,10 +235,11 @@ public class SampleQueueTest extends TestCase { sampleQueue.rewind(); assertAllocationCount(0); // Can't read again. - assertEquals(8, sampleQueue.getReadIndex()); + assertThat(sampleQueue.getReadIndex()).isEqualTo(8); assertReadEndOfStream(false); } + @Test public void testAdvanceToEnd() { writeTestData(); sampleQueue.advanceToEnd(); @@ -233,6 +253,7 @@ public class SampleQueueTest extends TestCase { assertNoSamplesToRead(TEST_FORMAT_2); } + @Test public void testAdvanceToEndRetainsUnassignedData() { sampleQueue.format(TEST_FORMAT_1); sampleQueue.sampleData(new ParsableByteArray(TEST_DATA), ALLOCATION_SIZE); @@ -256,56 +277,62 @@ public class SampleQueueTest extends TestCase { assertAllocationCount(0); } + @Test public void testAdvanceToBeforeBuffer() { writeTestData(); int skipCount = sampleQueue.advanceTo(TEST_SAMPLE_TIMESTAMPS[0] - 1, true, false); // Should fail and have no effect. - assertEquals(SampleQueue.ADVANCE_FAILED, skipCount); + assertThat(skipCount).isEqualTo(ADVANCE_FAILED); assertReadTestData(); assertNoSamplesToRead(TEST_FORMAT_2); } + @Test public void testAdvanceToStartOfBuffer() { writeTestData(); int skipCount = sampleQueue.advanceTo(TEST_SAMPLE_TIMESTAMPS[0], true, false); // Should succeed but have no effect (we're already at the first frame). - assertEquals(0, skipCount); + assertThat(skipCount).isEqualTo(0); assertReadTestData(); assertNoSamplesToRead(TEST_FORMAT_2); } + @Test public void testAdvanceToEndOfBuffer() { writeTestData(); int skipCount = sampleQueue.advanceTo(LAST_SAMPLE_TIMESTAMP, true, false); // Should succeed and skip to 2nd keyframe (the 4th frame). - assertEquals(4, skipCount); + assertThat(skipCount).isEqualTo(4); assertReadTestData(null, TEST_DATA_SECOND_KEYFRAME_INDEX); assertNoSamplesToRead(TEST_FORMAT_2); } + @Test public void testAdvanceToAfterBuffer() { writeTestData(); int skipCount = sampleQueue.advanceTo(LAST_SAMPLE_TIMESTAMP + 1, true, false); // Should fail and have no effect. - assertEquals(SampleQueue.ADVANCE_FAILED, skipCount); + assertThat(skipCount).isEqualTo(ADVANCE_FAILED); assertReadTestData(); assertNoSamplesToRead(TEST_FORMAT_2); } + @Test public void testAdvanceToAfterBufferAllowed() { writeTestData(); int skipCount = sampleQueue.advanceTo(LAST_SAMPLE_TIMESTAMP + 1, true, true); // Should succeed and skip to 2nd keyframe (the 4th frame). - assertEquals(4, skipCount); + assertThat(skipCount).isEqualTo(4); assertReadTestData(null, TEST_DATA_SECOND_KEYFRAME_INDEX); assertNoSamplesToRead(TEST_FORMAT_2); } + @Test public void testDiscardToEnd() { writeTestData(); // Should discard everything. sampleQueue.discardToEnd(); - assertEquals(8, sampleQueue.getReadIndex()); + assertThat(sampleQueue.getReadIndex()).isEqualTo(8); assertAllocationCount(0); // We should still be able to read the upstream format. assertReadFormat(false, TEST_FORMAT_2); @@ -314,17 +341,18 @@ public class SampleQueueTest extends TestCase { assertReadTestData(TEST_FORMAT_2); } + @Test public void testDiscardToStopAtReadPosition() { writeTestData(); // Shouldn't discard anything. sampleQueue.discardTo(LAST_SAMPLE_TIMESTAMP, false, true); - assertEquals(0, sampleQueue.getReadIndex()); + assertThat(sampleQueue.getReadIndex()).isEqualTo(0); assertAllocationCount(10); // Read the first sample. assertReadTestData(null, 0, 1); // Shouldn't discard anything. sampleQueue.discardTo(TEST_SAMPLE_TIMESTAMPS[1] - 1, false, true); - assertEquals(1, sampleQueue.getReadIndex()); + assertThat(sampleQueue.getReadIndex()).isEqualTo(1); assertAllocationCount(10); // Should discard the read sample. sampleQueue.discardTo(TEST_SAMPLE_TIMESTAMPS[1], false, true); @@ -334,7 +362,7 @@ public class SampleQueueTest extends TestCase { assertAllocationCount(9); // Should be able to read the remaining samples. assertReadTestData(TEST_FORMAT_1, 1, 7); - assertEquals(8, sampleQueue.getReadIndex()); + assertThat(sampleQueue.getReadIndex()).isEqualTo(8); // Should discard up to the second last sample sampleQueue.discardTo(LAST_SAMPLE_TIMESTAMP - 1, false, true); assertAllocationCount(3); @@ -343,20 +371,22 @@ public class SampleQueueTest extends TestCase { assertAllocationCount(1); } + @Test public void testDiscardToDontStopAtReadPosition() { writeTestData(); // Shouldn't discard anything. sampleQueue.discardTo(TEST_SAMPLE_TIMESTAMPS[1] - 1, false, false); - assertEquals(0, sampleQueue.getReadIndex()); + assertThat(sampleQueue.getReadIndex()).isEqualTo(0); assertAllocationCount(10); // Should discard the first sample. sampleQueue.discardTo(TEST_SAMPLE_TIMESTAMPS[1], false, false); - assertEquals(1, sampleQueue.getReadIndex()); + assertThat(sampleQueue.getReadIndex()).isEqualTo(1); assertAllocationCount(9); // Should be able to read the remaining samples. assertReadTestData(TEST_FORMAT_1, 1, 7); } + @Test public void testDiscardUpstream() { writeTestData(); sampleQueue.discardUpstreamSamples(8); @@ -381,6 +411,7 @@ public class SampleQueueTest extends TestCase { assertNoSamplesToRead(TEST_FORMAT_2); } + @Test public void testDiscardUpstreamMulti() { writeTestData(); sampleQueue.discardUpstreamSamples(4); @@ -391,6 +422,7 @@ public class SampleQueueTest extends TestCase { assertNoSamplesToRead(TEST_FORMAT_2); } + @Test public void testDiscardUpstreamBeforeRead() { writeTestData(); sampleQueue.discardUpstreamSamples(4); @@ -400,6 +432,7 @@ public class SampleQueueTest extends TestCase { assertNoSamplesToRead(TEST_FORMAT_2); } + @Test public void testDiscardUpstreamAfterRead() { writeTestData(); assertReadTestData(null, 0, 3); @@ -421,41 +454,44 @@ public class SampleQueueTest extends TestCase { assertNoSamplesToRead(TEST_FORMAT_2); } + @Test public void testLargestQueuedTimestampWithDiscardUpstream() { writeTestData(); - assertEquals(LAST_SAMPLE_TIMESTAMP, sampleQueue.getLargestQueuedTimestampUs()); + assertThat(sampleQueue.getLargestQueuedTimestampUs()).isEqualTo(LAST_SAMPLE_TIMESTAMP); sampleQueue.discardUpstreamSamples(TEST_SAMPLE_TIMESTAMPS.length - 1); // Discarding from upstream should reduce the largest timestamp. - assertEquals(TEST_SAMPLE_TIMESTAMPS[TEST_SAMPLE_TIMESTAMPS.length - 2], - sampleQueue.getLargestQueuedTimestampUs()); + assertThat(sampleQueue.getLargestQueuedTimestampUs()) + .isEqualTo(TEST_SAMPLE_TIMESTAMPS[TEST_SAMPLE_TIMESTAMPS.length - 2]); sampleQueue.discardUpstreamSamples(0); // Discarding everything from upstream without reading should unset the largest timestamp. - assertEquals(Long.MIN_VALUE, sampleQueue.getLargestQueuedTimestampUs()); + assertThat(sampleQueue.getLargestQueuedTimestampUs()).isEqualTo(MIN_VALUE); } + @Test public void testLargestQueuedTimestampWithDiscardUpstreamDecodeOrder() { long[] decodeOrderTimestamps = new long[] {0, 3000, 2000, 1000, 4000, 7000, 6000, 5000}; writeTestData(TEST_DATA, TEST_SAMPLE_SIZES, TEST_SAMPLE_OFFSETS, decodeOrderTimestamps, TEST_SAMPLE_FORMATS, TEST_SAMPLE_FLAGS); - assertEquals(7000, sampleQueue.getLargestQueuedTimestampUs()); + assertThat(sampleQueue.getLargestQueuedTimestampUs()).isEqualTo(7000); sampleQueue.discardUpstreamSamples(TEST_SAMPLE_TIMESTAMPS.length - 2); // Discarding the last two samples should not change the largest timestamp, due to the decode // ordering of the timestamps. - assertEquals(7000, sampleQueue.getLargestQueuedTimestampUs()); + assertThat(sampleQueue.getLargestQueuedTimestampUs()).isEqualTo(7000); sampleQueue.discardUpstreamSamples(TEST_SAMPLE_TIMESTAMPS.length - 3); // Once a third sample is discarded, the largest timestamp should have changed. - assertEquals(4000, sampleQueue.getLargestQueuedTimestampUs()); + assertThat(sampleQueue.getLargestQueuedTimestampUs()).isEqualTo(4000); sampleQueue.discardUpstreamSamples(0); // Discarding everything from upstream without reading should unset the largest timestamp. - assertEquals(Long.MIN_VALUE, sampleQueue.getLargestQueuedTimestampUs()); + assertThat(sampleQueue.getLargestQueuedTimestampUs()).isEqualTo(MIN_VALUE); } + @Test public void testLargestQueuedTimestampWithRead() { writeTestData(); - assertEquals(LAST_SAMPLE_TIMESTAMP, sampleQueue.getLargestQueuedTimestampUs()); + assertThat(sampleQueue.getLargestQueuedTimestampUs()).isEqualTo(LAST_SAMPLE_TIMESTAMP); assertReadTestData(); // Reading everything should not reduce the largest timestamp. - assertEquals(LAST_SAMPLE_TIMESTAMP, sampleQueue.getLargestQueuedTimestampUs()); + assertThat(sampleQueue.getLargestQueuedTimestampUs()).isEqualTo(LAST_SAMPLE_TIMESTAMP); } // Internal methods. @@ -580,9 +616,9 @@ public class SampleQueueTest extends TestCase { private void assertReadNothing(boolean formatRequired) { clearFormatHolderAndInputBuffer(); int result = sampleQueue.read(formatHolder, inputBuffer, formatRequired, false, 0); - assertEquals(C.RESULT_NOTHING_READ, result); + assertThat(result).isEqualTo(RESULT_NOTHING_READ); // formatHolder should not be populated. - assertNull(formatHolder.format); + assertThat(formatHolder.format).isNull(); // inputBuffer should not be populated. assertInputBufferContainsNoSampleData(); assertInputBufferHasNoDefaultFlagsSet(); @@ -597,14 +633,14 @@ public class SampleQueueTest extends TestCase { private void assertReadEndOfStream(boolean formatRequired) { clearFormatHolderAndInputBuffer(); int result = sampleQueue.read(formatHolder, inputBuffer, formatRequired, true, 0); - assertEquals(C.RESULT_BUFFER_READ, result); + assertThat(result).isEqualTo(RESULT_BUFFER_READ); // formatHolder should not be populated. - assertNull(formatHolder.format); + assertThat(formatHolder.format).isNull(); // inputBuffer should not contain sample data, but end of stream flag should be set. assertInputBufferContainsNoSampleData(); - assertTrue(inputBuffer.isEndOfStream()); - assertFalse(inputBuffer.isDecodeOnly()); - assertFalse(inputBuffer.isEncrypted()); + assertThat(inputBuffer.isEndOfStream()).isTrue(); + assertThat(inputBuffer.isDecodeOnly()).isFalse(); + assertThat(inputBuffer.isEncrypted()).isFalse(); } /** @@ -617,9 +653,9 @@ public class SampleQueueTest extends TestCase { private void assertReadFormat(boolean formatRequired, Format format) { clearFormatHolderAndInputBuffer(); int result = sampleQueue.read(formatHolder, inputBuffer, formatRequired, false, 0); - assertEquals(C.RESULT_FORMAT_READ, result); + assertThat(result).isEqualTo(RESULT_FORMAT_READ); // formatHolder should be populated. - assertEquals(format, formatHolder.format); + assertThat(formatHolder.format).isEqualTo(format); // inputBuffer should not be populated. assertInputBufferContainsNoSampleData(); assertInputBufferHasNoDefaultFlagsSet(); @@ -639,19 +675,19 @@ public class SampleQueueTest extends TestCase { int length) { clearFormatHolderAndInputBuffer(); int result = sampleQueue.read(formatHolder, inputBuffer, false, false, 0); - assertEquals(C.RESULT_BUFFER_READ, result); + assertThat(result).isEqualTo(RESULT_BUFFER_READ); // formatHolder should not be populated. - assertNull(formatHolder.format); + assertThat(formatHolder.format).isNull(); // inputBuffer should be populated. - assertEquals(timeUs, inputBuffer.timeUs); - assertEquals(isKeyframe, inputBuffer.isKeyFrame()); - assertFalse(inputBuffer.isDecodeOnly()); - assertFalse(inputBuffer.isEncrypted()); + assertThat(inputBuffer.timeUs).isEqualTo(timeUs); + assertThat(inputBuffer.isKeyFrame()).isEqualTo(isKeyframe); + assertThat(inputBuffer.isDecodeOnly()).isFalse(); + assertThat(inputBuffer.isEncrypted()).isFalse(); inputBuffer.flip(); - assertEquals(length, inputBuffer.data.limit()); + assertThat(inputBuffer.data.limit()).isEqualTo(length); byte[] readData = new byte[length]; inputBuffer.data.get(readData); - MoreAsserts.assertEquals(Arrays.copyOfRange(sampleData, offset, offset + length), readData); + assertThat(readData).isEqualTo(copyOfRange(sampleData, offset, offset + length)); } /** @@ -660,7 +696,7 @@ public class SampleQueueTest extends TestCase { * @param count The expected number of allocations. */ private void assertAllocationCount(int count) { - assertEquals(ALLOCATION_SIZE * count, allocator.getTotalBytesAllocated()); + assertThat(allocator.getTotalBytesAllocated()).isEqualTo(ALLOCATION_SIZE * count); } /** @@ -671,13 +707,13 @@ public class SampleQueueTest extends TestCase { return; } inputBuffer.flip(); - assertEquals(0, inputBuffer.data.limit()); + assertThat(inputBuffer.data.limit()).isEqualTo(0); } private void assertInputBufferHasNoDefaultFlagsSet() { - assertFalse(inputBuffer.isEndOfStream()); - assertFalse(inputBuffer.isDecodeOnly()); - assertFalse(inputBuffer.isEncrypted()); + assertThat(inputBuffer.isEndOfStream()).isFalse(); + assertThat(inputBuffer.isDecodeOnly()).isFalse(); + assertThat(inputBuffer.isEncrypted()).isFalse(); } private void clearFormatHolderAndInputBuffer() { diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ShuffleOrderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/ShuffleOrderTest.java similarity index 78% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/source/ShuffleOrderTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/source/ShuffleOrderTest.java index 5de6bdf3e1..1229e47883 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ShuffleOrderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/ShuffleOrderTest.java @@ -15,18 +15,27 @@ */ package com.google.android.exoplayer2.source; +import static com.google.android.exoplayer2.C.INDEX_UNSET; +import static com.google.common.truth.Truth.assertThat; + import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder; import com.google.android.exoplayer2.source.ShuffleOrder.UnshuffledShuffleOrder; -import junit.framework.TestCase; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; /** * Unit test for {@link ShuffleOrder}. */ -public final class ShuffleOrderTest extends TestCase { +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class ShuffleOrderTest { public static final long RANDOM_SEED = 1234567890L; + @Test public void testDefaultShuffleOrder() { assertShuffleOrderCorrectness(new DefaultShuffleOrder(0, RANDOM_SEED), 0); assertShuffleOrderCorrectness(new DefaultShuffleOrder(1, RANDOM_SEED), 1); @@ -44,6 +53,7 @@ public final class ShuffleOrderTest extends TestCase { testCloneAndRemove(new DefaultShuffleOrder(1, RANDOM_SEED), 0); } + @Test public void testUnshuffledShuffleOrder() { assertShuffleOrderCorrectness(new UnshuffledShuffleOrder(0), 0); assertShuffleOrderCorrectness(new UnshuffledShuffleOrder(1), 1); @@ -61,35 +71,36 @@ public final class ShuffleOrderTest extends TestCase { testCloneAndRemove(new UnshuffledShuffleOrder(1), 0); } + @Test public void testUnshuffledShuffleOrderIsUnshuffled() { ShuffleOrder shuffleOrder = new UnshuffledShuffleOrder(5); - assertEquals(0, shuffleOrder.getFirstIndex()); - assertEquals(4, shuffleOrder.getLastIndex()); + assertThat(shuffleOrder.getFirstIndex()).isEqualTo(0); + assertThat(shuffleOrder.getLastIndex()).isEqualTo(4); for (int i = 0; i < 4; i++) { - assertEquals(i + 1, shuffleOrder.getNextIndex(i)); + assertThat(shuffleOrder.getNextIndex(i)).isEqualTo(i + 1); } } private static void assertShuffleOrderCorrectness(ShuffleOrder shuffleOrder, int length) { - assertEquals(length, shuffleOrder.getLength()); + assertThat(shuffleOrder.getLength()).isEqualTo(length); if (length == 0) { - assertEquals(C.INDEX_UNSET, shuffleOrder.getFirstIndex()); - assertEquals(C.INDEX_UNSET, shuffleOrder.getLastIndex()); + assertThat(shuffleOrder.getFirstIndex()).isEqualTo(INDEX_UNSET); + assertThat(shuffleOrder.getLastIndex()).isEqualTo(INDEX_UNSET); } else { int[] indices = new int[length]; indices[0] = shuffleOrder.getFirstIndex(); - assertEquals(C.INDEX_UNSET, shuffleOrder.getPreviousIndex(indices[0])); + assertThat(shuffleOrder.getPreviousIndex(indices[0])).isEqualTo(INDEX_UNSET); for (int i = 1; i < length; i++) { indices[i] = shuffleOrder.getNextIndex(indices[i - 1]); - assertEquals(indices[i - 1], shuffleOrder.getPreviousIndex(indices[i])); + assertThat(shuffleOrder.getPreviousIndex(indices[i])).isEqualTo(indices[i - 1]); for (int j = 0; j < i; j++) { - assertTrue(indices[i] != indices[j]); + assertThat(indices[i] != indices[j]).isTrue(); } } - assertEquals(indices[length - 1], shuffleOrder.getLastIndex()); - assertEquals(C.INDEX_UNSET, shuffleOrder.getNextIndex(indices[length - 1])); + assertThat(shuffleOrder.getLastIndex()).isEqualTo(indices[length - 1]); + assertThat(shuffleOrder.getNextIndex(indices[length - 1])).isEqualTo(INDEX_UNSET); for (int i = 0; i < length; i++) { - assertTrue(indices[i] >= 0 && indices[i] < length); + assertThat(indices[i] >= 0 && indices[i] < length).isTrue(); } } } @@ -107,7 +118,7 @@ public final class ShuffleOrderTest extends TestCase { while (newNextIndex >= position && newNextIndex < position + count) { newNextIndex = newOrder.getNextIndex(newNextIndex); } - assertEquals(expectedNextIndex, newNextIndex); + assertThat(newNextIndex).isEqualTo(expectedNextIndex); } } @@ -127,7 +138,7 @@ public final class ShuffleOrderTest extends TestCase { expectedNextIndex--; } int newNextIndex = newOrder.getNextIndex(i < position ? i : i - 1); - assertEquals(expectedNextIndex, newNextIndex); + assertThat(newNextIndex).isEqualTo(expectedNextIndex); } } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtilTest.java similarity index 59% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtilTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtilTest.java index 82dc6b4ad5..557611c4ea 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtilTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlRenderUtilTest.java @@ -15,43 +15,62 @@ */ package com.google.android.exoplayer2.text.ttml; +import static android.graphics.Color.BLACK; +import static android.graphics.Color.RED; +import static android.graphics.Color.YELLOW; +import static com.google.android.exoplayer2.text.ttml.TtmlRenderUtil.resolveStyle; +import static com.google.android.exoplayer2.text.ttml.TtmlStyle.STYLE_BOLD; +import static com.google.android.exoplayer2.text.ttml.TtmlStyle.STYLE_BOLD_ITALIC; +import static com.google.common.truth.Truth.assertThat; + import android.graphics.Color; -import android.test.InstrumentationTestCase; import java.util.HashMap; import java.util.Map; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; /** - * Unit test for TtmlRenderUtil + * Unit test for {@link TtmlRenderUtil}. */ -public class TtmlRenderUtilTest extends InstrumentationTestCase { +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class TtmlRenderUtilTest { + @Test public void testResolveStyleNoStyleAtAll() { - assertNull(TtmlRenderUtil.resolveStyle(null, null, null)); + assertThat(resolveStyle(null, null, null)).isNull(); } + + @Test public void testResolveStyleSingleReferentialStyle() { Map globalStyles = getGlobalStyles(); String[] styleIds = {"s0"}; - assertSame(globalStyles.get("s0"), - TtmlRenderUtil.resolveStyle(null, styleIds, globalStyles)); + assertThat(TtmlRenderUtil.resolveStyle(null, styleIds, globalStyles)) + .isSameAs(globalStyles.get("s0")); } + + @Test public void testResolveStyleMultipleReferentialStyles() { Map globalStyles = getGlobalStyles(); String[] styleIds = {"s0", "s1"}; TtmlStyle resolved = TtmlRenderUtil.resolveStyle(null, styleIds, globalStyles); - assertNotSame(globalStyles.get("s0"), resolved); - assertNotSame(globalStyles.get("s1"), resolved); - assertNull(resolved.getId()); + assertThat(resolved).isNotSameAs(globalStyles.get("s0")); + assertThat(resolved).isNotSameAs(globalStyles.get("s1")); + assertThat(resolved.getId()).isNull(); // inherited from s0 - assertEquals(Color.BLACK, resolved.getBackgroundColor()); + assertThat(resolved.getBackgroundColor()).isEqualTo(BLACK); // inherited from s1 - assertEquals(Color.RED, resolved.getFontColor()); + assertThat(resolved.getFontColor()).isEqualTo(RED); // merged from s0 and s1 - assertEquals(TtmlStyle.STYLE_BOLD_ITALIC, resolved.getStyle()); + assertThat(resolved.getStyle()).isEqualTo(STYLE_BOLD_ITALIC); } + @Test public void testResolveMergeSingleReferentialStyleIntoInlineStyle() { Map globalStyles = getGlobalStyles(); String[] styleIds = {"s0"}; @@ -59,15 +78,15 @@ public class TtmlRenderUtilTest extends InstrumentationTestCase { style.setBackgroundColor(Color.YELLOW); TtmlStyle resolved = TtmlRenderUtil.resolveStyle(style, styleIds, globalStyles); - assertSame(style, resolved); + assertThat(resolved).isSameAs(style); // inline attribute not overridden - assertEquals(Color.YELLOW, resolved.getBackgroundColor()); + assertThat(resolved.getBackgroundColor()).isEqualTo(YELLOW); // inherited from referential style - assertEquals(TtmlStyle.STYLE_BOLD, resolved.getStyle()); + assertThat(resolved.getStyle()).isEqualTo(STYLE_BOLD); } - + @Test public void testResolveMergeMultipleReferentialStylesIntoInlineStyle() { Map globalStyles = getGlobalStyles(); String[] styleIds = {"s0", "s1"}; @@ -75,20 +94,21 @@ public class TtmlRenderUtilTest extends InstrumentationTestCase { style.setBackgroundColor(Color.YELLOW); TtmlStyle resolved = TtmlRenderUtil.resolveStyle(style, styleIds, globalStyles); - assertSame(style, resolved); + assertThat(resolved).isSameAs(style); // inline attribute not overridden - assertEquals(Color.YELLOW, resolved.getBackgroundColor()); + assertThat(resolved.getBackgroundColor()).isEqualTo(YELLOW); // inherited from both referential style - assertEquals(TtmlStyle.STYLE_BOLD_ITALIC, resolved.getStyle()); + assertThat(resolved.getStyle()).isEqualTo(STYLE_BOLD_ITALIC); } + @Test public void testResolveStyleOnlyInlineStyle() { TtmlStyle inlineStyle = new TtmlStyle(); - assertSame(inlineStyle, TtmlRenderUtil.resolveStyle(inlineStyle, null, null)); + assertThat(TtmlRenderUtil.resolveStyle(inlineStyle, null, null)).isSameAs(inlineStyle); } - private Map getGlobalStyles() { + private static Map getGlobalStyles() { Map globalStyles = new HashMap<>(); TtmlStyle s0 = new TtmlStyle(); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlStyleTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlStyleTest.java new file mode 100644 index 0000000000..4c35e259ff --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/ttml/TtmlStyleTest.java @@ -0,0 +1,155 @@ +/* + * 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.ttml; + +import static android.graphics.Color.BLACK; +import static android.graphics.Color.WHITE; +import static com.google.android.exoplayer2.text.ttml.TtmlStyle.STYLE_BOLD; +import static com.google.android.exoplayer2.text.ttml.TtmlStyle.STYLE_BOLD_ITALIC; +import static com.google.android.exoplayer2.text.ttml.TtmlStyle.STYLE_ITALIC; +import static com.google.android.exoplayer2.text.ttml.TtmlStyle.STYLE_NORMAL; +import static com.google.android.exoplayer2.text.ttml.TtmlStyle.UNSPECIFIED; +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; + +import android.graphics.Color; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** Unit test for {@link TtmlStyle}. */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class TtmlStyleTest { + + private static final String FONT_FAMILY = "serif"; + private static final String ID = "id"; + public static final int FOREGROUND_COLOR = Color.WHITE; + public static final int BACKGROUND_COLOR = Color.BLACK; + private TtmlStyle style; + + @Before + public void setUp() throws Exception { + style = new TtmlStyle(); + } + + @Test + public void testInheritStyle() { + style.inherit(createAncestorStyle()); + assertWithMessage("id must not be inherited").that(style.getId()).isNull(); + assertThat(style.isUnderline()).isTrue(); + assertThat(style.isLinethrough()).isTrue(); + assertThat(style.getStyle()).isEqualTo(STYLE_BOLD_ITALIC); + assertThat(style.getFontFamily()).isEqualTo(FONT_FAMILY); + assertThat(style.getFontColor()).isEqualTo(WHITE); + assertWithMessage("do not inherit backgroundColor").that(style.hasBackgroundColor()).isFalse(); + } + + @Test + public void testChainStyle() { + style.chain(createAncestorStyle()); + assertWithMessage("id must not be inherited").that(style.getId()).isNull(); + assertThat(style.isUnderline()).isTrue(); + assertThat(style.isLinethrough()).isTrue(); + assertThat(style.getStyle()).isEqualTo(STYLE_BOLD_ITALIC); + assertThat(style.getFontFamily()).isEqualTo(FONT_FAMILY); + assertThat(style.getFontColor()).isEqualTo(FOREGROUND_COLOR); + // do inherit backgroundColor when chaining + assertWithMessage("do not inherit backgroundColor when chaining") + .that(style.getBackgroundColor()).isEqualTo(BACKGROUND_COLOR); + } + + private static TtmlStyle createAncestorStyle() { + TtmlStyle ancestor = new TtmlStyle(); + ancestor.setId(ID); + ancestor.setItalic(true); + ancestor.setBold(true); + ancestor.setBackgroundColor(BACKGROUND_COLOR); + ancestor.setFontColor(FOREGROUND_COLOR); + ancestor.setLinethrough(true); + ancestor.setUnderline(true); + ancestor.setFontFamily(FONT_FAMILY); + return ancestor; + } + + @Test + public void testStyle() { + assertThat(style.getStyle()).isEqualTo(UNSPECIFIED); + style.setItalic(true); + assertThat(style.getStyle()).isEqualTo(STYLE_ITALIC); + style.setBold(true); + assertThat(style.getStyle()).isEqualTo(STYLE_BOLD_ITALIC); + style.setItalic(false); + assertThat(style.getStyle()).isEqualTo(STYLE_BOLD); + style.setBold(false); + assertThat(style.getStyle()).isEqualTo(STYLE_NORMAL); + } + + @Test + public void testLinethrough() { + assertThat(style.isLinethrough()).isFalse(); + style.setLinethrough(true); + assertThat(style.isLinethrough()).isTrue(); + style.setLinethrough(false); + assertThat(style.isLinethrough()).isFalse(); + } + + @Test + public void testUnderline() { + assertThat(style.isUnderline()).isFalse(); + style.setUnderline(true); + assertThat(style.isUnderline()).isTrue(); + style.setUnderline(false); + assertThat(style.isUnderline()).isFalse(); + } + + @Test + public void testFontFamily() { + assertThat(style.getFontFamily()).isNull(); + style.setFontFamily(FONT_FAMILY); + assertThat(style.getFontFamily()).isEqualTo(FONT_FAMILY); + style.setFontFamily(null); + assertThat(style.getFontFamily()).isNull(); + } + + @Test + public void testColor() { + assertThat(style.hasFontColor()).isFalse(); + style.setFontColor(Color.BLACK); + assertThat(style.getFontColor()).isEqualTo(BLACK); + assertThat(style.hasFontColor()).isTrue(); + } + + @Test + public void testBackgroundColor() { + assertThat(style.hasBackgroundColor()).isFalse(); + style.setBackgroundColor(Color.BLACK); + assertThat(style.getBackgroundColor()).isEqualTo(BLACK); + assertThat(style.hasBackgroundColor()).isTrue(); + } + + @Test + public void testId() { + assertThat(style.getId()).isNull(); + style.setId(ID); + assertThat(style.getId()).isEqualTo(ID); + style.setId(null); + assertThat(style.getId()).isNull(); + } + +} diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/CssParserTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/CssParserTest.java similarity index 61% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/CssParserTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/CssParserTest.java index d6be100877..6ade85be28 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/CssParserTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/CssParserTest.java @@ -15,22 +15,32 @@ */ package com.google.android.exoplayer2.text.webvtt; -import android.test.InstrumentationTestCase; +import static com.google.android.exoplayer2.text.webvtt.CssParser.parseNextToken; +import static com.google.common.truth.Truth.assertThat; + import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; /** * Unit test for {@link CssParser}. */ -public final class CssParserTest extends InstrumentationTestCase { +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class CssParserTest { private CssParser parser; - @Override + @Before public void setUp() { parser = new CssParser(); } + @Test public void testSkipWhitespacesAndComments() { // Skip only whitespaces String skipOnlyWhitespaces = " \t\r\n\f End of skip\n /* */"; @@ -53,6 +63,7 @@ public final class CssParserTest extends InstrumentationTestCase { assertSkipsToEndOfSkip(null, skipEverything); } + @Test public void testGetInputLimit() { // \r After 3 lines. String threeLinesThen3Cr = "One Line\nThen other\rAnd finally\r\r\r"; @@ -78,6 +89,7 @@ public final class CssParserTest extends InstrumentationTestCase { assertInputLimit(null, ""); } + @Test public void testParseMethodSimpleInput() { String styleBlock1 = " ::cue { color : black; background-color: PapayaWhip }"; WebvttCssStyle expectedStyle = new WebvttCssStyle(); @@ -96,6 +108,7 @@ public final class CssParserTest extends InstrumentationTestCase { assertParserProduces(expectedStyle, styleBlock3); } + @Test public void testMultiplePropertiesInBlock() { String styleBlock = "::cue(#id){text-decoration:underline; background-color:green;" + "color:red; font-family:Courier; font-weight:bold}"; @@ -110,6 +123,7 @@ public final class CssParserTest extends InstrumentationTestCase { assertParserProduces(expectedStyle, styleBlock); } + @Test public void testRgbaColorExpression() { String styleBlock = "::cue(#rgb){background-color: rgba(\n10/* Ugly color */,11\t, 12\n,.1);" + "color:rgb(1,1,\n1)}"; @@ -121,59 +135,62 @@ public final class CssParserTest extends InstrumentationTestCase { assertParserProduces(expectedStyle, styleBlock); } + @Test public void testGetNextToken() { String stringInput = " lorem:ipsum\n{dolor}#sit,amet;lorem:ipsum\r\t\f\ndolor(())\n"; ParsableByteArray input = new ParsableByteArray(Util.getUtf8Bytes(stringInput)); StringBuilder builder = new StringBuilder(); - assertEquals("lorem", CssParser.parseNextToken(input, builder)); - assertEquals(":", CssParser.parseNextToken(input, builder)); - assertEquals("ipsum", CssParser.parseNextToken(input, builder)); - assertEquals("{", CssParser.parseNextToken(input, builder)); - assertEquals("dolor", CssParser.parseNextToken(input, builder)); - assertEquals("}", CssParser.parseNextToken(input, builder)); - assertEquals("#sit", CssParser.parseNextToken(input, builder)); - assertEquals(",", CssParser.parseNextToken(input, builder)); - assertEquals("amet", CssParser.parseNextToken(input, builder)); - assertEquals(";", CssParser.parseNextToken(input, builder)); - assertEquals("lorem", CssParser.parseNextToken(input, builder)); - assertEquals(":", CssParser.parseNextToken(input, builder)); - assertEquals("ipsum", CssParser.parseNextToken(input, builder)); - assertEquals("dolor", CssParser.parseNextToken(input, builder)); - assertEquals("(", CssParser.parseNextToken(input, builder)); - assertEquals("(", CssParser.parseNextToken(input, builder)); - assertEquals(")", CssParser.parseNextToken(input, builder)); - assertEquals(")", CssParser.parseNextToken(input, builder)); - assertEquals(null, CssParser.parseNextToken(input, builder)); + assertThat(parseNextToken(input, builder)).isEqualTo("lorem"); + assertThat(parseNextToken(input, builder)).isEqualTo(":"); + assertThat(parseNextToken(input, builder)).isEqualTo("ipsum"); + assertThat(parseNextToken(input, builder)).isEqualTo("{"); + assertThat(parseNextToken(input, builder)).isEqualTo("dolor"); + assertThat(parseNextToken(input, builder)).isEqualTo("}"); + assertThat(parseNextToken(input, builder)).isEqualTo("#sit"); + assertThat(parseNextToken(input, builder)).isEqualTo(","); + assertThat(parseNextToken(input, builder)).isEqualTo("amet"); + assertThat(parseNextToken(input, builder)).isEqualTo(";"); + assertThat(parseNextToken(input, builder)).isEqualTo("lorem"); + assertThat(parseNextToken(input, builder)).isEqualTo(":"); + assertThat(parseNextToken(input, builder)).isEqualTo("ipsum"); + assertThat(parseNextToken(input, builder)).isEqualTo("dolor"); + assertThat(parseNextToken(input, builder)).isEqualTo("("); + assertThat(parseNextToken(input, builder)).isEqualTo("("); + assertThat(parseNextToken(input, builder)).isEqualTo(")"); + assertThat(parseNextToken(input, builder)).isEqualTo(")"); + assertThat(parseNextToken(input, builder)).isNull(); } + @Test public void testStyleScoreSystem() { WebvttCssStyle style = new WebvttCssStyle(); // Universal selector. - assertEquals(1, style.getSpecificityScore("", "", new String[0], "")); + assertThat(style.getSpecificityScore("", "", new String[0], "")).isEqualTo(1); // Class match without tag match. style.setTargetClasses(new String[] { "class1", "class2"}); - assertEquals(8, style.getSpecificityScore("", "", new String[] { "class1", "class2", "class3" }, - "")); + assertThat(style.getSpecificityScore("", "", new String[]{"class1", "class2", "class3"}, + "")).isEqualTo(8); // Class and tag match style.setTargetTagName("b"); - assertEquals(10, style.getSpecificityScore("", "b", - new String[] { "class1", "class2", "class3" }, "")); + assertThat(style.getSpecificityScore("", "b", + new String[]{"class1", "class2", "class3"}, "")).isEqualTo(10); // Class insufficiency. - assertEquals(0, style.getSpecificityScore("", "b", new String[] { "class1", "class" }, "")); + assertThat(style.getSpecificityScore("", "b", new String[]{"class1", "class"}, "")) + .isEqualTo(0); // Voice, classes and tag match. style.setTargetVoice("Manuel Cráneo"); - assertEquals(14, style.getSpecificityScore("", "b", - new String[] { "class1", "class2", "class3" }, "Manuel Cráneo")); + assertThat(style.getSpecificityScore("", "b", + new String[]{"class1", "class2", "class3"}, "Manuel Cráneo")).isEqualTo(14); // Voice mismatch. - assertEquals(0, style.getSpecificityScore(null, "b", - new String[] { "class1", "class2", "class3" }, "Manuel Craneo")); + assertThat(style.getSpecificityScore(null, "b", + new String[]{"class1", "class2", "class3"}, "Manuel Craneo")).isEqualTo(0); // Id, voice, classes and tag match. style.setTargetId("id"); - assertEquals(0x40000000 + 14, style.getSpecificityScore("id", "b", - new String[] { "class1", "class2", "class3" }, "Manuel Cráneo")); + assertThat(style.getSpecificityScore("id", "b", + new String[]{"class1", "class2", "class3"}, "Manuel Cráneo")).isEqualTo(0x40000000 + 14); // Id mismatch. - assertEquals(0, style.getSpecificityScore("id1", "b", - new String[] { "class1", "class2", "class3" }, "")); + assertThat(style.getSpecificityScore("id1", "b", + new String[]{"class1", "class2", "class3"}, "")).isEqualTo(0); } // Utility methods. @@ -181,34 +198,34 @@ public final class CssParserTest extends InstrumentationTestCase { private void assertSkipsToEndOfSkip(String expectedLine, String s) { ParsableByteArray input = new ParsableByteArray(Util.getUtf8Bytes(s)); CssParser.skipWhitespaceAndComments(input); - assertEquals(expectedLine, input.readLine()); + assertThat(input.readLine()).isEqualTo(expectedLine); } private void assertInputLimit(String expectedLine, String s) { ParsableByteArray input = new ParsableByteArray(Util.getUtf8Bytes(s)); CssParser.skipStyleBlock(input); - assertEquals(expectedLine, input.readLine()); + assertThat(input.readLine()).isEqualTo(expectedLine); } private void assertParserProduces(WebvttCssStyle expected, String styleBlock){ ParsableByteArray input = new ParsableByteArray(Util.getUtf8Bytes(styleBlock)); WebvttCssStyle actualElem = parser.parseBlock(input); - assertEquals(expected.hasBackgroundColor(), actualElem.hasBackgroundColor()); + assertThat(actualElem.hasBackgroundColor()).isEqualTo(expected.hasBackgroundColor()); if (expected.hasBackgroundColor()) { - assertEquals(expected.getBackgroundColor(), actualElem.getBackgroundColor()); + assertThat(actualElem.getBackgroundColor()).isEqualTo(expected.getBackgroundColor()); } - assertEquals(expected.hasFontColor(), actualElem.hasFontColor()); + assertThat(actualElem.hasFontColor()).isEqualTo(expected.hasFontColor()); if (expected.hasFontColor()) { - assertEquals(expected.getFontColor(), actualElem.getFontColor()); + assertThat(actualElem.getFontColor()).isEqualTo(expected.getFontColor()); } - assertEquals(expected.getFontFamily(), actualElem.getFontFamily()); - assertEquals(expected.getFontSize(), actualElem.getFontSize()); - assertEquals(expected.getFontSizeUnit(), actualElem.getFontSizeUnit()); - assertEquals(expected.getStyle(), actualElem.getStyle()); - assertEquals(expected.isLinethrough(), actualElem.isLinethrough()); - assertEquals(expected.isUnderline(), actualElem.isUnderline()); - assertEquals(expected.getTextAlign(), actualElem.getTextAlign()); + assertThat(actualElem.getFontFamily()).isEqualTo(expected.getFontFamily()); + assertThat(actualElem.getFontSize()).isEqualTo(expected.getFontSize()); + assertThat(actualElem.getFontSizeUnit()).isEqualTo(expected.getFontSizeUnit()); + assertThat(actualElem.getStyle()).isEqualTo(expected.getStyle()); + assertThat(actualElem.isLinethrough()).isEqualTo(expected.isLinethrough()); + assertThat(actualElem.isUnderline()).isEqualTo(expected.isUnderline()); + assertThat(actualElem.getTextAlign()).isEqualTo(expected.getTextAlign()); } } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoderTest.java similarity index 82% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoderTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoderTest.java index 2cdad081c5..8937007990 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/Mp4WebvttDecoderTest.java @@ -15,16 +15,24 @@ */ package com.google.android.exoplayer2.text.webvtt; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.Subtitle; import com.google.android.exoplayer2.text.SubtitleDecoderException; import java.util.List; -import junit.framework.TestCase; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; /** * Unit test for {@link Mp4WebvttDecoder}. */ -public final class Mp4WebvttDecoderTest extends TestCase { +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class Mp4WebvttDecoderTest { private static final byte[] SINGLE_CUE_SAMPLE = { 0x00, 0x00, 0x00, 0x1C, // Size @@ -79,6 +87,7 @@ public final class Mp4WebvttDecoderTest extends TestCase { // Positive tests. + @Test public void testSingleCueSample() throws SubtitleDecoderException { Mp4WebvttDecoder decoder = new Mp4WebvttDecoder(); Subtitle result = decoder.decode(SINGLE_CUE_SAMPLE, SINGLE_CUE_SAMPLE.length, false); @@ -86,6 +95,7 @@ public final class Mp4WebvttDecoderTest extends TestCase { assertMp4WebvttSubtitleEquals(result, expectedCue); } + @Test public void testTwoCuesSample() throws SubtitleDecoderException { Mp4WebvttDecoder decoder = new Mp4WebvttDecoder(); Subtitle result = decoder.decode(DOUBLE_CUE_SAMPLE, DOUBLE_CUE_SAMPLE.length, false); @@ -94,6 +104,7 @@ public final class Mp4WebvttDecoderTest extends TestCase { assertMp4WebvttSubtitleEquals(result, firstExpectedCue, secondExpectedCue); } + @Test public void testNoCueSample() throws SubtitleDecoderException { Mp4WebvttDecoder decoder = new Mp4WebvttDecoder(); Subtitle result = decoder.decode(NO_CUE_SAMPLE, NO_CUE_SAMPLE.length, false); @@ -102,6 +113,7 @@ public final class Mp4WebvttDecoderTest extends TestCase { // Negative tests. + @Test public void testSampleWithIncompleteHeader() { Mp4WebvttDecoder decoder = new Mp4WebvttDecoder(); try { @@ -122,10 +134,10 @@ public final class Mp4WebvttDecoderTest extends TestCase { * @param expectedCues The expected {@link Cue}s. */ private static void assertMp4WebvttSubtitleEquals(Subtitle subtitle, Cue... expectedCues) { - assertEquals(1, subtitle.getEventTimeCount()); - assertEquals(0, subtitle.getEventTime(0)); + assertThat(subtitle.getEventTimeCount()).isEqualTo(1); + assertThat(subtitle.getEventTime(0)).isEqualTo(0); List subtitleCues = subtitle.getCues(0); - assertEquals(expectedCues.length, subtitleCues.size()); + assertThat(subtitleCues).hasSize(expectedCues.length); for (int i = 0; i < subtitleCues.size(); i++) { assertCueEquals(expectedCues[i], subtitleCues.get(i)); } @@ -135,14 +147,14 @@ public final class Mp4WebvttDecoderTest extends TestCase { * Asserts that two cues are equal. */ private static void assertCueEquals(Cue expected, Cue actual) { - assertEquals(expected.line, actual.line); - assertEquals(expected.lineAnchor, actual.lineAnchor); - assertEquals(expected.lineType, actual.lineType); - assertEquals(expected.position, actual.position); - assertEquals(expected.positionAnchor, actual.positionAnchor); - assertEquals(expected.size, actual.size); - assertEquals(expected.text.toString(), actual.text.toString()); - assertEquals(expected.textAlignment, actual.textAlignment); + assertThat(actual.line).isEqualTo(expected.line); + assertThat(actual.lineAnchor).isEqualTo(expected.lineAnchor); + assertThat(actual.lineType).isEqualTo(expected.lineType); + assertThat(actual.position).isEqualTo(expected.position); + assertThat(actual.positionAnchor).isEqualTo(expected.positionAnchor); + assertThat(actual.size).isEqualTo(expected.size); + assertThat(actual.text.toString()).isEqualTo(expected.text.toString()); + assertThat(actual.textAlignment).isEqualTo(expected.textAlignment); } } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParserTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParserTest.java similarity index 50% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParserTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParserTest.java index 1ee8976a7e..2a6e461627 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParserTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttCueParserTest.java @@ -15,209 +15,235 @@ */ package com.google.android.exoplayer2.text.webvtt; +import static android.graphics.Typeface.BOLD; +import static android.graphics.Typeface.ITALIC; +import static com.google.common.truth.Truth.assertThat; + import android.graphics.Typeface; -import android.test.InstrumentationTestCase; import android.text.Spanned; import android.text.style.StyleSpan; import android.text.style.UnderlineSpan; import java.util.Collections; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; /** * Unit test for {@link WebvttCueParser}. */ -public final class WebvttCueParserTest extends InstrumentationTestCase { +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class WebvttCueParserTest { + @Test public void testParseStrictValidClassesAndTrailingTokens() throws Exception { Spanned text = parseCueText("" + "This is text with html tags"); - assertEquals("This is text with html tags", text.toString()); + assertThat(text.toString()).isEqualTo("This is text with html tags"); UnderlineSpan[] underlineSpans = getSpans(text, UnderlineSpan.class); StyleSpan[] styleSpans = getSpans(text, StyleSpan.class); - assertEquals(1, underlineSpans.length); - assertEquals(2, styleSpans.length); - assertEquals(Typeface.ITALIC, styleSpans[0].getStyle()); - assertEquals(Typeface.BOLD, styleSpans[1].getStyle()); + assertThat(underlineSpans).hasLength(1); + assertThat(styleSpans).hasLength(2); + assertThat(styleSpans[0].getStyle()).isEqualTo(ITALIC); + assertThat(styleSpans[1].getStyle()).isEqualTo(BOLD); - assertEquals(5, text.getSpanStart(underlineSpans[0])); - assertEquals(7, text.getSpanEnd(underlineSpans[0])); - assertEquals(18, text.getSpanStart(styleSpans[0])); - assertEquals(18, text.getSpanStart(styleSpans[1])); - assertEquals(22, text.getSpanEnd(styleSpans[0])); - assertEquals(22, text.getSpanEnd(styleSpans[1])); + assertThat(text.getSpanStart(underlineSpans[0])).isEqualTo(5); + assertThat(text.getSpanEnd(underlineSpans[0])).isEqualTo(7); + assertThat(text.getSpanStart(styleSpans[0])).isEqualTo(18); + assertThat(text.getSpanStart(styleSpans[1])).isEqualTo(18); + assertThat(text.getSpanEnd(styleSpans[0])).isEqualTo(22); + assertThat(text.getSpanEnd(styleSpans[1])).isEqualTo(22); } + @Test public void testParseStrictValidUnsupportedTagsStrippedOut() throws Exception { Spanned text = parseCueText("This is text with " + "html tags"); - assertEquals("This is text with html tags", text.toString()); - assertEquals(0, getSpans(text, UnderlineSpan.class).length); - assertEquals(0, getSpans(text, StyleSpan.class).length); + assertThat(text.toString()).isEqualTo("This is text with html tags"); + assertThat(getSpans(text, UnderlineSpan.class)).hasLength(0); + assertThat(getSpans(text, StyleSpan.class)).hasLength(0); } + @Test public void testParseWellFormedUnclosedEndAtCueEnd() throws Exception { Spanned text = parseCueText("An unclosed u tag with " + "italic inside"); - assertEquals("An unclosed u tag with italic inside", text.toString()); + assertThat(text.toString()).isEqualTo("An unclosed u tag with italic inside"); UnderlineSpan[] underlineSpans = getSpans(text, UnderlineSpan.class); StyleSpan[] styleSpans = getSpans(text, StyleSpan.class); - assertEquals(1, underlineSpans.length); - assertEquals(1, styleSpans.length); - assertEquals(Typeface.ITALIC, styleSpans[0].getStyle()); + assertThat(underlineSpans).hasLength(1); + assertThat(styleSpans).hasLength(1); + assertThat(styleSpans[0].getStyle()).isEqualTo(ITALIC); - assertEquals(3, text.getSpanStart(underlineSpans[0])); - assertEquals(23, text.getSpanStart(styleSpans[0])); - assertEquals(29, text.getSpanEnd(styleSpans[0])); - assertEquals(36, text.getSpanEnd(underlineSpans[0])); + assertThat(text.getSpanStart(underlineSpans[0])).isEqualTo(3); + assertThat(text.getSpanStart(styleSpans[0])).isEqualTo(23); + assertThat(text.getSpanEnd(styleSpans[0])).isEqualTo(29); + assertThat(text.getSpanEnd(underlineSpans[0])).isEqualTo(36); } + @Test public void testParseWellFormedUnclosedEndAtParent() throws Exception { Spanned text = parseCueText("An unclosed u tag with underline and italic inside"); - assertEquals("An unclosed u tag with underline and italic inside", text.toString()); + assertThat(text.toString()).isEqualTo("An unclosed u tag with underline and italic inside"); UnderlineSpan[] underlineSpans = getSpans(text, UnderlineSpan.class); StyleSpan[] styleSpans = getSpans(text, StyleSpan.class); - assertEquals(1, underlineSpans.length); - assertEquals(1, styleSpans.length); + assertThat(underlineSpans).hasLength(1); + assertThat(styleSpans).hasLength(1); - assertEquals(23, text.getSpanStart(underlineSpans[0])); - assertEquals(23, text.getSpanStart(styleSpans[0])); - assertEquals(43, text.getSpanEnd(underlineSpans[0])); - assertEquals(43, text.getSpanEnd(styleSpans[0])); + assertThat(text.getSpanStart(underlineSpans[0])).isEqualTo(23); + assertThat(text.getSpanStart(styleSpans[0])).isEqualTo(23); + assertThat(text.getSpanEnd(underlineSpans[0])).isEqualTo(43); + assertThat(text.getSpanEnd(styleSpans[0])).isEqualTo(43); - assertEquals(Typeface.ITALIC, styleSpans[0].getStyle()); + assertThat(styleSpans[0].getStyle()).isEqualTo(ITALIC); } + @Test public void testParseMalformedNestedElements() throws Exception { Spanned text = parseCueText("An unclosed u tag with italic inside"); - assertEquals("An unclosed u tag with italic inside", text.toString()); + assertThat(text.toString()).isEqualTo("An unclosed u tag with italic inside"); UnderlineSpan[] underlineSpans = getSpans(text, UnderlineSpan.class); StyleSpan[] styleSpans = getSpans(text, StyleSpan.class); - assertEquals(1, underlineSpans.length); - assertEquals(2, styleSpans.length); + assertThat(underlineSpans).hasLength(1); + assertThat(styleSpans).hasLength(2); // all tags applied until matching start tag found - assertEquals(0, text.getSpanStart(underlineSpans[0])); - assertEquals(29, text.getSpanEnd(underlineSpans[0])); + assertThat(text.getSpanStart(underlineSpans[0])).isEqualTo(0); + assertThat(text.getSpanEnd(underlineSpans[0])).isEqualTo(29); if (styleSpans[0].getStyle() == Typeface.BOLD) { - assertEquals(0, text.getSpanStart(styleSpans[0])); - assertEquals(23, text.getSpanStart(styleSpans[1])); - assertEquals(29, text.getSpanEnd(styleSpans[1])); - assertEquals(36, text.getSpanEnd(styleSpans[0])); + assertThat(text.getSpanStart(styleSpans[0])).isEqualTo(0); + assertThat(text.getSpanStart(styleSpans[1])).isEqualTo(23); + assertThat(text.getSpanEnd(styleSpans[1])).isEqualTo(29); + assertThat(text.getSpanEnd(styleSpans[0])).isEqualTo(36); } else { - assertEquals(0, text.getSpanStart(styleSpans[1])); - assertEquals(23, text.getSpanStart(styleSpans[0])); - assertEquals(29, text.getSpanEnd(styleSpans[0])); - assertEquals(36, text.getSpanEnd(styleSpans[1])); + assertThat(text.getSpanStart(styleSpans[1])).isEqualTo(0); + assertThat(text.getSpanStart(styleSpans[0])).isEqualTo(23); + assertThat(text.getSpanEnd(styleSpans[0])).isEqualTo(29); + assertThat(text.getSpanEnd(styleSpans[1])).isEqualTo(36); } } + @Test public void testParseCloseNonExistingTag() throws Exception { Spanned text = parseCueText("blahblahblahblah"); - assertEquals("blahblahblahblah", text.toString()); + assertThat(text.toString()).isEqualTo("blahblahblahblah"); StyleSpan[] spans = getSpans(text, StyleSpan.class); - assertEquals(1, spans.length); - assertEquals(Typeface.BOLD, spans[0].getStyle()); - assertEquals(4, text.getSpanStart(spans[0])); - assertEquals(8, text.getSpanEnd(spans[0])); // should be 12 when valid + assertThat(spans).hasLength(1); + assertThat(spans[0].getStyle()).isEqualTo(BOLD); + assertThat(text.getSpanStart(spans[0])).isEqualTo(4); + assertThat(text.getSpanEnd(spans[0])).isEqualTo(8); // should be 12 when valid } + @Test public void testParseEmptyTagName() throws Exception { Spanned text = parseCueText("An unclosed u tag with <>italic inside"); - assertEquals("An unclosed u tag with italic inside", text.toString()); + assertThat(text.toString()).isEqualTo("An unclosed u tag with italic inside"); } + @Test public void testParseEntities() throws Exception { Spanned text = parseCueText("& > <  "); - assertEquals("& > < ", text.toString()); + assertThat(text.toString()).isEqualTo("& > < "); } + @Test public void testParseEntitiesUnsupported() throws Exception { Spanned text = parseCueText("&noway; &sure;"); - assertEquals(" ", text.toString()); + assertThat(text.toString()).isEqualTo(" "); } + @Test public void testParseEntitiesNotTerminated() throws Exception { Spanned text = parseCueText("& here comes text"); - assertEquals("& here comes text", text.toString()); + assertThat(text.toString()).isEqualTo("& here comes text"); } + @Test public void testParseEntitiesNotTerminatedUnsupported() throws Exception { Spanned text = parseCueText("&surenot here comes text"); - assertEquals(" here comes text", text.toString()); + assertThat(text.toString()).isEqualTo(" here comes text"); } + @Test public void testParseEntitiesNotTerminatedNoSpace() throws Exception { Spanned text = parseCueText("&surenot"); - assertEquals("&surenot", text.toString()); + assertThat(text.toString()).isEqualTo("&surenot"); } + @Test public void testParseVoidTag() throws Exception { Spanned text = parseCueText("here comes
      text
      "); - assertEquals("here comes text", text.toString()); + assertThat(text.toString()).isEqualTo("here comes text"); } + @Test public void testParseMultipleTagsOfSameKind() { Spanned text = parseCueText("blah blah blah foo"); - assertEquals("blah blah blah foo", text.toString()); + assertThat(text.toString()).isEqualTo("blah blah blah foo"); StyleSpan[] spans = getSpans(text, StyleSpan.class); - assertEquals(2, spans.length); - assertEquals(5, text.getSpanStart(spans[0])); - assertEquals(9, text.getSpanEnd(spans[0])); - assertEquals(15, text.getSpanStart(spans[1])); - assertEquals(18, text.getSpanEnd(spans[1])); - assertEquals(Typeface.BOLD, spans[0].getStyle()); - assertEquals(Typeface.BOLD, spans[1].getStyle()); + assertThat(spans).hasLength(2); + assertThat(text.getSpanStart(spans[0])).isEqualTo(5); + assertThat(text.getSpanEnd(spans[0])).isEqualTo(9); + assertThat(text.getSpanStart(spans[1])).isEqualTo(15); + assertThat(text.getSpanEnd(spans[1])).isEqualTo(18); + assertThat(spans[0].getStyle()).isEqualTo(BOLD); + assertThat(spans[1].getStyle()).isEqualTo(BOLD); } + @Test public void testParseInvalidVoidSlash() { Spanned text = parseCueText("blah blah"); - assertEquals("blah blah", text.toString()); + assertThat(text.toString()).isEqualTo("blah blah"); StyleSpan[] spans = getSpans(text, StyleSpan.class); - assertEquals(0, spans.length); + assertThat(spans).hasLength(0); } + @Test public void testParseMonkey() throws Exception { Spanned text = parseCueText("< u>An unclosed u tag with <<<<< i>italic
      " + " inside"); - assertEquals("An unclosed u tag with italic inside", text.toString()); + assertThat(text.toString()).isEqualTo("An unclosed u tag with italic inside"); text = parseCueText(">>>>>>>>>An unclosed u tag with <<<<< italic" + " inside"); - assertEquals(">>>>>>>>>An unclosed u tag with inside", text.toString()); + assertThat(text.toString()).isEqualTo(">>>>>>>>>An unclosed u tag with inside"); } + @Test public void testParseCornerCases() throws Exception { Spanned text = parseCueText(">"); - assertEquals(">", text.toString()); + assertThat(text.toString()).isEqualTo(">"); text = parseCueText("<"); - assertEquals("", text.toString()); + assertThat(text.toString()).isEmpty(); text = parseCueText("><<<<<<<<<<"); - assertEquals(">", text.toString()); + assertThat(text.toString()).isEqualTo(">"); text = parseCueText("<>"); - assertEquals("", text.toString()); + assertThat(text.toString()).isEmpty(); text = parseCueText("&"); - assertEquals("&", text.toString()); + assertThat(text.toString()).isEqualTo("&"); text = parseCueText("&&&&&&&"); - assertEquals("&&&&&&&", text.toString()); + assertThat(text.toString()).isEqualTo("&&&&&&&"); } private static Spanned parseCueText(String string) { diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/WebvttSubtitleTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttSubtitleTest.java similarity index 78% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/WebvttSubtitleTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttSubtitleTest.java index 164c6c149a..c3c30e44a8 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/text/webvtt/WebvttSubtitleTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/webvtt/WebvttSubtitleTest.java @@ -15,17 +15,25 @@ */ package com.google.android.exoplayer2.text.webvtt; -import com.google.android.exoplayer2.C; +import static com.google.android.exoplayer2.C.INDEX_UNSET; +import static com.google.common.truth.Truth.assertThat; +import static java.lang.Long.MAX_VALUE; + import com.google.android.exoplayer2.text.Cue; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import junit.framework.TestCase; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; /** * Unit test for {@link WebvttSubtitle}. */ -public class WebvttSubtitleTest extends TestCase { +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public class WebvttSubtitleTest { private static final String FIRST_SUBTITLE_STRING = "This is the first subtitle."; private static final String SECOND_SUBTITLE_STRING = "This is the second subtitle."; @@ -65,21 +73,25 @@ public class WebvttSubtitleTest extends TestCase { nestedSubtitle = new WebvttSubtitle(nestedSubtitleCues); } + @Test public void testEventCount() { - assertEquals(0, emptySubtitle.getEventTimeCount()); - assertEquals(4, simpleSubtitle.getEventTimeCount()); - assertEquals(4, overlappingSubtitle.getEventTimeCount()); - assertEquals(4, nestedSubtitle.getEventTimeCount()); + assertThat(emptySubtitle.getEventTimeCount()).isEqualTo(0); + assertThat(simpleSubtitle.getEventTimeCount()).isEqualTo(4); + assertThat(overlappingSubtitle.getEventTimeCount()).isEqualTo(4); + assertThat(nestedSubtitle.getEventTimeCount()).isEqualTo(4); } + @Test public void testSimpleSubtitleEventTimes() { testSubtitleEventTimesHelper(simpleSubtitle); } + @Test public void testSimpleSubtitleEventIndices() { testSubtitleEventIndicesHelper(simpleSubtitle); } + @Test public void testSimpleSubtitleText() { // Test before first subtitle assertSingleCueEmpty(simpleSubtitle.getCues(0)); @@ -107,14 +119,17 @@ public class WebvttSubtitleTest extends TestCase { assertSingleCueEmpty(simpleSubtitle.getCues(Long.MAX_VALUE)); } + @Test public void testOverlappingSubtitleEventTimes() { testSubtitleEventTimesHelper(overlappingSubtitle); } + @Test public void testOverlappingSubtitleEventIndices() { testSubtitleEventIndicesHelper(overlappingSubtitle); } + @Test public void testOverlappingSubtitleText() { // Test before first subtitle assertSingleCueEmpty(overlappingSubtitle.getCues(0)); @@ -145,14 +160,17 @@ public class WebvttSubtitleTest extends TestCase { assertSingleCueEmpty(overlappingSubtitle.getCues(Long.MAX_VALUE)); } + @Test public void testNestedSubtitleEventTimes() { testSubtitleEventTimesHelper(nestedSubtitle); } + @Test public void testNestedSubtitleEventIndices() { testSubtitleEventIndicesHelper(nestedSubtitle); } + @Test public void testNestedSubtitleText() { // Test before first subtitle assertSingleCueEmpty(nestedSubtitle.getCues(0)); @@ -181,46 +199,46 @@ public class WebvttSubtitleTest extends TestCase { } private void testSubtitleEventTimesHelper(WebvttSubtitle subtitle) { - assertEquals(1000000, subtitle.getEventTime(0)); - assertEquals(2000000, subtitle.getEventTime(1)); - assertEquals(3000000, subtitle.getEventTime(2)); - assertEquals(4000000, subtitle.getEventTime(3)); + assertThat(subtitle.getEventTime(0)).isEqualTo(1000000); + assertThat(subtitle.getEventTime(1)).isEqualTo(2000000); + assertThat(subtitle.getEventTime(2)).isEqualTo(3000000); + assertThat(subtitle.getEventTime(3)).isEqualTo(4000000); } private void testSubtitleEventIndicesHelper(WebvttSubtitle subtitle) { // Test first event - assertEquals(0, subtitle.getNextEventTimeIndex(0)); - assertEquals(0, subtitle.getNextEventTimeIndex(500000)); - assertEquals(0, subtitle.getNextEventTimeIndex(999999)); + assertThat(subtitle.getNextEventTimeIndex(0)).isEqualTo(0); + assertThat(subtitle.getNextEventTimeIndex(500000)).isEqualTo(0); + assertThat(subtitle.getNextEventTimeIndex(999999)).isEqualTo(0); // Test second event - assertEquals(1, subtitle.getNextEventTimeIndex(1000000)); - assertEquals(1, subtitle.getNextEventTimeIndex(1500000)); - assertEquals(1, subtitle.getNextEventTimeIndex(1999999)); + assertThat(subtitle.getNextEventTimeIndex(1000000)).isEqualTo(1); + assertThat(subtitle.getNextEventTimeIndex(1500000)).isEqualTo(1); + assertThat(subtitle.getNextEventTimeIndex(1999999)).isEqualTo(1); // Test third event - assertEquals(2, subtitle.getNextEventTimeIndex(2000000)); - assertEquals(2, subtitle.getNextEventTimeIndex(2500000)); - assertEquals(2, subtitle.getNextEventTimeIndex(2999999)); + assertThat(subtitle.getNextEventTimeIndex(2000000)).isEqualTo(2); + assertThat(subtitle.getNextEventTimeIndex(2500000)).isEqualTo(2); + assertThat(subtitle.getNextEventTimeIndex(2999999)).isEqualTo(2); // Test fourth event - assertEquals(3, subtitle.getNextEventTimeIndex(3000000)); - assertEquals(3, subtitle.getNextEventTimeIndex(3500000)); - assertEquals(3, subtitle.getNextEventTimeIndex(3999999)); + assertThat(subtitle.getNextEventTimeIndex(3000000)).isEqualTo(3); + assertThat(subtitle.getNextEventTimeIndex(3500000)).isEqualTo(3); + assertThat(subtitle.getNextEventTimeIndex(3999999)).isEqualTo(3); // Test null event (i.e. look for events after the last event) - assertEquals(C.INDEX_UNSET, subtitle.getNextEventTimeIndex(4000000)); - assertEquals(C.INDEX_UNSET, subtitle.getNextEventTimeIndex(4500000)); - assertEquals(C.INDEX_UNSET, subtitle.getNextEventTimeIndex(Long.MAX_VALUE)); + assertThat(subtitle.getNextEventTimeIndex(4000000)).isEqualTo(INDEX_UNSET); + assertThat(subtitle.getNextEventTimeIndex(4500000)).isEqualTo(INDEX_UNSET); + assertThat(subtitle.getNextEventTimeIndex(MAX_VALUE)).isEqualTo(INDEX_UNSET); } private void assertSingleCueEmpty(List cues) { - assertTrue(cues.size() == 0); + assertThat(cues).isEmpty(); } private void assertSingleCueTextEquals(String expected, List cues) { - assertTrue(cues.size() == 1); - assertEquals(expected, cues.get(0).text.toString()); + assertThat(cues).hasSize(1); + assertThat(cues.get(0).text.toString()).isEqualTo(expected); } } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/trackselection/MappingTrackSelectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/MappingTrackSelectorTest.java similarity index 87% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/trackselection/MappingTrackSelectorTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/trackselection/MappingTrackSelectorTest.java index c31c651384..cffc530354 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/trackselection/MappingTrackSelectorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/MappingTrackSelectorTest.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.trackselection; +import static com.google.common.truth.Truth.assertThat; + import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; @@ -22,12 +24,17 @@ import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.util.MimeTypes; -import junit.framework.TestCase; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; /** * Unit tests for {@link MappingTrackSelector}. */ -public final class MappingTrackSelectorTest extends TestCase { +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class MappingTrackSelectorTest { private static final RendererCapabilities VIDEO_CAPABILITIES = new FakeRendererCapabilities(C.TRACK_TYPE_VIDEO); @@ -54,6 +61,7 @@ public final class MappingTrackSelectorTest extends TestCase { /** * Tests that the video and audio track groups are mapped onto the correct renderers. */ + @Test public void testMapping() throws ExoPlaybackException { FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector(); trackSelector.selectTracks(RENDERER_CAPABILITIES, TRACK_GROUPS); @@ -65,6 +73,7 @@ public final class MappingTrackSelectorTest extends TestCase { * Tests that the video and audio track groups are mapped onto the correct renderers when the * renderer ordering is reversed. */ + @Test public void testMappingReverseOrder() throws ExoPlaybackException { FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector(); RendererCapabilities[] reverseOrderRendererCapabilities = new RendererCapabilities[] { @@ -78,6 +87,7 @@ public final class MappingTrackSelectorTest extends TestCase { * Tests video and audio track groups are mapped onto the correct renderers when there are * multiple track groups of the same type. */ + @Test public void testMappingMulti() throws ExoPlaybackException { FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector(); TrackGroupArray multiTrackGroups = new TrackGroupArray(VIDEO_TRACK_GROUP, AUDIO_TRACK_GROUP, @@ -92,46 +102,50 @@ public final class MappingTrackSelectorTest extends TestCase { * TrackGroupArray[], int[][][])} is propagated correctly to the result of * {@link MappingTrackSelector#selectTracks(RendererCapabilities[], TrackGroupArray)}. */ + @Test public void testSelectTracks() throws ExoPlaybackException { FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector(TRACK_SELECTIONS); TrackSelectorResult result = trackSelector.selectTracks(RENDERER_CAPABILITIES, TRACK_GROUPS); - assertEquals(TRACK_SELECTIONS[0], result.selections.get(0)); - assertEquals(TRACK_SELECTIONS[1], result.selections.get(1)); + assertThat(result.selections.get(0)).isEqualTo(TRACK_SELECTIONS[0]); + assertThat(result.selections.get(1)).isEqualTo(TRACK_SELECTIONS[1]); } /** * Tests that a null override clears a track selection. */ + @Test public void testSelectTracksWithNullOverride() throws ExoPlaybackException { FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector(TRACK_SELECTIONS); trackSelector.setSelectionOverride(0, new TrackGroupArray(VIDEO_TRACK_GROUP), null); TrackSelectorResult result = trackSelector.selectTracks(RENDERER_CAPABILITIES, TRACK_GROUPS); - assertNull(result.selections.get(0)); - assertEquals(TRACK_SELECTIONS[1], result.selections.get(1)); + assertThat(result.selections.get(0)).isNull(); + assertThat(result.selections.get(1)).isEqualTo(TRACK_SELECTIONS[1]); } /** * Tests that a null override can be cleared. */ + @Test public void testSelectTracksWithClearedNullOverride() throws ExoPlaybackException { FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector(TRACK_SELECTIONS); trackSelector.setSelectionOverride(0, new TrackGroupArray(VIDEO_TRACK_GROUP), null); trackSelector.clearSelectionOverride(0, new TrackGroupArray(VIDEO_TRACK_GROUP)); TrackSelectorResult result = trackSelector.selectTracks(RENDERER_CAPABILITIES, TRACK_GROUPS); - assertEquals(TRACK_SELECTIONS[0], result.selections.get(0)); - assertEquals(TRACK_SELECTIONS[1], result.selections.get(1)); + assertThat(result.selections.get(0)).isEqualTo(TRACK_SELECTIONS[0]); + assertThat(result.selections.get(1)).isEqualTo(TRACK_SELECTIONS[1]); } /** * Tests that an override is not applied for a different set of available track groups. */ + @Test public void testSelectTracksWithNullOverrideForDifferentTracks() throws ExoPlaybackException { FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector(TRACK_SELECTIONS); trackSelector.setSelectionOverride(0, new TrackGroupArray(VIDEO_TRACK_GROUP), null); TrackSelectorResult result = trackSelector.selectTracks(RENDERER_CAPABILITIES, new TrackGroupArray(VIDEO_TRACK_GROUP, AUDIO_TRACK_GROUP, VIDEO_TRACK_GROUP)); - assertEquals(TRACK_SELECTIONS[0], result.selections.get(0)); - assertEquals(TRACK_SELECTIONS[1], result.selections.get(1)); + assertThat(result.selections.get(0)).isEqualTo(TRACK_SELECTIONS[0]); + assertThat(result.selections.get(1)).isEqualTo(TRACK_SELECTIONS[1]); } /** @@ -156,9 +170,9 @@ public final class MappingTrackSelectorTest extends TestCase { } public void assertMappedTrackGroups(int rendererIndex, TrackGroup... expected) { - assertEquals(expected.length, lastRendererTrackGroupArrays[rendererIndex].length); + assertThat(lastRendererTrackGroupArrays[rendererIndex].length).isEqualTo(expected.length); for (int i = 0; i < expected.length; i++) { - assertEquals(expected[i], lastRendererTrackGroupArrays[rendererIndex].get(i)); + assertThat(lastRendererTrackGroupArrays[rendererIndex].get(i)).isEqualTo(expected[i]); } } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ByteArrayDataSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/ByteArrayDataSourceTest.java similarity index 82% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ByteArrayDataSourceTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/upstream/ByteArrayDataSourceTest.java index b805ccbdd5..a72d060287 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ByteArrayDataSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/ByteArrayDataSourceTest.java @@ -15,26 +15,37 @@ */ package com.google.android.exoplayer2.upstream; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + import com.google.android.exoplayer2.C; import java.io.IOException; -import junit.framework.TestCase; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; /** * Unit tests for {@link ByteArrayDataSource}. */ -public class ByteArrayDataSourceTest extends TestCase { +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class ByteArrayDataSourceTest { private static final byte[] TEST_DATA = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; private static final byte[] TEST_DATA_ODD = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + @Test public void testFullReadSingleBytes() { readTestData(TEST_DATA, 0, C.LENGTH_UNSET, 1, 0, 1, false); } + @Test public void testFullReadAllBytes() { readTestData(TEST_DATA, 0, C.LENGTH_UNSET, 100, 0, 100, false); } + @Test public void testLimitReadSingleBytes() { // Limit set to the length of the data. readTestData(TEST_DATA, 0, TEST_DATA.length, 1, 0, 1, false); @@ -42,6 +53,7 @@ public class ByteArrayDataSourceTest extends TestCase { readTestData(TEST_DATA, 0, 6, 1, 0, 1, false); } + @Test public void testFullReadTwoBytes() { // Try with the total data length an exact multiple of the size of each individual read. readTestData(TEST_DATA, 0, C.LENGTH_UNSET, 2, 0, 2, false); @@ -49,6 +61,7 @@ public class ByteArrayDataSourceTest extends TestCase { readTestData(TEST_DATA_ODD, 0, C.LENGTH_UNSET, 2, 0, 2, false); } + @Test public void testLimitReadTwoBytes() { // Try with the limit an exact multiple of the size of each individual read. readTestData(TEST_DATA, 0, 6, 2, 0, 2, false); @@ -56,6 +69,7 @@ public class ByteArrayDataSourceTest extends TestCase { readTestData(TEST_DATA, 0, 7, 2, 0, 2, false); } + @Test public void testReadFromValidOffsets() { // Read from an offset without bound. readTestData(TEST_DATA, 1, C.LENGTH_UNSET, 1, 0, 1, false); @@ -67,6 +81,7 @@ public class ByteArrayDataSourceTest extends TestCase { readTestData(TEST_DATA, TEST_DATA.length - 1, 1, 1, 0, 1, false); } + @Test public void testReadFromInvalidOffsets() { // Read from first invalid offset and check failure without bound. readTestData(TEST_DATA, TEST_DATA.length, C.LENGTH_UNSET, 1, 0, 1, true); @@ -74,6 +89,7 @@ public class ByteArrayDataSourceTest extends TestCase { readTestData(TEST_DATA, TEST_DATA.length, 1, 1, 0, 1, true); } + @Test public void testReadWithInvalidLength() { // Read more data than is available. readTestData(TEST_DATA, 0, TEST_DATA.length + 1, 1, 0, 1, true); @@ -102,10 +118,10 @@ public class ByteArrayDataSourceTest extends TestCase { // Open the source. long length = dataSource.open(new DataSpec(null, dataOffset, dataLength, null)); opened = true; - assertFalse(expectFailOnOpen); + assertThat(expectFailOnOpen).isFalse(); // Verify the resolved length is as we expect. - assertEquals(expectedFinalBytesRead, length); + assertThat(length).isEqualTo(expectedFinalBytesRead); byte[] outputBuffer = new byte[outputBufferLength]; int accumulatedBytesRead = 0; @@ -113,26 +129,26 @@ public class ByteArrayDataSourceTest extends TestCase { // Calculate a valid length for the next read, constraining by the specified output buffer // length, write offset and maximum write length input parameters. int requestedReadLength = Math.min(maxReadLength, outputBufferLength - writeOffset); - assertTrue(requestedReadLength > 0); + assertThat(requestedReadLength).isGreaterThan(0); int bytesRead = dataSource.read(outputBuffer, writeOffset, requestedReadLength); if (bytesRead != C.RESULT_END_OF_INPUT) { - assertTrue(bytesRead > 0); - assertTrue(bytesRead <= requestedReadLength); + assertThat(bytesRead).isGreaterThan(0); + assertThat(bytesRead).isAtMost(requestedReadLength); // Check the data read was correct. for (int i = 0; i < bytesRead; i++) { - assertEquals(testData[dataOffset + accumulatedBytesRead + i], - outputBuffer[writeOffset + i]); + assertThat(outputBuffer[writeOffset + i]) + .isEqualTo(testData[dataOffset + accumulatedBytesRead + i]); } // Check that we haven't read more data than we were expecting. accumulatedBytesRead += bytesRead; - assertTrue(accumulatedBytesRead <= expectedFinalBytesRead); + assertThat(accumulatedBytesRead).isAtMost(expectedFinalBytesRead); // If we haven't read all of the bytes the request should have been satisfied in full. - assertTrue(accumulatedBytesRead == expectedFinalBytesRead - || bytesRead == requestedReadLength); + assertThat(accumulatedBytesRead == expectedFinalBytesRead + || bytesRead == requestedReadLength).isTrue(); } else { // We're done. Check we read the expected number of bytes. - assertEquals(expectedFinalBytesRead, accumulatedBytesRead); + assertThat(accumulatedBytesRead).isEqualTo(expectedFinalBytesRead); return; } } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/DataSchemeDataSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSchemeDataSourceTest.java similarity index 61% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/DataSchemeDataSourceTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSchemeDataSourceTest.java index 5ba9e18e7d..85c4341232 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/DataSchemeDataSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSchemeDataSourceTest.java @@ -15,50 +15,65 @@ */ package com.google.android.exoplayer2.upstream; +import static com.google.android.exoplayer2.C.RESULT_END_OF_INPUT; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + import android.net.Uri; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.testutil.TestUtil; import java.io.IOException; -import junit.framework.TestCase; +import java.nio.charset.Charset; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; /** * Unit tests for {@link DataSchemeDataSource}. */ -public final class DataSchemeDataSourceTest extends TestCase { +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class DataSchemeDataSourceTest { private DataSource schemeDataDataSource; - @Override + @Before public void setUp() { schemeDataDataSource = new DataSchemeDataSource(); } + @Test public void testBase64Data() throws IOException { DataSpec dataSpec = buildDataSpec("data:text/plain;base64,eyJwcm92aWRlciI6IndpZGV2aW5lX3Rlc3QiL" + "CJjb250ZW50X2lkIjoiTWpBeE5WOTBaV0Z5Y3c9PSIsImtleV9pZHMiOlsiMDAwMDAwMDAwMDAwMDAwMDAwMDAwM" + "DAwMDAwMDAwMDAiXX0="); - TestUtil.assertDataSourceContent(schemeDataDataSource, dataSpec, + DataSourceAsserts.assertDataSourceContent(schemeDataDataSource, dataSpec, ("{\"provider\":\"widevine_test\",\"content_id\":\"MjAxNV90ZWFycw==\",\"key_ids\":" - + "[\"00000000000000000000000000000000\"]}").getBytes()); + + "[\"00000000000000000000000000000000\"]}").getBytes(Charset.forName(C.UTF8_NAME))); } + @Test public void testAsciiData() throws IOException { - TestUtil.assertDataSourceContent(schemeDataDataSource, buildDataSpec("data:,A%20brief%20note"), - "A brief note".getBytes()); + DataSourceAsserts.assertDataSourceContent(schemeDataDataSource, + buildDataSpec("data:,A%20brief%20note"), + "A brief note".getBytes(Charset.forName(C.UTF8_NAME))); } + @Test public void testPartialReads() throws IOException { byte[] buffer = new byte[18]; DataSpec dataSpec = buildDataSpec("data:,012345678901234567"); - assertEquals(18, schemeDataDataSource.open(dataSpec)); - assertEquals(9, schemeDataDataSource.read(buffer, 0, 9)); - assertEquals(0, schemeDataDataSource.read(buffer, 3, 0)); - assertEquals(9, schemeDataDataSource.read(buffer, 9, 15)); - assertEquals(0, schemeDataDataSource.read(buffer, 1, 0)); - assertEquals(C.RESULT_END_OF_INPUT, schemeDataDataSource.read(buffer, 1, 1)); - assertEquals("012345678901234567", new String(buffer, 0, 18)); + assertThat(schemeDataDataSource.open(dataSpec)).isEqualTo(18); + assertThat(schemeDataDataSource.read(buffer, 0, 9)).isEqualTo(9); + assertThat(schemeDataDataSource.read(buffer, 3, 0)).isEqualTo(0); + assertThat(schemeDataDataSource.read(buffer, 9, 15)).isEqualTo(9); + assertThat(schemeDataDataSource.read(buffer, 1, 0)).isEqualTo(0); + assertThat(schemeDataDataSource.read(buffer, 1, 1)).isEqualTo(RESULT_END_OF_INPUT); + assertThat(new String(buffer, 0, 18, C.UTF8_NAME)).isEqualTo("012345678901234567"); } + @Test public void testIncorrectScheme() { try { schemeDataDataSource.open(buildDataSpec("http://www.google.com")); @@ -68,6 +83,7 @@ public final class DataSchemeDataSourceTest extends TestCase { } } + @Test public void testMalformedData() { try { schemeDataDataSource.open(buildDataSpec("data:text/plain;base64,,This%20is%20Content")); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSourceAsserts.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSourceAsserts.java new file mode 100644 index 0000000000..eff3245923 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSourceAsserts.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.upstream; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.android.exoplayer2.testutil.TestUtil; +import java.io.IOException; + +/** + * Assertions for data source tests. + */ +/* package */ final class DataSourceAsserts { + + /** + * Asserts that data read from a {@link DataSource} matches {@code expected}. + * + * @param dataSource The {@link DataSource} through which to read. + * @param dataSpec The {@link DataSpec} to use when opening the {@link DataSource}. + * @param expectedData The expected data. + * @throws IOException If an error occurs reading fom the {@link DataSource}. + */ + public static void assertDataSourceContent(DataSource dataSource, DataSpec dataSpec, + byte[] expectedData) throws IOException { + try { + long length = dataSource.open(dataSpec); + assertThat(length).isEqualTo(expectedData.length); + byte[] readData = TestUtil.readToEnd(dataSource); + assertThat(readData).isEqualTo(expectedData); + } finally { + dataSource.close(); + } + } + + private DataSourceAsserts() {} + +} diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/DataSourceInputStreamTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSourceInputStreamTest.java similarity index 65% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/DataSourceInputStreamTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSourceInputStreamTest.java index 38797ede66..8cd6c23fb1 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/DataSourceInputStreamTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/DataSourceInputStreamTest.java @@ -15,75 +15,84 @@ */ package com.google.android.exoplayer2.upstream; +import static com.google.common.truth.Truth.assertThat; + import android.net.Uri; -import android.test.MoreAsserts; import com.google.android.exoplayer2.testutil.FakeDataSource; import com.google.android.exoplayer2.testutil.TestUtil; import java.io.IOException; import java.util.Arrays; -import junit.framework.TestCase; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; /** * Unit tests for {@link DataSourceInputStream}. */ -public class DataSourceInputStreamTest extends TestCase { +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class DataSourceInputStreamTest { private static final byte[] TEST_DATA = TestUtil.buildTestData(16); + @Test public void testReadSingleBytes() throws IOException { DataSourceInputStream inputStream = buildTestInputStream(); // No bytes read yet. - assertEquals(0, inputStream.bytesRead()); + assertThat(inputStream.bytesRead()).isEqualTo(0); // Read bytes. for (int i = 0; i < TEST_DATA.length; i++) { int readByte = inputStream.read(); - assertTrue(0 <= readByte && readByte < 256); - assertEquals(TEST_DATA[i] & 0xFF, readByte); - assertEquals(i + 1, inputStream.bytesRead()); + assertThat(0 <= readByte && readByte < 256).isTrue(); + assertThat(readByte).isEqualTo(TEST_DATA[i] & 0xFF); + assertThat(inputStream.bytesRead()).isEqualTo(i + 1); } // Check end of stream. - assertEquals(-1, inputStream.read()); - assertEquals(TEST_DATA.length, inputStream.bytesRead()); + assertThat(inputStream.read()).isEqualTo(-1); + assertThat(inputStream.bytesRead()).isEqualTo(TEST_DATA.length); // Check close succeeds. inputStream.close(); } + @Test public void testRead() throws IOException { DataSourceInputStream inputStream = buildTestInputStream(); // Read bytes. byte[] readBytes = new byte[TEST_DATA.length]; int totalBytesRead = 0; while (totalBytesRead < TEST_DATA.length) { - long bytesRead = inputStream.read(readBytes, totalBytesRead, + int bytesRead = inputStream.read(readBytes, totalBytesRead, TEST_DATA.length - totalBytesRead); - assertTrue(bytesRead > 0); + assertThat(bytesRead).isGreaterThan(0); totalBytesRead += bytesRead; - assertEquals(totalBytesRead, inputStream.bytesRead()); + assertThat(inputStream.bytesRead()).isEqualTo(totalBytesRead); } // Check the read data. - MoreAsserts.assertEquals(TEST_DATA, readBytes); + assertThat(readBytes).isEqualTo(TEST_DATA); // Check end of stream. - assertEquals(TEST_DATA.length, inputStream.bytesRead()); - assertEquals(TEST_DATA.length, totalBytesRead); - assertEquals(-1, inputStream.read()); + assertThat(inputStream.bytesRead()).isEqualTo(TEST_DATA.length); + assertThat(totalBytesRead).isEqualTo(TEST_DATA.length); + assertThat(inputStream.read()).isEqualTo(-1); // Check close succeeds. inputStream.close(); } + @Test public void testSkip() throws IOException { DataSourceInputStream inputStream = buildTestInputStream(); // Skip bytes. long totalBytesSkipped = 0; while (totalBytesSkipped < TEST_DATA.length) { long bytesSkipped = inputStream.skip(Long.MAX_VALUE); - assertTrue(bytesSkipped > 0); + assertThat(bytesSkipped > 0).isTrue(); totalBytesSkipped += bytesSkipped; - assertEquals(totalBytesSkipped, inputStream.bytesRead()); + assertThat(inputStream.bytesRead()).isEqualTo(totalBytesSkipped); } // Check end of stream. - assertEquals(TEST_DATA.length, inputStream.bytesRead()); - assertEquals(TEST_DATA.length, totalBytesSkipped); - assertEquals(-1, inputStream.read()); + assertThat(inputStream.bytesRead()).isEqualTo(TEST_DATA.length); + assertThat(totalBytesSkipped).isEqualTo(TEST_DATA.length); + assertThat(inputStream.read()).isEqualTo(-1); // Check close succeeds. inputStream.close(); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheAsserts.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheAsserts.java new file mode 100644 index 0000000000..aa98ad3179 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheAsserts.java @@ -0,0 +1,115 @@ +/* + * 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 static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; + +import android.net.Uri; +import com.google.android.exoplayer2.testutil.FakeDataSet; +import com.google.android.exoplayer2.testutil.FakeDataSet.FakeData; +import com.google.android.exoplayer2.upstream.DataSourceInputStream; +import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.upstream.DummyDataSource; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.ArrayList; + +/** Assertion methods for {@link com.google.android.exoplayer2.upstream.cache.Cache}. */ +/* package */ final class CacheAsserts { + + /** Asserts that the cache content is equal to the data in the {@code fakeDataSet}. */ + public static void assertCachedData(Cache cache, FakeDataSet fakeDataSet) throws IOException { + ArrayList allData = fakeDataSet.getAllData(); + Uri[] uris = new Uri[allData.size()]; + for (int i = 0; i < allData.size(); i++) { + uris[i] = allData.get(i).uri; + } + assertCachedData(cache, fakeDataSet, uris); + } + + /** + * Asserts that the cache content is equal to the given subset of data in the {@code fakeDataSet}. + */ + public static void assertCachedData(Cache cache, FakeDataSet fakeDataSet, String... uriStrings) + throws IOException { + Uri[] uris = new Uri[uriStrings.length]; + for (int i = 0; i < uriStrings.length; i++) { + uris[i] = Uri.parse(uriStrings[i]); + } + assertCachedData(cache, fakeDataSet, uris); + } + + /** + * Asserts that the cache content is equal to the given subset of data in the {@code fakeDataSet}. + */ + public static void assertCachedData(Cache cache, FakeDataSet fakeDataSet, Uri... uris) + throws IOException { + int totalLength = 0; + for (Uri uri : uris) { + byte[] data = fakeDataSet.getData(uri).getData(); + assertDataCached(cache, uri, data); + totalLength += data.length; + } + assertThat(cache.getCacheSpace()).isEqualTo(totalLength); + } + + /** Asserts that the cache contains the given subset of data in the {@code fakeDataSet}. */ + public static void assertDataCached(Cache cache, FakeDataSet fakeDataSet, Uri... uris) + throws IOException { + for (Uri uri : uris) { + assertDataCached(cache, uri, fakeDataSet.getData(uri).getData()); + } + } + + /** Asserts that the cache contains the given data for {@code uriString}. */ + public static void assertDataCached(Cache cache, Uri uri, byte[] expected) throws IOException { + CacheDataSource dataSource = new CacheDataSource(cache, DummyDataSource.INSTANCE, 0); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + DataSourceInputStream inputStream = new DataSourceInputStream(dataSource, + new DataSpec(uri, DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH)); + try { + inputStream.open(); + byte[] buffer = new byte[1024]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, bytesRead); + } + } catch (IOException e) { + // Ignore + } finally { + inputStream.close(); + } + assertWithMessage("Cached data doesn't match expected for '" + uri + "'") + .that(outputStream.toByteArray()).isEqualTo(expected); + } + + /** Asserts that there is no cache content for the given {@code uriStrings}. */ + public static void assertDataNotCached(Cache cache, String... uriStrings) { + for (String uriString : uriStrings) { + assertWithMessage("There is cached data for '" + uriString + "'") + .that(cache.getCachedSpans(CacheUtil.generateKey(Uri.parse(uriString)))).isNull(); + } + } + + /** Asserts that the cache is empty. */ + public static void assertCacheEmpty(Cache cache) { + assertThat(cache.getCacheSpace()).isEqualTo(0); + } + + private CacheAsserts() {} + +} diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java similarity index 77% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java index e7ff2a6811..e92f072dc2 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java @@ -15,11 +15,16 @@ */ package com.google.android.exoplayer2.upstream.cache; -import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCacheEmpty; +import static android.net.Uri.EMPTY; +import static com.google.android.exoplayer2.C.LENGTH_UNSET; +import static com.google.android.exoplayer2.upstream.cache.CacheAsserts.assertCacheEmpty; +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; +import static java.util.Arrays.copyOf; +import static java.util.Arrays.copyOfRange; +import static org.junit.Assert.fail; import android.net.Uri; -import android.test.InstrumentationTestCase; -import android.test.MoreAsserts; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.testutil.FakeDataSet.FakeData; import com.google.android.exoplayer2.testutil.FakeDataSource; @@ -28,12 +33,21 @@ import com.google.android.exoplayer2.upstream.FileDataSource; import com.google.android.exoplayer2.util.Util; import java.io.File; import java.io.IOException; -import java.util.Arrays; +import org.junit.After; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; /** * Unit tests for {@link CacheDataSource}. */ -public class CacheDataSourceTest extends InstrumentationTestCase { +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class CacheDataSourceTest { private static final byte[] TEST_DATA = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; private static final int MAX_CACHE_FILE_SIZE = 3; @@ -43,47 +57,52 @@ public class CacheDataSourceTest extends InstrumentationTestCase { private File tempFolder; private SimpleCache cache; - @Override + @Before public void setUp() throws Exception { - super.setUp(); - tempFolder = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest"); + tempFolder = Util.createTempDirectory(RuntimeEnvironment.application, "ExoPlayerTest"); cache = new SimpleCache(tempFolder, new NoOpCacheEvictor()); } - @Override + @After public void tearDown() throws Exception { Util.recursiveDelete(tempFolder); - super.tearDown(); } + @Test public void testMaxCacheFileSize() throws Exception { CacheDataSource cacheDataSource = createCacheDataSource(false, false); assertReadDataContentLength(cacheDataSource, false, false); for (String key : cache.getKeys()) { for (CacheSpan cacheSpan : cache.getCachedSpans(key)) { - assertTrue(cacheSpan.length <= MAX_CACHE_FILE_SIZE); - assertTrue(cacheSpan.file.length() <= MAX_CACHE_FILE_SIZE); + assertThat(cacheSpan.length <= MAX_CACHE_FILE_SIZE).isTrue(); + assertThat(cacheSpan.file.length() <= MAX_CACHE_FILE_SIZE).isTrue(); } } } + @Test public void testCacheAndRead() throws Exception { assertCacheAndRead(false, false); } + @Test public void testCacheAndReadUnboundedRequest() throws Exception { assertCacheAndRead(true, false); } + @Test public void testCacheAndReadUnknownLength() throws Exception { assertCacheAndRead(false, true); } // Disabled test as we don't support caching of definitely unknown length content + @Ignore + @Test public void disabledTestCacheAndReadUnboundedRequestUnknownLength() throws Exception { assertCacheAndRead(true, true); } + @Test public void testUnsatisfiableRange() throws Exception { // Bounded request but the content length is unknown. This forces all data to be cached but not // the length @@ -104,11 +123,12 @@ public class CacheDataSourceTest extends InstrumentationTestCase { } } + @Test public void testContentLengthEdgeCases() throws Exception { // Read partial at EOS but don't cross it so length is unknown CacheDataSource cacheDataSource = createCacheDataSource(false, true); assertReadData(cacheDataSource, true, TEST_DATA.length - 2, 2); - assertEquals(C.LENGTH_UNSET, cache.getContentLength(KEY_1)); + assertThat(cache.getContentLength(KEY_1)).isEqualTo(LENGTH_UNSET); // Now do an unbounded request for whole data. This will cause a bounded request from upstream. // End of data from upstream shouldn't be mixed up with EOS and cause length set wrong. @@ -116,21 +136,23 @@ public class CacheDataSourceTest extends InstrumentationTestCase { assertReadDataContentLength(cacheDataSource, true, true); // Now the length set correctly do an unbounded request with offset - assertEquals(2, cacheDataSource.open(new DataSpec(Uri.EMPTY, TEST_DATA.length - 2, - C.LENGTH_UNSET, KEY_1))); + assertThat(cacheDataSource.open(new DataSpec(EMPTY, TEST_DATA.length - 2, + LENGTH_UNSET, KEY_1))).isEqualTo(2); // An unbounded request with offset for not cached content - assertEquals(C.LENGTH_UNSET, cacheDataSource.open(new DataSpec(Uri.EMPTY, TEST_DATA.length - 2, - C.LENGTH_UNSET, KEY_2))); + assertThat(cacheDataSource.open(new DataSpec(EMPTY, TEST_DATA.length - 2, + LENGTH_UNSET, KEY_2))).isEqualTo(LENGTH_UNSET); } + @Test public void testIgnoreCacheForUnsetLengthRequests() throws Exception { CacheDataSource cacheDataSource = createCacheDataSource(false, true, CacheDataSource.FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS); assertReadData(cacheDataSource, true, 0, C.LENGTH_UNSET); - MoreAsserts.assertEmpty(cache.getKeys()); + assertThat(cache.getKeys()).isEmpty(); } + @Test public void testReadOnlyCache() throws Exception { CacheDataSource cacheDataSource = createCacheDataSource(false, false, 0, null); assertReadDataContentLength(cacheDataSource, false, false); @@ -157,9 +179,9 @@ public class CacheDataSourceTest extends InstrumentationTestCase { boolean unboundedRequest, boolean unknownLength) throws IOException { int length = unboundedRequest ? C.LENGTH_UNSET : TEST_DATA.length; assertReadData(cacheDataSource, unknownLength, 0, length); - assertEquals("When the range specified, CacheDataSource doesn't reach EOS so shouldn't cache " - + "content length", !unboundedRequest ? C.LENGTH_UNSET : TEST_DATA.length, - cache.getContentLength(KEY_1)); + assertWithMessage("When the range specified, CacheDataSource doesn't reach EOS so shouldn't " + + "cache content length").that(cache.getContentLength(KEY_1)) + .isEqualTo(!unboundedRequest ? C.LENGTH_UNSET : TEST_DATA.length); } private void assertReadData(CacheDataSource cacheDataSource, boolean unknownLength, int position, @@ -168,8 +190,8 @@ public class CacheDataSourceTest extends InstrumentationTestCase { if (length != C.LENGTH_UNSET) { testDataLength = Math.min(testDataLength, length); } - assertEquals(unknownLength ? length : testDataLength, - cacheDataSource.open(new DataSpec(Uri.EMPTY, position, length, KEY_1))); + assertThat(cacheDataSource.open(new DataSpec(EMPTY, position, length, KEY_1))) + .isEqualTo(unknownLength ? length : testDataLength); byte[] buffer = new byte[100]; int totalBytesRead = 0; @@ -180,9 +202,9 @@ public class CacheDataSourceTest extends InstrumentationTestCase { } totalBytesRead += read; } - assertEquals(testDataLength, totalBytesRead); - MoreAsserts.assertEquals(Arrays.copyOfRange(TEST_DATA, position, position + testDataLength), - Arrays.copyOf(buffer, totalBytesRead)); + assertThat(totalBytesRead).isEqualTo(testDataLength); + assertThat(copyOf(buffer, totalBytesRead)) + .isEqualTo(copyOfRange(TEST_DATA, position, position + testDataLength)); cacheDataSource.close(); } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest2.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest2.java similarity index 86% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest2.java rename to library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest2.java index 7e8088f3be..3b8276c731 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest2.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest2.java @@ -15,10 +15,12 @@ */ package com.google.android.exoplayer2.upstream.cache; +import static com.google.common.truth.Truth.assertThat; +import static java.util.Arrays.copyOf; +import static java.util.Arrays.copyOfRange; + import android.content.Context; import android.net.Uri; -import android.test.AndroidTestCase; -import android.test.MoreAsserts; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.testutil.FakeDataSource; import com.google.android.exoplayer2.testutil.TestUtil; @@ -32,13 +34,19 @@ import com.google.android.exoplayer2.upstream.crypto.AesCipherDataSource; import com.google.android.exoplayer2.util.Util; import java.io.File; import java.io.IOException; -import java.util.Arrays; import java.util.Random; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; /** * Additional tests for {@link CacheDataSource}. */ -public class CacheDataSourceTest2 extends AndroidTestCase { +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class CacheDataSourceTest2 { private static final String EXO_CACHE_DIR = "exo"; private static final int EXO_CACHE_MAX_FILESIZE = 128; @@ -64,17 +72,20 @@ public class CacheDataSourceTest2 extends AndroidTestCase { private static final DataSpec START_OFF_BOUNDARY = new DataSpec(URI, OFFSET_OFF_BOUNDARY, DATA.length - OFFSET_OFF_BOUNDARY, KEY); + @Test public void testWithoutEncryption() throws IOException { testReads(false); } + @Test public void testWithEncryption() throws IOException { testReads(true); } private void testReads(boolean useEncryption) throws IOException { FakeDataSource upstreamSource = buildFakeUpstreamSource(); - CacheDataSource source = buildCacheDataSource(getContext(), upstreamSource, useEncryption); + CacheDataSource source = + buildCacheDataSource(RuntimeEnvironment.application, upstreamSource, useEncryption); // First read, should arrive from upstream. testRead(END_ON_BOUNDARY, source); assertSingleOpen(upstreamSource, 0, OFFSET_ON_BOUNDARY); @@ -110,8 +121,8 @@ public class CacheDataSourceTest2 extends AndroidTestCase { int maxBytesToRead = random.nextInt(scratch.length) + 1; bytesRead = source.read(scratch, 0, maxBytesToRead); if (bytesRead != C.RESULT_END_OF_INPUT) { - MoreAsserts.assertEquals(Arrays.copyOfRange(DATA, position, position + bytesRead), - Arrays.copyOf(scratch, bytesRead)); + assertThat(copyOf(scratch, bytesRead)) + .isEqualTo(copyOfRange(DATA, position, position + bytesRead)); position += bytesRead; } } @@ -124,10 +135,10 @@ public class CacheDataSourceTest2 extends AndroidTestCase { */ private void assertSingleOpen(FakeDataSource upstreamSource, int start, int end) { DataSpec[] openedDataSpecs = upstreamSource.getAndClearOpenedDataSpecs(); - assertEquals(1, openedDataSpecs.length); - assertEquals(start, openedDataSpecs[0].position); - assertEquals(start, openedDataSpecs[0].absoluteStreamPosition); - assertEquals(end - start, openedDataSpecs[0].length); + assertThat(openedDataSpecs).hasLength(1); + assertThat(openedDataSpecs[0].position).isEqualTo(start); + assertThat(openedDataSpecs[0].absoluteStreamPosition).isEqualTo(start); + assertThat(openedDataSpecs[0].length).isEqualTo(end - start); } /** @@ -135,7 +146,7 @@ public class CacheDataSourceTest2 extends AndroidTestCase { */ private void assertNoOpen(FakeDataSource upstreamSource) { DataSpec[] openedDataSpecs = upstreamSource.getAndClearOpenedDataSpecs(); - assertEquals(0, openedDataSpecs.length); + assertThat(openedDataSpecs).hasLength(0); } private static FakeDataSource buildFakeUpstreamSource() { @@ -177,7 +188,7 @@ public class CacheDataSourceTest2 extends AndroidTestCase { } } // Sanity check that the cache really is empty now. - assertTrue(cache.getKeys().isEmpty()); + assertThat(cache.getKeys().isEmpty()).isTrue(); } } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheUtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheUtilTest.java similarity index 83% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheUtilTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheUtilTest.java index df9975d43b..c8231ec4ac 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheUtilTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheUtilTest.java @@ -15,11 +15,17 @@ */ package com.google.android.exoplayer2.upstream.cache; -import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCacheEmpty; -import static com.google.android.exoplayer2.testutil.CacheAsserts.assertCachedData; +import static android.net.Uri.EMPTY; +import static android.net.Uri.parse; +import static com.google.android.exoplayer2.C.LENGTH_UNSET; +import static com.google.android.exoplayer2.upstream.cache.CacheAsserts.assertCacheEmpty; +import static com.google.android.exoplayer2.upstream.cache.CacheAsserts.assertCachedData; +import static com.google.android.exoplayer2.upstream.cache.CacheUtil.generateKey; +import static com.google.android.exoplayer2.upstream.cache.CacheUtil.getKey; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; import android.net.Uri; -import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.testutil.FakeDataSet; import com.google.android.exoplayer2.testutil.FakeDataSource; @@ -29,13 +35,23 @@ import com.google.android.exoplayer2.upstream.cache.CacheUtil.CachingCounters; import com.google.android.exoplayer2.util.Util; import java.io.EOFException; import java.io.File; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; import org.mockito.Answers; import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; /** * Tests {@link CacheUtil}. */ -public class CacheUtilTest extends InstrumentationTestCase { +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class CacheUtilTest { /** * Abstract fake Cache implementation used by the test. This class must be public so Mockito can @@ -78,45 +94,46 @@ public class CacheUtilTest extends InstrumentationTestCase { private File tempFolder; private SimpleCache cache; - @Override + @Before public void setUp() throws Exception { - super.setUp(); - TestUtil.setUpMockito(this); + MockitoAnnotations.initMocks(this); mockCache.init(); - tempFolder = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest"); + tempFolder = Util.createTempDirectory(RuntimeEnvironment.application, "ExoPlayerTest"); cache = new SimpleCache(tempFolder, new NoOpCacheEvictor()); } - @Override + @After public void tearDown() throws Exception { Util.recursiveDelete(tempFolder); - super.tearDown(); } + @Test public void testGenerateKey() throws Exception { - assertNotNull(CacheUtil.generateKey(Uri.EMPTY)); + assertThat(generateKey(EMPTY)).isNotNull(); Uri testUri = Uri.parse("test"); String key = CacheUtil.generateKey(testUri); - assertNotNull(key); + assertThat(key).isNotNull(); // Should generate the same key for the same input - assertEquals(key, CacheUtil.generateKey(testUri)); + assertThat(generateKey(testUri)).isEqualTo(key); // Should generate different key for different input - assertFalse(key.equals(CacheUtil.generateKey(Uri.parse("test2")))); + assertThat(key.equals(generateKey(parse("test2")))).isFalse(); } + @Test public void testGetKey() throws Exception { Uri testUri = Uri.parse("test"); String key = "key"; // If DataSpec.key is present, returns it - assertEquals(key, CacheUtil.getKey(new DataSpec(testUri, 0, C.LENGTH_UNSET, key))); + assertThat(getKey(new DataSpec(testUri, 0, LENGTH_UNSET, key))).isEqualTo(key); // If not generates a new one using DataSpec.uri - assertEquals(CacheUtil.generateKey(testUri), - CacheUtil.getKey(new DataSpec(testUri, 0, C.LENGTH_UNSET, null))); + assertThat(getKey(new DataSpec(testUri, 0, LENGTH_UNSET, null))) + .isEqualTo(generateKey(testUri)); } + @Test public void testGetCachedNoData() throws Exception { CachingCounters counters = new CachingCounters(); CacheUtil.getCached(new DataSpec(Uri.parse("test")), mockCache, counters); @@ -124,6 +141,7 @@ public class CacheUtilTest extends InstrumentationTestCase { assertCounters(counters, 0, 0, C.LENGTH_UNSET); } + @Test public void testGetCachedDataUnknownLength() throws Exception { // Mock there is 100 bytes cached at the beginning mockCache.spansAndGaps = new int[] {100}; @@ -133,6 +151,7 @@ public class CacheUtilTest extends InstrumentationTestCase { assertCounters(counters, 100, 0, C.LENGTH_UNSET); } + @Test public void testGetCachedNoDataKnownLength() throws Exception { mockCache.contentLength = 1000; CachingCounters counters = new CachingCounters(); @@ -141,6 +160,7 @@ public class CacheUtilTest extends InstrumentationTestCase { assertCounters(counters, 0, 0, 1000); } + @Test public void testGetCached() throws Exception { mockCache.contentLength = 1000; mockCache.spansAndGaps = new int[] {100, 100, 200}; @@ -150,6 +170,7 @@ public class CacheUtilTest extends InstrumentationTestCase { assertCounters(counters, 300, 0, 1000); } + @Test public void testCache() throws Exception { FakeDataSet fakeDataSet = new FakeDataSet().setRandomData("test_data", 100); FakeDataSource dataSource = new FakeDataSource(fakeDataSet); @@ -161,6 +182,7 @@ public class CacheUtilTest extends InstrumentationTestCase { assertCachedData(cache, fakeDataSet); } + @Test public void testCacheSetOffsetAndLength() throws Exception { FakeDataSet fakeDataSet = new FakeDataSet().setRandomData("test_data", 100); FakeDataSource dataSource = new FakeDataSource(fakeDataSet); @@ -178,6 +200,7 @@ public class CacheUtilTest extends InstrumentationTestCase { assertCachedData(cache, fakeDataSet); } + @Test public void testCacheUnknownLength() throws Exception { FakeDataSet fakeDataSet = new FakeDataSet().newData("test_data") .setSimulateUnknownLength(true) @@ -192,6 +215,7 @@ public class CacheUtilTest extends InstrumentationTestCase { assertCachedData(cache, fakeDataSet); } + @Test public void testCacheUnknownLengthPartialCaching() throws Exception { FakeDataSet fakeDataSet = new FakeDataSet().newData("test_data") .setSimulateUnknownLength(true) @@ -211,6 +235,7 @@ public class CacheUtilTest extends InstrumentationTestCase { assertCachedData(cache, fakeDataSet); } + @Test public void testCacheLengthExceedsActualDataLength() throws Exception { FakeDataSet fakeDataSet = new FakeDataSet().setRandomData("test_data", 100); FakeDataSource dataSource = new FakeDataSource(fakeDataSet); @@ -224,6 +249,7 @@ public class CacheUtilTest extends InstrumentationTestCase { assertCachedData(cache, fakeDataSet); } + @Test public void testCacheThrowEOFException() throws Exception { FakeDataSet fakeDataSet = new FakeDataSet().setRandomData("test_data", 100); FakeDataSource dataSource = new FakeDataSource(fakeDataSet); @@ -241,6 +267,7 @@ public class CacheUtilTest extends InstrumentationTestCase { } } + @Test public void testCachePolling() throws Exception { final CachingCounters counters = new CachingCounters(); FakeDataSet fakeDataSet = new FakeDataSet().newData("test_data") @@ -267,6 +294,7 @@ public class CacheUtilTest extends InstrumentationTestCase { assertCachedData(cache, fakeDataSet); } + @Test public void testRemove() throws Exception { FakeDataSet fakeDataSet = new FakeDataSet().setRandomData("test_data", 100); FakeDataSource dataSource = new FakeDataSource(fakeDataSet); @@ -283,9 +311,9 @@ public class CacheUtilTest extends InstrumentationTestCase { private static void assertCounters(CachingCounters counters, int alreadyCachedBytes, int newlyCachedBytes, int contentLength) { - assertEquals(alreadyCachedBytes, counters.alreadyCachedBytes); - assertEquals(newlyCachedBytes, counters.newlyCachedBytes); - assertEquals(contentLength, counters.contentLength); + assertThat(counters.alreadyCachedBytes).isEqualTo(alreadyCachedBytes); + assertThat(counters.newlyCachedBytes).isEqualTo(newlyCachedBytes); + assertThat(counters.contentLength).isEqualTo(contentLength); } } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java similarity index 69% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java index 1a6beeb6ba..ed55045835 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java @@ -15,8 +15,10 @@ */ package com.google.android.exoplayer2.upstream.cache; -import android.test.InstrumentationTestCase; -import android.test.MoreAsserts; +import static com.google.android.exoplayer2.C.LENGTH_UNSET; +import static com.google.android.exoplayer2.util.Util.toByteArray; +import static com.google.common.truth.Truth.assertThat; + import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Util; import java.io.File; @@ -26,60 +28,71 @@ import java.io.IOException; import java.util.NavigableSet; import java.util.Random; import java.util.Set; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; /** * Unit tests for {@link SimpleCache}. */ -public class SimpleCacheTest extends InstrumentationTestCase { +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public class SimpleCacheTest { private static final String KEY_1 = "key1"; private File cacheDir; - @Override - protected void setUp() throws Exception { - cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest"); + @Before + public void setUp() throws Exception { + cacheDir = Util.createTempDirectory(RuntimeEnvironment.application, "ExoPlayerTest"); } - @Override - protected void tearDown() throws Exception { + @After + public void tearDown() throws Exception { Util.recursiveDelete(cacheDir); } + @Test public void testCommittingOneFile() throws Exception { SimpleCache simpleCache = getSimpleCache(); CacheSpan cacheSpan1 = simpleCache.startReadWrite(KEY_1, 0); - assertFalse(cacheSpan1.isCached); - assertTrue(cacheSpan1.isOpenEnded()); + assertThat(cacheSpan1.isCached).isFalse(); + assertThat(cacheSpan1.isOpenEnded()).isTrue(); - assertNull(simpleCache.startReadWriteNonBlocking(KEY_1, 0)); + assertThat(simpleCache.startReadWriteNonBlocking(KEY_1, 0)).isNull(); - assertEquals(0, simpleCache.getKeys().size()); + assertThat(simpleCache.getKeys()).isEmpty(); NavigableSet cachedSpans = simpleCache.getCachedSpans(KEY_1); - assertTrue(cachedSpans == null || cachedSpans.size() == 0); - assertEquals(0, simpleCache.getCacheSpace()); - assertEquals(0, cacheDir.listFiles().length); + assertThat(cachedSpans == null || cachedSpans.isEmpty()).isTrue(); + assertThat(simpleCache.getCacheSpace()).isEqualTo(0); + assertThat(cacheDir.listFiles()).hasLength(0); addCache(simpleCache, KEY_1, 0, 15); Set cachedKeys = simpleCache.getKeys(); - assertEquals(1, cachedKeys.size()); - assertTrue(cachedKeys.contains(KEY_1)); + assertThat(cachedKeys).hasSize(1); + assertThat(cachedKeys.contains(KEY_1)).isTrue(); cachedSpans = simpleCache.getCachedSpans(KEY_1); - assertEquals(1, cachedSpans.size()); - assertTrue(cachedSpans.contains(cacheSpan1)); - assertEquals(15, simpleCache.getCacheSpace()); + assertThat(cachedSpans).hasSize(1); + assertThat(cachedSpans.contains(cacheSpan1)).isTrue(); + assertThat(simpleCache.getCacheSpace()).isEqualTo(15); simpleCache.releaseHoleSpan(cacheSpan1); CacheSpan cacheSpan2 = simpleCache.startReadWrite(KEY_1, 0); - assertTrue(cacheSpan2.isCached); - assertFalse(cacheSpan2.isOpenEnded()); - assertEquals(15, cacheSpan2.length); + assertThat(cacheSpan2.isCached).isTrue(); + assertThat(cacheSpan2.isOpenEnded()).isFalse(); + assertThat(cacheSpan2.length).isEqualTo(15); assertCachedDataReadCorrect(cacheSpan2); } + @Test public void testReadCacheWithoutReleasingWriteCacheSpan() throws Exception { SimpleCache simpleCache = getSimpleCache(); @@ -90,19 +103,20 @@ public class SimpleCacheTest extends InstrumentationTestCase { simpleCache.releaseHoleSpan(cacheSpan1); } + @Test public void testSetGetLength() throws Exception { SimpleCache simpleCache = getSimpleCache(); - assertEquals(C.LENGTH_UNSET, simpleCache.getContentLength(KEY_1)); + assertThat(simpleCache.getContentLength(KEY_1)).isEqualTo(LENGTH_UNSET); simpleCache.setContentLength(KEY_1, 15); - assertEquals(15, simpleCache.getContentLength(KEY_1)); + assertThat(simpleCache.getContentLength(KEY_1)).isEqualTo(15); simpleCache.startReadWrite(KEY_1, 0); addCache(simpleCache, KEY_1, 0, 15); simpleCache.setContentLength(KEY_1, 150); - assertEquals(150, simpleCache.getContentLength(KEY_1)); + assertThat(simpleCache.getContentLength(KEY_1)).isEqualTo(150); addCache(simpleCache, KEY_1, 140, 10); @@ -110,19 +124,20 @@ public class SimpleCacheTest extends InstrumentationTestCase { SimpleCache simpleCache2 = getSimpleCache(); Set keys = simpleCache.getKeys(); Set keys2 = simpleCache2.getKeys(); - assertEquals(keys, keys2); + assertThat(keys2).isEqualTo(keys); for (String key : keys) { - assertEquals(simpleCache.getContentLength(key), simpleCache2.getContentLength(key)); - assertEquals(simpleCache.getCachedSpans(key), simpleCache2.getCachedSpans(key)); + assertThat(simpleCache2.getContentLength(key)).isEqualTo(simpleCache.getContentLength(key)); + assertThat(simpleCache2.getCachedSpans(key)).isEqualTo(simpleCache.getCachedSpans(key)); } // Removing the last span shouldn't cause the length be change next time cache loaded SimpleCacheSpan lastSpan = simpleCache2.startReadWrite(KEY_1, 145); simpleCache2.removeSpan(lastSpan); simpleCache2 = getSimpleCache(); - assertEquals(150, simpleCache2.getContentLength(KEY_1)); + assertThat(simpleCache2.getContentLength(KEY_1)).isEqualTo(150); } + @Test public void testReloadCache() throws Exception { SimpleCache simpleCache = getSimpleCache(); @@ -139,6 +154,7 @@ public class SimpleCacheTest extends InstrumentationTestCase { assertCachedDataReadCorrect(cacheSpan2); } + @Test public void testEncryptedIndex() throws Exception { byte[] key = "Bar12345Bar12345".getBytes(C.UTF8_NAME); // 128 bit key SimpleCache simpleCache = getEncryptedSimpleCache(key); @@ -156,6 +172,7 @@ public class SimpleCacheTest extends InstrumentationTestCase { assertCachedDataReadCorrect(cacheSpan2); } + @Test public void testEncryptedIndexWrongKey() throws Exception { byte[] key = "Bar12345Bar12345".getBytes(C.UTF8_NAME); // 128 bit key SimpleCache simpleCache = getEncryptedSimpleCache(key); @@ -170,10 +187,11 @@ public class SimpleCacheTest extends InstrumentationTestCase { simpleCache = getEncryptedSimpleCache(key2); // Cache should be cleared - assertEquals(0, simpleCache.getKeys().size()); - assertEquals(0, cacheDir.listFiles().length); + assertThat(simpleCache.getKeys()).isEmpty(); + assertThat(cacheDir.listFiles()).hasLength(0); } + @Test public void testEncryptedIndexLostKey() throws Exception { byte[] key = "Bar12345Bar12345".getBytes(C.UTF8_NAME); // 128 bit key SimpleCache simpleCache = getEncryptedSimpleCache(key); @@ -187,41 +205,42 @@ public class SimpleCacheTest extends InstrumentationTestCase { simpleCache = getSimpleCache(); // Cache should be cleared - assertEquals(0, simpleCache.getKeys().size()); - assertEquals(0, cacheDir.listFiles().length); + assertThat(simpleCache.getKeys()).isEmpty(); + assertThat(cacheDir.listFiles()).hasLength(0); } + @Test public void testGetCachedBytes() throws Exception { SimpleCache simpleCache = getSimpleCache(); CacheSpan cacheSpan = simpleCache.startReadWrite(KEY_1, 0); // No cached bytes, returns -'length' - assertEquals(-100, simpleCache.getCachedBytes(KEY_1, 0, 100)); + assertThat(simpleCache.getCachedBytes(KEY_1, 0, 100)).isEqualTo(-100); // Position value doesn't affect the return value - assertEquals(-100, simpleCache.getCachedBytes(KEY_1, 20, 100)); + assertThat(simpleCache.getCachedBytes(KEY_1, 20, 100)).isEqualTo(-100); addCache(simpleCache, KEY_1, 0, 15); // Returns the length of a single span - assertEquals(15, simpleCache.getCachedBytes(KEY_1, 0, 100)); + assertThat(simpleCache.getCachedBytes(KEY_1, 0, 100)).isEqualTo(15); // Value is capped by the 'length' - assertEquals(10, simpleCache.getCachedBytes(KEY_1, 0, 10)); + assertThat(simpleCache.getCachedBytes(KEY_1, 0, 10)).isEqualTo(10); addCache(simpleCache, KEY_1, 15, 35); // Returns the length of two adjacent spans - assertEquals(50, simpleCache.getCachedBytes(KEY_1, 0, 100)); + assertThat(simpleCache.getCachedBytes(KEY_1, 0, 100)).isEqualTo(50); addCache(simpleCache, KEY_1, 60, 10); // Not adjacent span doesn't affect return value - assertEquals(50, simpleCache.getCachedBytes(KEY_1, 0, 100)); + assertThat(simpleCache.getCachedBytes(KEY_1, 0, 100)).isEqualTo(50); // Returns length of hole up to the next cached span - assertEquals(-5, simpleCache.getCachedBytes(KEY_1, 55, 100)); + assertThat(simpleCache.getCachedBytes(KEY_1, 55, 100)).isEqualTo(-5); simpleCache.releaseHoleSpan(cacheSpan); } @@ -247,11 +266,11 @@ public class SimpleCacheTest extends InstrumentationTestCase { } private static void assertCachedDataReadCorrect(CacheSpan cacheSpan) throws IOException { - assertTrue(cacheSpan.isCached); + assertThat(cacheSpan.isCached).isTrue(); byte[] expected = generateData(cacheSpan.key, (int) cacheSpan.position, (int) cacheSpan.length); FileInputStream inputStream = new FileInputStream(cacheSpan.file); try { - MoreAsserts.assertEquals(expected, Util.toByteArray(inputStream)); + assertThat(toByteArray(inputStream)).isEqualTo(expected); } finally { inputStream.close(); } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipherTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipherTest.java similarity index 77% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipherTest.java rename to library/core/src/test/java/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipherTest.java index b4e7e6e7f6..833a7e10c1 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipherTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/crypto/AesFlushingCipherTest.java @@ -15,16 +15,25 @@ */ package com.google.android.exoplayer2.upstream.crypto; +import static com.google.common.truth.Truth.assertThat; + import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.util.Util; import java.util.Random; import javax.crypto.Cipher; -import junit.framework.TestCase; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; /** * Unit tests for {@link AesFlushingCipher}. */ -public class AesFlushingCipherTest extends TestCase { +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public class AesFlushingCipherTest { private static final int DATA_LENGTH = 65536; private static final byte[] KEY = Util.getUtf8Bytes("testKey:12345678"); @@ -35,26 +44,26 @@ public class AesFlushingCipherTest extends TestCase { private AesFlushingCipher encryptCipher; private AesFlushingCipher decryptCipher; - @Override - protected void setUp() { + @Before + public void setUp() { encryptCipher = new AesFlushingCipher(Cipher.ENCRYPT_MODE, KEY, NONCE, START_OFFSET); decryptCipher = new AesFlushingCipher(Cipher.DECRYPT_MODE, KEY, NONCE, START_OFFSET); } - @Override - protected void tearDown() { + @After + public void tearDown() { encryptCipher = null; decryptCipher = null; } - private long getMaxUnchangedBytesAllowedPostEncryption(long length) { + private static long getMaxUnchangedBytesAllowedPostEncryption(long length) { // Assuming that not more than 10% of the resultant bytes should be identical. // The value of 10% is arbitrary, ciphers standards do not name a value. return length / 10; } // Count the number of bytes that do not match. - private int getDifferingByteCount(byte[] data1, byte[] data2, int startOffset) { + private static int getDifferingByteCount(byte[] data1, byte[] data2, int startOffset) { int count = 0; for (int i = startOffset; i < data1.length; i++) { if (data1[i] != data2[i]) { @@ -65,25 +74,28 @@ public class AesFlushingCipherTest extends TestCase { } // Count the number of bytes that do not match. - private int getDifferingByteCount(byte[] data1, byte[] data2) { + private static int getDifferingByteCount(byte[] data1, byte[] data2) { return getDifferingByteCount(data1, data2, 0); } - // Test a single encrypt and decrypt call + // Test a single encrypt and decrypt call. + @Test public void testSingle() { byte[] reference = TestUtil.buildTestData(DATA_LENGTH); byte[] data = reference.clone(); encryptCipher.updateInPlace(data, 0, data.length); int unchangedByteCount = data.length - getDifferingByteCount(reference, data); - assertTrue(unchangedByteCount <= getMaxUnchangedBytesAllowedPostEncryption(data.length)); + assertThat(unchangedByteCount <= getMaxUnchangedBytesAllowedPostEncryption(data.length)) + .isTrue(); decryptCipher.updateInPlace(data, 0, data.length); int differingByteCount = getDifferingByteCount(reference, data); - assertEquals(0, differingByteCount); + assertThat(differingByteCount).isEqualTo(0); } - // Test several encrypt and decrypt calls, each aligned on a 16 byte block size + // Test several encrypt and decrypt calls, each aligned on a 16 byte block size. + @Test public void testAligned() { byte[] reference = TestUtil.buildTestData(DATA_LENGTH); byte[] data = reference.clone(); @@ -93,28 +105,30 @@ public class AesFlushingCipherTest extends TestCase { while (offset < data.length) { int bytes = (1 + random.nextInt(50)) * 16; bytes = Math.min(bytes, data.length - offset); - assertEquals(0, bytes % 16); + assertThat(bytes % 16).isEqualTo(0); encryptCipher.updateInPlace(data, offset, bytes); offset += bytes; } int unchangedByteCount = data.length - getDifferingByteCount(reference, data); - assertTrue(unchangedByteCount <= getMaxUnchangedBytesAllowedPostEncryption(data.length)); + assertThat(unchangedByteCount <= getMaxUnchangedBytesAllowedPostEncryption(data.length)) + .isTrue(); offset = 0; while (offset < data.length) { int bytes = (1 + random.nextInt(50)) * 16; bytes = Math.min(bytes, data.length - offset); - assertEquals(0, bytes % 16); + assertThat(bytes % 16).isEqualTo(0); decryptCipher.updateInPlace(data, offset, bytes); offset += bytes; } int differingByteCount = getDifferingByteCount(reference, data); - assertEquals(0, differingByteCount); + assertThat(differingByteCount).isEqualTo(0); } - // Test several encrypt and decrypt calls, not aligned on block boundary + // Test several encrypt and decrypt calls, not aligned on block boundary. + @Test public void testUnAligned() { byte[] reference = TestUtil.buildTestData(DATA_LENGTH); byte[] data = reference.clone(); @@ -130,7 +144,8 @@ public class AesFlushingCipherTest extends TestCase { } int unchangedByteCount = data.length - getDifferingByteCount(reference, data); - assertTrue(unchangedByteCount <= getMaxUnchangedBytesAllowedPostEncryption(data.length)); + assertThat(unchangedByteCount <= getMaxUnchangedBytesAllowedPostEncryption(data.length)) + .isTrue(); offset = 0; while (offset < data.length) { @@ -141,10 +156,11 @@ public class AesFlushingCipherTest extends TestCase { } int differingByteCount = getDifferingByteCount(reference, data); - assertEquals(0, differingByteCount); + assertThat(differingByteCount).isEqualTo(0); } - // Test decryption starting from the middle of an encrypted block + // Test decryption starting from the middle of an encrypted block. + @Test public void testMidJoin() { byte[] reference = TestUtil.buildTestData(DATA_LENGTH); byte[] data = reference.clone(); @@ -161,7 +177,8 @@ public class AesFlushingCipherTest extends TestCase { // Verify int unchangedByteCount = data.length - getDifferingByteCount(reference, data); - assertTrue(unchangedByteCount <= getMaxUnchangedBytesAllowedPostEncryption(data.length)); + assertThat(unchangedByteCount <= getMaxUnchangedBytesAllowedPostEncryption(data.length)) + .isTrue(); // Setup decryption from random location offset = random.nextInt(4096); @@ -180,7 +197,7 @@ public class AesFlushingCipherTest extends TestCase { // Verify int differingByteCount = getDifferingByteCount(reference, data, originalOffset); - assertEquals(0, differingByteCount); + assertThat(differingByteCount).isEqualTo(0); } } diff --git a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/MockitoUtil.java b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/MockitoUtil.java new file mode 100644 index 0000000000..e7cd9baf59 --- /dev/null +++ b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/MockitoUtil.java @@ -0,0 +1,38 @@ +/* + * 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 android.test.InstrumentationTestCase; +import org.mockito.MockitoAnnotations; + +/** + * Utility for setting up Mockito for instrumentation tests. + */ +public final class MockitoUtil { + + /** + * Sets up Mockito for an instrumentation test. + */ + public static void setUpMockito(InstrumentationTestCase instrumentationTestCase) { + // Workaround for https://code.google.com/p/dexmaker/issues/detail?id=2. + System.setProperty("dexmaker.dexcache", + instrumentationTestCase.getInstrumentation().getTargetContext().getCacheDir().getPath()); + MockitoAnnotations.initMocks(instrumentationTestCase); + } + + private MockitoUtil() {} + +} diff --git a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java index f5e00d2cec..8532e65a68 100644 --- a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java +++ b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java @@ -27,6 +27,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.offline.DownloadException; import com.google.android.exoplayer2.offline.Downloader.ProgressListener; import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; +import com.google.android.exoplayer2.source.dash.MockitoUtil; import com.google.android.exoplayer2.source.dash.manifest.DashManifest; import com.google.android.exoplayer2.source.dash.manifest.RepresentationKey; import com.google.android.exoplayer2.testutil.FakeDataSet; @@ -54,7 +55,7 @@ public class DashDownloaderTest extends InstrumentationTestCase { @Override public void setUp() throws Exception { super.setUp(); - TestUtil.setUpMockito(this); + MockitoUtil.setUpMockito(this); tempFolder = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest"); cache = new SimpleCache(tempFolder, new NoOpCacheEvictor()); } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/TestData.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/OggTestData.java similarity index 99% rename from library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/TestData.java rename to testutils/src/main/java/com/google/android/exoplayer2/testutil/OggTestData.java index cef033bf17..88b5de7f65 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/extractor/ogg/TestData.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/OggTestData.java @@ -13,19 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.extractor.ogg; +package com.google.android.exoplayer2.testutil; -import com.google.android.exoplayer2.testutil.FakeExtractorInput; -import com.google.android.exoplayer2.testutil.TestUtil; /** * Provides ogg/vorbis test data in bytes for unit tests. */ -/* package */ final class TestData { +public final class OggTestData { - /* package */ static FakeExtractorInput createInput(byte[] data, boolean simulateUnkownLength) { + public static FakeExtractorInput createInput(byte[] data, boolean simulateUnknownLength) { return new FakeExtractorInput.Builder().setData(data).setSimulateIOErrors(true) - .setSimulateUnknownLength(simulateUnkownLength).setSimulatePartialReads(true).build(); + .setSimulateUnknownLength(simulateUnknownLength).setSimulatePartialReads(true).build(); } public static byte[] buildOggHeader(int headerType, long granule, int pageSequenceCounter, diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java index 2e59b33c0b..0f737ec23a 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java @@ -16,7 +16,6 @@ package com.google.android.exoplayer2.testutil; import android.app.Instrumentation; -import android.test.InstrumentationTestCase; import android.test.MoreAsserts; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Timeline; @@ -33,7 +32,6 @@ import java.io.InputStream; import java.util.Arrays; import java.util.Random; import junit.framework.Assert; -import org.mockito.MockitoAnnotations; /** * Utility methods for tests. @@ -121,13 +119,6 @@ public class TestUtil { return joined; } - public static void setUpMockito(InstrumentationTestCase instrumentationTestCase) { - // Workaround for https://code.google.com/p/dexmaker/issues/detail?id=2. - System.setProperty("dexmaker.dexcache", - instrumentationTestCase.getInstrumentation().getTargetContext().getCacheDir().getPath()); - MockitoAnnotations.initMocks(instrumentationTestCase); - } - public static byte[] getByteArray(Instrumentation instrumentation, String fileName) throws IOException { return Util.toByteArray(getInputStream(instrumentation, fileName)); From b2627d63fdb0215f65096ef2fe0c8e47c341e4a2 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 8 Sep 2017 04:05:38 -0700 Subject: [PATCH 0360/2472] Allow ExoPlayerTestRunner to end when Player.stop() is called. In this case the playback state transitions to IDLE, which isn't caught so far. (This code is equivalent to the one in ExoHostedTest.java) ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167980981 --- .../android/exoplayer2/testutil/ExoPlayerTestRunner.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java index 2b5ea11d94..f65cb39bfc 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java @@ -212,6 +212,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener { private Exception exception; private TrackGroupArray trackGroups; private int positionDiscontinuityCount; + private boolean playerWasPrepared; private ExoPlayerTestRunner(PlayerFactory playerFactory, MediaSource mediaSource, RenderersFactory renderersFactory, MappingTrackSelector trackSelector, @@ -350,7 +351,9 @@ public final class ExoPlayerTestRunner implements Player.EventListener { if (periodIndices.isEmpty() && playbackState == Player.STATE_READY) { periodIndices.add(player.getCurrentPeriodIndex()); } - if (playbackState == Player.STATE_ENDED) { + playerWasPrepared |= playbackState != Player.STATE_IDLE; + if (playbackState == Player.STATE_ENDED + || (playbackState == Player.STATE_IDLE && playerWasPrepared)) { endedCountDownLatch.countDown(); } } From 8f43dcd424c02ae15ceae578764d620409506a06 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 8 Sep 2017 07:34:07 -0700 Subject: [PATCH 0361/2472] Clear buffer in fake renderer in each iteration. This simulates reading from the buffer (which is what actual renderers would do). Otherwise the buffer always gets expanded and might cause memory issues. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167994899 --- .../com/google/android/exoplayer2/testutil/FakeRenderer.java | 1 + 1 file changed, 1 insertion(+) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeRenderer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeRenderer.java index a66043b77f..c4270eb9c4 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeRenderer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeRenderer.java @@ -59,6 +59,7 @@ public class FakeRenderer extends BaseRenderer { @Override public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { if (!isEnded) { + buffer.clear(); // Verify the format matches the expected format. FormatHolder formatHolder = new FormatHolder(); int result = readSource(formatHolder, buffer, false); From ec38d0d8ab8cc0d810d16d052fbf700b8fc603d8 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 8 Sep 2017 07:54:10 -0700 Subject: [PATCH 0362/2472] Check thread is still alive before sending message in Loader. The release callback handler in Loader might not be alive anymore. Catch this case to prevent warnings about sending messages on dead threads. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167996538 --- .../java/com/google/android/exoplayer2/upstream/Loader.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/Loader.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/Loader.java index 02ccfafa89..02e9a32116 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/Loader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/Loader.java @@ -420,7 +420,9 @@ public final class Loader implements LoaderErrorThrower { @Override public void run() { - sendEmptyMessage(0); + if (getLooper().getThread().isAlive()) { + sendEmptyMessage(0); + } } @Override From 40d443dc027c7a3e6ba45dd62ef6777906197dc1 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 8 Sep 2017 09:42:13 -0700 Subject: [PATCH 0363/2472] Enable rtmp in external demo app with extensions ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=168007345 --- demos/main/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/demos/main/build.gradle b/demos/main/build.gradle index 099741d167..adad8f0e58 100644 --- a/demos/main/build.gradle +++ b/demos/main/build.gradle @@ -62,4 +62,5 @@ dependencies { withExtensionsCompile project(path: modulePrefix + 'extension-ima') withExtensionsCompile project(path: modulePrefix + 'extension-opus') withExtensionsCompile project(path: modulePrefix + 'extension-vp9') + withExtensionsCompile project(path: modulePrefix + 'extension-rtmp') } From 5a4155f09fee738da4ebd024a4f1b0a071d0fd42 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 8 Sep 2017 11:14:53 -0700 Subject: [PATCH 0364/2472] Destroy EGLSurface during DummySurface cleanup ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=168020525 --- .../android/exoplayer2/video/DummySurface.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java index 20fe862dd2..450b4af38c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java @@ -35,6 +35,7 @@ import static android.opengl.EGL14.eglChooseConfig; import static android.opengl.EGL14.eglCreateContext; import static android.opengl.EGL14.eglCreatePbufferSurface; import static android.opengl.EGL14.eglDestroyContext; +import static android.opengl.EGL14.eglDestroySurface; import static android.opengl.EGL14.eglGetDisplay; import static android.opengl.EGL14.eglInitialize; import static android.opengl.EGL14.eglMakeCurrent; @@ -175,8 +176,9 @@ public final class DummySurface extends Surface { private static final int MSG_RELEASE = 3; private final int[] textureIdHolder; - private EGLContext context; private EGLDisplay display; + private EGLContext context; + private EGLSurface pbuffer; private Handler handler; private SurfaceTexture surfaceTexture; @@ -315,7 +317,7 @@ public final class DummySurface extends Surface { EGL_HEIGHT, 1, EGL_NONE}; } - EGLSurface pbuffer = eglCreatePbufferSurface(display, config, pbufferAttributes, 0); + pbuffer = eglCreatePbufferSurface(display, config, pbufferAttributes, 0); Assertions.checkState(pbuffer != null, "eglCreatePbufferSurface failed"); boolean eglMadeCurrent = eglMakeCurrent(display, pbuffer, pbuffer, context); @@ -334,11 +336,15 @@ public final class DummySurface extends Surface { glDeleteTextures(1, textureIdHolder, 0); } } finally { + if (pbuffer != null) { + eglDestroySurface(display, pbuffer); + } if (context != null) { eglDestroyContext(display, context); } - display = null; + pbuffer = null; context = null; + display = null; surface = null; surfaceTexture = null; } From 74e8acf14f23fc60abe7c0a06dd3c03eeb3e2316 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 8 Aug 2017 10:54:06 -0700 Subject: [PATCH 0365/2472] Minimal change to expose segment indices in DefaultDashChunkSource Issue: #3037 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164614478 --- .../source/dash/DefaultDashChunkSource.java | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/library/dash/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 index 297052f65a..dd62d47621 100644 --- a/library/dash/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 @@ -85,14 +85,14 @@ public class DefaultDashChunkSource implements DashChunkSource { private final int[] adaptationSetIndices; private final TrackSelection trackSelection; private final int trackType; - private final RepresentationHolder[] representationHolders; private final DataSource dataSource; private final long elapsedRealtimeOffsetMs; private final int maxSegmentsPerLoad; + protected final RepresentationHolder[] representationHolders; + private DashManifest manifest; private int periodIndex; - private IOException fatalError; private boolean missingLastSegment; @@ -377,9 +377,12 @@ public class DefaultDashChunkSource implements DashChunkSource { // Protected classes. + /** + * Holds information about a single {@link Representation}. + */ protected static final class RepresentationHolder { - public final ChunkExtractorWrapper extractorWrapper; + /* package */ final ChunkExtractorWrapper extractorWrapper; public Representation representation; public DashSegmentIndex segmentIndex; @@ -387,7 +390,7 @@ public class DefaultDashChunkSource implements DashChunkSource { private long periodDurationUs; private int segmentNumShift; - public RepresentationHolder(long periodDurationUs, Representation representation, + /* package */ RepresentationHolder(long periodDurationUs, Representation representation, boolean enableEventMessageTrack, boolean enableCea608Track) { this.periodDurationUs = periodDurationUs; this.representation = representation; @@ -417,8 +420,8 @@ public class DefaultDashChunkSource implements DashChunkSource { segmentIndex = representation.getIndex(); } - public void updateRepresentation(long newPeriodDurationUs, Representation newRepresentation) - throws BehindLiveWindowException{ + /* package */ void updateRepresentation(long newPeriodDurationUs, + Representation newRepresentation) throws BehindLiveWindowException { DashSegmentIndex oldIndex = representation.getIndex(); DashSegmentIndex newIndex = newRepresentation.getIndex(); From bc0829503acd094d58fecb6bde64f4b734654c5d Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 9 Aug 2017 03:21:25 -0700 Subject: [PATCH 0366/2472] Clean up terminology for MediaSession extension ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164705750 --- extensions/mediasession/README.md | 8 ++++---- .../ext/mediasession/MediaSessionConnector.java | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/extensions/mediasession/README.md b/extensions/mediasession/README.md index 3acf8e4c79..60fec9fb60 100644 --- a/extensions/mediasession/README.md +++ b/extensions/mediasession/README.md @@ -2,10 +2,10 @@ ## Description ## -The MediaSession extension mediates between an ExoPlayer instance and a -[MediaSession][]. It automatically retrieves and implements playback actions -and syncs the player state with the state of the media session. The behaviour -can be extended to support other playback and custom actions. +The MediaSession extension mediates between a Player (or ExoPlayer) instance +and a [MediaSession][]. It automatically retrieves and implements playback +actions and syncs the player state with the state of the media session. The +behaviour can be extended to support other playback and custom actions. [MediaSession]: https://developer.android.com/reference/android/support/v4/media/session/MediaSessionCompat.html diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java index 0e839b8083..33807930a5 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java @@ -47,7 +47,7 @@ import java.util.Map; * Connects a {@link MediaSessionCompat} to a {@link Player}. *

      * The connector listens for actions sent by the media session's controller and implements these - * actions by calling appropriate ExoPlayer methods. The playback state of the media session is + * actions by calling appropriate player methods. The playback state of the media session is * automatically synced with the player. The connector can also be optionally extended by providing * various collaborators: *

        From 154f17ef1e8abbcd7c3a006378dd1935748e186a Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 9 Aug 2017 03:26:18 -0700 Subject: [PATCH 0367/2472] Fix minor Javadoc error ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164706078 --- .../android/exoplayer2/ui/SimpleExoPlayerView.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index 2bba9071fd..17923767d1 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -668,10 +668,15 @@ public final class SimpleExoPlayerView extends FrameLayout { } /** - * 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. + * Gets the view onto which video is rendered. This is a: + *
          + *
        • {@link SurfaceView} by default, or if the {@code surface_type} attribute is set to + * {@code surface_view}.
        • + *
        • {@link TextureView} if {@code surface_type} is {@code texture_view}.
        • + *
        • {@code null} if {@code surface_type} is {@code none}.
        • + *
        * - * @return Either a {@link SurfaceView} or a {@link TextureView}. + * @return The {@link SurfaceView}, {@link TextureView} or {@code null}. */ public View getVideoSurfaceView() { return surfaceView; From 5fedbf76db277bb2bb39ddba5a00f3d8cff66b51 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 9 Aug 2017 03:27:13 -0700 Subject: [PATCH 0368/2472] Document usage of the RTMP extension ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164706135 --- extensions/rtmp/README.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/extensions/rtmp/README.md b/extensions/rtmp/README.md index 042d7078dc..80074f119c 100644 --- a/extensions/rtmp/README.md +++ b/extensions/rtmp/README.md @@ -9,7 +9,7 @@ streams using [LibRtmp Client for Android][]. [RTMP]: https://en.wikipedia.org/wiki/Real-Time_Messaging_Protocol [LibRtmp Client for Android]: https://github.com/ant-media/LibRtmp-Client-for-Android -## Using the extension ## +## Getting the extension ## The easiest way to use the extension is to add it as a gradle dependency: @@ -25,3 +25,19 @@ locally. Instructions for doing this can be found in ExoPlayer's [top level README][]. [top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md + +## Using the extension ## + +ExoPlayer requests data through `DataSource` instances. These instances are +either instantiated and injected from application code, or obtained from +instances of `DataSource.Factory` that are instantiated and injected from +application code. + +`DefaultDataSource` will automatically use uses the RTMP extension whenever it's +available. Hence if your application is using `DefaultDataSource` or +`DefaultDataSourceFactory`, adding support for RTMP streams is as simple as +adding a dependency to the RTMP extension as described above. No changes to your +application code are required. Alternatively, if you know that your application +doesn't need to handle any other protocols, you can update any `DataSource`s and +`DataSource.Factory` instantiations in your application code to use +`RtmpDataSource` and `RtmpDataSourceFactory` directly. From cf6c247bec92a17e2126b8a47be80cefa8f99e87 Mon Sep 17 00:00:00 2001 From: bachinger Date: Wed, 9 Aug 2017 14:35:38 -0700 Subject: [PATCH 0369/2472] set mediaSession flags properly and keep queue in sync when timeline changes ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164774783 --- .../mediasession/MediaSessionConnector.java | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java index 33807930a5..329d446506 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java @@ -73,6 +73,10 @@ public final class MediaSessionConnector { } public static final String EXTRAS_PITCH = "EXO_PITCH"; + private static final int BASE_MEDIA_SESSION_FLAGS = MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS + | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS; + private static final int EDITOR_MEDIA_SESSION_FLAGS = BASE_MEDIA_SESSION_FLAGS + | MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS; /** * Interface to which playback preparation actions are delegated. @@ -318,7 +322,6 @@ public final class MediaSessionConnector { private Player player; private CustomActionProvider[] customActionProviders; - private int currentWindowIndex; private Map customActionMap; private ErrorMessageProvider errorMessageProvider; private PlaybackPreparer playbackPreparer; @@ -369,8 +372,7 @@ public final class MediaSessionConnector { this.handler = new Handler(Looper.myLooper() != null ? Looper.myLooper() : Looper.getMainLooper()); this.doMaintainMetadata = doMaintainMetadata; - mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS - | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS); + mediaSession.setFlags(BASE_MEDIA_SESSION_FLAGS); mediaController = mediaSession.getController(); mediaSessionCallback = new MediaSessionCallback(); exoPlayerEventListener = new ExoPlayerEventListener(); @@ -433,6 +435,8 @@ public final class MediaSessionConnector { */ public void setQueueEditor(QueueEditor queueEditor) { this.queueEditor = queueEditor; + mediaSession.setFlags(queueEditor == null ? BASE_MEDIA_SESSION_FLAGS + : EDITOR_MEDIA_SESSION_FLAGS); } private void updateMediaSessionPlaybackState() { @@ -583,11 +587,20 @@ public final class MediaSessionConnector { private class ExoPlayerEventListener implements Player.EventListener { + private int currentWindowIndex; + private int currentWindowCount; + @Override public void onTimelineChanged(Timeline timeline, Object manifest) { if (queueNavigator != null) { queueNavigator.onTimelineChanged(player); } + int windowCount = player.getCurrentTimeline().getWindowCount(); + if (currentWindowCount != windowCount) { + // active queue item and queue navigation actions may need to be updated + updateMediaSessionPlaybackState(); + } + currentWindowCount = windowCount; currentWindowIndex = player.getCurrentWindowIndex(); updateMediaSessionMetadata(); } From 99f603c404302c4b0073e0111ba4a1422ad91d5b Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 10 Aug 2017 04:42:27 -0700 Subject: [PATCH 0370/2472] Support multiple video/text/metadata outputs We've seen more than one issue filed where a developer has registered a video listener and been confused by the fact their SimpleExoPlayerView no longer works properly. There are also valid use cases for having multiple metadata/text outputs. Issue: #2933 Issue: #2800 Issue: #2286 Issue: #2240 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164839882 --- .../exoplayer2/demo/PlayerActivity.java | 2 +- .../android/exoplayer2/SimpleExoPlayer.java | 139 ++++++++++++++---- .../exoplayer2/ui/SimpleExoPlayerView.java | 12 +- 3 files changed, 113 insertions(+), 40 deletions(-) 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 9e53dff857..6416cd5aa2 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 @@ -294,9 +294,9 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi player = ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector); player.addListener(this); player.addListener(eventLogger); + player.addMetadataOutput(eventLogger); player.setAudioDebugListener(eventLogger); player.setVideoDebugListener(eventLogger); - player.setMetadataOutput(eventLogger); simpleExoPlayerView.setPlayer(player); player.setPlayWhenReady(shouldAutoPlay); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 3a3768bcc2..1887e0d243 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -41,6 +41,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.video.VideoRendererEventListener; import java.util.List; +import java.util.concurrent.CopyOnWriteArraySet; /** * An {@link ExoPlayer} implementation that uses default {@link Renderer} components. Instances can @@ -87,6 +88,9 @@ public class SimpleExoPlayer implements ExoPlayer { private final ExoPlayer player; private final ComponentListener componentListener; + private final CopyOnWriteArraySet videoListeners; + private final CopyOnWriteArraySet textOutputs; + private final CopyOnWriteArraySet metadataOutputs; private final int videoRendererCount; private final int audioRendererCount; @@ -99,9 +103,6 @@ public class SimpleExoPlayer implements ExoPlayer { private int videoScalingMode; private SurfaceHolder surfaceHolder; private TextureView textureView; - private TextRenderer.Output textOutput; - private MetadataRenderer.Output metadataOutput; - private VideoListener videoListener; private AudioRendererEventListener audioDebugListener; private VideoRendererEventListener videoDebugListener; private DecoderCounters videoDecoderCounters; @@ -113,6 +114,9 @@ public class SimpleExoPlayer implements ExoPlayer { protected SimpleExoPlayer(RenderersFactory renderersFactory, TrackSelector trackSelector, LoadControl loadControl) { componentListener = new ComponentListener(); + videoListeners = new CopyOnWriteArraySet<>(); + textOutputs = new CopyOnWriteArraySet<>(); + metadataOutputs = new CopyOnWriteArraySet<>(); Looper eventLooper = Looper.myLooper() != null ? Looper.myLooper() : Looper.getMainLooper(); Handler eventHandler = new Handler(eventLooper); renderers = renderersFactory.createRenderers(eventHandler, componentListener, componentListener, @@ -440,63 +444,132 @@ public class SimpleExoPlayer implements ExoPlayer { } /** - * Sets a listener to receive video events. + * Adds a listener to receive video events. + * + * @param listener The listener to register. + */ + public void addVideoListener(VideoListener listener) { + videoListeners.add(listener); + } + + /** + * Removes a listener of video events. + * + * @param listener The listener to unregister. + */ + public void removeVideoListener(VideoListener listener) { + videoListeners.remove(listener); + } + + /** + * Sets a listener to receive video events, removing all existing listeners. * * @param listener The listener. + * @deprecated Use {@link #addVideoListener(VideoListener)}. */ + @Deprecated public void setVideoListener(VideoListener listener) { - videoListener = listener; + videoListeners.clear(); + if (listener != null) { + addVideoListener(listener); + } } /** - * Clears the listener receiving video events if it matches the one passed. Else does nothing. + * Equivalent to {@link #removeVideoListener(VideoListener)}. * * @param listener The listener to clear. + * @deprecated Use {@link #removeVideoListener(VideoListener)}. */ + @Deprecated public void clearVideoListener(VideoListener listener) { - if (videoListener == listener) { - videoListener = null; - } + removeVideoListener(listener); } /** - * Sets an output to receive text events. + * Registers an output to receive text events. + * + * @param listener The output to register. + */ + public void addTextOutput(TextRenderer.Output listener) { + textOutputs.add(listener); + } + + /** + * Removes a text output. + * + * @param listener The output to remove. + */ + public void removeTextOutput(TextRenderer.Output listener) { + textOutputs.remove(listener); + } + + /** + * Sets an output to receive text events, removing all existing outputs. * * @param output The output. + * @deprecated Use {@link #addTextOutput(TextRenderer.Output)}. */ + @Deprecated 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; + textOutputs.clear(); + if (output != null) { + addTextOutput(output); } } /** - * Sets a listener to receive metadata events. + * Equivalent to {@link #removeTextOutput(TextRenderer.Output)}. + * + * @param output The output to clear. + * @deprecated Use {@link #removeTextOutput(TextRenderer.Output)}. + */ + @Deprecated + public void clearTextOutput(TextRenderer.Output output) { + removeTextOutput(output); + } + + /** + * Registers an output to receive metadata events. + * + * @param listener The output to register. + */ + public void addMetadataOutput(MetadataRenderer.Output listener) { + metadataOutputs.add(listener); + } + + /** + * Removes a metadata output. + * + * @param listener The output to remove. + */ + public void removeMetadataOutput(MetadataRenderer.Output listener) { + metadataOutputs.remove(listener); + } + + /** + * Sets an output to receive metadata events, removing all existing outputs. * * @param output The output. + * @deprecated Use {@link #addMetadataOutput(MetadataRenderer.Output)}. */ + @Deprecated public void setMetadataOutput(MetadataRenderer.Output output) { - metadataOutput = output; + metadataOutputs.clear(); + if (output != null) { + addMetadataOutput(output); + } } /** - * Clears the output receiving metadata events if it matches the one passed. Else does nothing. + * Equivalent to {@link #removeMetadataOutput(MetadataRenderer.Output)}. * * @param output The output to clear. + * @deprecated Use {@link #removeMetadataOutput(MetadataRenderer.Output)}. */ + @Deprecated public void clearMetadataOutput(MetadataRenderer.Output output) { - if (metadataOutput == output) { - metadataOutput = null; - } + removeMetadataOutput(output); } /** @@ -803,7 +876,7 @@ public class SimpleExoPlayer implements ExoPlayer { @Override public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { - if (videoListener != null) { + for (VideoListener videoListener : videoListeners) { videoListener.onVideoSizeChanged(width, height, unappliedRotationDegrees, pixelWidthHeightRatio); } @@ -815,8 +888,10 @@ public class SimpleExoPlayer implements ExoPlayer { @Override public void onRenderedFirstFrame(Surface surface) { - if (videoListener != null && SimpleExoPlayer.this.surface == surface) { - videoListener.onRenderedFirstFrame(); + if (SimpleExoPlayer.this.surface == surface) { + for (VideoListener videoListener : videoListeners) { + videoListener.onRenderedFirstFrame(); + } } if (videoDebugListener != null) { videoDebugListener.onRenderedFirstFrame(surface); @@ -889,7 +964,7 @@ public class SimpleExoPlayer implements ExoPlayer { @Override public void onCues(List cues) { - if (textOutput != null) { + for (TextRenderer.Output textOutput : textOutputs) { textOutput.onCues(cues); } } @@ -898,7 +973,7 @@ public class SimpleExoPlayer implements ExoPlayer { @Override public void onMetadata(Metadata metadata) { - if (metadataOutput != null) { + for (MetadataRenderer.Output metadataOutput : metadataOutputs) { metadataOutput.onMetadata(metadata); } } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index 17923767d1..b3dc3c7264 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -379,9 +379,7 @@ 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. + * Set the {@link SimpleExoPlayer} to use. *

        * To transition a {@link SimpleExoPlayer} from targeting one view to another, it's recommended to * use {@link #switchTargetView(SimpleExoPlayer, SimpleExoPlayerView, SimpleExoPlayerView)} rather @@ -397,8 +395,8 @@ public final class SimpleExoPlayerView extends FrameLayout { } if (this.player != null) { this.player.removeListener(componentListener); - this.player.clearTextOutput(componentListener); - this.player.clearVideoListener(componentListener); + this.player.removeTextOutput(componentListener); + this.player.removeVideoListener(componentListener); if (surfaceView instanceof TextureView) { this.player.clearVideoTextureView((TextureView) surfaceView); } else if (surfaceView instanceof SurfaceView) { @@ -418,8 +416,8 @@ public final class SimpleExoPlayerView extends FrameLayout { } else if (surfaceView instanceof SurfaceView) { player.setVideoSurfaceView((SurfaceView) surfaceView); } - player.setVideoListener(componentListener); - player.setTextOutput(componentListener); + player.addVideoListener(componentListener); + player.addTextOutput(componentListener); player.addListener(componentListener); maybeShowController(false); updateForCurrentTrackSelections(); From 1f20e6f31ed0a8f4755be4e188632e27076a98fb Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 10 Aug 2017 04:45:40 -0700 Subject: [PATCH 0371/2472] Fix maskingX variables when timeline becomes empty ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164840037 --- .../com/google/android/exoplayer2/ExoPlayerImpl.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index c3a76cd962..f22c08f585 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -448,6 +448,12 @@ import java.util.concurrent.CopyOnWriteArraySet; case ExoPlayerImplInternal.MSG_SEEK_ACK: { if (--pendingSeekAcks == 0) { playbackInfo = (ExoPlayerImplInternal.PlaybackInfo) msg.obj; + if (timeline.isEmpty()) { + // Update the masking variables, which are used when the timeline is empty. + maskingPeriodIndex = 0; + maskingWindowIndex = 0; + maskingWindowPositionMs = 0; + } if (msg.arg1 != 0) { for (Player.EventListener listener : listeners) { listener.onPositionDiscontinuity(); @@ -472,6 +478,12 @@ import java.util.concurrent.CopyOnWriteArraySet; timeline = sourceInfo.timeline; manifest = sourceInfo.manifest; playbackInfo = sourceInfo.playbackInfo; + if (pendingSeekAcks == 0 && timeline.isEmpty()) { + // Update the masking variables, which are used when the timeline is empty. + maskingPeriodIndex = 0; + maskingWindowIndex = 0; + maskingWindowPositionMs = 0; + } for (Player.EventListener listener : listeners) { listener.onTimelineChanged(timeline, manifest); } From b9ead4b7d1a89b6cbcdcf9d50688e9f9e6d1c6fe Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 11 Aug 2017 15:47:03 +0100 Subject: [PATCH 0372/2472] Restrict usage of secure DummySurface for all Samsung devices. --- .../java/com/google/android/exoplayer2/video/DummySurface.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java index e32f23fed7..7a80294929 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java @@ -154,7 +154,7 @@ public final class DummySurface extends Surface { */ private static boolean deviceNeedsSecureDummySurfaceWorkaround(Context context) { return Util.SDK_INT == 24 - && (Util.MODEL.startsWith("SM-G950") || Util.MODEL.startsWith("SM-G955")) + && "samsung".equals(Util.MANUFACTURER) && !hasVrModeHighPerformanceSystemFeatureV24(context.getPackageManager()); } From 44fa0b446ebf1c4008e9e5e1dda1df4fafd4ef13 Mon Sep 17 00:00:00 2001 From: WeiChungChang Date: Thu, 17 Aug 2017 13:49:41 +0800 Subject: [PATCH 0373/2472] Support H262 video in MP4 --- .../google/android/exoplayer2/extractor/mp4/AtomParsers.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/library/core/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 index f7e3e846e9..4645e45ae8 100644 --- a/library/core/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 @@ -1027,6 +1027,10 @@ import java.util.List; case 0xAB: mimeType = MimeTypes.AUDIO_DTS_HD; return Pair.create(mimeType, null); + case 0x60: /* Visual 13818-2 Simple Profile */ + case 0x61: /* Visual 13818-2 Main Profile */ + mimeType = MimeTypes.VIDEO_MPEG2; + break; default: mimeType = null; break; From 85d6c0f0a9ec7159880db6d5f7598409a85e9032 Mon Sep 17 00:00:00 2001 From: Danny Brain Date: Thu, 17 Aug 2017 21:30:46 +1000 Subject: [PATCH 0374/2472] Fix possible subrip timing line NPE --- .../google/android/exoplayer2/text/subrip/SubripDecoder.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/core/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 index e76f0fd7e2..49ebe84d67 100644 --- a/library/core/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 @@ -69,8 +69,8 @@ public final class SubripDecoder extends SimpleSubtitleDecoder { // Read and parse the timing line. boolean haveEndTimecode = false; currentLine = subripData.readLine(); - Matcher matcher = SUBRIP_TIMING_LINE.matcher(currentLine); - if (matcher.matches()) { + Matcher matcher = currentLine == null ? null : SUBRIP_TIMING_LINE.matcher(currentLine); + if (matcher != null && matcher.matches()) { cueTimesUs.add(parseTimecode(matcher, 1)); if (!TextUtils.isEmpty(matcher.group(6))) { haveEndTimecode = true; From 7bde58a46d3c992ba17d8016223d8ba41a42f486 Mon Sep 17 00:00:00 2001 From: Bei Yi Date: Thu, 17 Aug 2017 11:01:42 -0700 Subject: [PATCH 0375/2472] Support crop mode for AspectRatioFrameLayout --- .../exoplayer2/ui/AspectRatioFrameLayout.java | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java index b0df16b484..2f04b8800d 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java @@ -32,7 +32,7 @@ public final class AspectRatioFrameLayout extends FrameLayout { * Resize modes for {@link AspectRatioFrameLayout}. */ @Retention(RetentionPolicy.SOURCE) - @IntDef({RESIZE_MODE_FIT, RESIZE_MODE_FIXED_WIDTH, RESIZE_MODE_FIXED_HEIGHT, RESIZE_MODE_FILL}) + @IntDef({RESIZE_MODE_FIT, RESIZE_MODE_FIXED_WIDTH, RESIZE_MODE_FIXED_HEIGHT, RESIZE_MODE_FILL, RESIZE_MODE_CROP}) public @interface ResizeMode {} /** @@ -51,6 +51,10 @@ public final class AspectRatioFrameLayout extends FrameLayout { * The specified aspect ratio is ignored. */ public static final int RESIZE_MODE_FILL = 3; + /** + * The height or width is increased or decreased to crop and to obtain the desired aspect ratio. + */ + public static final int RESIZE_MODE_CROP = 4; /** * The {@link FrameLayout} will not resize itself if the fractional difference between its natural @@ -96,6 +100,15 @@ public final class AspectRatioFrameLayout extends FrameLayout { } } + /** + * Gets the resize mode. + * + * @return The resize mode. + */ + public int getResizeMode() { + return this.resizeMode; + } + /** * Sets the resize mode. * @@ -132,6 +145,13 @@ public final class AspectRatioFrameLayout extends FrameLayout { case RESIZE_MODE_FIXED_HEIGHT: width = (int) (height * videoAspectRatio); break; + case RESIZE_MODE_CROP: + if (videoAspectRatio > viewAspectRatio) { + width = (int) (height * videoAspectRatio); + } else { + height = (int) (width / videoAspectRatio); + } + break; default: if (aspectDeformation > 0) { height = (int) (width / videoAspectRatio); From e198667251f56c0900f42286d95fbcf308d1ec3f Mon Sep 17 00:00:00 2001 From: mdoucleff Date: Mon, 14 Aug 2017 10:35:37 -0700 Subject: [PATCH 0376/2472] Add flag to CachedContentIndex to disable encryption. This allows the encryption feature to be disabled gracefully: encrypted index files may be read, but plaintext will be written. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=165196508 --- .../upstream/cache/CachedContentIndex.java | 21 +++++++++++++++---- .../upstream/cache/SimpleCache.java | 16 +++++++++++++- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/library/core/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 index 58cc70d68d..809f15b5a6 100644 --- a/library/core/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 @@ -64,6 +64,7 @@ import javax.crypto.spec.SecretKeySpec; private final AtomicFile atomicFile; private final Cipher cipher; private final SecretKeySpec secretKeySpec; + private final boolean encrypt; private boolean changed; private ReusableBufferedOutputStream bufferedOutputStream; @@ -80,10 +81,21 @@ import javax.crypto.spec.SecretKeySpec; * Creates a CachedContentIndex which works on the index file in the given cacheDir. * * @param cacheDir Directory where the index file is kept. - * @param secretKey If not null, cache keys will be stored encrypted on filesystem using AES/CBC. - * The key must be 16 bytes long. + * @param secretKey 16 byte AES key for reading and writing the cache index. */ public CachedContentIndex(File cacheDir, byte[] secretKey) { + this(cacheDir, secretKey, secretKey != null); + } + + /** + * Creates a CachedContentIndex which works on the index file in the given cacheDir. + * + * @param cacheDir Directory where the index file is kept. + * @param secretKey 16 byte AES key for reading, and optionally writing, the cache index. + * @param encrypt When false, a plaintext index will be written. + */ + public CachedContentIndex(File cacheDir, byte[] secretKey, boolean encrypt) { + this.encrypt = encrypt; if (secretKey != null) { Assertions.checkArgument(secretKey.length == 16); try { @@ -288,10 +300,11 @@ import javax.crypto.spec.SecretKeySpec; output = new DataOutputStream(bufferedOutputStream); output.writeInt(VERSION); - int flags = cipher != null ? FLAG_ENCRYPTED_INDEX : 0; + boolean writeEncrypted = encrypt && cipher != null; + int flags = writeEncrypted ? FLAG_ENCRYPTED_INDEX : 0; output.writeInt(flags); - if (cipher != null) { + if (writeEncrypted) { byte[] initializationVector = new byte[16]; new Random().nextBytes(initializationVector); output.write(initializationVector); diff --git a/library/core/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 index 2da6ba759b..15a5673a4e 100644 --- a/library/core/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 @@ -61,10 +61,24 @@ public final class SimpleCache implements Cache { * The key must be 16 bytes long. */ public SimpleCache(File cacheDir, CacheEvictor evictor, byte[] secretKey) { + this(cacheDir, evictor, secretKey, secretKey != null); + } + + /** + * Constructs the cache. The cache will delete any unrecognized files from the directory. Hence + * the directory cannot be used to store other files. + * + * @param cacheDir A dedicated cache directory. + * @param evictor The evictor to be used. + * @param secretKey If not null, cache keys will be stored encrypted on filesystem using AES/CBC. + * The key must be 16 bytes long. + * @param encrypt When false, a plaintext index will be written. + */ + public SimpleCache(File cacheDir, CacheEvictor evictor, byte[] secretKey, boolean encrypt) { this.cacheDir = cacheDir; this.evictor = evictor; this.lockedSpans = new HashMap<>(); - this.index = new CachedContentIndex(cacheDir, secretKey); + this.index = new CachedContentIndex(cacheDir, secretKey, encrypt); this.listeners = new HashMap<>(); // Start cache initialization. final ConditionVariable conditionVariable = new ConditionVariable(); From a2a2acfd6a5ded91bcd37aa15ad0be7ed1fa6d83 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 15 Aug 2017 04:06:29 -0700 Subject: [PATCH 0377/2472] Disable secure dummy surface on all Samsung N devices ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=165291627 --- .../google/android/exoplayer2/video/DummySurface.java | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java index 7a80294929..a45616c6ed 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java @@ -42,7 +42,6 @@ import static android.opengl.GLES20.glGenTextures; import android.annotation.TargetApi; import android.content.Context; -import android.content.pm.PackageManager; import android.graphics.SurfaceTexture; import android.graphics.SurfaceTexture.OnFrameAvailableListener; import android.opengl.EGL14; @@ -152,15 +151,9 @@ public final class DummySurface extends Surface { * * @param context Any {@link Context}. */ + @SuppressWarnings("unused") // Context may be needed in the future for better targeting. private static boolean deviceNeedsSecureDummySurfaceWorkaround(Context context) { - return Util.SDK_INT == 24 - && "samsung".equals(Util.MANUFACTURER) - && !hasVrModeHighPerformanceSystemFeatureV24(context.getPackageManager()); - } - - @TargetApi(24) - private static boolean hasVrModeHighPerformanceSystemFeatureV24(PackageManager packageManager) { - return packageManager.hasSystemFeature(PackageManager.FEATURE_VR_MODE_HIGH_PERFORMANCE); + return Util.SDK_INT == 24 && "samsung".equals(Util.MANUFACTURER); } private static class DummySurfaceThread extends HandlerThread implements OnFrameAvailableListener, From 04131a1ee143c44526238cb5685360ce7a491a39 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 15 Aug 2017 04:14:24 -0700 Subject: [PATCH 0378/2472] Update instructions to include Google Maven repository ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=165291982 --- README.md | 11 +++++++---- extensions/gvr/README.md | 12 +----------- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index d7bc23f700..f4dd9b69ec 100644 --- a/README.md +++ b/README.md @@ -20,18 +20,21 @@ and extend, and can be updated through Play Store application updates. ## Using ExoPlayer ## -ExoPlayer modules can be obtained via jCenter. It's also possible to clone the +ExoPlayer modules can be obtained via JCenter. It's also possible to clone the repository and depend on the modules locally. -### Via jCenter ### +### Via JCenter ### The easiest way to get started using ExoPlayer is to add it as a gradle -dependency. You need to make sure you have the jcenter repository included in -the `build.gradle` file in the root of your project: +dependency. You need to make sure you have the JCenter and Google Maven +repositories included in the `build.gradle` file in the root of your project: ```gradle repositories { jcenter() + maven { + url "https://maven.google.com" + } } ``` diff --git a/extensions/gvr/README.md b/extensions/gvr/README.md index 7e072d070c..4e08ee6387 100644 --- a/extensions/gvr/README.md +++ b/extensions/gvr/README.md @@ -11,17 +11,7 @@ of surround sound and ambisonic soundfields. ## Getting the extension ## -The easiest way to use the extension is to add it as a gradle dependency. You -need to make sure you have the jcenter repository included in the `build.gradle` -file in the root of your project: - -```gradle -repositories { - jcenter() -} -``` - -Next, include the following in your module's `build.gradle` file: +The easiest way to use the extension is to add it as a gradle dependency: ```gradle compile 'com.google.android.exoplayer:extension-gvr:rX.X.X' From 0e16c43f0cd73bc6ea00e680bee5f72aa13bf29f Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 15 Aug 2017 04:45:22 -0700 Subject: [PATCH 0379/2472] Destroy GL context when releasing dummy surface ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=165293386 --- .../exoplayer2/video/DummySurface.java | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java index a45616c6ed..a1820ed7a1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java @@ -34,6 +34,7 @@ import static android.opengl.EGL14.EGL_WINDOW_BIT; import static android.opengl.EGL14.eglChooseConfig; import static android.opengl.EGL14.eglCreateContext; import static android.opengl.EGL14.eglCreatePbufferSurface; +import static android.opengl.EGL14.eglDestroyContext; import static android.opengl.EGL14.eglGetDisplay; import static android.opengl.EGL14.eglInitialize; import static android.opengl.EGL14.eglMakeCurrent; @@ -164,6 +165,8 @@ public final class DummySurface extends Surface { private static final int MSG_RELEASE = 3; private final int[] textureIdHolder; + private EGLContext context; + private EGLDisplay display; private Handler handler; private SurfaceTexture surfaceTexture; @@ -248,7 +251,7 @@ public final class DummySurface extends Surface { } private void initInternal(boolean secure) { - EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY); + display = eglGetDisplay(EGL_DEFAULT_DISPLAY); Assertions.checkState(display != null, "eglGetDisplay failed"); int[] version = new int[2]; @@ -285,8 +288,8 @@ public final class DummySurface extends Surface { EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE}; } - EGLContext context = eglCreateContext(display, config, android.opengl.EGL14.EGL_NO_CONTEXT, - glAttributes, 0); + context = eglCreateContext(display, config, android.opengl.EGL14.EGL_NO_CONTEXT, glAttributes, + 0); Assertions.checkState(context != null, "eglCreateContext failed"); int[] pbufferAttributes; @@ -316,11 +319,18 @@ public final class DummySurface extends Surface { private void releaseInternal() { try { - surfaceTexture.release(); + if (surfaceTexture != null) { + surfaceTexture.release(); + glDeleteTextures(1, textureIdHolder, 0); + } } finally { + if (context != null) { + eglDestroyContext(display, context); + } + display = null; + context = null; surface = null; surfaceTexture = null; - glDeleteTextures(1, textureIdHolder, 0); } } From 94a5e9bc3abd955360c61ada39f293079081894d Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 16 Aug 2017 04:47:51 -0700 Subject: [PATCH 0380/2472] Restore the interrupted flag after blocking operations If the main thread was interrupted during ExoPlayerImplInternal.blockingSendMessage/release, the interrupted flag was immediately set but then wait() was called on the next iteration. wait() would immediately throw InterruptedException, causing the main thread to spin until the blocking operation completed. Instead of resetting the flag immediately, reset it after the blocking operation completes. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=165426493 --- .../android/exoplayer2/ExoPlayerImplInternal.java | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index a789dbc1b2..b8274126b5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -263,13 +263,18 @@ import java.io.IOException; } int messageNumber = customMessagesSent++; handler.obtainMessage(MSG_CUSTOM, messages).sendToTarget(); + boolean wasInterrupted = false; while (customMessagesProcessed <= messageNumber) { try { wait(); } catch (InterruptedException e) { - Thread.currentThread().interrupt(); + wasInterrupted = true; } } + if (wasInterrupted) { + // Restore the interrupted status. + Thread.currentThread().interrupt(); + } } public synchronized void release() { @@ -277,13 +282,18 @@ import java.io.IOException; return; } handler.sendEmptyMessage(MSG_RELEASE); + boolean wasInterrupted = false; while (!released) { try { wait(); } catch (InterruptedException e) { - Thread.currentThread().interrupt(); + wasInterrupted = true; } } + if (wasInterrupted) { + // Restore the interrupted status. + Thread.currentThread().interrupt(); + } internalPlaybackThread.quit(); } From 7e9b1fc8ae793e360b094876175a0fcf3f94ccfe Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 17 Aug 2017 07:48:49 -0700 Subject: [PATCH 0381/2472] Work around issue with Xiaomi JB devices Issue: #3171 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=165577562 --- .../android/exoplayer2/mediacodec/MediaCodecUtil.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java index d3f3dae344..1073e8d9c1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java @@ -288,9 +288,11 @@ public final class MediaCodecUtil { return false; } - // Work around https://github.com/google/ExoPlayer/issues/1528 + // Work around https://github.com/google/ExoPlayer/issues/1528 and + // https://github.com/google/ExoPlayer/issues/3171 if (Util.SDK_INT < 18 && "OMX.MTK.AUDIO.DECODER.AAC".equals(name) - && "a70".equals(Util.DEVICE)) { + && ("a70".equals(Util.DEVICE) + || ("Xiaomi".equals(Util.MANUFACTURER) && Util.DEVICE.startsWith("HM")))) { return false; } From 38bc289e71bc5a2516d6a3ff21076cbf932cf2ea Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 17 Aug 2017 08:14:46 -0700 Subject: [PATCH 0382/2472] Improve FORMAT_UNSUPPORTED_DRM related documentation and logging ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=165580016 --- .../android/exoplayer2/demo/EventLogger.java | 2 ++ .../android/exoplayer2/RendererCapabilities.java | 6 +++--- .../trackselection/MappingTrackSelector.java | 14 ++++++++------ 3 files changed, 13 insertions(+), 9 deletions(-) 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 30dfb5140a..2ea4b5b7cf 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 @@ -431,6 +431,8 @@ import java.util.Locale; return "YES"; case RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES: return "NO_EXCEEDS_CAPABILITIES"; + case RendererCapabilities.FORMAT_UNSUPPORTED_DRM: + return "NO_UNSUPPORTED_DRM"; case RendererCapabilities.FORMAT_UNSUPPORTED_SUBTYPE: return "NO_UNSUPPORTED_TYPE"; case RendererCapabilities.FORMAT_UNSUPPORTED_TYPE: diff --git a/library/core/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java b/library/core/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java index f841a1b8b5..3f1be20cfb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java @@ -24,7 +24,7 @@ public interface RendererCapabilities { /** * A mask to apply to the result of {@link #supportsFormat(Format)} to obtain one of - * {@link #FORMAT_HANDLED}, {@link #FORMAT_EXCEEDS_CAPABILITIES}, + * {@link #FORMAT_HANDLED}, {@link #FORMAT_EXCEEDS_CAPABILITIES}, {@link #FORMAT_UNSUPPORTED_DRM}, * {@link #FORMAT_UNSUPPORTED_SUBTYPE} and {@link #FORMAT_UNSUPPORTED_TYPE}. */ int FORMAT_SUPPORT_MASK = 0b111; @@ -117,8 +117,8 @@ public interface RendererCapabilities { * the bitwise OR of three properties: *

          *
        • The level of support for the format itself. One of {@link #FORMAT_HANDLED}, - * {@link #FORMAT_EXCEEDS_CAPABILITIES}, {@link #FORMAT_UNSUPPORTED_SUBTYPE} and - * {@link #FORMAT_UNSUPPORTED_TYPE}.
        • + * {@link #FORMAT_EXCEEDS_CAPABILITIES}, {@link #FORMAT_UNSUPPORTED_DRM}, + * {@link #FORMAT_UNSUPPORTED_SUBTYPE} and {@link #FORMAT_UNSUPPORTED_TYPE}. *
        • The level of support for adapting from the format to another format of the same mime type. * One of {@link #ADAPTIVE_SEAMLESS}, {@link #ADAPTIVE_NOT_SEAMLESS} and * {@link #ADAPTIVE_NOT_SUPPORTED}.
        • diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java index 45ac9eab6e..d518b5a6be 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java @@ -199,6 +199,7 @@ public abstract class MappingTrackSelector extends TrackSelector { * @param trackIndex The index of the track within the track group. * @return One of {@link RendererCapabilities#FORMAT_HANDLED}, * {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}, + * {@link RendererCapabilities#FORMAT_UNSUPPORTED_DRM}, * {@link RendererCapabilities#FORMAT_UNSUPPORTED_SUBTYPE} and * {@link RendererCapabilities#FORMAT_UNSUPPORTED_TYPE}. */ @@ -214,6 +215,7 @@ public abstract class MappingTrackSelector extends TrackSelector { * Tracks for which {@link #getTrackFormatSupport(int, int, int)} returns * {@link RendererCapabilities#FORMAT_HANDLED} are always considered. * Tracks for which {@link #getTrackFormatSupport(int, int, int)} returns + * {@link RendererCapabilities#FORMAT_UNSUPPORTED_DRM}, * {@link RendererCapabilities#FORMAT_UNSUPPORTED_TYPE} or * {@link RendererCapabilities#FORMAT_UNSUPPORTED_SUBTYPE} are never considered. * Tracks for which {@link #getTrackFormatSupport(int, int, int)} returns @@ -615,12 +617,12 @@ public abstract class MappingTrackSelector extends TrackSelector { /** * Finds the renderer to which the provided {@link TrackGroup} should be mapped. *

          - * A {@link TrackGroup} is mapped to the renderer that reports - * {@link RendererCapabilities#FORMAT_HANDLED} support for one or more of the tracks in the group, - * or {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES} if no such renderer exists, or - * {@link RendererCapabilities#FORMAT_UNSUPPORTED_SUBTYPE} if again no such renderer exists. In - * the case that two or more renderers report the same level of support, the renderer with the - * lowest index is associated. + * A {@link TrackGroup} is mapped to the renderer that reports the highest of (listed in + * decreasing order of support) {@link RendererCapabilities#FORMAT_HANDLED}, + * {@link RendererCapabilities#FORMAT_EXCEEDS_CAPABILITIES}, + * {@link RendererCapabilities#FORMAT_UNSUPPORTED_DRM} and + * {@link RendererCapabilities#FORMAT_UNSUPPORTED_SUBTYPE}. In the case that two or more renderers + * report the same level of support, the renderer with the lowest index is associated. *

          * If all renderers report {@link RendererCapabilities#FORMAT_UNSUPPORTED_TYPE} for all of the * tracks in the group, then {@code renderers.length} is returned to indicate that the group was From d824aa9db712a57da39201829125a677c90dd925 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 17 Aug 2017 14:19:41 -0700 Subject: [PATCH 0383/2472] Improve MediaSource/MediaPeriod documentation ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=165628229 --- .../android/exoplayer2/source/MediaPeriod.java | 5 ++++- .../android/exoplayer2/source/MediaSource.java | 14 +++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java index 7a43dd7562..514b96ae8d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java @@ -16,12 +16,15 @@ package com.google.android.exoplayer2.source; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.trackselection.TrackSelection; import java.io.IOException; /** - * A source of a single period of media. + * Loads media corresponding to a {@link Timeline.Period}, and allows that media to be read. All + * methods are called on the player's internal playback thread, as described in the + * {@link ExoPlayer} Javadoc. */ public interface MediaPeriod extends SequenceableLoader { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java index 790620a80c..11489cfbb8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java @@ -23,7 +23,19 @@ import com.google.android.exoplayer2.upstream.Allocator; import java.io.IOException; /** - * A source of media consisting of one or more {@link MediaPeriod}s. + * Defines and provides media to be played by an {@link ExoPlayer}. A MediaSource has two main + * responsibilities: + *

            + *
          • To provide the player with a {@link Timeline} defining the structure of its media, and to + * provide a new timeline whenever the structure of the media changes. The MediaSource provides + * these timelines by calling {@link Listener#onSourceInfoRefreshed} on the {@link Listener} + * passed to {@link #prepareSource(ExoPlayer, boolean, Listener)}.
          • + *
          • To provide {@link MediaPeriod} instances for the periods in its timeline. MediaPeriods are + * obtained by calling {@link #createPeriod(MediaPeriodId, Allocator)}, and provide a way for the + * player to load and read the media.
          • + *
          + * All methods are called on the player's internal playback thread, as described in the + * {@link ExoPlayer} Javadoc. */ public interface MediaSource { From 51dac5838da0e3466f36136d13fcaabfdcb89460 Mon Sep 17 00:00:00 2001 From: Dulmandakh Date: Wed, 9 Aug 2017 15:42:58 +0900 Subject: [PATCH 0384/2472] expose setPropertyByteArray, setPropertyString export setPropertyByteArray, setPropertyString of DefaultDrmSessionManager for easy customization. --- .../exoplayer2/drm/OfflineLicenseHelper.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java index 040ca50c76..b5927dcd95 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java @@ -185,6 +185,22 @@ public final class OfflineLicenseHelper { } return licenseDurationRemainingSec; } + + public byte[] getPropertyByteArray(String key) { + return drmSessionManager.getPropertyByteArray(key); + } + + public void setPropertyByteArray(String key, byte[] value) { + drmSessionManager.setPropertyByteArray(key, value); + } + + public String getPropertyString(String key) { + return drmSessionManager.getPropertyString(key); + } + + public void setPropertyString(String key, String value) { + drmSessionManager.setPropertyString(key, value); + } private byte[] blockingKeyRequest(@Mode int licenseMode, byte[] offlineLicenseKeySetId, DrmInitData drmInitData) throws DrmSessionException { From 47e3b4dc3e66b7174cdc9e4eb9e25261ab5a63bb Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 17 Aug 2017 23:13:12 +0100 Subject: [PATCH 0385/2472] Minor style tweaks --- .../assets/subrip/typical_unexpected_end | 10 ++++ .../text/subrip/SubripDecoderTest.java | 12 +++++ .../exoplayer2/drm/OfflineLicenseHelper.java | 48 ++++++++++++------- .../exoplayer2/extractor/mp4/AtomParsers.java | 18 +++---- .../exoplayer2/text/subrip/SubripDecoder.java | 9 +++- 5 files changed, 69 insertions(+), 28 deletions(-) create mode 100644 library/core/src/androidTest/assets/subrip/typical_unexpected_end diff --git a/library/core/src/androidTest/assets/subrip/typical_unexpected_end b/library/core/src/androidTest/assets/subrip/typical_unexpected_end new file mode 100644 index 0000000000..8e2949b8db --- /dev/null +++ b/library/core/src/androidTest/assets/subrip/typical_unexpected_end @@ -0,0 +1,10 @@ +1 +00:00:00,000 --> 00:00:01,234 +This is the first subtitle. + +2 +00:00:02,345 --> 00:00:03,456 +This is the second subtitle. +Second subtitle with second line. + +3 \ No newline at end of file diff --git a/library/core/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 index 167499fcdc..744634edda 100644 --- a/library/core/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 @@ -31,6 +31,7 @@ public final class SubripDecoderTest extends InstrumentationTestCase { private static final String TYPICAL_MISSING_TIMECODE = "subrip/typical_missing_timecode"; private static final String TYPICAL_MISSING_SEQUENCE = "subrip/typical_missing_sequence"; private static final String TYPICAL_NEGATIVE_TIMESTAMPS = "subrip/typical_negative_timestamps"; + private static final String TYPICAL_UNEXPECTED_END = "subrip/typical_unexpected_end"; private static final String NO_END_TIMECODES_FILE = "subrip/no_end_timecodes"; public void testDecodeEmpty() throws IOException { @@ -107,6 +108,17 @@ public final class SubripDecoderTest extends InstrumentationTestCase { assertTypicalCue3(subtitle, 0); } + public void testDecodeTypicalUnexpectedEnd() throws IOException { + // Parsing should succeed, parsing the first and second cues only. + SubripDecoder decoder = new SubripDecoder(); + byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_UNEXPECTED_END); + SubripSubtitle subtitle = decoder.decode(bytes, bytes.length, false); + + assertEquals(4, subtitle.getEventTimeCount()); + assertTypicalCue1(subtitle, 0); + assertTypicalCue2(subtitle, 2); + } + public void testDecodeNoEndTimecodes() throws IOException { SubripDecoder decoder = new SubripDecoder(); byte[] bytes = TestUtil.getByteArray(getInstrumentation(), NO_END_TIMECODES_FILE); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java index b5927dcd95..cdeb1bd10b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java @@ -116,9 +116,32 @@ public final class OfflineLicenseHelper { optionalKeyRequestParameters, new Handler(handlerThread.getLooper()), eventListener); } - /** Releases the helper. Should be called when the helper is no longer required. */ - public void release() { - handlerThread.quit(); + /** + * @see DefaultDrmSessionManager#getPropertyByteArray + */ + public synchronized byte[] getPropertyByteArray(String key) { + return drmSessionManager.getPropertyByteArray(key); + } + + /** + * @see DefaultDrmSessionManager#setPropertyByteArray + */ + public synchronized void setPropertyByteArray(String key, byte[] value) { + drmSessionManager.setPropertyByteArray(key, value); + } + + /** + * @see DefaultDrmSessionManager#getPropertyString + */ + public synchronized String getPropertyString(String key) { + return drmSessionManager.getPropertyString(key); + } + + /** + * @see DefaultDrmSessionManager#setPropertyString + */ + public synchronized void setPropertyString(String key, String value) { + drmSessionManager.setPropertyString(key, value); } /** @@ -185,21 +208,12 @@ public final class OfflineLicenseHelper { } return licenseDurationRemainingSec; } - - public byte[] getPropertyByteArray(String key) { - return drmSessionManager.getPropertyByteArray(key); - } - public void setPropertyByteArray(String key, byte[] value) { - drmSessionManager.setPropertyByteArray(key, value); - } - - public String getPropertyString(String key) { - return drmSessionManager.getPropertyString(key); - } - - public void setPropertyString(String key, String value) { - drmSessionManager.setPropertyString(key, value); + /** + * Releases the helper. Should be called when the helper is no longer required. + */ + public void release() { + handlerThread.quit(); } private byte[] blockingKeyRequest(@Mode int licenseMode, byte[] offlineLicenseKeySetId, diff --git a/library/core/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 index 4645e45ae8..9a03311ccf 100644 --- a/library/core/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 @@ -995,9 +995,10 @@ import java.util.List; int objectTypeIndication = parent.readUnsignedByte(); String mimeType; switch (objectTypeIndication) { - case 0x6B: - mimeType = MimeTypes.AUDIO_MPEG; - return Pair.create(mimeType, null); + case 0x60: + case 0x61: + mimeType = MimeTypes.VIDEO_MPEG2; + break; case 0x20: mimeType = MimeTypes.VIDEO_MP4V; break; @@ -1007,6 +1008,9 @@ import java.util.List; case 0x23: mimeType = MimeTypes.VIDEO_H265; break; + case 0x6B: + mimeType = MimeTypes.AUDIO_MPEG; + return Pair.create(mimeType, null); case 0x40: case 0x66: case 0x67: @@ -1027,10 +1031,6 @@ import java.util.List; case 0xAB: mimeType = MimeTypes.AUDIO_DTS_HD; return Pair.create(mimeType, null); - case 0x60: /* Visual 13818-2 Simple Profile */ - case 0x61: /* Visual 13818-2 Main Profile */ - mimeType = MimeTypes.VIDEO_MPEG2; - break; default: mimeType = null; break; @@ -1038,8 +1038,8 @@ import java.util.List; parent.skipBytes(12); - // Start of the AudioSpecificConfig. - parent.skipBytes(1); // AudioSpecificConfig tag + // Start of the DecoderSpecificInfo. + parent.skipBytes(1); // DecoderSpecificInfo tag int initializationDataSize = parseExpandableClassSize(parent); byte[] initializationData = new byte[initializationDataSize]; parent.readBytes(initializationData, 0, initializationDataSize); diff --git a/library/core/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 index 49ebe84d67..6cce902e87 100644 --- a/library/core/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 @@ -69,8 +69,13 @@ public final class SubripDecoder extends SimpleSubtitleDecoder { // Read and parse the timing line. boolean haveEndTimecode = false; currentLine = subripData.readLine(); - Matcher matcher = currentLine == null ? null : SUBRIP_TIMING_LINE.matcher(currentLine); - if (matcher != null && matcher.matches()) { + if (currentLine == null) { + Log.w(TAG, "Unexpected end"); + break; + } + + Matcher matcher = SUBRIP_TIMING_LINE.matcher(currentLine); + if (matcher.matches()) { cueTimesUs.add(parseTimecode(matcher, 1)); if (!TextUtils.isEmpty(matcher.group(6))) { haveEndTimecode = true; From 4c51d386d5864cd7983a36ba683757672635c2d4 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 21 Aug 2017 00:47:34 -0700 Subject: [PATCH 0386/2472] Allow the app to specify extra ad markers Issue: #3184 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=165895259 --- .../exoplayer2/ui/PlaybackControlView.java | 38 ++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java index 6ddbfed973..ce641b8209 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java @@ -22,6 +22,7 @@ import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.drawable.Drawable; import android.os.SystemClock; +import android.support.annotation.Nullable; import android.util.AttributeSet; import android.view.KeyEvent; import android.view.LayoutInflater; @@ -312,6 +313,8 @@ public class PlaybackControlView extends FrameLayout { private long hideAtMs; private long[] adGroupTimesMs; private boolean[] playedAdGroups; + private long[] extraAdGroupTimesMs; + private boolean[] extraPlayedAdGroups; private final Runnable updateProgressAction = new Runnable() { @Override @@ -364,6 +367,8 @@ public class PlaybackControlView extends FrameLayout { formatter = new Formatter(formatBuilder, Locale.getDefault()); adGroupTimesMs = new long[0]; playedAdGroups = new boolean[0]; + extraAdGroupTimesMs = new long[0]; + extraPlayedAdGroups = new boolean[0]; componentListener = new ComponentListener(); controlDispatcher = DEFAULT_CONTROL_DISPATCHER; @@ -462,6 +467,29 @@ public class PlaybackControlView extends FrameLayout { updateTimeBarMode(); } + /** + * Sets the millisecond positions of extra ad markers relative to the start of the window (or + * timeline, if in multi-window mode) and whether each extra ad has been played or not. The + * markers are shown in addition to any ad markers for ads in the player's timeline. + * + * @param extraAdGroupTimesMs The millisecond timestamps of the extra ad markers to show, or + * {@code null} to show no extra ad markers. + * @param extraPlayedAdGroups Whether each ad has been played, or {@code null} to show no extra ad + * markers. + */ + public void setExtraAdGroupMarkers(@Nullable long[] extraAdGroupTimesMs, + @Nullable boolean[] extraPlayedAdGroups) { + if (extraAdGroupTimesMs == null) { + this.extraAdGroupTimesMs = new long[0]; + this.extraPlayedAdGroups = new boolean[0]; + } else { + Assertions.checkArgument(extraAdGroupTimesMs.length == extraPlayedAdGroups.length); + this.extraAdGroupTimesMs = extraAdGroupTimesMs; + this.extraPlayedAdGroups = extraPlayedAdGroups; + } + updateProgress(); + } + /** * Sets the {@link VisibilityListener}. * @@ -768,7 +796,15 @@ public class PlaybackControlView extends FrameLayout { bufferedPosition += player.getBufferedPosition(); } if (timeBar != null) { - timeBar.setAdGroupTimesMs(adGroupTimesMs, playedAdGroups, adGroupCount); + int extraAdGroupCount = extraAdGroupTimesMs.length; + int totalAdGroupCount = adGroupCount + extraAdGroupCount; + if (totalAdGroupCount > adGroupTimesMs.length) { + adGroupTimesMs = Arrays.copyOf(adGroupTimesMs, totalAdGroupCount); + playedAdGroups = Arrays.copyOf(playedAdGroups, totalAdGroupCount); + } + System.arraycopy(extraAdGroupTimesMs, 0, adGroupTimesMs, adGroupCount, extraAdGroupCount); + System.arraycopy(extraPlayedAdGroups, 0, playedAdGroups, adGroupCount, extraAdGroupCount); + timeBar.setAdGroupTimesMs(adGroupTimesMs, playedAdGroups, totalAdGroupCount); } } if (durationView != null) { From 351b8a60d1ec8c89ca22d85e9f99facd89175fe3 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 21 Aug 2017 03:49:38 -0700 Subject: [PATCH 0387/2472] Remove isFirstWindow/isLastWindow from Timeline. These methods are only used in one place, and offer duplicate functionality to checking getNext(Previous)WindowIndex == C.INDEX_UNSET. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=165910258 --- .../google/android/exoplayer2/Timeline.java | 24 ------------------- .../exoplayer2/ui/PlaybackControlView.java | 7 +++--- 2 files changed, 4 insertions(+), 27 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java index 414c0804ad..7d4c1995eb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java @@ -593,30 +593,6 @@ public abstract class Timeline { } } - /** - * Returns whether the given window is the last window of the timeline depending on the - * {@code repeatMode}. - * - * @param windowIndex A window index. - * @param repeatMode A repeat mode. - * @return Whether the window of the given index is the last window of the timeline. - */ - public final boolean isLastWindow(int windowIndex, @Player.RepeatMode int repeatMode) { - return getNextWindowIndex(windowIndex, repeatMode) == C.INDEX_UNSET; - } - - /** - * Returns whether the given window is the first window of the timeline depending on the - * {@code repeatMode}. - * - * @param windowIndex A window index. - * @param repeatMode A repeat mode. - * @return Whether the window of the given index is the first window of the timeline. - */ - public final boolean isFirstWindow(int windowIndex, @Player.RepeatMode int repeatMode) { - return getPreviousWindowIndex(windowIndex, repeatMode) == C.INDEX_UNSET; - } - /** * Populates a {@link Window} with data for the window at the specified index. Does not populate * {@link Window#id}. diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java index ce641b8209..017a200005 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java @@ -675,9 +675,10 @@ public class PlaybackControlView extends FrameLayout { int windowIndex = player.getCurrentWindowIndex(); timeline.getWindow(windowIndex, window); isSeekable = window.isSeekable; - enablePrevious = !timeline.isFirstWindow(windowIndex, player.getRepeatMode()) - || isSeekable || !window.isDynamic; - enableNext = !timeline.isLastWindow(windowIndex, player.getRepeatMode()) || window.isDynamic; + enablePrevious = isSeekable || !window.isDynamic + || timeline.getPreviousWindowIndex(windowIndex, player.getRepeatMode()) != C.INDEX_UNSET; + enableNext = window.isDynamic + || timeline.getNextWindowIndex(windowIndex, player.getRepeatMode()) != C.INDEX_UNSET; if (player.isPlayingAd()) { // Always hide player controls during ads. hide(); From 85bc8a08dbb7aa960ec091d2b8cfb4c3a81991a1 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 21 Aug 2017 06:31:41 -0700 Subject: [PATCH 0388/2472] Fix broken link + minor doc tweak ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=165920927 --- extensions/cronet/README.md | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/extensions/cronet/README.md b/extensions/cronet/README.md index 2287c4c19b..bd19c07ea5 100644 --- a/extensions/cronet/README.md +++ b/extensions/cronet/README.md @@ -4,7 +4,7 @@ The Cronet extension is an [HttpDataSource][] implementation using [Cronet][]. -[HttpDataSource]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer/upstream/HttpDataSource.html +[HttpDataSource]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/upstream/HttpDataSource.html [Cronet]: https://chromium.googlesource.com/chromium/src/+/master/components/cronet?autodive=0%2F%2F ## Build instructions ## @@ -22,12 +22,9 @@ and enable the extension: 1. Copy the content of the downloaded `libs` directory into the `jniLibs` directory of this extension -* In your `settings.gradle` file, add the following line before the line that - applies `core_settings.gradle`: - -```gradle -gradle.ext.exoplayerIncludeCronetExtension = true; -``` +* In your `settings.gradle` file, add + `gradle.ext.exoplayerIncludeCronetExtension = true` before the line that + applies `core_settings.gradle`. [top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md [here]: https://console.cloud.google.com/storage/browser/chromium-cronet/android From cc5cfd46c01afda58b43d0e5b8982ddacbd3a6e8 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 21 Aug 2017 07:26:50 -0700 Subject: [PATCH 0389/2472] Handle size==0 in MP4 atoms Issue: #3191 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=165925148 --- .../mp4/sample_fragmented_zero_size_atom.mp4 | Bin 1903 -> 0 bytes .../mp4/FragmentedMp4ExtractorTest.java | 6 ------ .../exoplayer2/extractor/mp4/Atom.java | 9 +++++++-- .../extractor/mp4/FragmentedMp4Extractor.java | 14 ++++++++++++-- .../extractor/mp4/Mp4Extractor.java | 18 ++++++++++++++++-- .../exoplayer2/extractor/mp4/Sniffer.java | 9 ++++++++- 6 files changed, 43 insertions(+), 13 deletions(-) delete mode 100644 library/core/src/androidTest/assets/mp4/sample_fragmented_zero_size_atom.mp4 diff --git a/library/core/src/androidTest/assets/mp4/sample_fragmented_zero_size_atom.mp4 b/library/core/src/androidTest/assets/mp4/sample_fragmented_zero_size_atom.mp4 deleted file mode 100644 index 3d3c63786ef7f40a9b4307fd17fcdc47f006f350..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1903 zcmb_cO-NKx6h3!;RP4t|4V9wCw3TEk7PhDcl$s#0h$JG&mv1H~=lSHlH$z4XsZ9h$ zl#3QcK~O}tX<>`XBKmKUB`u0t^dl4!Ve7)abMLizjTtSv%=gaEcka3Oo^$UIQ8elG z_oZChA_@>opvlN~HClbmjYOfFmThN=C~alCO-LGZ-LDLy;3u|8$e&cF?VO=_za8@% zGxY#ml_}eFnTiYy3=~rP6c4QO)^m&=xOaIyaxW#hz3?uGR2*x*A(`3j7^qMSPCv>q zqTe!829&5}=ASpy!1=e|<2YK;ZKfTm;ge07iD{i>2W&fT^qT1e$B0@h)tiJ;p0#9B z|CVY^#Vt0S1jq1Tes0D|N45UZ_4cHppLRW0HbMK3aHF8}@aL3{Pz#O}hsxkFBSN`- z-%2hsar;|^NlT~RQPp0^p;xiC@W^fvAzF8W=3k`A!&RMv3c4pgJY9ANGa|0%$%g4% zu-z`LvnYpsz-P0Hm@Yf#6IcGyT)q_f%@y)E`Dei0iL#rrmWfS|6E9yB?@>0CZ+)ml^JI`z9^PL&fGvME0 zC8Xpz)&6&db~#30A7A}nqb`+pJyIx*GtuC%zF$S2!|g8)gIt1~TjX5?PgM81Oi^!n#C56SBX@PEtm==&dPrb;1e7UF58 z+A*qX7T}uIli2!0`%7VSAmc2~fWHip0Zsxqueb)>1+Zr+Z$mx?SUv)d1FwPezykn1 z5U8&>1H1 Date: Tue, 22 Aug 2017 07:41:32 -0700 Subject: [PATCH 0390/2472] Use flavorDimensions for external demo app - This is soon becoming mandatory. - It also looks like future versions of com.android.tools.build are being distributed via Google's Maven repository. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166058299 --- build.gradle | 3 +++ demo/build.gradle | 10 ++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index dbc8a41eb0..d5cc64baa1 100644 --- a/build.gradle +++ b/build.gradle @@ -14,6 +14,9 @@ buildscript { repositories { jcenter() + maven { + url "https://maven.google.com" + } } dependencies { classpath 'com.android.tools.build:gradle:2.3.3' diff --git a/demo/build.gradle b/demo/build.gradle index 7eea25478f..1f8bd670e8 100644 --- a/demo/build.gradle +++ b/demo/build.gradle @@ -39,9 +39,15 @@ android { disable 'MissingTranslation' } + flavorDimensions "extensions" + productFlavors { - noExtensions - withExtensions + noExtensions { + dimension "extensions" + } + withExtensions { + dimension "extensions" + } } } From 9aa4aa8ff5667f6e9bce9a112708f6f91dcaea14 Mon Sep 17 00:00:00 2001 From: Bei Yi Date: Thu, 24 Aug 2017 14:31:33 -0700 Subject: [PATCH 0391/2472] Support aspect ratio fill mode for AspectRatioFrameLayout --- .../android/exoplayer2/ui/AspectRatioFrameLayout.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java index 2f04b8800d..9b93b3a867 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java @@ -32,7 +32,7 @@ public final class AspectRatioFrameLayout extends FrameLayout { * Resize modes for {@link AspectRatioFrameLayout}. */ @Retention(RetentionPolicy.SOURCE) - @IntDef({RESIZE_MODE_FIT, RESIZE_MODE_FIXED_WIDTH, RESIZE_MODE_FIXED_HEIGHT, RESIZE_MODE_FILL, RESIZE_MODE_CROP}) + @IntDef({RESIZE_MODE_FIT, RESIZE_MODE_FIXED_WIDTH, RESIZE_MODE_FIXED_HEIGHT, RESIZE_MODE_FILL, RESIZE_MODE_ASPECT_FILL}) public @interface ResizeMode {} /** @@ -52,9 +52,9 @@ public final class AspectRatioFrameLayout extends FrameLayout { */ public static final int RESIZE_MODE_FILL = 3; /** - * The height or width is increased or decreased to crop and to obtain the desired aspect ratio. + * Either height or width is increased to obtain the desired aspect ratio. */ - public static final int RESIZE_MODE_CROP = 4; + public static final int RESIZE_MODE_ASPECT_FILL = 4; /** * The {@link FrameLayout} will not resize itself if the fractional difference between its natural @@ -145,7 +145,7 @@ public final class AspectRatioFrameLayout extends FrameLayout { case RESIZE_MODE_FIXED_HEIGHT: width = (int) (height * videoAspectRatio); break; - case RESIZE_MODE_CROP: + case RESIZE_MODE_ASPECT_FILL: if (videoAspectRatio > viewAspectRatio) { width = (int) (height * videoAspectRatio); } else { From b80e3af2c978a482bceb7a8cb6fdfe41998ffe12 Mon Sep 17 00:00:00 2001 From: Bei Yi Date: Thu, 24 Aug 2017 16:13:44 -0700 Subject: [PATCH 0392/2472] Support zoom mode for AspectRatioFrameLayout --- .../android/exoplayer2/ui/AspectRatioFrameLayout.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java index 9b93b3a867..3367a46374 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java @@ -32,7 +32,7 @@ public final class AspectRatioFrameLayout extends FrameLayout { * Resize modes for {@link AspectRatioFrameLayout}. */ @Retention(RetentionPolicy.SOURCE) - @IntDef({RESIZE_MODE_FIT, RESIZE_MODE_FIXED_WIDTH, RESIZE_MODE_FIXED_HEIGHT, RESIZE_MODE_FILL, RESIZE_MODE_ASPECT_FILL}) + @IntDef({RESIZE_MODE_FIT, RESIZE_MODE_FIXED_WIDTH, RESIZE_MODE_FIXED_HEIGHT, RESIZE_MODE_FILL, RESIZE_MODE_ZOOM}) public @interface ResizeMode {} /** @@ -54,7 +54,7 @@ public final class AspectRatioFrameLayout extends FrameLayout { /** * Either height or width is increased to obtain the desired aspect ratio. */ - public static final int RESIZE_MODE_ASPECT_FILL = 4; + public static final int RESIZE_MODE_ZOOM = 4; /** * The {@link FrameLayout} will not resize itself if the fractional difference between its natural @@ -145,7 +145,7 @@ public final class AspectRatioFrameLayout extends FrameLayout { case RESIZE_MODE_FIXED_HEIGHT: width = (int) (height * videoAspectRatio); break; - case RESIZE_MODE_ASPECT_FILL: + case RESIZE_MODE_ZOOM: if (videoAspectRatio > viewAspectRatio) { width = (int) (height * videoAspectRatio); } else { From bbb030829524a4efd56cb3898a3e4840714dc3cc Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 23 Aug 2017 06:53:11 -0700 Subject: [PATCH 0393/2472] Allow subclasses to customize the MediaFormat Make getMediaFormat protected so that subclasses can set additional MediaFormat keys. For example, if the decoder output needs to be read back via an ImageReader as YUV data it is necessary to set KEY_COLOR_FORMAT to COLOR_FormatYUV420Flexible. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166195211 --- .../video/MediaCodecVideoRenderer.java | 56 +++++++++++-------- 1 file changed, 34 insertions(+), 22 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index 9a2927cc3f..f82c3800a0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -735,28 +735,6 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { return earlyUs < -30000; } - @SuppressLint("InlinedApi") - private static MediaFormat getMediaFormat(Format format, CodecMaxValues codecMaxValues, - boolean deviceNeedsAutoFrcWorkaround, int tunnelingAudioSessionId) { - MediaFormat frameworkMediaFormat = format.getFrameworkMediaFormatV16(); - // Set the maximum adaptive video dimensions. - frameworkMediaFormat.setInteger(MediaFormat.KEY_MAX_WIDTH, codecMaxValues.width); - frameworkMediaFormat.setInteger(MediaFormat.KEY_MAX_HEIGHT, codecMaxValues.height); - // Set the maximum input size. - if (codecMaxValues.inputSize != Format.NO_VALUE) { - frameworkMediaFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, codecMaxValues.inputSize); - } - // Set FRC workaround. - if (deviceNeedsAutoFrcWorkaround) { - frameworkMediaFormat.setInteger("auto-frc", 0); - } - // Configure tunneling if enabled. - if (tunnelingAudioSessionId != C.AUDIO_SESSION_ID_UNSET) { - configureTunnelingV21(frameworkMediaFormat, tunnelingAudioSessionId); - } - return frameworkMediaFormat; - } - @TargetApi(23) private static void setOutputSurfaceV23(MediaCodec codec, Surface surface) { codec.setOutputSurface(surface); @@ -812,6 +790,40 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { return new CodecMaxValues(maxWidth, maxHeight, maxInputSize); } + /** + * Returns the framework {@link MediaFormat} that should be used to configure the decoder when + * playing media in the specified input format. + * + * @param format The format of input media. + * @param codecMaxValues The codec's maximum supported values. + * @param deviceNeedsAutoFrcWorkaround Whether the device is known to enable frame-rate conversion + * logic that negatively impacts ExoPlayer. + * @param tunnelingAudioSessionId The audio session id to use for tunneling, or + * {@link C#AUDIO_SESSION_ID_UNSET} if tunneling should not be enabled. + * @return The framework {@link MediaFormat} that should be used to configure the decoder. + */ + @SuppressLint("InlinedApi") + protected MediaFormat getMediaFormat(Format format, CodecMaxValues codecMaxValues, + boolean deviceNeedsAutoFrcWorkaround, int tunnelingAudioSessionId) { + MediaFormat frameworkMediaFormat = format.getFrameworkMediaFormatV16(); + // Set the maximum adaptive video dimensions. + frameworkMediaFormat.setInteger(MediaFormat.KEY_MAX_WIDTH, codecMaxValues.width); + frameworkMediaFormat.setInteger(MediaFormat.KEY_MAX_HEIGHT, codecMaxValues.height); + // Set the maximum input size. + if (codecMaxValues.inputSize != Format.NO_VALUE) { + frameworkMediaFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, codecMaxValues.inputSize); + } + // Set FRC workaround. + if (deviceNeedsAutoFrcWorkaround) { + frameworkMediaFormat.setInteger("auto-frc", 0); + } + // Configure tunneling if enabled. + if (tunnelingAudioSessionId != C.AUDIO_SESSION_ID_UNSET) { + configureTunnelingV21(frameworkMediaFormat, tunnelingAudioSessionId); + } + return frameworkMediaFormat; + } + /** * Returns a maximum video size to use when configuring a codec for {@code format} in a way * that will allow possible adaptation to other compatible formats that are expected to have the From 64b1566ad4230dd092e40e80a054288366bae051 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 29 Aug 2017 02:15:08 -0700 Subject: [PATCH 0394/2472] Use Math.abs in Sonic.java ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=166820970 --- .../main/java/com/google/android/exoplayer2/audio/Sonic.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index ef7877ae1e..5c5ac06da3 100644 --- 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 @@ -241,7 +241,7 @@ import java.util.Arrays; 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; + diff += Math.abs(sVal - pVal); } // 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 From 3137fcd1ca590a615fd5bbbac53f55024c160cdd Mon Sep 17 00:00:00 2001 From: Shyri Villar Date: Wed, 30 Aug 2017 16:25:50 +0200 Subject: [PATCH 0395/2472] Add support for new codecs parameter string --- .../java/com/google/android/exoplayer2/util/MimeTypes.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java b/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java index 2d4a1ec96f..1c8bb62a75 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java @@ -184,9 +184,9 @@ public final class MimeTypes { return MimeTypes.VIDEO_H264; } else if (codec.startsWith("hev1") || codec.startsWith("hvc1")) { return MimeTypes.VIDEO_H265; - } else if (codec.startsWith("vp9")) { + } else if (codec.startsWith("vp9") || codec.startsWith("vp09")) { return MimeTypes.VIDEO_VP9; - } else if (codec.startsWith("vp8")) { + } else if (codec.startsWith("vp8") || codec.startsWith("vp08")) { return MimeTypes.VIDEO_VP8; } else if (codec.startsWith("mp4a")) { return MimeTypes.AUDIO_AAC; From 219ad8a73e51e3740b0c6bb03ec8f47e42f2f5ae Mon Sep 17 00:00:00 2001 From: Danny Brain Date: Thu, 31 Aug 2017 14:31:18 +1000 Subject: [PATCH 0396/2472] #3215 Additional secure DummySurface device exclusions --- .../android/exoplayer2/video/DummySurface.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java index a1820ed7a1..8551f2541d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java @@ -43,6 +43,7 @@ import static android.opengl.GLES20.glGenTextures; import android.annotation.TargetApi; import android.content.Context; +import android.content.pm.PackageManager; import android.graphics.SurfaceTexture; import android.graphics.SurfaceTexture.OnFrameAvailableListener; import android.opengl.EGL14; @@ -152,9 +153,16 @@ public final class DummySurface extends Surface { * * @param context Any {@link Context}. */ - @SuppressWarnings("unused") // Context may be needed in the future for better targeting. + @SuppressWarnings("unused") private static boolean deviceNeedsSecureDummySurfaceWorkaround(Context context) { - return Util.SDK_INT == 24 && "samsung".equals(Util.MANUFACTURER); + return (Util.SDK_INT == 24 && "samsung".equals(Util.MANUFACTURER)) + || (Util.SDK_INT >= 24 && Util.SDK_INT < 26 + && !hasVrModeHighPerformanceSystemFeatureV24(context.getPackageManager())); + } + + @TargetApi(24) + private static boolean hasVrModeHighPerformanceSystemFeatureV24(PackageManager packageManager) { + return packageManager.hasSystemFeature(PackageManager.FEATURE_VR_MODE_HIGH_PERFORMANCE); } private static class DummySurfaceThread extends HandlerThread implements OnFrameAvailableListener, From e70df7c22007ff67e8c10e4f387dbc30d5f08d61 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 30 Aug 2017 10:20:37 -0700 Subject: [PATCH 0397/2472] Use UTF-8 everywhere UTF-8 is the default charset on Android so this should be a no-op change, but makes the code portable (in case it runs on another platform). ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167011583 --- .../com/google/android/exoplayer2/util/ParsableByteArray.java | 3 ++- .../src/main/java/com/google/android/exoplayer2/util/Util.java | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java b/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java index 2a907e5955..70cb584085 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.util; +import com.google.android.exoplayer2.C; import java.nio.ByteBuffer; import java.nio.charset.Charset; @@ -428,7 +429,7 @@ public final class ParsableByteArray { * @return The string encoded by the bytes. */ public String readString(int length) { - return readString(length, Charset.defaultCharset()); + return readString(length, Charset.forName(C.UTF8_NAME)); } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java index b958a54244..519919f129 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -253,7 +253,7 @@ public final class Util { * @return The code points encoding using UTF-8. */ public static byte[] getUtf8Bytes(String value) { - return value.getBytes(Charset.defaultCharset()); // UTF-8 is the default on Android. + return value.getBytes(Charset.forName(C.UTF8_NAME)); } /** From 2d4efffd1208a715536fc0907b641c0677571514 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 30 Aug 2017 10:51:37 -0700 Subject: [PATCH 0398/2472] Fix ContentDataSource bytesRemaining calculation The bytesRemaining didn't always take into account any skipped bytes, which meant that reaching the end of the file was not correctly detected in read(). Issue: #3216 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167016672 --- .../upstream/ContentDataSourceTest.java | 27 +++++++++++++++++++ .../upstream/ContentDataSource.java | 10 ++++--- .../android/exoplayer2/testutil/TestUtil.java | 2 +- 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java index 834e7e1374..2b70c83ca5 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/upstream/ContentDataSourceTest.java @@ -23,9 +23,12 @@ import android.database.Cursor; import android.net.Uri; import android.support.annotation.NonNull; import android.test.InstrumentationTestCase; +import android.test.MoreAsserts; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.testutil.TestUtil; import java.io.FileNotFoundException; import java.io.IOException; +import java.util.Arrays; /** * Unit tests for {@link ContentDataSource}. @@ -35,6 +38,9 @@ public final class ContentDataSourceTest extends InstrumentationTestCase { private static final String AUTHORITY = "com.google.android.exoplayer2.core.test"; private static final String DATA_PATH = "binary/1024_incrementing_bytes.mp3"; + private static final int TEST_DATA_OFFSET = 1; + private static final int TEST_DATA_LENGTH = 1023; + public void testReadValidUri() throws Exception { ContentDataSource dataSource = new ContentDataSource(getInstrumentation().getContext()); Uri contentUri = new Uri.Builder() @@ -64,6 +70,27 @@ public final class ContentDataSourceTest extends InstrumentationTestCase { } } + public void testReadFromOffsetToEndOfInput() throws Exception { + ContentDataSource dataSource = new ContentDataSource(getInstrumentation().getContext()); + Uri contentUri = new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority(AUTHORITY) + .path(DATA_PATH).build(); + try { + DataSpec dataSpec = new DataSpec(contentUri, TEST_DATA_OFFSET, C.LENGTH_UNSET, null); + long length = dataSource.open(dataSpec); + assertEquals(TEST_DATA_LENGTH, length); + byte[] expectedData = Arrays.copyOfRange( + TestUtil.getByteArray(getInstrumentation(), DATA_PATH), TEST_DATA_OFFSET, + TEST_DATA_OFFSET + TEST_DATA_LENGTH); + byte[] readData = TestUtil.readToEnd(dataSource); + MoreAsserts.assertEquals(expectedData, readData); + assertEquals(C.RESULT_END_OF_INPUT, dataSource.read(new byte[1], 0, 1)); + } finally { + dataSource.close(); + } + } + /** * A {@link ContentProvider} for the test. */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java index d118b91378..c37599eccc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ContentDataSource.java @@ -76,8 +76,8 @@ public final class ContentDataSource implements DataSource { throw new FileNotFoundException("Could not open file descriptor for: " + uri); } inputStream = new FileInputStream(assetFileDescriptor.getFileDescriptor()); - long assertStartOffset = assetFileDescriptor.getStartOffset(); - long skipped = inputStream.skip(assertStartOffset + dataSpec.position) - assertStartOffset; + long assetStartOffset = assetFileDescriptor.getStartOffset(); + long skipped = inputStream.skip(assetStartOffset + dataSpec.position) - assetStartOffset; if (skipped != dataSpec.position) { // We expect the skip to be satisfied in full. If it isn't then we're probably trying to // skip beyond the end of the data. @@ -86,8 +86,8 @@ public final class ContentDataSource implements DataSource { if (dataSpec.length != C.LENGTH_UNSET) { bytesRemaining = dataSpec.length; } else { - bytesRemaining = assetFileDescriptor.getLength(); - if (bytesRemaining == AssetFileDescriptor.UNKNOWN_LENGTH) { + long assetFileDescriptorLength = assetFileDescriptor.getLength(); + if (assetFileDescriptorLength == AssetFileDescriptor.UNKNOWN_LENGTH) { // The asset must extend to the end of the file. bytesRemaining = inputStream.available(); if (bytesRemaining == 0) { @@ -96,6 +96,8 @@ public final class ContentDataSource implements DataSource { // case, so treat as unbounded. bytesRemaining = C.LENGTH_UNSET; } + } else { + bytesRemaining = assetFileDescriptorLength - skipped; } } } catch (IOException e) { diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java index 5819a4b711..2e59b33c0b 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java @@ -181,7 +181,7 @@ public class TestUtil { byte[] expectedData) throws IOException { try { long length = dataSource.open(dataSpec); - Assert.assertEquals(length, expectedData.length); + Assert.assertEquals(expectedData.length, length); byte[] readData = TestUtil.readToEnd(dataSource); MoreAsserts.assertEquals(expectedData, readData); } finally { From d9427750682dbca40a6d41170eed9379e8a29fba Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 31 Aug 2017 04:13:13 -0700 Subject: [PATCH 0399/2472] Allow more aggressive switching for HLS with independent segments We currently switch without downloading overlapping segments, but we do not actually switch more aggressively. This change fixes this. Note there's an implicit assumption made that if one media playlist declares independent segments, the others will too. This is almost certainly true in practice, and if it's not the penalty isn't too bad (the player may try and switch to a higher quality variant one segment's worth of buffer too soon). ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167120992 --- .../exoplayer2/source/hls/HlsChunkSource.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/library/hls/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 index bca62ed230..7173d0d6d5 100644 --- a/library/hls/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 @@ -92,6 +92,7 @@ import java.util.List; private byte[] scratchSpace; private IOException fatalError; private HlsUrl expectedPlaylistUrl; + private boolean independentSegments; private Uri encryptionKeyUri; private byte[] encryptionKey; @@ -206,10 +207,11 @@ import java.util.List; int oldVariantIndex = previous == null ? C.INDEX_UNSET : trackGroup.indexOf(previous.trackFormat); expectedPlaylistUrl = null; - // Use start time of the previous chunk rather than its end time because switching format will - // require downloading overlapping segments. - long bufferedDurationUs = previous == null ? 0 - : Math.max(0, previous.startTimeUs - playbackPositionUs); + // Unless segments are known to be independent, switching variant will require downloading + // overlapping segments. Hence we use the start time of the previous chunk rather than its end + // time for this case. + long bufferedDurationUs = previous == null ? 0 : Math.max(0, + (independentSegments ? previous.endTimeUs : previous.startTimeUs) - playbackPositionUs); // Select the variant. trackSelection.updateSelectedTrack(bufferedDurationUs); @@ -224,12 +226,13 @@ import java.util.List; return; } HlsMediaPlaylist mediaPlaylist = playlistTracker.getPlaylistSnapshot(selectedUrl); + independentSegments = mediaPlaylist.hasIndependentSegmentsTag; // Select the chunk. int chunkMediaSequence; if (previous == null || switchingVariant) { long targetPositionUs = previous == null ? playbackPositionUs - : mediaPlaylist.hasIndependentSegmentsTag ? previous.endTimeUs : previous.startTimeUs; + : independentSegments ? previous.endTimeUs : previous.startTimeUs; if (!mediaPlaylist.hasEndTag && targetPositionUs >= mediaPlaylist.getEndTimeUs()) { // If the playlist is too old to contain the chunk, we need to refresh it. chunkMediaSequence = mediaPlaylist.mediaSequence + mediaPlaylist.segments.size(); From acd16d6eea178f21af897039c4086d9600652af2 Mon Sep 17 00:00:00 2001 From: anjalibh Date: Fri, 1 Sep 2017 18:02:20 -0700 Subject: [PATCH 0400/2472] HDR 10 bits: Use a separate sampler for U and V dithering. Using the same sampler introduced some minor horizontal scratches. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167347995 --- extensions/vp9/src/main/jni/vpx_jni.cc | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/extensions/vp9/src/main/jni/vpx_jni.cc b/extensions/vp9/src/main/jni/vpx_jni.cc index d02d524713..f0b93b1dc2 100644 --- a/extensions/vp9/src/main/jni/vpx_jni.cc +++ b/extensions/vp9/src/main/jni/vpx_jni.cc @@ -197,12 +197,12 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) { const int32_t uvHeight = (img->d_h + 1) / 2; const uint64_t yLength = img->stride[VPX_PLANE_Y] * img->d_h; const uint64_t uvLength = img->stride[VPX_PLANE_U] * uvHeight; - int sample = 0; if (img->fmt == VPX_IMG_FMT_I42016) { // HBD planar 420. // Note: The stride for BT2020 is twice of what we use so this is wasting // memory. The long term goal however is to upload half-float/short so // it's not important to optimize the stride at this time. // Y + int sampleY = 0; for (int y = 0; y < img->d_h; y++) { const uint16_t* srcBase = reinterpret_cast( img->planes[VPX_PLANE_Y] + img->stride[VPX_PLANE_Y] * y); @@ -210,12 +210,14 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) { for (int x = 0; x < img->d_w; x++) { // Lightweight dither. Carryover the remainder of each 10->8 bit // conversion to the next pixel. - sample += *srcBase++; - *destBase++ = sample >> 2; - sample = sample & 3; // Remainder. + sampleY += *srcBase++; + *destBase++ = sampleY >> 2; + sampleY = sampleY & 3; // Remainder. } } // UV + int sampleU = 0; + int sampleV = 0; const int32_t uvWidth = (img->d_w + 1) / 2; for (int y = 0; y < uvHeight; y++) { const uint16_t* srcUBase = reinterpret_cast( @@ -228,11 +230,12 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) { for (int x = 0; x < uvWidth; x++) { // Lightweight dither. Carryover the remainder of each 10->8 bit // conversion to the next pixel. - sample += *srcUBase++; - *destUBase++ = sample >> 2; - sample = (*srcVBase++) + (sample & 3); // srcV + previousRemainder. - *destVBase++ = sample >> 2; - sample = sample & 3; // Remainder. + sampleU += *srcUBase++; + *destUBase++ = sampleU >> 2; + sampleU = sampleU & 3; // Remainder. + sampleV += *srcVBase++; + *destVBase++ = sampleV >> 2; + sampleV = sampleV & 3; // Remainder. } } } else { From 5cb78fa4c7b293b8c2d8cb32065bee9e7d8d21ef Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 4 Sep 2017 02:11:00 -0700 Subject: [PATCH 0401/2472] Fix typo ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167474040 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f4dd9b69ec..bec434e802 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,7 @@ depend on them as you would on any other local module, for example: ```gradle compile project(':exoplayer-library-core') compile project(':exoplayer-library-dash') -compile project(':exoplayer-library-ui) +compile project(':exoplayer-library-ui') ``` ## Developing ExoPlayer ## From 92185aa33c1e681a679479dc80cf1f1d4fa2b607 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 4 Sep 2017 11:34:57 +0100 Subject: [PATCH 0402/2472] Minor cleanup to AspectRatioFrameLayout --- .../exoplayer2/ui/AspectRatioFrameLayout.java | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java index 3367a46374..037519b7a4 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/AspectRatioFrameLayout.java @@ -32,7 +32,8 @@ public final class AspectRatioFrameLayout extends FrameLayout { * Resize modes for {@link AspectRatioFrameLayout}. */ @Retention(RetentionPolicy.SOURCE) - @IntDef({RESIZE_MODE_FIT, RESIZE_MODE_FIXED_WIDTH, RESIZE_MODE_FIXED_HEIGHT, RESIZE_MODE_FILL, RESIZE_MODE_ZOOM}) + @IntDef({RESIZE_MODE_FIT, RESIZE_MODE_FIXED_WIDTH, RESIZE_MODE_FIXED_HEIGHT, RESIZE_MODE_FILL, + RESIZE_MODE_ZOOM}) public @interface ResizeMode {} /** @@ -52,7 +53,7 @@ public final class AspectRatioFrameLayout extends FrameLayout { */ public static final int RESIZE_MODE_FILL = 3; /** - * Either height or width is increased to obtain the desired aspect ratio. + * Either the width or height is increased to obtain the desired aspect ratio. */ public static final int RESIZE_MODE_ZOOM = 4; @@ -89,7 +90,7 @@ public final class AspectRatioFrameLayout extends FrameLayout { } /** - * Set the aspect ratio that this view should satisfy. + * Sets the aspect ratio that this view should satisfy. * * @param widthHeightRatio The width to height ratio. */ @@ -101,12 +102,10 @@ public final class AspectRatioFrameLayout extends FrameLayout { } /** - * Gets the resize mode. - * - * @return The resize mode. + * Returns the resize mode. */ - public int getResizeMode() { - return this.resizeMode; + public @ResizeMode int getResizeMode() { + return resizeMode; } /** @@ -146,7 +145,7 @@ public final class AspectRatioFrameLayout extends FrameLayout { width = (int) (height * videoAspectRatio); break; case RESIZE_MODE_ZOOM: - if (videoAspectRatio > viewAspectRatio) { + if (aspectDeformation > 0) { width = (int) (height * videoAspectRatio); } else { height = (int) (width / videoAspectRatio); From 65b14baab879fa27ceb2dec5273c9d37ee4c0557 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 4 Sep 2017 05:32:54 -0700 Subject: [PATCH 0403/2472] Update moe eqiuvalence ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167488837 --- .../android/exoplayer2/video/DummySurface.java | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java index 8551f2541d..a1820ed7a1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java @@ -43,7 +43,6 @@ import static android.opengl.GLES20.glGenTextures; import android.annotation.TargetApi; import android.content.Context; -import android.content.pm.PackageManager; import android.graphics.SurfaceTexture; import android.graphics.SurfaceTexture.OnFrameAvailableListener; import android.opengl.EGL14; @@ -153,16 +152,9 @@ public final class DummySurface extends Surface { * * @param context Any {@link Context}. */ - @SuppressWarnings("unused") + @SuppressWarnings("unused") // Context may be needed in the future for better targeting. private static boolean deviceNeedsSecureDummySurfaceWorkaround(Context context) { - return (Util.SDK_INT == 24 && "samsung".equals(Util.MANUFACTURER)) - || (Util.SDK_INT >= 24 && Util.SDK_INT < 26 - && !hasVrModeHighPerformanceSystemFeatureV24(context.getPackageManager())); - } - - @TargetApi(24) - private static boolean hasVrModeHighPerformanceSystemFeatureV24(PackageManager packageManager) { - return packageManager.hasSystemFeature(PackageManager.FEATURE_VR_MODE_HIGH_PERFORMANCE); + return Util.SDK_INT == 24 && "samsung".equals(Util.MANUFACTURER); } private static class DummySurfaceThread extends HandlerThread implements OnFrameAvailableListener, From a73601f36f68b867b794b944cdb7da0ccd2ffe7e Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 4 Sep 2017 06:47:03 -0700 Subject: [PATCH 0404/2472] Allow EXIF tracks to be exposed ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167493800 --- .../main/java/com/google/android/exoplayer2/util/MimeTypes.java | 1 + 1 file changed, 1 insertion(+) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java b/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java index 1c8bb62a75..2daf16d3d2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java @@ -85,6 +85,7 @@ public final class MimeTypes { 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"; + public static final String APPLICATION_EXIF = BASE_TYPE_APPLICATION + "/x-exif"; private MimeTypes() {} From 3d38e62205e2396864c5f4c45943377b1a38fdcb Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 4 Sep 2017 07:28:24 -0700 Subject: [PATCH 0405/2472] Adding missing license header in IMA build.gradle ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167496569 --- extensions/ima/build.gradle | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/extensions/ima/build.gradle b/extensions/ima/build.gradle index a4ead9e01f..7b193cc8fc 100644 --- a/extensions/ima/build.gradle +++ b/extensions/ima/build.gradle @@ -1,3 +1,16 @@ +// 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 from: '../../constants.gradle' apply plugin: 'com.android.library' @@ -6,7 +19,7 @@ android { buildToolsVersion project.ext.buildToolsVersion defaultConfig { - minSdkVersion 14 + minSdkVersion project.ext.minSdkVersion targetSdkVersion project.ext.targetSdkVersion testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } From 5ceccac63d15ba9ac9b6d8189ae95f71a0f6b6ac Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 4 Sep 2017 08:55:20 -0700 Subject: [PATCH 0406/2472] Additional secure DummySurface device exclusions Merge: https://github.com/google/ExoPlayer/pull/3225 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167502127 --- .../google/android/exoplayer2/video/DummySurface.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java index a1820ed7a1..d623ea33ea 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java @@ -43,6 +43,7 @@ import static android.opengl.GLES20.glGenTextures; import android.annotation.TargetApi; import android.content.Context; +import android.content.pm.PackageManager; import android.graphics.SurfaceTexture; import android.graphics.SurfaceTexture.OnFrameAvailableListener; import android.opengl.EGL14; @@ -152,9 +153,15 @@ public final class DummySurface extends Surface { * * @param context Any {@link Context}. */ - @SuppressWarnings("unused") // Context may be needed in the future for better targeting. private static boolean deviceNeedsSecureDummySurfaceWorkaround(Context context) { - return Util.SDK_INT == 24 && "samsung".equals(Util.MANUFACTURER); + return (Util.SDK_INT == 24 && "samsung".equals(Util.MANUFACTURER)) + || (Util.SDK_INT < 26 + && !hasVrModeHighPerformanceSystemFeatureV24(context.getPackageManager())); + } + + @TargetApi(24) + private static boolean hasVrModeHighPerformanceSystemFeatureV24(PackageManager packageManager) { + return packageManager.hasSystemFeature(PackageManager.FEATURE_VR_MODE_HIGH_PERFORMANCE); } private static class DummySurfaceThread extends HandlerThread implements OnFrameAvailableListener, From 61ee1f6a27447ca89e3d6a12250d579d13f9a419 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 4 Sep 2017 09:26:59 -0700 Subject: [PATCH 0407/2472] Be robust against unexpected EOS in WebvttCueParser Issue: #3228 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167504122 --- .../text/webvtt/WebvttCueParser.java | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/library/core/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 index 30c9c8737e..54af4dbf63 100644 --- a/library/core/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 @@ -21,6 +21,7 @@ import android.text.Layout.Alignment; import android.text.Spannable; import android.text.SpannableStringBuilder; import android.text.Spanned; +import android.text.TextUtils; import android.text.style.AbsoluteSizeSpan; import android.text.style.AlignmentSpan; import android.text.style.BackgroundColorSpan; @@ -92,19 +93,24 @@ import java.util.regex.Pattern; /* package */ boolean parseCue(ParsableByteArray webvttData, WebvttCue.Builder builder, List styles) { String firstLine = webvttData.readLine(); + if (firstLine == null) { + return false; + } Matcher cueHeaderMatcher = WebvttCueParser.CUE_HEADER_PATTERN.matcher(firstLine); if (cueHeaderMatcher.matches()) { // We have found the timestamps in the first line. No id present. return parseCue(null, cueHeaderMatcher, webvttData, builder, textBuilder, styles); - } else { - // The first line is not the timestamps, but could be the cue id. - String secondLine = webvttData.readLine(); - cueHeaderMatcher = WebvttCueParser.CUE_HEADER_PATTERN.matcher(secondLine); - if (cueHeaderMatcher.matches()) { - // We can do the rest of the parsing, including the id. - return parseCue(firstLine.trim(), cueHeaderMatcher, webvttData, builder, textBuilder, - styles); - } + } + // The first line is not the timestamps, but could be the cue id. + String secondLine = webvttData.readLine(); + if (secondLine == null) { + return false; + } + cueHeaderMatcher = WebvttCueParser.CUE_HEADER_PATTERN.matcher(secondLine); + if (cueHeaderMatcher.matches()) { + // We can do the rest of the parsing, including the id. + return parseCue(firstLine.trim(), cueHeaderMatcher, webvttData, builder, textBuilder, + styles); } return false; } @@ -233,7 +239,7 @@ import java.util.regex.Pattern; // Parse the cue text. textBuilder.setLength(0); String line; - while ((line = webvttData.readLine()) != null && !line.isEmpty()) { + while (!TextUtils.isEmpty(line = webvttData.readLine())) { if (textBuilder.length() > 0) { textBuilder.append("\n"); } From 8676c4a0f42939d96c5462651f5b2f30a4d9104b Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 4 Sep 2017 09:57:56 -0700 Subject: [PATCH 0408/2472] Rewrite logic for enabling secure DummySurface Issue: #3215 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167505797 --- .../exoplayer2/video/DummySurface.java | 35 ++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java index d623ea33ea..20fe862dd2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java @@ -90,12 +90,7 @@ public final class DummySurface extends Surface { */ public static synchronized boolean isSecureSupported(Context context) { if (!secureSupportedInitialized) { - if (Util.SDK_INT >= 17) { - EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY); - String extensions = EGL14.eglQueryString(display, EGL10.EGL_EXTENSIONS); - secureSupported = extensions != null && extensions.contains("EGL_EXT_protected_content") - && !deviceNeedsSecureDummySurfaceWorkaround(context); - } + secureSupported = Util.SDK_INT >= 24 && enableSecureDummySurfaceV24(context); secureSupportedInitialized = true; } return secureSupported; @@ -148,20 +143,28 @@ public final class DummySurface extends Surface { } /** - * Returns whether the device is known to advertise secure surface textures but not implement them - * correctly. + * Returns whether use of secure dummy surfaces should be enabled. * * @param context Any {@link Context}. */ - private static boolean deviceNeedsSecureDummySurfaceWorkaround(Context context) { - return (Util.SDK_INT == 24 && "samsung".equals(Util.MANUFACTURER)) - || (Util.SDK_INT < 26 - && !hasVrModeHighPerformanceSystemFeatureV24(context.getPackageManager())); - } - @TargetApi(24) - private static boolean hasVrModeHighPerformanceSystemFeatureV24(PackageManager packageManager) { - return packageManager.hasSystemFeature(PackageManager.FEATURE_VR_MODE_HIGH_PERFORMANCE); + private static boolean enableSecureDummySurfaceV24(Context context) { + EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY); + String eglExtensions = EGL14.eglQueryString(display, EGL10.EGL_EXTENSIONS); + if (eglExtensions == null || !eglExtensions.contains("EGL_EXT_protected_content")) { + // EGL_EXT_protected_content is required to enable secure dummy surfaces. + return false; + } + if (Util.SDK_INT == 24 && "samsung".equals(Util.MANUFACTURER)) { + // Samsung devices running API level 24 are known to be broken [Internal: b/37197802]. + return false; + } + if (Util.SDK_INT < 26 && !context.getPackageManager().hasSystemFeature( + PackageManager.FEATURE_VR_MODE_HIGH_PERFORMANCE)) { + // Pre API level 26 devices were not well tested unless they supported VR mode. + return false; + } + return true; } private static class DummySurfaceThread extends HandlerThread implements OnFrameAvailableListener, From 2853bf8575ba3a0cc025d43b660f69cbac7394bf Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 5 Sep 2017 08:03:07 -0700 Subject: [PATCH 0409/2472] Upgrade gradle plugin / wrapper ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167579719 --- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index d5cc64baa1..8ec24a6e82 100644 --- a/build.gradle +++ b/build.gradle @@ -19,7 +19,7 @@ buildscript { } } dependencies { - classpath 'com.android.tools.build:gradle:2.3.3' + classpath 'com.android.tools.build:gradle:3.0.0-beta4' classpath 'com.novoda:bintray-release:0.5.0' } // Workaround for the following test coverage issue. Remove when fixed: diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index fc42154505..32ec7e3327 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Wed Jul 12 10:31:13 BST 2017 +#Tue Sep 05 13:43:42 BST 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.0.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip From d187d294e517b7025fc5a263d5e5dc15a95b0f52 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 5 Sep 2017 08:17:44 -0700 Subject: [PATCH 0410/2472] Don't use MediaCodec.setOutputSurface on Nexus 7 with qcom decoder Issue: #3236 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167581198 --- .../video/MediaCodecVideoRenderer.java | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index f82c3800a0..9d769b2050 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -77,6 +77,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { private Format[] streamFormats; private CodecMaxValues codecMaxValues; + private boolean codecNeedsSetOutputSurfaceWorkaround; private Surface surface; private Surface dummySurface; @@ -354,7 +355,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { int state = getState(); if (state == STATE_ENABLED || state == STATE_STARTED) { MediaCodec codec = getCodec(); - if (Util.SDK_INT >= 23 && codec != null && surface != null) { + if (Util.SDK_INT >= 23 && codec != null && surface != null + && !codecNeedsSetOutputSurfaceWorkaround) { setOutputSurfaceV23(codec, surface); } else { releaseCodec(); @@ -425,6 +427,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { protected void onCodecInitialized(String name, long initializedTimestampMs, long initializationDurationMs) { eventDispatcher.decoderInitialized(name, initializedTimestampMs, initializationDurationMs); + codecNeedsSetOutputSurfaceWorkaround = codecNeedsSetOutputSurfaceWorkaround(name); } @Override @@ -963,6 +966,18 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { return Util.SDK_INT <= 22 && "foster".equals(Util.DEVICE) && "NVIDIA".equals(Util.MANUFACTURER); } + /** + * Returns whether the device is known to implement {@link MediaCodec#setOutputSurface(Surface)} + * incorrectly. + *

          + * If true is returned then we fall back to releasing and re-instantiating the codec instead. + */ + private static boolean codecNeedsSetOutputSurfaceWorkaround(String name) { + // Work around https://github.com/google/ExoPlayer/issues/3236 + return ("deb".equals(Util.DEVICE) || "flo".equals(Util.DEVICE)) + && "OMX.qcom.video.decoder.avc".equals(name); + } + /** * Returns whether a codec with suitable {@link CodecMaxValues} will support adaptation between * two {@link Format}s. From 8719d41e34af4c9c1bfeb7a5301e5226b3d12eaf Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 6 Sep 2017 03:51:04 -0700 Subject: [PATCH 0411/2472] Fix position reporting during ads when period has non-zero window offset. Reporting incorrect positions for ad playbacks was causing IMA to think the ad wasn't playing, when in fact it was. Issue: #3180 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167702032 --- .../exoplayer2/ext/ima/ImaAdsLoader.java | 4 +++- .../android/exoplayer2/ExoPlayerImpl.java | 17 ++++++++++++----- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index 8c4fb4c51c..11aab906e0 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -430,7 +430,9 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, } else if (!playingAd) { return VideoProgressUpdate.VIDEO_TIME_NOT_READY; } else { - return new VideoProgressUpdate(player.getCurrentPosition(), player.getDuration()); + long adDuration = player.getDuration(); + return adDuration == C.TIME_UNSET ? VideoProgressUpdate.VIDEO_TIME_NOT_READY + : new VideoProgressUpdate(player.getCurrentPosition(), adDuration); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index f22c08f585..b53cce1f74 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -319,8 +319,7 @@ import java.util.concurrent.CopyOnWriteArraySet; if (timeline.isEmpty() || pendingSeekAcks > 0) { return maskingWindowPositionMs; } else { - timeline.getPeriod(playbackInfo.periodId.periodIndex, period); - return period.getPositionInWindowMs() + C.usToMs(playbackInfo.positionUs); + return playbackInfoPositionUsToWindowPositionMs(playbackInfo.positionUs); } } @@ -330,8 +329,7 @@ import java.util.concurrent.CopyOnWriteArraySet; if (timeline.isEmpty() || pendingSeekAcks > 0) { return maskingWindowPositionMs; } else { - timeline.getPeriod(playbackInfo.periodId.periodIndex, period); - return period.getPositionInWindowMs() + C.usToMs(playbackInfo.bufferedPositionUs); + return playbackInfoPositionUsToWindowPositionMs(playbackInfo.bufferedPositionUs); } } @@ -358,7 +356,7 @@ import java.util.concurrent.CopyOnWriteArraySet; @Override public boolean isPlayingAd() { - return pendingSeekAcks == 0 && playbackInfo.periodId.adGroupIndex != C.INDEX_UNSET; + return pendingSeekAcks == 0 && playbackInfo.periodId.isAd(); } @Override @@ -512,4 +510,13 @@ import java.util.concurrent.CopyOnWriteArraySet; } } + private long playbackInfoPositionUsToWindowPositionMs(long positionUs) { + long positionMs = C.usToMs(positionUs); + if (!playbackInfo.periodId.isAd()) { + timeline.getPeriod(playbackInfo.periodId.periodIndex, period); + positionMs += period.getPositionInWindowMs(); + } + return positionMs; + } + } From 613950143ce1f891a8803a237badbaa77248b6b5 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 6 Sep 2017 05:40:33 -0700 Subject: [PATCH 0412/2472] Workaround for SurfaceView not being hidden properly This appears to be fixed in Oreo, but given how harmless the workaround is we can probably just apply it on all API levels to be sure. Issue: #3160 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167709070 --- .../android/exoplayer2/ui/SimpleExoPlayerView.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index b3dc3c7264..70526ff655 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -427,6 +427,15 @@ public final class SimpleExoPlayerView extends FrameLayout { } } + @Override + public void setVisibility(int visibility) { + super.setVisibility(visibility); + if (surfaceView instanceof SurfaceView) { + // Work around https://github.com/google/ExoPlayer/issues/3160 + surfaceView.setVisibility(visibility); + } + } + /** * Sets the resize mode. * From cdcdea2f98da3f1bfe5d18b4edb14f2437acfba7 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 6 Sep 2017 06:17:09 -0700 Subject: [PATCH 0413/2472] DecryptionException cleanup + add missing header ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167711928 --- .../exoplayer2/drm/DecryptionException.java | 33 ++++++++++++++----- .../android/exoplayer2/drm/DrmSession.java | 4 ++- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DecryptionException.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DecryptionException.java index 6916b972b2..81cfc26393 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DecryptionException.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DecryptionException.java @@ -1,20 +1,37 @@ +/* + * 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.drm; /** - * An exception when doing drm decryption using the In-App Drm + * Thrown when a non-platform component fails to decrypt data. */ public class DecryptionException extends Exception { - private final int errorCode; + /** + * A component specific error code. + */ + public final int errorCode; + + /** + * @param errorCode A component specific error code. + * @param message The detail message. + */ public DecryptionException(int errorCode, String message) { super(message); this.errorCode = errorCode; } - /** - * Get error code - */ - public int getErrorCode() { - return errorCode; - } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java index 0c17b102fd..a3ae1d8b71 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java @@ -28,7 +28,9 @@ import java.util.Map; @TargetApi(16) public interface DrmSession { - /** Wraps the throwable which is the cause of the error state. */ + /** + * Wraps the throwable which is the cause of the error state. + */ class DrmSessionException extends Exception { public DrmSessionException(Throwable cause) { From ab94bd8b3bdc8185749209f7edce6ce8b2f23fbe Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 6 Sep 2017 07:26:23 -0700 Subject: [PATCH 0414/2472] Pick up rtmpClient fix Issue: #3156 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167718081 --- extensions/rtmp/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/rtmp/build.gradle b/extensions/rtmp/build.gradle index c832cb82e9..7687f03e32 100644 --- a/extensions/rtmp/build.gradle +++ b/extensions/rtmp/build.gradle @@ -26,7 +26,7 @@ android { dependencies { compile project(modulePrefix + 'library-core') - compile 'net.butterflytv.utils:rtmp-client:0.2.8' + compile 'net.butterflytv.utils:rtmp-client:3.0.0' } ext { From 09a8c7cd6ef02f7177363dcd4fb96975803c380a Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 8 Sep 2017 09:42:13 -0700 Subject: [PATCH 0415/2472] Enable rtmp in external demo app with extensions ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=168007345 --- demo/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/demo/build.gradle b/demo/build.gradle index 1f8bd670e8..e0874e3147 100644 --- a/demo/build.gradle +++ b/demo/build.gradle @@ -62,4 +62,5 @@ dependencies { withExtensionsCompile project(path: modulePrefix + 'extension-ima') withExtensionsCompile project(path: modulePrefix + 'extension-opus') withExtensionsCompile project(path: modulePrefix + 'extension-vp9') + withExtensionsCompile project(path: modulePrefix + 'extension-rtmp') } From a5302be664bf9f1f37477bdc0b70102e24cb98de Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 8 Sep 2017 11:14:53 -0700 Subject: [PATCH 0416/2472] Destroy EGLSurface during DummySurface cleanup ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=168020525 --- .../android/exoplayer2/video/DummySurface.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java index 20fe862dd2..450b4af38c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java @@ -35,6 +35,7 @@ import static android.opengl.EGL14.eglChooseConfig; import static android.opengl.EGL14.eglCreateContext; import static android.opengl.EGL14.eglCreatePbufferSurface; import static android.opengl.EGL14.eglDestroyContext; +import static android.opengl.EGL14.eglDestroySurface; import static android.opengl.EGL14.eglGetDisplay; import static android.opengl.EGL14.eglInitialize; import static android.opengl.EGL14.eglMakeCurrent; @@ -175,8 +176,9 @@ public final class DummySurface extends Surface { private static final int MSG_RELEASE = 3; private final int[] textureIdHolder; - private EGLContext context; private EGLDisplay display; + private EGLContext context; + private EGLSurface pbuffer; private Handler handler; private SurfaceTexture surfaceTexture; @@ -315,7 +317,7 @@ public final class DummySurface extends Surface { EGL_HEIGHT, 1, EGL_NONE}; } - EGLSurface pbuffer = eglCreatePbufferSurface(display, config, pbufferAttributes, 0); + pbuffer = eglCreatePbufferSurface(display, config, pbufferAttributes, 0); Assertions.checkState(pbuffer != null, "eglCreatePbufferSurface failed"); boolean eglMadeCurrent = eglMakeCurrent(display, pbuffer, pbuffer, context); @@ -334,11 +336,15 @@ public final class DummySurface extends Surface { glDeleteTextures(1, textureIdHolder, 0); } } finally { + if (pbuffer != null) { + eglDestroySurface(display, pbuffer); + } if (context != null) { eglDestroyContext(display, context); } - display = null; + pbuffer = null; context = null; + display = null; surface = null; surfaceTexture = null; } From 24b4cb844b0aedc624ffdf52baabf640c12aa681 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 5 Sep 2017 13:06:02 -0700 Subject: [PATCH 0417/2472] Fix attr inheritance in SimpleExoPlayerView When creating PlaybackControlView inside SimpleExoPlayerView, we want certain attributes to be passed through. This lets you set control attributes on the SimpleExoPlayerView directly. We don't want all attributes to be propagated though; only our own custom ones. Not sure if there's a cleaner way to do this. Pragmatically this solution seems ... ok :)? ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=167619801 --- .../android/exoplayer2/ui/PlaybackControlView.java | 10 +++++++--- .../android/exoplayer2/ui/SimpleExoPlayerView.java | 8 ++++---- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java index 017a200005..123b3051e5 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java @@ -339,15 +339,19 @@ public class PlaybackControlView extends FrameLayout { } public PlaybackControlView(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); + this(context, attrs, defStyleAttr, attrs); + } + public PlaybackControlView(Context context, AttributeSet attrs, int defStyleAttr, + AttributeSet playbackAttrs) { + super(context, attrs, defStyleAttr); int controllerLayoutId = R.layout.exo_playback_control_view; rewindMs = DEFAULT_REWIND_MS; fastForwardMs = DEFAULT_FAST_FORWARD_MS; showTimeoutMs = DEFAULT_SHOW_TIMEOUT_MS; repeatToggleModes = DEFAULT_REPEAT_TOGGLE_MODES; - if (attrs != null) { - TypedArray a = context.getTheme().obtainStyledAttributes(attrs, + if (playbackAttrs != null) { + TypedArray a = context.getTheme().obtainStyledAttributes(playbackAttrs, R.styleable.PlaybackControlView, 0, 0); try { rewindMs = a.getInt(R.styleable.PlaybackControlView_rewind_increment, rewindMs); diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index 70526ff655..a4fb539175 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -239,7 +239,7 @@ public final class SimpleExoPlayerView extends FrameLayout { controller = null; componentListener = null; overlayFrameLayout = null; - ImageView logo = new ImageView(context, attrs); + ImageView logo = new ImageView(context); if (Util.SDK_INT >= 23) { configureEditModeLogoV23(getResources(), logo); } else { @@ -329,9 +329,9 @@ public final class SimpleExoPlayerView extends FrameLayout { if (customController != null) { this.controller = customController; } else if (controllerPlaceholder != null) { - // Note: rewindMs and fastForwardMs are passed via attrs, so we don't need to make explicit - // calls to set them. - this.controller = new PlaybackControlView(context, attrs); + // Propagate attrs as playbackAttrs so that PlaybackControlView's custom attributes are + // transferred, but standard FrameLayout attributes (e.g. background) are not. + this.controller = new PlaybackControlView(context, null, 0, attrs); controller.setLayoutParams(controllerPlaceholder.getLayoutParams()); ViewGroup parent = ((ViewGroup) controllerPlaceholder.getParent()); int controllerIndex = parent.indexOfChild(controllerPlaceholder); From 4c1fd23d8e8d448a1ce2c688a598953610319a07 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 11 Aug 2017 06:27:59 -0700 Subject: [PATCH 0418/2472] Add possibility of forcing a specific license URL in HttpMediaDrmCallback Resubmit of https://github.com/google/ExoPlayer/pull/3136 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=164971900 --- .../exoplayer2/drm/HttpMediaDrmCallback.java | 35 ++++++++-------- .../exoplayer2/drm/OfflineLicenseHelper.java | 40 +++++++++++++++---- 2 files changed, 50 insertions(+), 25 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java index f08d9b59b5..dfbf3dee07 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/HttpMediaDrmCallback.java @@ -24,7 +24,6 @@ import com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest; import com.google.android.exoplayer2.upstream.DataSourceInputStream; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.HttpDataSource; -import com.google.android.exoplayer2.upstream.HttpDataSource.Factory; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.io.IOException; @@ -39,33 +38,33 @@ import java.util.UUID; public final class HttpMediaDrmCallback implements MediaDrmCallback { private final HttpDataSource.Factory dataSourceFactory; - private final String defaultUrl; + private final String defaultLicenseUrl; + private final boolean forceDefaultLicenseUrl; private final Map keyRequestProperties; /** - * @param defaultUrl The default license URL. + * @param defaultLicenseUrl The default license URL. Used for key requests that do not specify + * their own license URL. * @param dataSourceFactory A factory from which to obtain {@link HttpDataSource} instances. */ - public HttpMediaDrmCallback(String defaultUrl, HttpDataSource.Factory dataSourceFactory) { - this(defaultUrl, dataSourceFactory, null); + public HttpMediaDrmCallback(String defaultLicenseUrl, HttpDataSource.Factory dataSourceFactory) { + this(defaultLicenseUrl, false, dataSourceFactory); } /** - * @deprecated Use {@link HttpMediaDrmCallback#HttpMediaDrmCallback(String, Factory)}. Request - * properties can be set by calling {@link #setKeyRequestProperty(String, String)}. - * @param defaultUrl The default license URL. + * @param defaultLicenseUrl The default license URL. Used for key requests that do not specify + * their own license URL, or for all key requests if {@code forceDefaultLicenseUrl} is + * set to true. + * @param forceDefaultLicenseUrl Whether to use {@code defaultLicenseUrl} for key requests that + * include their own license URL. * @param dataSourceFactory A factory from which to obtain {@link HttpDataSource} instances. - * @param keyRequestProperties Request properties to set when making key requests, or null. */ - @Deprecated - public HttpMediaDrmCallback(String defaultUrl, HttpDataSource.Factory dataSourceFactory, - Map keyRequestProperties) { + public HttpMediaDrmCallback(String defaultLicenseUrl, boolean forceDefaultLicenseUrl, + HttpDataSource.Factory dataSourceFactory) { this.dataSourceFactory = dataSourceFactory; - this.defaultUrl = defaultUrl; + this.defaultLicenseUrl = defaultLicenseUrl; + this.forceDefaultLicenseUrl = forceDefaultLicenseUrl; this.keyRequestProperties = new HashMap<>(); - if (keyRequestProperties != null) { - this.keyRequestProperties.putAll(keyRequestProperties); - } } /** @@ -112,8 +111,8 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback { @Override public byte[] executeKeyRequest(UUID uuid, KeyRequest request) throws Exception { String url = request.getDefaultUrl(); - if (TextUtils.isEmpty(url)) { - url = defaultUrl; + if (forceDefaultLicenseUrl || TextUtils.isEmpty(url)) { + url = defaultLicenseUrl; } Map requestProperties = new HashMap<>(); // Add standard request properties for supported schemes. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java index cdeb1bd10b..62e7f5ed29 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/OfflineLicenseHelper.java @@ -44,23 +44,47 @@ public final class OfflineLicenseHelper { * 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 defaultLicenseUrl The default license URL. Used for key requests that do not specify + * their own license URL. * @param httpDataSourceFactory A factory from which to obtain {@link HttpDataSource} instances. * @return A new instance which uses Widevine CDM. * @throws UnsupportedDrmException If the Widevine DRM scheme is unsupported or cannot be * instantiated. */ public static OfflineLicenseHelper newWidevineInstance( - String licenseUrl, Factory httpDataSourceFactory) throws UnsupportedDrmException { - return newWidevineInstance( - new HttpMediaDrmCallback(licenseUrl, httpDataSourceFactory), null); + String defaultLicenseUrl, Factory httpDataSourceFactory) + throws UnsupportedDrmException { + return newWidevineInstance(defaultLicenseUrl, false, httpDataSourceFactory, null); } /** * 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 defaultLicenseUrl The default license URL. Used for key requests that do not specify + * their own license URL. + * @param forceDefaultLicenseUrl Whether to use {@code defaultLicenseUrl} for key requests that + * include their own license URL. + * @param httpDataSourceFactory A factory from which to obtain {@link HttpDataSource} instances. + * @return A new instance which uses Widevine CDM. + * @throws UnsupportedDrmException If the Widevine DRM scheme is unsupported or cannot be + * instantiated. + */ + public static OfflineLicenseHelper newWidevineInstance( + String defaultLicenseUrl, boolean forceDefaultLicenseUrl, Factory httpDataSourceFactory) + throws UnsupportedDrmException { + return newWidevineInstance(defaultLicenseUrl, forceDefaultLicenseUrl, httpDataSourceFactory, + null); + } + + /** + * Instantiates a new instance which uses Widevine CDM. Call {@link #release()} when the instance + * is no longer required. + * + * @param defaultLicenseUrl The default license URL. Used for key requests that do not specify + * their own license URL. + * @param forceDefaultLicenseUrl Whether to use {@code defaultLicenseUrl} for key requests that + * include their own license URL. * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument * to {@link MediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null. * @return A new instance which uses Widevine CDM. @@ -70,9 +94,11 @@ public final class OfflineLicenseHelper { * MediaDrmCallback, HashMap, Handler, EventListener) */ public static OfflineLicenseHelper newWidevineInstance( - MediaDrmCallback callback, HashMap optionalKeyRequestParameters) + String defaultLicenseUrl, boolean forceDefaultLicenseUrl, Factory httpDataSourceFactory, + HashMap optionalKeyRequestParameters) throws UnsupportedDrmException { - return new OfflineLicenseHelper<>(FrameworkMediaDrm.newInstance(C.WIDEVINE_UUID), callback, + return new OfflineLicenseHelper<>(FrameworkMediaDrm.newInstance(C.WIDEVINE_UUID), + new HttpMediaDrmCallback(defaultLicenseUrl, forceDefaultLicenseUrl, httpDataSourceFactory), optionalKeyRequestParameters); } From 2d2062bf642661e6fb994f9a17d08ff5a8fcf13e Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Sat, 9 Sep 2017 01:05:09 +0100 Subject: [PATCH 0419/2472] Fix build for release --- extensions/ima/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/ima/build.gradle b/extensions/ima/build.gradle index 7b193cc8fc..c9285162a8 100644 --- a/extensions/ima/build.gradle +++ b/extensions/ima/build.gradle @@ -19,7 +19,7 @@ android { buildToolsVersion project.ext.buildToolsVersion defaultConfig { - minSdkVersion project.ext.minSdkVersion + minSdkVersion 14 targetSdkVersion project.ext.targetSdkVersion testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } From 340d0be40a6a1d744c79245289075b97db26f78e Mon Sep 17 00:00:00 2001 From: olly Date: Sun, 10 Sep 2017 08:39:15 -0700 Subject: [PATCH 0420/2472] Bump to 2.5.2 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=168155713 --- RELEASENOTES.md | 26 +++++++++++++++++++ constants.gradle | 2 +- demos/cast/src/main/AndroidManifest.xml | 4 +-- demos/main/src/main/AndroidManifest.xml | 4 +-- .../exoplayer2/ExoPlayerLibraryInfo.java | 6 ++--- .../trackselection/DefaultTrackSelector.java | 7 ++--- 6 files changed, 38 insertions(+), 11 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 8ad866395e..b694143542 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,31 @@ # Release notes # +### r2.5.2 ### + +* IMA extension: Fix issue where ad playback could end prematurely for some + content types ([#3180](https://github.com/google/ExoPlayer/issues/3180)). +* RTMP extension: Fix SIGABRT on fast RTMP stream restart + ([#3156](https://github.com/google/ExoPlayer/issues/3156)). +* UI: Allow app to manually specify ad markers + ([#3184](https://github.com/google/ExoPlayer/issues/3184)). +* DASH: Expose segment indices to subclasses of DefaultDashChunkSource + ([#3037](https://github.com/google/ExoPlayer/issues/3037)). +* Captions: Added robustness against malformed WebVTT captions + ([#3228](https://github.com/google/ExoPlayer/issues/3228)). +* DRM: Support forcing a specific license URL. +* Fix playback error when seeking in media loaded through content:// URIs + ([#3216](https://github.com/google/ExoPlayer/issues/3216)). +* Fix issue playing MP4s in which the last atom specifies a size of zero + ([#3191](https://github.com/google/ExoPlayer/issues/3191)). +* Workaround playback failures on some Xiaomi devices + ([#3171](https://github.com/google/ExoPlayer/issues/3171)). +* Workaround SIGSEGV issue on some devices when setting and swapping surface for + secure playbacks ([#3215](https://github.com/google/ExoPlayer/issues/3215)). +* Workaround for Nexus 7 issue when swapping output surface + ([#3236](https://github.com/google/ExoPlayer/issues/3236)). +* Workaround for SimpleExoPlayerView's surface not being hidden properly + ([#3160](https://github.com/google/ExoPlayer/issues/3160)). + ### r2.5.1 ### * Fix an issue that could cause the reported playback position to stop advancing diff --git a/constants.gradle b/constants.gradle index dcec53efba..7bdd50e829 100644 --- a/constants.gradle +++ b/constants.gradle @@ -28,7 +28,7 @@ project.ext { junitVersion = '4.12' truthVersion = '0.35' robolectricVersion = '3.4.2' - releaseVersion = 'r2.5.1' + releaseVersion = 'r2.5.2' modulePrefix = ':' if (gradle.ext.has('exoplayerModulePrefix')) { modulePrefix += gradle.ext.exoplayerModulePrefix diff --git a/demos/cast/src/main/AndroidManifest.xml b/demos/cast/src/main/AndroidManifest.xml index eeb28438bd..ee3177323e 100644 --- a/demos/cast/src/main/AndroidManifest.xml +++ b/demos/cast/src/main/AndroidManifest.xml @@ -15,8 +15,8 @@ --> + android:versionCode="2502" + android:versionName="2.5.2"> diff --git a/demos/main/src/main/AndroidManifest.xml b/demos/main/src/main/AndroidManifest.xml index 4f90cef623..50a39f11a6 100644 --- a/demos/main/src/main/AndroidManifest.xml +++ b/demos/main/src/main/AndroidManifest.xml @@ -16,8 +16,8 @@ + android:versionCode="2502" + android:versionName="2.5.2"> diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java index 33f992964a..98eeb99ad8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java @@ -31,13 +31,13 @@ public final class ExoPlayerLibraryInfo { * The version of the library expressed as a string, for example "1.2.3". */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. - public static final String VERSION = "2.5.1"; + public static final String VERSION = "2.5.2"; /** * The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final String VERSION_SLASHY = "ExoPlayerLib/2.5.1"; + public static final String VERSION_SLASHY = "ExoPlayerLib/2.5.2"; /** * The version of the library expressed as an integer, for example 1002003. @@ -47,7 +47,7 @@ public final class ExoPlayerLibraryInfo { * integer version 123045006 (123-045-006). */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final int VERSION_INT = 2005001; + public static final int VERSION_INT = 2005002; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index fe2b920933..ba0f63b0bb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -767,7 +767,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @throws ExoPlaybackException If an error occurs while selecting the tracks. */ protected TrackSelection selectAudioTrack(TrackGroupArray groups, int[][] formatSupport, - Parameters params, TrackSelection.Factory adaptiveTrackSelectionFactory) { + Parameters params, TrackSelection.Factory adaptiveTrackSelectionFactory) + throws ExoPlaybackException { int selectedGroupIndex = C.INDEX_UNSET; int selectedTrackIndex = C.INDEX_UNSET; int selectedTrackScore = 0; @@ -893,7 +894,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @throws ExoPlaybackException If an error occurs while selecting the tracks. */ protected TrackSelection selectTextTrack(TrackGroupArray groups, int[][] formatSupport, - Parameters params) { + Parameters params) throws ExoPlaybackException { TrackGroup selectedGroup = null; int selectedTrackIndex = 0; int selectedTrackScore = 0; @@ -960,7 +961,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @throws ExoPlaybackException If an error occurs while selecting the tracks. */ protected TrackSelection selectOtherTrack(int trackType, TrackGroupArray groups, - int[][] formatSupport, Parameters params) { + int[][] formatSupport, Parameters params) throws ExoPlaybackException { TrackGroup selectedGroup = null; int selectedTrackIndex = 0; int selectedTrackScore = 0; From 9b18130eccce4c9bd6e974e24a0e641b7b1102fc Mon Sep 17 00:00:00 2001 From: olly Date: Sun, 10 Sep 2017 08:39:15 -0700 Subject: [PATCH 0421/2472] Bump to 2.5.2 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=168155713 --- RELEASENOTES.md | 26 +++++++++++++++++++ constants.gradle | 2 +- demo/src/main/AndroidManifest.xml | 4 +-- .../exoplayer2/ExoPlayerLibraryInfo.java | 6 ++--- .../trackselection/DefaultTrackSelector.java | 7 ++--- 5 files changed, 36 insertions(+), 9 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 8ad866395e..b694143542 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,31 @@ # Release notes # +### r2.5.2 ### + +* IMA extension: Fix issue where ad playback could end prematurely for some + content types ([#3180](https://github.com/google/ExoPlayer/issues/3180)). +* RTMP extension: Fix SIGABRT on fast RTMP stream restart + ([#3156](https://github.com/google/ExoPlayer/issues/3156)). +* UI: Allow app to manually specify ad markers + ([#3184](https://github.com/google/ExoPlayer/issues/3184)). +* DASH: Expose segment indices to subclasses of DefaultDashChunkSource + ([#3037](https://github.com/google/ExoPlayer/issues/3037)). +* Captions: Added robustness against malformed WebVTT captions + ([#3228](https://github.com/google/ExoPlayer/issues/3228)). +* DRM: Support forcing a specific license URL. +* Fix playback error when seeking in media loaded through content:// URIs + ([#3216](https://github.com/google/ExoPlayer/issues/3216)). +* Fix issue playing MP4s in which the last atom specifies a size of zero + ([#3191](https://github.com/google/ExoPlayer/issues/3191)). +* Workaround playback failures on some Xiaomi devices + ([#3171](https://github.com/google/ExoPlayer/issues/3171)). +* Workaround SIGSEGV issue on some devices when setting and swapping surface for + secure playbacks ([#3215](https://github.com/google/ExoPlayer/issues/3215)). +* Workaround for Nexus 7 issue when swapping output surface + ([#3236](https://github.com/google/ExoPlayer/issues/3236)). +* Workaround for SimpleExoPlayerView's surface not being hidden properly + ([#3160](https://github.com/google/ExoPlayer/issues/3160)). + ### r2.5.1 ### * Fix an issue that could cause the reported playback position to stop advancing diff --git a/constants.gradle b/constants.gradle index b7cc8b6906..7391228853 100644 --- a/constants.gradle +++ b/constants.gradle @@ -24,7 +24,7 @@ project.ext { supportLibraryVersion = '25.4.0' dexmakerVersion = '1.2' mockitoVersion = '1.9.5' - releaseVersion = 'r2.5.1' + releaseVersion = 'r2.5.2' modulePrefix = ':' if (gradle.ext.has('exoplayerModulePrefix')) { modulePrefix += gradle.ext.exoplayerModulePrefix diff --git a/demo/src/main/AndroidManifest.xml b/demo/src/main/AndroidManifest.xml index 1f66822dc7..081ca00077 100644 --- a/demo/src/main/AndroidManifest.xml +++ b/demo/src/main/AndroidManifest.xml @@ -16,8 +16,8 @@ + android:versionCode="2502" + android:versionName="2.5.2"> diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java index 33f992964a..98eeb99ad8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java @@ -31,13 +31,13 @@ public final class ExoPlayerLibraryInfo { * The version of the library expressed as a string, for example "1.2.3". */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. - public static final String VERSION = "2.5.1"; + public static final String VERSION = "2.5.2"; /** * The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final String VERSION_SLASHY = "ExoPlayerLib/2.5.1"; + public static final String VERSION_SLASHY = "ExoPlayerLib/2.5.2"; /** * The version of the library expressed as an integer, for example 1002003. @@ -47,7 +47,7 @@ public final class ExoPlayerLibraryInfo { * integer version 123045006 (123-045-006). */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final int VERSION_INT = 2005001; + public static final int VERSION_INT = 2005002; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index fe2b920933..ba0f63b0bb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -767,7 +767,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @throws ExoPlaybackException If an error occurs while selecting the tracks. */ protected TrackSelection selectAudioTrack(TrackGroupArray groups, int[][] formatSupport, - Parameters params, TrackSelection.Factory adaptiveTrackSelectionFactory) { + Parameters params, TrackSelection.Factory adaptiveTrackSelectionFactory) + throws ExoPlaybackException { int selectedGroupIndex = C.INDEX_UNSET; int selectedTrackIndex = C.INDEX_UNSET; int selectedTrackScore = 0; @@ -893,7 +894,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @throws ExoPlaybackException If an error occurs while selecting the tracks. */ protected TrackSelection selectTextTrack(TrackGroupArray groups, int[][] formatSupport, - Parameters params) { + Parameters params) throws ExoPlaybackException { TrackGroup selectedGroup = null; int selectedTrackIndex = 0; int selectedTrackScore = 0; @@ -960,7 +961,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @throws ExoPlaybackException If an error occurs while selecting the tracks. */ protected TrackSelection selectOtherTrack(int trackType, TrackGroupArray groups, - int[][] formatSupport, Parameters params) { + int[][] formatSupport, Parameters params) throws ExoPlaybackException { TrackGroup selectedGroup = null; int selectedTrackIndex = 0; int selectedTrackScore = 0; From b174bcc173f34b63fde94ca1a1a32ccf0fa7164d Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 17 Aug 2017 06:40:44 -0700 Subject: [PATCH 0422/2472] Update extension README with usage instructions Issue: #3162 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=165572088 --- extensions/ffmpeg/README.md | 49 ++++++++++++++++++++++++++++++------ extensions/flac/README.md | 50 +++++++++++++++++++++++++++++++------ extensions/opus/README.md | 41 ++++++++++++++++++++++++------ extensions/vp9/README.md | 48 +++++++++++++++++++++++++++++------ 4 files changed, 160 insertions(+), 28 deletions(-) diff --git a/extensions/ffmpeg/README.md b/extensions/ffmpeg/README.md index b4514effbc..fbc919c36d 100644 --- a/extensions/ffmpeg/README.md +++ b/extensions/ffmpeg/README.md @@ -2,17 +2,18 @@ ## Description ## -The FFmpeg extension is a [Renderer][] implementation that uses FFmpeg to decode -audio. - -[Renderer]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/Renderer.html +The FFmpeg extension provides `FfmpegAudioRenderer`, which uses FFmpeg for +decoding and can render audio encoded in a variety of formats. ## Build instructions ## To use this extension you need to clone the ExoPlayer repository and depend on its modules locally. Instructions for doing this can be found in ExoPlayer's -[top level README][]. In addition, it's necessary to build the extension's -native components as follows: +[top level README][]. The extension is not provided via JCenter (see [#2781][] +for more information). + +In addition, it's necessary to build the extension's native components as +follows: * Set the following environment variables: @@ -34,7 +35,11 @@ NDK_PATH="" HOST_PLATFORM="linux-x86_64" ``` -* Fetch and build FFmpeg. For example, to fetch and build for armeabi-v7a, +* Fetch and build FFmpeg. The configuration flags determine which formats will + be supported. See the [Supported formats][] page for more details of the + available flags. + +For example, to fetch and build for armeabi-v7a, arm64-v8a and x86 on Linux x86_64: ``` @@ -103,5 +108,35 @@ cd "${FFMPEG_EXT_PATH}"/jni && \ ${NDK_PATH}/ndk-build APP_ABI="armeabi-v7a arm64-v8a x86" -j4 ``` +## Using the extension ## + +Once you've followed the instructions above to check out, build and depend on +the extension, the next step is to tell ExoPlayer to use `FfmpegAudioRenderer`. +How you do this depends on which player API you're using: + +* If you're passing a `DefaultRenderersFactory` to + `ExoPlayerFactory.newSimpleInstance`, you can enable using the extension by + setting the `extensionRendererMode` parameter of the `DefaultRenderersFactory` + constructor to `EXTENSION_RENDERER_MODE_ON`. This will use + `FfmpegAudioRenderer` for playback if `MediaCodecAudioRenderer` doesn't + support the input format. Pass `EXTENSION_RENDERER_MODE_PREFER` to give + `FfmpegAudioRenderer` priority over `MediaCodecAudioRenderer`. +* If you've subclassed `DefaultRenderersFactory`, add an `FfmpegAudioRenderer` + to the output list in `buildAudioRenderers`. ExoPlayer will use the first + `Renderer` in the list that supports the input media format. +* If you've implemented your own `RenderersFactory`, return an + `FfmpegAudioRenderer` instance from `createRenderers`. ExoPlayer will use the + first `Renderer` in the returned array that supports the input media format. +* If you're using `ExoPlayerFactory.newInstance`, pass an `FfmpegAudioRenderer` + in the array of `Renderer`s. ExoPlayer will use the first `Renderer` in the + list that supports the input media format. + +Note: These instructions assume you're using `DefaultTrackSelector`. If you have +a custom track selector the choice of `Renderer` is up to your implementation, +so you need to make sure you are passing an `FfmpegAudioRenderer` to the player, +then implement your own logic to use the renderer for a given track. + [top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md [Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html +[#2781]: https://github.com/google/ExoPlayer/issues/2781 +[Supported formats]: https://google.github.io/ExoPlayer/supported-formats.html#ffmpeg-extension diff --git a/extensions/flac/README.md b/extensions/flac/README.md index 9db2e5727d..505482f7ed 100644 --- a/extensions/flac/README.md +++ b/extensions/flac/README.md @@ -2,18 +2,17 @@ ## Description ## -The Flac extension is a [Renderer][] implementation that helps you bundle -libFLAC (the Flac decoding library) into your app and use it along with -ExoPlayer to play Flac audio on Android devices. - -[Renderer]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/Renderer.html +The Flac extension provides `FlacExtractor` and `LibflacAudioRenderer`, which +use libFLAC (the Flac decoding library) to extract and decode FLAC audio. ## Build instructions ## To use this extension you need to clone the ExoPlayer repository and depend on its modules locally. Instructions for doing this can be found in ExoPlayer's -[top level README][]. In addition, it's necessary to build the extension's -native components as follows: +[top level README][]. + +In addition, it's necessary to build the extension's native components as +follows: * Set the following environment variables: @@ -46,3 +45,40 @@ ${NDK_PATH}/ndk-build APP_ABI=all -j4 [top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md [Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html + +## Using the extension ## + +Once you've followed the instructions above to check out, build and depend on +the extension, the next step is to tell ExoPlayer to use the extractor and/or +renderer. + +### Using `FlacExtractor` ### + +`FlacExtractor` is used via `ExtractorMediaSource`. If you're using +`DefaultExtractorsFactory`, `FlacExtractor` will automatically be used to read +`.flac` files. If you're not using `DefaultExtractorsFactory`, return a +`FlacExtractor` from your `ExtractorsFactory.createExtractors` implementation. + +### Using `LibflacAudioRenderer` ### + +* If you're passing a `DefaultRenderersFactory` to + `ExoPlayerFactory.newSimpleInstance`, you can enable using the extension by + setting the `extensionRendererMode` parameter of the `DefaultRenderersFactory` + constructor to `EXTENSION_RENDERER_MODE_ON`. This will use + `LibflacAudioRenderer` for playback if `MediaCodecAudioRenderer` doesn't + support the input format. Pass `EXTENSION_RENDERER_MODE_PREFER` to give + `LibflacAudioRenderer` priority over `MediaCodecAudioRenderer`. +* If you've subclassed `DefaultRenderersFactory`, add a `LibflacAudioRenderer` + to the output list in `buildAudioRenderers`. ExoPlayer will use the first + `Renderer` in the list that supports the input media format. +* If you've implemented your own `RenderersFactory`, return a + `LibflacAudioRenderer` instance from `createRenderers`. ExoPlayer will use the + first `Renderer` in the returned array that supports the input media format. +* If you're using `ExoPlayerFactory.newInstance`, pass a `LibflacAudioRenderer` + in the array of `Renderer`s. ExoPlayer will use the first `Renderer` in the + list that supports the input media format. + +Note: These instructions assume you're using `DefaultTrackSelector`. If you have +a custom track selector the choice of `Renderer` is up to your implementation, +so you need to make sure you are passing an `LibflacAudioRenderer` to the +player, then implement your own logic to use the renderer for a given track. diff --git a/extensions/opus/README.md b/extensions/opus/README.md index e5f5bcb168..cc21c77cf9 100644 --- a/extensions/opus/README.md +++ b/extensions/opus/README.md @@ -2,18 +2,17 @@ ## Description ## -The Opus extension is a [Renderer][] implementation that helps you bundle -libopus (the Opus decoding library) into your app and use it along with -ExoPlayer to play Opus audio on Android devices. - -[Renderer]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/Renderer.html +The Opus extension provides `LibopusAudioRenderer`, which uses +libopus (the Opus decoding library) to decode Opus audio. ## Build instructions ## To use this extension you need to clone the ExoPlayer repository and depend on its modules locally. Instructions for doing this can be found in ExoPlayer's -[top level README][]. In addition, it's necessary to build the extension's -native components as follows: +[top level README][]. + +In addition, it's necessary to build the extension's native components as +follows: * Set the following environment variables: @@ -59,3 +58,31 @@ ${NDK_PATH}/ndk-build APP_ABI=all -j4 * Clean and re-build the project. * If you want to use your own version of libopus, place it in `${OPUS_EXT_PATH}/jni/libopus`. + +## Using the extension ## + +Once you've followed the instructions above to check out, build and depend on +the extension, the next step is to tell ExoPlayer to use `LibopusAudioRenderer`. +How you do this depends on which player API you're using: + +* If you're passing a `DefaultRenderersFactory` to + `ExoPlayerFactory.newSimpleInstance`, you can enable using the extension by + setting the `extensionRendererMode` parameter of the `DefaultRenderersFactory` + constructor to `EXTENSION_RENDERER_MODE_ON`. This will use + `LibopusAudioRenderer` for playback if `MediaCodecAudioRenderer` doesn't + support the input format. Pass `EXTENSION_RENDERER_MODE_PREFER` to give + `LibopusAudioRenderer` priority over `MediaCodecAudioRenderer`. +* If you've subclassed `DefaultRenderersFactory`, add a `LibopusAudioRenderer` + to the output list in `buildAudioRenderers`. ExoPlayer will use the first + `Renderer` in the list that supports the input media format. +* If you've implemented your own `RenderersFactory`, return a + `LibopusAudioRenderer` instance from `createRenderers`. ExoPlayer will use the + first `Renderer` in the returned array that supports the input media format. +* If you're using `ExoPlayerFactory.newInstance`, pass a `LibopusAudioRenderer` + in the array of `Renderer`s. ExoPlayer will use the first `Renderer` in the + list that supports the input media format. + +Note: These instructions assume you're using `DefaultTrackSelector`. If you have +a custom track selector the choice of `Renderer` is up to your implementation, +so you need to make sure you are passing an `LibopusAudioRenderer` to the +player, then implement your own logic to use the renderer for a given track. diff --git a/extensions/vp9/README.md b/extensions/vp9/README.md index 87c5c8d54f..d28aa70db0 100644 --- a/extensions/vp9/README.md +++ b/extensions/vp9/README.md @@ -2,18 +2,17 @@ ## Description ## -The VP9 extension is a [Renderer][] implementation that helps you bundle libvpx -(the VP9 decoding library) into your app and use it along with ExoPlayer to play -VP9 video on Android devices. - -[Renderer]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/Renderer.html +The VP9 extension provides `LibvpxVideoRenderer`, which uses +libvpx (the VPx decoding library) to decode VP9 video. ## Build instructions ## To use this extension you need to clone the ExoPlayer repository and depend on its modules locally. Instructions for doing this can be found in ExoPlayer's -[top level README][]. In addition, it's necessary to build the extension's -native components as follows: +[top level README][]. + +In addition, it's necessary to build the extension's native components as +follows: * Set the following environment variables: @@ -76,3 +75,38 @@ ${NDK_PATH}/ndk-build APP_ABI=all -j4 `${VP9_EXT_PATH}/jni/libvpx` or `${VP9_EXT_PATH}/jni/libyuv` respectively. But please note that `generate_libvpx_android_configs.sh` and the makefiles need to be modified to work with arbitrary versions of libvpx and libyuv. + +## Using the extension ## + +Once you've followed the instructions above to check out, build and depend on +the extension, the next step is to tell ExoPlayer to use `LibvpxVideoRenderer`. +How you do this depends on which player API you're using: + +* If you're passing a `DefaultRenderersFactory` to + `ExoPlayerFactory.newSimpleInstance`, you can enable using the extension by + setting the `extensionRendererMode` parameter of the `DefaultRenderersFactory` + constructor to `EXTENSION_RENDERER_MODE_ON`. This will use + `LibvpxVideoRenderer` for playback if `MediaCodecVideoRenderer` doesn't + support decoding the input VP9 stream. Pass `EXTENSION_RENDERER_MODE_PREFER` + to give `LibvpxVideoRenderer` priority over `MediaCodecVideoRenderer`. +* If you've subclassed `DefaultRenderersFactory`, add a `LibvpxVideoRenderer` + to the output list in `buildVideoRenderers`. ExoPlayer will use the first + `Renderer` in the list that supports the input media format. +* If you've implemented your own `RenderersFactory`, return a + `LibvpxVideoRenderer` instance from `createRenderers`. ExoPlayer will use the + first `Renderer` in the returned array that supports the input media format. +* If you're using `ExoPlayerFactory.newInstance`, pass a `LibvpxVideoRenderer` + in the array of `Renderer`s. ExoPlayer will use the first `Renderer` in the + list that supports the input media format. + +Note: These instructions assume you're using `DefaultTrackSelector`. If you have +a custom track selector the choice of `Renderer` is up to your implementation, +so you need to make sure you are passing an `LibvpxVideoRenderer` to the +player, then implement your own logic to use the renderer for a given track. + +`LibvpxVideoRenderer` can optionally output to a `VpxVideoSurfaceView` when not +being used via `SimpleExoPlayer`, in which case color space conversion will be +performed using a GL shader. To enable this mode, send the renderer a message of +type `LibvpxVideoRenderer.MSG_SET_OUTPUT_BUFFER_RENDERER` with the +`VpxVideoSurfaceView` as its object, instead of sending `MSG_SET_SURFACE` with a +`Surface`. From 4ce862adf285120709d60c655bf197877259b8d2 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 17 Aug 2017 07:59:29 -0700 Subject: [PATCH 0423/2472] Tweak and add READMEs + remove refs to V1 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=165578518 --- README.md | 51 +++++++++++++++---------------- demo/README.md | 4 +-- extensions/README.md | 5 +++ extensions/cronet/README.md | 9 ++++-- extensions/ffmpeg/README.md | 9 ++++-- extensions/flac/README.md | 9 ++++-- extensions/gvr/README.md | 18 +++++++---- extensions/ima/README.md | 9 ++++-- extensions/mediasession/README.md | 9 ++++-- extensions/okhttp/README.md | 9 ++++-- extensions/opus/README.md | 13 +++++--- extensions/rtmp/README.md | 9 ++++-- extensions/vp9/README.md | 13 +++++--- library/README.md | 7 +++++ library/all/README.md | 13 ++++++++ library/core/README.md | 9 ++++++ library/dash/README.md | 12 ++++++++ library/hls/README.md | 11 +++++++ library/smoothstreaming/README.md | 12 ++++++++ library/ui/README.md | 10 ++++++ 20 files changed, 185 insertions(+), 56 deletions(-) create mode 100644 extensions/README.md create mode 100644 library/README.md create mode 100644 library/all/README.md create mode 100644 library/core/README.md create mode 100644 library/dash/README.md create mode 100644 library/hls/README.md create mode 100644 library/smoothstreaming/README.md create mode 100644 library/ui/README.md diff --git a/README.md b/README.md index bec434e802..b21db8b13a 100644 --- a/README.md +++ b/README.md @@ -9,37 +9,37 @@ and extend, and can be updated through Play Store application updates. ## Documentation ## -* The [developer guide][] provides a wealth of information to help you get - started. -* The [class reference][] documents the ExoPlayer library classes. +* The [developer guide][] provides a wealth of information. +* The [class reference][] documents ExoPlayer classes. * The [release notes][] document the major changes in each release. +* Follow our [developer blog][] to keep up to date with the latest ExoPlayer + developments! [developer guide]: https://google.github.io/ExoPlayer/guide.html [class reference]: https://google.github.io/ExoPlayer/doc/reference -[release notes]: https://github.com/google/ExoPlayer/blob/dev-v2/RELEASENOTES.md +[release notes]: https://github.com/google/ExoPlayer/blob/release-v2/RELEASENOTES.md +[developer blog]: https://medium.com/google-exoplayer ## Using ExoPlayer ## -ExoPlayer modules can be obtained via JCenter. It's also possible to clone the +ExoPlayer modules can be obtained from JCenter. It's also possible to clone the repository and depend on the modules locally. -### Via JCenter ### +### From JCenter ### The easiest way to get started using ExoPlayer is to add it as a gradle -dependency. You need to make sure you have the JCenter and Google Maven -repositories included in the `build.gradle` file in the root of your project: +dependency. You need to make sure you have the JCenter and Google repositories +included in the `build.gradle` file in the root of your project: ```gradle repositories { jcenter() - maven { - url "https://maven.google.com" - } + google() } ``` 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: +module. The following will add a dependency to the full library: ```gradle compile 'com.google.android.exoplayer:exoplayer:r2.X.X' @@ -56,8 +56,8 @@ 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 +The available library modules are listed below. Adding a dependency to the full +library is equivalent to adding dependencies on all of the library modules individually. * `exoplayer-core`: Core functionality (required). @@ -66,11 +66,16 @@ individually. * `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][]. +In addition to library modules, ExoPlayer has multiple extension modules that +depend on external libraries to provide additional functionality. Some +extensions are available from JCenter, whereas others must be built manaully. +Browse the [extensions directory] and their individual READMEs for details. +More information on the library and extension modules that are available from +JCenter can be found on [Bintray][]. + +[extensions directory][]: https://github.com/google/ExoPlayer/tree/release-v2/extensions/ [Bintray]: https://bintray.com/google/exoplayer -[Release notes]: https://github.com/google/ExoPlayer/blob/release-v2/RELEASENOTES.md ### Locally ### @@ -109,15 +114,9 @@ compile project(':exoplayer-library-ui') #### Project branches #### - * The project has `dev-vX` and `release-vX` branches, where `X` is the major - version number. - * Most development work happens on the `dev-vX` branch with the highest major - version number. Pull requests should normally be made to this branch. - * Bug fixes may be submitted to older `dev-vX` branches. When doing this, the - same (or an equivalent) fix should also be submitted to all subsequent - `dev-vX` branches. - * A `release-vX` branch holds the most recent stable release for major version - `X`. +* Development work happens on the `dev-v2` branch. Pull requests should + normally be made to this branch. +* The `release-v2` branch holds the most recent release. #### Using Android Studio #### diff --git a/demo/README.md b/demo/README.md index ca37392623..bdb04e5ba8 100644 --- a/demo/README.md +++ b/demo/README.md @@ -1,5 +1,5 @@ -# Demo application # +# ExoPlayer main demo # -This folder contains a demo application that uses ExoPlayer to play a number +This is the main ExoPlayer demo application. It uses ExoPlayer to play a number of test streams. It can be used as a starting point or reference project when developing other applications that make use of the ExoPlayer library. diff --git a/extensions/README.md b/extensions/README.md new file mode 100644 index 0000000000..bf0effb358 --- /dev/null +++ b/extensions/README.md @@ -0,0 +1,5 @@ +# ExoPlayer extensions # + +ExoPlayer extensions are modules that depend on external libraries to provide +additional functionality. Browse the individual extensions and their READMEs to +learn more. diff --git a/extensions/cronet/README.md b/extensions/cronet/README.md index bd19c07ea5..66da774978 100644 --- a/extensions/cronet/README.md +++ b/extensions/cronet/README.md @@ -1,7 +1,5 @@ # ExoPlayer Cronet extension # -## Description ## - The Cronet extension is an [HttpDataSource][] implementation using [Cronet][]. [HttpDataSource]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/upstream/HttpDataSource.html @@ -53,3 +51,10 @@ new DefaultDataSourceFactory( new CronetDataSourceFactory(...) /* baseDataSourceFactory argument */); ``` respectively. + +## Links ## + +* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.cronet.*` + belong to this module. + +[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html diff --git a/extensions/ffmpeg/README.md b/extensions/ffmpeg/README.md index fbc919c36d..57b637d1e2 100644 --- a/extensions/ffmpeg/README.md +++ b/extensions/ffmpeg/README.md @@ -1,7 +1,5 @@ # ExoPlayer FFmpeg extension # -## Description ## - The FFmpeg extension provides `FfmpegAudioRenderer`, which uses FFmpeg for decoding and can render audio encoded in a variety of formats. @@ -140,3 +138,10 @@ then implement your own logic to use the renderer for a given track. [Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html [#2781]: https://github.com/google/ExoPlayer/issues/2781 [Supported formats]: https://google.github.io/ExoPlayer/supported-formats.html#ffmpeg-extension + +## Links ## + +* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.ffmpeg.*` + belong to this module. + +[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html diff --git a/extensions/flac/README.md b/extensions/flac/README.md index 505482f7ed..113b41a93d 100644 --- a/extensions/flac/README.md +++ b/extensions/flac/README.md @@ -1,7 +1,5 @@ # ExoPlayer Flac extension # -## Description ## - The Flac extension provides `FlacExtractor` and `LibflacAudioRenderer`, which use libFLAC (the Flac decoding library) to extract and decode FLAC audio. @@ -82,3 +80,10 @@ Note: These instructions assume you're using `DefaultTrackSelector`. If you have a custom track selector the choice of `Renderer` is up to your implementation, so you need to make sure you are passing an `LibflacAudioRenderer` to the player, then implement your own logic to use the renderer for a given track. + +## Links ## + +* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.flac.*` + belong to this module. + +[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html diff --git a/extensions/gvr/README.md b/extensions/gvr/README.md index 4e08ee6387..250cf58c2f 100644 --- a/extensions/gvr/README.md +++ b/extensions/gvr/README.md @@ -1,7 +1,5 @@ # ExoPlayer GVR extension # -## Description ## - The GVR extension wraps the [Google VR SDK for Android][]. It provides a GvrAudioProcessor, which uses [GvrAudioSurround][] to provide binaural rendering of surround sound and ambisonic soundfields. @@ -26,9 +24,17 @@ locally. Instructions for doing this can be found in ExoPlayer's ## Using the extension ## -* If using SimpleExoPlayer, override SimpleExoPlayer.buildAudioProcessors to - return a GvrAudioProcessor. -* If constructing renderers directly, pass a GvrAudioProcessor to - MediaCodecAudioRenderer's constructor. +* If using `DefaultRenderersFactory`, override + `DefaultRenderersFactory.buildAudioProcessors` to return a + `GvrAudioProcessor`. +* If constructing renderers directly, pass a `GvrAudioProcessor` to + `MediaCodecAudioRenderer`'s constructor. [top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md + +## Links ## + +* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.gvr.*` + belong to this module. + +[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html diff --git a/extensions/ima/README.md b/extensions/ima/README.md index f328bb44cb..4f63214f04 100644 --- a/extensions/ima/README.md +++ b/extensions/ima/README.md @@ -1,7 +1,5 @@ # ExoPlayer IMA extension # -## Description ## - The IMA extension is a [MediaSource][] implementation wrapping the [Interactive Media Ads SDK for Android][IMA]. You can use it to insert ads alongside content. @@ -55,3 +53,10 @@ playback. [top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md [sample ad tags]: https://developers.google.com/interactive-media-ads/docs/sdks/android/tags + +## Links ## + +* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.ima.*` + belong to this module. + +[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html diff --git a/extensions/mediasession/README.md b/extensions/mediasession/README.md index 60fec9fb60..3278e8dba5 100644 --- a/extensions/mediasession/README.md +++ b/extensions/mediasession/README.md @@ -1,7 +1,5 @@ # ExoPlayer MediaSession extension # -## Description ## - The MediaSession extension mediates between a Player (or ExoPlayer) instance and a [MediaSession][]. It automatically retrieves and implements playback actions and syncs the player state with the state of the media session. The @@ -25,3 +23,10 @@ locally. Instructions for doing this can be found in ExoPlayer's [top level README][]. [top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md + +## Links ## + +* [Javadoc][]: Classes matching + `com.google.android.exoplayer2.ext.mediasession.*` belong to this module. + +[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html diff --git a/extensions/okhttp/README.md b/extensions/okhttp/README.md index b10c4ba629..f84d0c35f2 100644 --- a/extensions/okhttp/README.md +++ b/extensions/okhttp/README.md @@ -1,7 +1,5 @@ # ExoPlayer OkHttp extension # -## Description ## - The OkHttp extension is an [HttpDataSource][] implementation using Square's [OkHttp][]. @@ -49,3 +47,10 @@ new DefaultDataSourceFactory( new OkHttpDataSourceFactory(...) /* baseDataSourceFactory argument */); ``` respectively. + +## Links ## + +* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.okhttp.*` + belong to this module. + +[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html diff --git a/extensions/opus/README.md b/extensions/opus/README.md index cc21c77cf9..d766e8c9c4 100644 --- a/extensions/opus/README.md +++ b/extensions/opus/README.md @@ -1,9 +1,7 @@ # ExoPlayer Opus extension # -## Description ## - -The Opus extension provides `LibopusAudioRenderer`, which uses -libopus (the Opus decoding library) to decode Opus audio. +The Opus extension provides `LibopusAudioRenderer`, which uses libopus (the Opus +decoding library) to decode Opus audio. ## Build instructions ## @@ -86,3 +84,10 @@ Note: These instructions assume you're using `DefaultTrackSelector`. If you have a custom track selector the choice of `Renderer` is up to your implementation, so you need to make sure you are passing an `LibopusAudioRenderer` to the player, then implement your own logic to use the renderer for a given track. + +## Links ## + +* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.opus.*` + belong to this module. + +[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html diff --git a/extensions/rtmp/README.md b/extensions/rtmp/README.md index 80074f119c..7e6bc0d641 100644 --- a/extensions/rtmp/README.md +++ b/extensions/rtmp/README.md @@ -1,7 +1,5 @@ # ExoPlayer RTMP extension # -## Description ## - The RTMP extension is a [DataSource][] implementation for playing [RTMP][] streams using [LibRtmp Client for Android][]. @@ -41,3 +39,10 @@ application code are required. Alternatively, if you know that your application doesn't need to handle any other protocols, you can update any `DataSource`s and `DataSource.Factory` instantiations in your application code to use `RtmpDataSource` and `RtmpDataSourceFactory` directly. + +## Links ## + +* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.rtmp.*` + belong to this module. + +[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html diff --git a/extensions/vp9/README.md b/extensions/vp9/README.md index d28aa70db0..7bce4a2a25 100644 --- a/extensions/vp9/README.md +++ b/extensions/vp9/README.md @@ -1,9 +1,7 @@ # ExoPlayer VP9 extension # -## Description ## - -The VP9 extension provides `LibvpxVideoRenderer`, which uses -libvpx (the VPx decoding library) to decode VP9 video. +The VP9 extension provides `LibvpxVideoRenderer`, which uses libvpx (the VPx +decoding library) to decode VP9 video. ## Build instructions ## @@ -110,3 +108,10 @@ performed using a GL shader. To enable this mode, send the renderer a message of type `LibvpxVideoRenderer.MSG_SET_OUTPUT_BUFFER_RENDERER` with the `VpxVideoSurfaceView` as its object, instead of sending `MSG_SET_SURFACE` with a `Surface`. + +## Links ## + +* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.vp9.*` + belong to this module. + +[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html diff --git a/library/README.md b/library/README.md new file mode 100644 index 0000000000..7aa07fc521 --- /dev/null +++ b/library/README.md @@ -0,0 +1,7 @@ +# ExoPlayer library # + +The ExoPlayer library is split into multiple modules. See ExoPlayer's +[top level README][] for more information about the available library modules +and how to use them. + +[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md diff --git a/library/all/README.md b/library/all/README.md new file mode 100644 index 0000000000..8746e3afc6 --- /dev/null +++ b/library/all/README.md @@ -0,0 +1,13 @@ +# ExoPlayer full library # + +An empty module that depends on all of the other library modules. Depending on +the full library is equivalent to depending on all of the other library modules +individually. See ExoPlayer's [top level README][] for more information. + +[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md + +## Links ## + +* [Javadoc][]: Note that this Javadoc is combined with that of other modules. + +[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html diff --git a/library/core/README.md b/library/core/README.md new file mode 100644 index 0000000000..f31ffed131 --- /dev/null +++ b/library/core/README.md @@ -0,0 +1,9 @@ +# ExoPlayer core library module # + +The core of the ExoPlayer library. + +## Links ## + +* [Javadoc][]: Note that this Javadoc is combined with that of other modules. + +[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html diff --git a/library/dash/README.md b/library/dash/README.md new file mode 100644 index 0000000000..394a38a332 --- /dev/null +++ b/library/dash/README.md @@ -0,0 +1,12 @@ +# ExoPlayer DASH library module # + +Provides support for Dynamic Adaptive Streaming over HTTP (DASH) content. To +play DASH content, instantiate a `DashMediaSource` and pass it to +`ExoPlayer.prepare`. + +## Links ## + +* [Javadoc][]: Classes matching `com.google.android.exoplayer2.source.dash.*` + belong to this module. + +[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html diff --git a/library/hls/README.md b/library/hls/README.md new file mode 100644 index 0000000000..6f7e9d08d9 --- /dev/null +++ b/library/hls/README.md @@ -0,0 +1,11 @@ +# ExoPlayer HLS library module # + +Provides support for HTTP Live Streaming (HLS) content. To play HLS content, +instantiate a `HlsMediaSource` and pass it to `ExoPlayer.prepare`. + +## Links ## + +* [Javadoc][]: Classes matching `com.google.android.exoplayer2.source.hls.*` + belong to this module. + +[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html diff --git a/library/smoothstreaming/README.md b/library/smoothstreaming/README.md new file mode 100644 index 0000000000..69265e8702 --- /dev/null +++ b/library/smoothstreaming/README.md @@ -0,0 +1,12 @@ +# ExoPlayer SmoothStreaming library module # + +Provides support for Smooth Streaming content. To play Smooth Streaming content, +instantiate a `SsMediaSource` and pass it to `ExoPlayer.prepare`. + +## Links ## + +* [Javadoc][]: Classes matching + `com.google.android.exoplayer2.source.smoothstreaming.*` belong to this + module. + +[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html diff --git a/library/ui/README.md b/library/ui/README.md new file mode 100644 index 0000000000..34e93e43af --- /dev/null +++ b/library/ui/README.md @@ -0,0 +1,10 @@ +# ExoPlayer UI library module # + +Provides UI components and resources for use with ExoPlayer. + +## Links ## + +* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ui.*` + belong to this module. + +[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html From 9a026671301f8a6f9d987bf689b7df07360c0a28 Mon Sep 17 00:00:00 2001 From: ojw28 Date: Sun, 10 Sep 2017 18:38:40 +0100 Subject: [PATCH 0424/2472] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b21db8b13a..92b15d7c62 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ Browse the [extensions directory] and their individual READMEs for details. More information on the library and extension modules that are available from JCenter can be found on [Bintray][]. -[extensions directory][]: https://github.com/google/ExoPlayer/tree/release-v2/extensions/ +[extensions directory]: https://github.com/google/ExoPlayer/tree/release-v2/extensions/ [Bintray]: https://bintray.com/google/exoplayer ### Locally ### From 75d5adce6fd5ce9b45b74236fb7f9cd93990e8f2 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 11 Sep 2017 00:25:36 -0700 Subject: [PATCH 0425/2472] Update dependency versions ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=168194589 --- README.md | 8 +++----- build.gradle | 8 ++------ constants.gradle | 4 ++-- extensions/gvr/build.gradle | 2 +- extensions/ima/build.gradle | 6 +++--- extensions/okhttp/build.gradle | 2 +- 6 files changed, 12 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index bd261e75ff..3902ec5cbd 100644 --- a/README.md +++ b/README.md @@ -28,15 +28,13 @@ repository and depend on the modules locally. ### From JCenter ### The easiest way to get started using ExoPlayer is to add it as a gradle -dependency. You need to make sure you have the JCenter and Google Maven -repositories included in the `build.gradle` file in the root of your project: +dependency. You need to make sure you have the JCenter and Google repositories +included in the `build.gradle` file in the root of your project: ```gradle repositories { jcenter() - maven { - url "https://maven.google.com" - } + google() } ``` diff --git a/build.gradle b/build.gradle index 8ec24a6e82..e95dd83d90 100644 --- a/build.gradle +++ b/build.gradle @@ -14,9 +14,7 @@ buildscript { repositories { jcenter() - maven { - url "https://maven.google.com" - } + google() } dependencies { classpath 'com.android.tools.build:gradle:3.0.0-beta4' @@ -34,9 +32,7 @@ buildscript { allprojects { repositories { jcenter() - maven { - url "https://maven.google.com" - } + google() } project.ext { exoplayerPublishEnabled = true diff --git a/constants.gradle b/constants.gradle index 7bdd50e829..78dd343a94 100644 --- a/constants.gradle +++ b/constants.gradle @@ -21,8 +21,8 @@ project.ext { targetSdkVersion = 26 buildToolsVersion = '26' testSupportLibraryVersion = '0.5' - supportLibraryVersion = '26.0.1' - playServicesLibraryVersion = '11.0.2' + supportLibraryVersion = '26.0.2' + playServicesLibraryVersion = '11.2.0' dexmakerVersion = '1.2' mockitoVersion = '1.9.5' junitVersion = '4.12' diff --git a/extensions/gvr/build.gradle b/extensions/gvr/build.gradle index 66665576bb..8236024512 100644 --- a/extensions/gvr/build.gradle +++ b/extensions/gvr/build.gradle @@ -26,7 +26,7 @@ android { dependencies { compile project(modulePrefix + 'library-core') - compile 'com.google.vr:sdk-audio:1.60.1' + compile 'com.google.vr:sdk-audio:1.80.0' } ext { diff --git a/extensions/ima/build.gradle b/extensions/ima/build.gradle index cb69e92990..90c0a911d9 100644 --- a/extensions/ima/build.gradle +++ b/extensions/ima/build.gradle @@ -29,9 +29,9 @@ dependencies { compile project(modulePrefix + 'library-core') // This dependency is necessary to force the supportLibraryVersion of // com.android.support:support-v4 to be used. Else an older version (25.2.0) is included via: - // com.google.android.gms:play-services-ads:11.0.2 - // |-- com.google.android.gms:play-services-ads-lite:[11.0.2] -> 11.0.2 - // |-- com.google.android.gms:play-services-basement:[11.0.2] -> 11.0.2 + // com.google.android.gms:play-services-ads:11.2.0 + // |-- com.google.android.gms:play-services-ads-lite:11.2.0 + // |-- com.google.android.gms:play-services-basement:11.2.0 // |-- com.android.support:support-v4:25.2.0 compile 'com.android.support:support-v4:' + supportLibraryVersion compile 'com.google.ads.interactivemedia.v3:interactivemedia:3.7.4' diff --git a/extensions/okhttp/build.gradle b/extensions/okhttp/build.gradle index bc9e0eba3e..13bcff8a4e 100644 --- a/extensions/okhttp/build.gradle +++ b/extensions/okhttp/build.gradle @@ -31,7 +31,7 @@ android { dependencies { compile project(modulePrefix + 'library-core') - compile('com.squareup.okhttp3:okhttp:3.8.1') { + compile('com.squareup.okhttp3:okhttp:3.9.0') { exclude group: 'org.json' } } From 9a91482a1b3ded48c2c163c53e198dbc870ebd75 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 11 Sep 2017 01:50:04 -0700 Subject: [PATCH 0426/2472] Release streams in fake adaptive media period. Releasing the media period should also release the sample streams to allow resources to be cleaned up. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=168201377 --- .../exoplayer2/testutil/FakeAdaptiveMediaPeriod.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java index c8757e69cd..3dcf551943 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java @@ -53,6 +53,14 @@ public class FakeAdaptiveMediaPeriod extends FakeMediaPeriod this.durationUs = durationUs; } + @Override + public void release() { + super.release(); + for (ChunkSampleStream sampleStream : sampleStreams) { + sampleStream.release(); + } + } + @Override public void prepare(Callback callback, long positionUs) { super.prepare(callback, positionUs); From c1810575506764f7aa8ba0a4ec03f9ea2aa51a24 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 11 Sep 2017 01:57:00 -0700 Subject: [PATCH 0427/2472] Fix bug in ActionSchedule. When having a repeat() action and another subsequent action, the next action should only be scheduled once (and not repeatedly). Thus, the "next" pointer in the repeated action needs to be nulled in the first iteration to prevent repeated scheduling of the next action. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=168202212 --- .../android/exoplayer2/testutil/ActionSchedule.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java index 28e62f3057..ba76c58d11 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java @@ -18,7 +18,6 @@ package com.google.android.exoplayer2.testutil; import android.os.Handler; import android.view.Surface; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; @@ -211,7 +210,7 @@ public final class ActionSchedule { /** * Schedules a new source preparation action to be executed. - * @see ExoPlayer#prepare(MediaSource, boolean, boolean). + * @see com.google.android.exoplayer2.ExoPlayer#prepare(MediaSource, boolean, boolean). * * @return The builder, for convenience. */ @@ -350,7 +349,13 @@ public final class ActionSchedule { public void run() { action.doActionAndScheduleNext(player, trackSelector, surface, mainHandler, next); if (repeatIntervalMs != C.TIME_UNSET) { - clock.postDelayed(mainHandler, this, repeatIntervalMs); + clock.postDelayed(mainHandler, new Runnable() { + @Override + public void run() { + action.doActionAndScheduleNext(player, trackSelector, surface, mainHandler, null); + clock.postDelayed(mainHandler, this, repeatIntervalMs); + } + }, repeatIntervalMs); } } From c4bf83dd1a713376e2ba5658de3b1b782a1717ec Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 11 Sep 2017 03:03:05 -0700 Subject: [PATCH 0428/2472] Support stop() in FakeExoPlayer and release media properly. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=168209817 --- .../testutil/FakeSimpleExoPlayer.java | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java index 67a83b84e1..b56c299e78 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java @@ -69,8 +69,8 @@ public class FakeSimpleExoPlayer extends SimpleExoPlayer { return player; } - private class FakeExoPlayer implements ExoPlayer, MediaSource.Listener, MediaPeriod.Callback, - Runnable { + private static class FakeExoPlayer implements ExoPlayer, MediaSource.Listener, + MediaPeriod.Callback, Runnable { private final Renderer[] renderers; private final TrackSelector trackSelector; @@ -201,12 +201,20 @@ public class FakeSimpleExoPlayer extends SimpleExoPlayer { @Override public void stop() { - throw new UnsupportedOperationException(); + playbackHandler.post(new Runnable() { + @Override + public void run () { + playbackHandler.removeCallbacksAndMessages(null); + releaseMedia(); + changePlaybackState(Player.STATE_IDLE); + } + }); } @Override public void release() { - playbackThread.quit(); + stop(); + playbackThread.quitSafely(); } @Override @@ -536,6 +544,17 @@ public class FakeSimpleExoPlayer extends SimpleExoPlayer { }); } + private void releaseMedia() { + if (mediaSource != null) { + if (mediaPeriod != null) { + mediaSource.releasePeriod(mediaPeriod); + mediaPeriod = null; + } + mediaSource.releaseSource(); + mediaSource = null; + } + } + } } From f9661b5373f0a88b131b2bddf84b0acbb8454a25 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 11 Sep 2017 03:40:58 -0700 Subject: [PATCH 0429/2472] Use Handler callback instead of sleep() to catch InteruptedException. The sleep used to simulate data load times is ignoring InterruptedExceptions. (This is intended and in line with SystemClock.sleep()). However, when a Loader cancels an ongoing load, it uses interrupts. To be able to catch these and to immediately return from the reading data source, a handler callback is used instead of the sleep() method which allows interuptable waiting. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=168212652 --- .../com/google/android/exoplayer2/testutil/FakeDataSource.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 aacd265e45..2675e1f0d7 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 @@ -217,7 +217,7 @@ public class FakeDataSource implements DataSource { return dataSpecs; } - protected void onDataRead(int bytesRead) { + protected void onDataRead(int bytesRead) throws IOException { // Do nothing. Can be overridden. } From 0adb4502f6476f83cdbfcc8cfb9e4ba4fbde28ce Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 11 Sep 2017 11:34:44 -0700 Subject: [PATCH 0430/2472] Ignore all-zero defaultKid ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=168259911 --- .../exoplayer2/source/dash/manifest/DashManifestParser.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/dash/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 index c973db79d7..72faf21b57 100644 --- a/library/dash/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 @@ -355,7 +355,7 @@ public class DashManifestParser extends DefaultHandler if ("urn:mpeg:dash:mp4protection:2011".equals(schemeIdUri)) { schemeType = xpp.getAttributeValue(null, "value"); String defaultKid = xpp.getAttributeValue(null, "cenc:default_KID"); - if (defaultKid != null) { + if (defaultKid != null && !"00000000-0000-0000-0000-000000000000".equals(defaultKid)) { UUID keyId = UUID.fromString(defaultKid); data = PsshAtomUtil.buildPsshAtom(C.COMMON_PSSH_UUID, new UUID[] {keyId}, null); uuid = C.COMMON_PSSH_UUID; From f257300d8e770b57a22cc4a9cbf1120943be7e91 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 12 Sep 2017 06:18:03 -0700 Subject: [PATCH 0431/2472] Add tv module for USB tuner support + demo app ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=168366847 --- .gitignore | 1 + demos/tv/README.md | 4 ++ demos/tv/build.gradle | 49 +++++++++++++++++++ extensions/tv/README.md | 41 ++++++++++++++++ extensions/tv/build.gradle | 39 +++++++++++++++ .../exoplayer2/util/ParsableBitArray.java | 11 +++++ 6 files changed, 145 insertions(+) create mode 100644 demos/tv/README.md create mode 100644 demos/tv/build.gradle create mode 100644 extensions/tv/README.md create mode 100644 extensions/tv/build.gradle diff --git a/.gitignore b/.gitignore index 1146c06456..1a946e2ade 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,7 @@ proguard-project.txt # Other .DS_Store +cmake-build-debug dist tmp diff --git a/demos/tv/README.md b/demos/tv/README.md new file mode 100644 index 0000000000..8a1ab807a0 --- /dev/null +++ b/demos/tv/README.md @@ -0,0 +1,4 @@ +# TV tuner demo application # + +This folder contains a demo application that uses ExoPlayer to play broadcast +TV from USB tuners. diff --git a/demos/tv/build.gradle b/demos/tv/build.gradle new file mode 100644 index 0000000000..9e87d5e06b --- /dev/null +++ b/demos/tv/build.gradle @@ -0,0 +1,49 @@ +// 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 from: '../../constants.gradle' +apply plugin: 'com.android.application' + +android { + compileSdkVersion project.ext.compileSdkVersion + buildToolsVersion project.ext.buildToolsVersion + + defaultConfig { + minSdkVersion 21 + targetSdkVersion project.ext.targetSdkVersion + } + + buildTypes { + release { + shrinkResources true + minifyEnabled true + proguardFiles getDefaultProguardFile('proguard-android.txt') + } + debug { + jniDebuggable = true + } + } + + lintOptions { + // The demo app does not have translations. + disable 'MissingTranslation' + } + +} + +dependencies { + compile project(modulePrefix + 'library-core') + compile project(modulePrefix + 'library-ui') + compile project(modulePrefix + 'internal-extension-tv') + compile project(modulePrefix + 'extension-ffmpeg') +} diff --git a/extensions/tv/README.md b/extensions/tv/README.md new file mode 100644 index 0000000000..0deb33794f --- /dev/null +++ b/extensions/tv/README.md @@ -0,0 +1,41 @@ +# ExoPlayer TV tuner extension # + +Provides components for broadcast TV playback with ExoPlayer. + +## Links ## + +* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ext.tv.*` + belong to this extension. + +[Javadoc]: https://google.github.io/ExoPlayer/doc/reference/index.html + +## Build Instructions ## + +* Checkout ExoPlayer: + +``` +git clone https://github.com/google/ExoPlayer.git +``` + +* Set the following environment variables: + +``` +cd "" +EXOPLAYER_ROOT="$(pwd)" +TV_MODULE_PATH="${EXOPLAYER_ROOT}/extensions/tv/src/main" +``` + +* Download the [Android NDK][] and set its location in an environment variable: + +[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html + +``` +NDK_PATH="" +``` + +* Build the JNI native libraries from the command line: + +``` +cd "${TV_MODULE_PATH}"/jni && \ +${NDK_PATH}/ndk-build APP_ABI=all -j +``` diff --git a/extensions/tv/build.gradle b/extensions/tv/build.gradle new file mode 100644 index 0000000000..ee54926650 --- /dev/null +++ b/extensions/tv/build.gradle @@ -0,0 +1,39 @@ +// 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 from: '../../constants.gradle' +apply plugin: 'com.android.library' + +android { + compileSdkVersion project.ext.compileSdkVersion + buildToolsVersion project.ext.buildToolsVersion + + defaultConfig { + minSdkVersion 21 + targetSdkVersion project.ext.targetSdkVersion + } + + sourceSets.main { + jniLibs.srcDir 'src/main/libs' + jni.srcDirs = [] // Disable the automatic ndk-build call by Android Studio. + } +} + +dependencies { + compile project(modulePrefix + 'library-core') +} + +ext { + javadocTitle = 'TV tuner extension' +} +apply from: '../../javadoc_library.gradle' diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java b/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java index fdee7fb5e6..19b303484f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableBitArray.java @@ -62,6 +62,17 @@ public final class ParsableBitArray { reset(data, data.length); } + /** + * Sets this instance's data, position and limit to match the provided {@code parsableByteArray}. + * Any modifications to the underlying data array will be visible in both instances + * + * @param parsableByteArray The {@link ParsableByteArray}. + */ + public void reset(ParsableByteArray parsableByteArray) { + reset(parsableByteArray.data, parsableByteArray.limit()); + setPosition(parsableByteArray.getPosition() * 8); + } + /** * Updates the instance to wrap {@code data}, and resets the position to zero. * From 39dbb9a7fcaac16704c196187d937b99ba442fd6 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 12 Sep 2017 08:21:43 -0700 Subject: [PATCH 0432/2472] De-dupe ACTION_DOWN events Issue: #3259 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=168378650 --- .../exoplayer2/ui/PlaybackControlView.java | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java index 9bbb2fa27b..16f555ffbc 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java @@ -986,30 +986,30 @@ public class PlaybackControlView extends FrameLayout { return false; } if (event.getAction() == KeyEvent.ACTION_DOWN) { - switch (keyCode) { - case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: - fastForward(); - break; - case KeyEvent.KEYCODE_MEDIA_REWIND: - rewind(); - break; - case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: - controlDispatcher.dispatchSetPlayWhenReady(player, !player.getPlayWhenReady()); - break; - case KeyEvent.KEYCODE_MEDIA_PLAY: - controlDispatcher.dispatchSetPlayWhenReady(player, true); - break; - case KeyEvent.KEYCODE_MEDIA_PAUSE: - controlDispatcher.dispatchSetPlayWhenReady(player, false); - break; - case KeyEvent.KEYCODE_MEDIA_NEXT: - next(); - break; - case KeyEvent.KEYCODE_MEDIA_PREVIOUS: - previous(); - break; - default: - break; + if (keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD) { + fastForward(); + } else if (keyCode == KeyEvent.KEYCODE_MEDIA_REWIND) { + rewind(); + } else if (event.getRepeatCount() == 0) { + switch (keyCode) { + case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: + controlDispatcher.dispatchSetPlayWhenReady(player, !player.getPlayWhenReady()); + break; + case KeyEvent.KEYCODE_MEDIA_PLAY: + controlDispatcher.dispatchSetPlayWhenReady(player, true); + break; + case KeyEvent.KEYCODE_MEDIA_PAUSE: + controlDispatcher.dispatchSetPlayWhenReady(player, false); + break; + case KeyEvent.KEYCODE_MEDIA_NEXT: + next(); + break; + case KeyEvent.KEYCODE_MEDIA_PREVIOUS: + previous(); + break; + default: + break; + } } } return true; From 5019da3e7b499f50dadfe691af807327e79b5d7c Mon Sep 17 00:00:00 2001 From: hoangtc Date: Wed, 13 Sep 2017 07:25:41 -0700 Subject: [PATCH 0433/2472] Update ABR logic in AdaptiveTrackSelection for live streaming case In live streaming, if the playback position is very close to live edge, the buffered duration will never reach minDurationForQualityIncreaseMs, which prevents switching from ever happening. So we will provide the durationToLiveEdgeUs to AdaptiveTrackSelection in live streaming case, so it can handle this edge case. GitHub: #3017 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=168535969 --- .../AdaptiveTrackSelection.java | 70 +++++++++++++++++-- .../trackselection/FixedTrackSelection.java | 2 +- .../trackselection/RandomTrackSelection.java | 2 +- .../trackselection/TrackSelection.java | 13 ++-- .../source/dash/DefaultDashChunkSource.java | 35 ++++++++-- .../exoplayer2/source/hls/HlsChunkSource.java | 19 ++++- .../source/hls/HlsSampleStreamWrapper.java | 4 +- .../smoothstreaming/DefaultSsChunkSource.java | 19 ++++- .../exoplayer2/testutil/FakeChunkSource.java | 3 +- 9 files changed, 141 insertions(+), 26 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java index 12f5952dd0..b999164f00 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java @@ -40,6 +40,7 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { private final int maxDurationForQualityDecreaseMs; private final int minDurationToRetainAfterDiscardMs; private final float bandwidthFraction; + private final float bufferedFractionToLiveEdgeForQualityIncrease; /** * @param bandwidthMeter Provides an estimate of the currently available bandwidth. @@ -48,7 +49,9 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { this (bandwidthMeter, DEFAULT_MAX_INITIAL_BITRATE, DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS, DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS, - DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, DEFAULT_BANDWIDTH_FRACTION); + DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, + DEFAULT_BANDWIDTH_FRACTION, + DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE); } /** @@ -70,19 +73,53 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { public Factory(BandwidthMeter bandwidthMeter, int maxInitialBitrate, int minDurationForQualityIncreaseMs, int maxDurationForQualityDecreaseMs, int minDurationToRetainAfterDiscardMs, float bandwidthFraction) { + this (bandwidthMeter, maxInitialBitrate, minDurationForQualityIncreaseMs, + maxDurationForQualityDecreaseMs, minDurationToRetainAfterDiscardMs, + bandwidthFraction, DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE); + } + + /** + * @param bandwidthMeter Provides an estimate of the currently available bandwidth. + * @param maxInitialBitrate The maximum bitrate in bits per second that should be assumed + * when a bandwidth estimate is unavailable. + * @param minDurationForQualityIncreaseMs The minimum duration of buffered data required for + * the selected track to switch to one of higher quality. + * @param maxDurationForQualityDecreaseMs The maximum duration of buffered data required for + * the selected track to switch to one of lower quality. + * @param minDurationToRetainAfterDiscardMs When switching to a track of significantly higher + * quality, the selection may indicate that media already buffered at the lower quality can + * be discarded to speed up the switch. This is the minimum duration of media that must be + * retained at the lower quality. + * @param bandwidthFraction The fraction of the available bandwidth that the selection should + * consider available for use. Setting to a value less than 1 is recommended to account + * for inaccuracies in the bandwidth estimator. + * @param bufferedFractionToLiveEdgeForQualityIncrease For live streaming, the fraction of + * the duration from current playback position to the live edge that has to be buffered + * before the selected track can be switched to one of higher quality. This parameter is + * only applied when the playback position is closer to the live edge than + * {@code minDurationForQualityIncreaseMs}, which would otherwise prevent switching to a + * higher quality from happening. + */ + public Factory(BandwidthMeter bandwidthMeter, int maxInitialBitrate, + int minDurationForQualityIncreaseMs, int maxDurationForQualityDecreaseMs, + int minDurationToRetainAfterDiscardMs, float bandwidthFraction, + float bufferedFractionToLiveEdgeForQualityIncrease) { this.bandwidthMeter = bandwidthMeter; this.maxInitialBitrate = maxInitialBitrate; this.minDurationForQualityIncreaseMs = minDurationForQualityIncreaseMs; this.maxDurationForQualityDecreaseMs = maxDurationForQualityDecreaseMs; this.minDurationToRetainAfterDiscardMs = minDurationToRetainAfterDiscardMs; this.bandwidthFraction = bandwidthFraction; + this.bufferedFractionToLiveEdgeForQualityIncrease = + bufferedFractionToLiveEdgeForQualityIncrease; } @Override public AdaptiveTrackSelection createTrackSelection(TrackGroup group, int... tracks) { return new AdaptiveTrackSelection(group, tracks, bandwidthMeter, maxInitialBitrate, minDurationForQualityIncreaseMs, maxDurationForQualityDecreaseMs, - minDurationToRetainAfterDiscardMs, bandwidthFraction); + minDurationToRetainAfterDiscardMs, bandwidthFraction, + bufferedFractionToLiveEdgeForQualityIncrease); } } @@ -92,6 +129,7 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { public static final int DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS = 25000; public static final int DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS = 25000; public static final float DEFAULT_BANDWIDTH_FRACTION = 0.75f; + public static final float DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE = 0.75f; private final BandwidthMeter bandwidthMeter; private final int maxInitialBitrate; @@ -99,6 +137,7 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { private final long maxDurationForQualityDecreaseUs; private final long minDurationToRetainAfterDiscardUs; private final float bandwidthFraction; + private final float bufferedFractionToLiveEdgeForQualityIncrease; private int selectedIndex; private int reason; @@ -114,7 +153,9 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { this (group, tracks, bandwidthMeter, DEFAULT_MAX_INITIAL_BITRATE, DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS, DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS, - DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, DEFAULT_BANDWIDTH_FRACTION); + DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, + DEFAULT_BANDWIDTH_FRACTION, + DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE); } /** @@ -135,11 +176,17 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { * @param bandwidthFraction The fraction of the available bandwidth that the selection should * consider available for use. Setting to a value less than 1 is recommended to account * for inaccuracies in the bandwidth estimator. + * @param bufferedFractionToLiveEdgeForQualityIncrease For live streaming, the fraction of + * the duration from current playback position to the live edge that has to be buffered + * before the selected track can be switched to one of higher quality. This parameter is + * only applied when the playback position is closer to the live edge than + * {@code minDurationForQualityIncreaseMs}, which would otherwise prevent switching to a + * higher quality from happening. */ public AdaptiveTrackSelection(TrackGroup group, int[] tracks, BandwidthMeter bandwidthMeter, int maxInitialBitrate, long minDurationForQualityIncreaseMs, long maxDurationForQualityDecreaseMs, long minDurationToRetainAfterDiscardMs, - float bandwidthFraction) { + float bandwidthFraction, float bufferedFractionToLiveEdgeForQualityIncrease) { super(group, tracks); this.bandwidthMeter = bandwidthMeter; this.maxInitialBitrate = maxInitialBitrate; @@ -147,12 +194,14 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { this.maxDurationForQualityDecreaseUs = maxDurationForQualityDecreaseMs * 1000L; this.minDurationToRetainAfterDiscardUs = minDurationToRetainAfterDiscardMs * 1000L; this.bandwidthFraction = bandwidthFraction; + this.bufferedFractionToLiveEdgeForQualityIncrease = + bufferedFractionToLiveEdgeForQualityIncrease; selectedIndex = determineIdealSelectedIndex(Long.MIN_VALUE); reason = C.SELECTION_REASON_INITIAL; } @Override - public void updateSelectedTrack(long bufferedDurationUs) { + public void updateSelectedTrack(long bufferedDurationUs, long availableDurationUs) { long nowMs = SystemClock.elapsedRealtime(); // Stash the current selection, then make a new one. int currentSelectedIndex = selectedIndex; @@ -160,12 +209,13 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { if (selectedIndex == currentSelectedIndex) { return; } + if (!isBlacklisted(currentSelectedIndex, nowMs)) { // Revert back to the current selection if conditions are not suitable for switching. Format currentFormat = getFormat(currentSelectedIndex); Format selectedFormat = getFormat(selectedIndex); if (selectedFormat.bitrate > currentFormat.bitrate - && bufferedDurationUs < minDurationForQualityIncreaseUs) { + && bufferedDurationUs < minDurationForQualityIncreaseUs(availableDurationUs)) { // The selected track is a higher quality, but we have insufficient buffer to safely switch // up. Defer switching up for now. selectedIndex = currentSelectedIndex; @@ -251,4 +301,12 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { return lowestBitrateNonBlacklistedIndex; } + private long minDurationForQualityIncreaseUs(long availableDurationUs) { + boolean isAvailableDurationTooShort = availableDurationUs != C.TIME_UNSET + && availableDurationUs <= minDurationForQualityIncreaseUs; + return isAvailableDurationTooShort + ? (long) (availableDurationUs * bufferedFractionToLiveEdgeForQualityIncrease) + : minDurationForQualityIncreaseUs; + } + } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/FixedTrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/FixedTrackSelection.java index de1b500c61..ca43258e3f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/FixedTrackSelection.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/FixedTrackSelection.java @@ -78,7 +78,7 @@ public final class FixedTrackSelection extends BaseTrackSelection { } @Override - public void updateSelectedTrack(long bufferedDurationUs) { + public void updateSelectedTrack(long bufferedDurationUs, long availableDurationUs) { // Do nothing. } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/RandomTrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/RandomTrackSelection.java index 5c7625d6b4..b70cc8e0d5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/RandomTrackSelection.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/RandomTrackSelection.java @@ -88,7 +88,7 @@ public final class RandomTrackSelection extends BaseTrackSelection { } @Override - public void updateSelectedTrack(long bufferedDurationUs) { + public void updateSelectedTrack(long bufferedDurationUs, long availableDurationUs) { // Count the number of non-blacklisted formats. long nowMs = SystemClock.elapsedRealtime(); int nonBlacklistedFormatCount = 0; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java index fe66946a65..aeb1d1d6e3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java @@ -26,7 +26,7 @@ import java.util.List; * {@link TrackGroup}, and a possibly varying individual selected track from the subset. *

          * Tracks belonging to the subset are exposed in decreasing bandwidth order. The individual selected - * track may change as a result of calling {@link #updateSelectedTrack(long)}. + * track may change as a result of calling {@link #updateSelectedTrack(long, long)}. */ public interface TrackSelection { @@ -126,8 +126,11 @@ public interface TrackSelection { * Updates the selected track. * * @param bufferedDurationUs The duration of media currently buffered in microseconds. + * @param availableDurationUs The duration of media available for buffering from the current + * playback position, in microseconds, or {@link C#TIME_UNSET} if media can be buffered + * to the end of the current period. */ - void updateSelectedTrack(long bufferedDurationUs); + void updateSelectedTrack(long bufferedDurationUs, long availableDurationUs); /** * May be called periodically by sources that load media in discrete {@link MediaChunk}s and @@ -148,10 +151,10 @@ public interface TrackSelection { /** * Attempts to blacklist the track at the specified index in the selection, making it ineligible - * for selection by calls to {@link #updateSelectedTrack(long)} for the specified period of time. - * Blacklisting will fail if all other tracks are currently blacklisted. If blacklisting the + * for selection by calls to {@link #updateSelectedTrack(long, long)} for the specified period of + * time. Blacklisting will fail if all other tracks are currently blacklisted. If blacklisting the * currently selected track, note that it will remain selected until the next call to - * {@link #updateSelectedTrack(long)}. + * {@link #updateSelectedTrack(long, long)}. * * @param index The index of the track in the selection. * @param blacklistDurationMs The duration of time for which the track should be blacklisted, in diff --git a/library/dash/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 index c6c1461001..cd7ef6a2bf 100644 --- a/library/dash/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 @@ -95,6 +95,7 @@ public class DefaultDashChunkSource implements DashChunkSource { private int periodIndex; private IOException fatalError; private boolean missingLastSegment; + private long liveEdgeTimeUs; /** * @param manifestLoaderErrorThrower Throws errors affecting loading of manifests. @@ -130,6 +131,7 @@ public class DefaultDashChunkSource implements DashChunkSource { this.maxSegmentsPerLoad = maxSegmentsPerLoad; long periodDurationUs = manifest.getPeriodDurationUs(periodIndex); + liveEdgeTimeUs = C.TIME_UNSET; List representations = getRepresentations(); representationHolders = new RepresentationHolder[trackSelection.length()]; for (int i = 0; i < representationHolders.length; i++) { @@ -179,7 +181,8 @@ public class DefaultDashChunkSource implements DashChunkSource { } long bufferedDurationUs = previous != null ? (previous.endTimeUs - playbackPositionUs) : 0; - trackSelection.updateSelectedTrack(bufferedDurationUs); + long timeToLiveEdgeUs = resolveTimeToLiveEdgeUs(playbackPositionUs, previous == null); + trackSelection.updateSelectedTrack(bufferedDurationUs, timeToLiveEdgeUs); RepresentationHolder representationHolder = representationHolders[trackSelection.getSelectedIndex()]; @@ -203,7 +206,6 @@ public class DefaultDashChunkSource implements DashChunkSource { } } - long nowUs = getNowUnixTimeUs(); int availableSegmentCount = representationHolder.getSegmentCount(); if (availableSegmentCount == 0) { // The index doesn't define any segments. @@ -216,21 +218,23 @@ public class DefaultDashChunkSource implements DashChunkSource { if (availableSegmentCount == DashSegmentIndex.INDEX_UNBOUNDED) { // The index is itself unbounded. We need to use the current time to calculate the range of // available segments. - long liveEdgeTimeUs = nowUs - manifest.availabilityStartTime * 1000; - long periodStartUs = manifest.getPeriod(periodIndex).startMs * 1000; + long liveEdgeTimeUs = getNowUnixTimeUs() - C.msToUs(manifest.availabilityStartTime); + long periodStartUs = C.msToUs(manifest.getPeriod(periodIndex).startMs); long liveEdgeTimeInPeriodUs = liveEdgeTimeUs - periodStartUs; if (manifest.timeShiftBufferDepth != C.TIME_UNSET) { - long bufferDepthUs = manifest.timeShiftBufferDepth * 1000; + long bufferDepthUs = C.msToUs(manifest.timeShiftBufferDepth); firstAvailableSegmentNum = Math.max(firstAvailableSegmentNum, representationHolder.getSegmentNum(liveEdgeTimeInPeriodUs - bufferDepthUs)); } - // getSegmentNum(liveEdgeTimestampUs) will not be completed yet, so subtract one to get the + // getSegmentNum(liveEdgeTimeInPeriodUs) will not be completed yet, so subtract one to get the // index of the last completed segment. lastAvailableSegmentNum = representationHolder.getSegmentNum(liveEdgeTimeInPeriodUs) - 1; } else { lastAvailableSegmentNum = firstAvailableSegmentNum + availableSegmentCount - 1; } + updateLiveEdgeTimeUs(representationHolder, lastAvailableSegmentNum); + int segmentNum; if (previous == null) { segmentNum = Util.constrainValue(representationHolder.getSegmentNum(playbackPositionUs), @@ -311,6 +315,19 @@ public class DefaultDashChunkSource implements DashChunkSource { return representations; } + private void updateLiveEdgeTimeUs(RepresentationHolder representationHolder, + int lastAvailableSegmentNum) { + if (manifest.dynamic) { + DashSegmentIndex segmentIndex = representationHolder.representation.getIndex(); + long lastSegmentDurationUs = segmentIndex.getDurationUs(lastAvailableSegmentNum, + manifest.getPeriodDurationUs(periodIndex)); + liveEdgeTimeUs = segmentIndex.getTimeUs(lastAvailableSegmentNum) + + lastSegmentDurationUs; + } else { + liveEdgeTimeUs = C.TIME_UNSET; + } + } + private long getNowUnixTimeUs() { if (elapsedRealtimeOffsetMs != 0) { return (SystemClock.elapsedRealtime() + elapsedRealtimeOffsetMs) * 1000; @@ -375,6 +392,12 @@ public class DefaultDashChunkSource implements DashChunkSource { } } + private long resolveTimeToLiveEdgeUs(long playbackPositionUs, boolean isAfterPositionReset) { + boolean resolveTimeToLiveEdgePossible = manifest.dynamic + && !isAfterPositionReset && liveEdgeTimeUs != C.TIME_UNSET; + return resolveTimeToLiveEdgePossible ? liveEdgeTimeUs - playbackPositionUs : C.TIME_UNSET; + } + // Protected classes. /** diff --git a/library/hls/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 index d0161d839c..0ad9dd1a6e 100644 --- a/library/hls/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 @@ -103,6 +103,7 @@ import java.util.List; // the way in which HlsSampleStreamWrapper generates track groups. Use only index based methods // in TrackSelection to avoid unexpected behavior. private TrackSelection trackSelection; + private long liveEdgeTimeUs; /** * @param playlistTracker The {@link HlsPlaylistTracker} from which to obtain media playlists. @@ -122,6 +123,7 @@ import java.util.List; this.variants = variants; this.timestampAdjusterProvider = timestampAdjusterProvider; this.muxedCaptionFormats = muxedCaptionFormats; + liveEdgeTimeUs = C.TIME_UNSET; Format[] variantFormats = new Format[variants.length]; int[] initialTrackSelection = new int[variants.length]; for (int i = 0; i < variants.length; i++) { @@ -214,7 +216,8 @@ import java.util.List; (independentSegments ? previous.endTimeUs : previous.startTimeUs) - playbackPositionUs); // Select the variant. - trackSelection.updateSelectedTrack(bufferedDurationUs); + long timeToLiveEdgeUs = resolveTimeToLiveEdgeUs(playbackPositionUs, previous == null); + trackSelection.updateSelectedTrack(bufferedDurationUs, timeToLiveEdgeUs); int selectedVariantIndex = trackSelection.getSelectedIndexInTrackGroup(); boolean switchingVariant = oldVariantIndex != selectedVariantIndex; @@ -228,6 +231,8 @@ import java.util.List; HlsMediaPlaylist mediaPlaylist = playlistTracker.getPlaylistSnapshot(selectedUrl); independentSegments = mediaPlaylist.hasIndependentSegmentsTag; + updateLiveEdgeTimeUs(mediaPlaylist); + // Select the chunk. int chunkMediaSequence; if (previous == null || switchingVariant) { @@ -360,6 +365,16 @@ import java.util.List; // Private methods. + private long resolveTimeToLiveEdgeUs(long playbackPositionUs, boolean isAfterPositionReset) { + final boolean resolveTimeToLiveEdgePossible = !isAfterPositionReset + && liveEdgeTimeUs != C.TIME_UNSET; + return resolveTimeToLiveEdgePossible ? liveEdgeTimeUs - playbackPositionUs : C.TIME_UNSET; + } + + private void updateLiveEdgeTimeUs(HlsMediaPlaylist mediaPlaylist) { + liveEdgeTimeUs = mediaPlaylist.hasEndTag ? C.TIME_UNSET : mediaPlaylist.getEndTimeUs(); + } + private EncryptionKeyChunk newEncryptionKeyChunk(Uri keyUri, String iv, int variantIndex, int trackSelectionReason, Object trackSelectionData) { DataSpec dataSpec = new DataSpec(keyUri, 0, C.LENGTH_UNSET, null, DataSpec.FLAG_ALLOW_GZIP); @@ -409,7 +424,7 @@ import java.util.List; } @Override - public void updateSelectedTrack(long bufferedDurationUs) { + public void updateSelectedTrack(long bufferedDurationUs, long availableDurationUs) { long nowMs = SystemClock.elapsedRealtime(); if (!isBlacklisted(selectedIndex, nowMs)) { return; diff --git a/library/hls/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 index 00a3cd4a85..b844988588 100644 --- a/library/hls/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 @@ -92,7 +92,6 @@ import java.util.LinkedList; private boolean prepared; private int enabledTrackCount; private Format downstreamTrackFormat; - private int upstreamChunkUid; private boolean released; // Tracks are complicated in HLS. See documentation of buildTracks for details. @@ -255,7 +254,7 @@ import java.util.LinkedList; // may need to be discarded. boolean primarySampleQueueDirty = false; if (!seenFirstTrackSelection) { - primaryTrackSelection.updateSelectedTrack(0); + primaryTrackSelection.updateSelectedTrack(0, C.TIME_UNSET); int chunkIndex = chunkSource.getTrackGroup().indexOf(mediaChunks.getLast().trackFormat); if (primaryTrackSelection.getSelectedIndexInTrackGroup() != chunkIndex) { // This is the first selection and the chunk loaded during preparation does not match @@ -553,7 +552,6 @@ import java.util.LinkedList; * samples already queued to the wrapper. */ public void init(int chunkUid, boolean shouldSpliceIn) { - upstreamChunkUid = chunkUid; for (SampleQueue sampleQueue : sampleQueues) { sampleQueue.sourceId(chunkUid); } diff --git a/library/smoothstreaming/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 index 1069527989..ae7dfc32e8 100644 --- a/library/smoothstreaming/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 @@ -155,7 +155,8 @@ public class DefaultSsChunkSource implements SsChunkSource { } long bufferedDurationUs = previous != null ? (previous.endTimeUs - playbackPositionUs) : 0; - trackSelection.updateSelectedTrack(bufferedDurationUs); + long timeToLiveEdgeUs = resolveTimeToLiveEdgeUs(playbackPositionUs); + trackSelection.updateSelectedTrack(bufferedDurationUs, timeToLiveEdgeUs); StreamElement streamElement = manifest.streamElements[elementIndex]; if (streamElement.chunkCount == 0) { @@ -222,4 +223,20 @@ public class DefaultSsChunkSource implements SsChunkSource { extractorWrapper); } + private long resolveTimeToLiveEdgeUs(long playbackPositionUs) { + if (!manifest.isLive) { + return C.TIME_UNSET; + } + + StreamElement currentElement = manifest.streamElements[elementIndex]; + if (currentElement.chunkCount == 0) { + return C.TIME_UNSET; + } + + int lastChunkIndex = currentElement.chunkCount - 1; + long lastChunkEndTimeUs = currentElement.getStartTimeUs(lastChunkIndex) + + currentElement.getChunkDurationUs(lastChunkIndex); + return lastChunkEndTimeUs - playbackPositionUs; + } + } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeChunkSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeChunkSource.java index 41fda178d7..40c91a5a81 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeChunkSource.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeChunkSource.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.testutil; import android.net.Uri; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.source.chunk.Chunk; import com.google.android.exoplayer2.source.chunk.ChunkHolder; @@ -83,7 +84,7 @@ public final class FakeChunkSource implements ChunkSource { @Override public void getNextChunk(MediaChunk previous, long playbackPositionUs, ChunkHolder out) { long bufferedDurationUs = previous != null ? (previous.endTimeUs - playbackPositionUs) : 0; - trackSelection.updateSelectedTrack(bufferedDurationUs); + trackSelection.updateSelectedTrack(bufferedDurationUs, C.TIME_UNSET); int chunkIndex = previous == null ? dataSet.getChunkIndexByPosition(playbackPositionUs) : previous.getNextChunkIndex(); if (chunkIndex >= dataSet.getChunkCount()) { From c9591d76179c1fd9f4bd49ab442c5f7f484ee64a Mon Sep 17 00:00:00 2001 From: hoangtc Date: Wed, 13 Sep 2017 08:54:43 -0700 Subject: [PATCH 0434/2472] Added support for No-Sample Renderer. Currently our Renderer is always associated with and consume data from some SampleStreams, which were constructed from the provided MediaSource. There are use-cases, in which the users want to have simple Renderer implementation that does not consume data from SampleStream at all, but render using their custom logic at each rendering position - they mostly just need ExoPlayer to keep track of the playback position and enable/disable the renderer. This CL adds support for such Renderer by adding a TRACK_TYPE_NONE. Renderer of such type will be: - Associated with null TrackSelection as the result of track-selection operation. - Associated with EmptySampleStream. GitHub: #3212 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=168545749 --- .../java/com/google/android/exoplayer2/C.java | 4 + .../exoplayer2/ExoPlayerImplInternal.java | 145 +++++---- .../android/exoplayer2/NoSampleRenderer.java | 278 ++++++++++++++++++ .../trackselection/MappingTrackSelector.java | 20 +- .../trackselection/TrackSelectorResult.java | 23 +- .../MappingTrackSelectorTest.java | 164 ++++++++++- 6 files changed, 569 insertions(+), 65 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/C.java b/library/core/src/main/java/com/google/android/exoplayer2/C.java index e25538a062..9d4049ada9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/C.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/C.java @@ -519,6 +519,10 @@ public final class C { * A type constant for metadata tracks. */ public static final int TRACK_TYPE_METADATA = 4; + /** + * A type constant for a dummy or empty track. + */ + public static final int TRACK_TYPE_NONE = 5; /** * Applications or extensions may define custom {@code TRACK_TYPE_*} constants greater than or * equal to this value. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 5d55652f61..c0fc36964c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -21,11 +21,13 @@ import android.os.Looper; import android.os.Message; import android.os.Process; import android.os.SystemClock; +import android.support.annotation.NonNull; import android.util.Log; import android.util.Pair; import com.google.android.exoplayer2.ExoPlayer.ExoPlayerMessage; import com.google.android.exoplayer2.MediaPeriodInfoSequence.MediaPeriodInfo; import com.google.android.exoplayer2.source.ClippingMediaPeriod; +import com.google.android.exoplayer2.source.EmptySampleStream; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; @@ -1343,24 +1345,22 @@ import java.io.IOException; readingPeriodHolder.mediaPeriod.readDiscontinuity() != C.TIME_UNSET; for (int i = 0; i < renderers.length; i++) { Renderer renderer = renderers[i]; - TrackSelection oldSelection = oldTrackSelectorResult.selections.get(i); - if (oldSelection == null) { - // The renderer has no current stream and will be enabled when we play the next period. + boolean rendererWasEnabled = oldTrackSelectorResult.renderersEnabled[i]; + if (!rendererWasEnabled) { + // The renderer was disabled and will be enabled when we play the next period. } else if (initialDiscontinuity) { // The new period starts with a discontinuity, so the renderer will play out all data then // be disabled and re-enabled when it starts playing the next period. renderer.setCurrentStreamFinal(); } else if (!renderer.isCurrentStreamFinal()) { TrackSelection newSelection = newTrackSelectorResult.selections.get(i); + boolean newRendererEnabled = newTrackSelectorResult.renderersEnabled[i]; RendererConfiguration oldConfig = oldTrackSelectorResult.rendererConfigurations[i]; RendererConfiguration newConfig = newTrackSelectorResult.rendererConfigurations[i]; - if (newSelection != null && newConfig.equals(oldConfig)) { + if (newRendererEnabled && newConfig.equals(oldConfig)) { // Replace the renderer's SampleStream so the transition to playing the next period can // be seamless. - Format[] formats = new Format[newSelection.length()]; - for (int j = 0; j < formats.length; j++) { - formats[j] = newSelection.getFormat(j); - } + Format[] formats = getFormats(newSelection); renderer.replaceStream(formats, readingPeriodHolder.sampleStreams[i], readingPeriodHolder.getRendererOffset()); } else { @@ -1461,11 +1461,10 @@ import java.io.IOException; for (int i = 0; i < renderers.length; i++) { Renderer renderer = renderers[i]; rendererWasEnabledFlags[i] = renderer.getState() != Renderer.STATE_DISABLED; - TrackSelection newSelection = periodHolder.trackSelectorResult.selections.get(i); - if (newSelection != null) { + if (periodHolder.trackSelectorResult.renderersEnabled[i]) { enabledRendererCount++; } - if (rendererWasEnabledFlags[i] && (newSelection == null + if (rendererWasEnabledFlags[i] && (!periodHolder.trackSelectorResult.renderersEnabled[i] || (renderer.isCurrentStreamFinal() && renderer.getStream() == playingPeriodHolder.sampleStreams[i]))) { // The renderer should be disabled before playing the next period, either because it's not @@ -1487,49 +1486,63 @@ import java.io.IOException; enableRenderers(rendererWasEnabledFlags, enabledRendererCount); } - private void enableRenderers(boolean[] rendererWasEnabledFlags, int enabledRendererCount) + private void enableRenderers(boolean[] rendererWasEnabledFlags, int totalEnabledRendererCount) throws ExoPlaybackException { - enabledRenderers = new Renderer[enabledRendererCount]; - enabledRendererCount = 0; + enabledRenderers = new Renderer[totalEnabledRendererCount]; + int enabledRendererCount = 0; for (int i = 0; i < renderers.length; i++) { - Renderer renderer = renderers[i]; - TrackSelection newSelection = playingPeriodHolder.trackSelectorResult.selections.get(i); - if (newSelection != null) { - enabledRenderers[enabledRendererCount++] = renderer; - if (renderer.getState() == Renderer.STATE_DISABLED) { - RendererConfiguration rendererConfiguration = - playingPeriodHolder.trackSelectorResult.rendererConfigurations[i]; - // The renderer needs enabling with its new track selection. - boolean playing = playWhenReady && state == Player.STATE_READY; - // Consider as joining only if the renderer was previously disabled. - boolean joining = !rendererWasEnabledFlags[i] && playing; - // Build an array of formats contained by the selection. - Format[] formats = new Format[newSelection.length()]; - for (int j = 0; j < formats.length; j++) { - formats[j] = newSelection.getFormat(j); - } - // Enable the renderer. - renderer.enable(rendererConfiguration, formats, playingPeriodHolder.sampleStreams[i], - rendererPositionUs, joining, playingPeriodHolder.getRendererOffset()); - MediaClock mediaClock = renderer.getMediaClock(); - if (mediaClock != null) { - if (rendererMediaClock != null) { - throw ExoPlaybackException.createForUnexpected( - new IllegalStateException("Multiple renderer media clocks enabled.")); - } - rendererMediaClock = mediaClock; - rendererMediaClockSource = renderer; - rendererMediaClock.setPlaybackParameters(playbackParameters); - } - // Start the renderer if playing. - if (playing) { - renderer.start(); - } - } + if (playingPeriodHolder.trackSelectorResult.renderersEnabled[i]) { + enableRenderer(i, rendererWasEnabledFlags[i], enabledRendererCount++); } } } + private void enableRenderer(int rendererIndex, boolean wasRendererEnabled, + int enabledRendererIndex) throws ExoPlaybackException { + Renderer renderer = renderers[rendererIndex]; + enabledRenderers[enabledRendererIndex] = renderer; + if (renderer.getState() == Renderer.STATE_DISABLED) { + RendererConfiguration rendererConfiguration = + playingPeriodHolder.trackSelectorResult.rendererConfigurations[rendererIndex]; + TrackSelection newSelection = playingPeriodHolder.trackSelectorResult.selections.get( + rendererIndex); + Format[] formats = getFormats(newSelection); + // The renderer needs enabling with its new track selection. + boolean playing = playWhenReady && state == Player.STATE_READY; + // Consider as joining only if the renderer was previously disabled. + boolean joining = !wasRendererEnabled && playing; + // Enable the renderer. + renderer.enable(rendererConfiguration, formats, + playingPeriodHolder.sampleStreams[rendererIndex], rendererPositionUs, + joining, playingPeriodHolder.getRendererOffset()); + MediaClock mediaClock = renderer.getMediaClock(); + if (mediaClock != null) { + if (rendererMediaClock != null) { + throw ExoPlaybackException.createForUnexpected( + new IllegalStateException("Multiple renderer media clocks enabled.")); + } + rendererMediaClock = mediaClock; + rendererMediaClockSource = renderer; + rendererMediaClock.setPlaybackParameters(playbackParameters); + } + // Start the renderer if playing. + if (playing) { + renderer.start(); + } + } + } + + @NonNull + private static Format[] getFormats(TrackSelection newSelection) { + // Build an array of formats contained by the selection. + int length = newSelection != null ? newSelection.length() : 0; + Format[] formats = new Format[length]; + for (int i = 0; i < length; i++) { + formats[i] = newSelection.getFormat(i); + } + return formats; + } + /** * Holds a {@link MediaPeriod} with information required to play it as part of a timeline. */ @@ -1656,17 +1669,24 @@ import java.io.IOException; && trackSelectorResult.isEquivalent(periodTrackSelectorResult, i); } + // Undo the effect of previous call to associate no-sample renderers with empty tracks + // so the mediaPeriod receives back whatever it sent us before. + disassociateNoSampleRenderersWithEmptySampleStream(sampleStreams); // Disable streams on the period and get new streams for updated/newly-enabled tracks. positionUs = mediaPeriod.selectTracks(trackSelections.getAll(), mayRetainStreamFlags, sampleStreams, streamResetFlags, positionUs); + associateNoSampleRenderersWithEmptySampleStream(sampleStreams); periodTrackSelectorResult = trackSelectorResult; // Update whether we have enabled tracks and sanity check the expected streams are non-null. hasEnabledTracks = false; for (int i = 0; i < sampleStreams.length; i++) { if (sampleStreams[i] != null) { - Assertions.checkState(trackSelections.get(i) != null); - hasEnabledTracks = true; + Assertions.checkState(trackSelectorResult.renderersEnabled[i]); + // hasEnabledTracks should be true only when non-empty streams exists. + if (rendererCapabilities[i].getTrackType() != C.TRACK_TYPE_NONE) { + hasEnabledTracks = true; + } } else { Assertions.checkState(trackSelections.get(i) == null); } @@ -1690,6 +1710,31 @@ import java.io.IOException; } } + /** + * For each renderer of type {@link C#TRACK_TYPE_NONE}, we will remove the dummy + * {@link EmptySampleStream} that was associated with it. + */ + private void disassociateNoSampleRenderersWithEmptySampleStream(SampleStream[] sampleStreams) { + for (int i = 0; i < rendererCapabilities.length; i++) { + if (rendererCapabilities[i].getTrackType() == C.TRACK_TYPE_NONE) { + sampleStreams[i] = null; + } + } + } + + /** + * For each renderer of type {@link C#TRACK_TYPE_NONE} that was enabled, we will + * associate it with a dummy {@link EmptySampleStream}. + */ + private void associateNoSampleRenderersWithEmptySampleStream(SampleStream[] sampleStreams) { + for (int i = 0; i < rendererCapabilities.length; i++) { + if (rendererCapabilities[i].getTrackType() == C.TRACK_TYPE_NONE + && trackSelectorResult.renderersEnabled[i]) { + sampleStreams[i] = new EmptySampleStream(); + } + } + } + } private static final class SeekPosition { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java new file mode 100644 index 0000000000..978f4f7a97 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java @@ -0,0 +1,278 @@ +/* + * 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 com.google.android.exoplayer2.source.SampleStream; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.MediaClock; +import java.io.IOException; + +/** + * A {@link Renderer} implementation whose track type is {@link C#TRACK_TYPE_NONE} and does not + * consume data from its {@link SampleStream}. + */ +public abstract class NoSampleRenderer implements Renderer, RendererCapabilities { + + private RendererConfiguration configuration; + private int index; + private int state; + private SampleStream stream; + private boolean streamIsFinal; + + @Override + public final int getTrackType() { + return C.TRACK_TYPE_NONE; + } + + @Override + public final RendererCapabilities getCapabilities() { + return this; + } + + @Override + public final void setIndex(int index) { + this.index = index; + } + + @Override + public MediaClock getMediaClock() { + return null; + } + + @Override + public final int getState() { + return state; + } + + /** + * Replaces the {@link SampleStream} that will be associated with this renderer. + *

          + * This method may be called when the renderer is in the following states: + * {@link #STATE_DISABLED}. + * + * @param configuration The renderer configuration. + * @param formats The enabled formats. Should be empty. + * @param stream The {@link SampleStream} from which the renderer should consume. + * @param positionUs The player's current position. + * @param joining Whether this renderer is being enabled to join an ongoing playback. + * @param offsetUs The offset that should be subtracted from {@code positionUs} + * to get the playback position with respect to the media. + * @throws ExoPlaybackException If an error occurs. + */ + @Override + public final void enable(RendererConfiguration configuration, Format[] formats, + SampleStream stream, long positionUs, boolean joining, long offsetUs) + throws ExoPlaybackException { + Assertions.checkState(state == STATE_DISABLED); + this.configuration = configuration; + state = STATE_ENABLED; + onEnabled(joining); + replaceStream(formats, stream, offsetUs); + onPositionReset(positionUs, joining); + } + + @Override + public final void start() throws ExoPlaybackException { + Assertions.checkState(state == STATE_ENABLED); + state = STATE_STARTED; + onStarted(); + } + + /** + * Replaces the {@link SampleStream} that will be associated with this renderer. + *

          + * This method may be called when the renderer is in the following states: + * {@link #STATE_ENABLED}, {@link #STATE_STARTED}. + * + * @param formats The enabled formats. Should be empty. + * @param stream The {@link SampleStream} to be associated with this renderer. + * @param offsetUs The offset that should be subtracted from {@code positionUs} in + * {@link #render(long, long)} to get the playback position with respect to the media. + * @throws ExoPlaybackException If an error occurs. + */ + @Override + public final void replaceStream(Format[] formats, SampleStream stream, long offsetUs) + throws ExoPlaybackException { + Assertions.checkState(!streamIsFinal); + this.stream = stream; + onRendererOffsetChanged(offsetUs); + } + + @Override + public final SampleStream getStream() { + return stream; + } + + @Override + public final boolean hasReadStreamToEnd() { + return true; + } + + @Override + public final void setCurrentStreamFinal() { + streamIsFinal = true; + } + + @Override + public final boolean isCurrentStreamFinal() { + return streamIsFinal; + } + + @Override + public final void maybeThrowStreamError() throws IOException { + } + + @Override + public final void resetPosition(long positionUs) throws ExoPlaybackException { + streamIsFinal = false; + onPositionReset(positionUs, false); + } + + @Override + public final void stop() throws ExoPlaybackException { + Assertions.checkState(state == STATE_STARTED); + state = STATE_ENABLED; + onStopped(); + } + + @Override + public final void disable() { + Assertions.checkState(state == STATE_ENABLED); + state = STATE_DISABLED; + stream = null; + streamIsFinal = false; + onDisabled(); + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public boolean isEnded() { + return true; + } + + // RendererCapabilities implementation. + + @Override + public int supportsFormat(Format format) throws ExoPlaybackException { + return FORMAT_UNSUPPORTED_TYPE; + } + + @Override + public int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException { + return ADAPTIVE_NOT_SUPPORTED; + } + + // ExoPlayerComponent implementation. + + @Override + public void handleMessage(int what, Object object) throws ExoPlaybackException { + // Do nothing. + } + + // Methods to be overridden by subclasses. + + /** + * Called when the renderer is enabled. + *

          + * The default implementation is a no-op. + * + * @param joining Whether this renderer is being enabled to join an ongoing playback. + * @throws ExoPlaybackException If an error occurs. + */ + protected void onEnabled(boolean joining) throws ExoPlaybackException { + // Do nothing. + } + + /** + * Called when the renderer's offset has been changed. + *

          + * The default implementation is a no-op. + * + * @param offsetUs The offset that should be subtracted from {@code positionUs} in + * {@link #render(long, long)} to get the playback position with respect to the media. + * @throws ExoPlaybackException If an error occurs. + */ + protected void onRendererOffsetChanged(long offsetUs) throws ExoPlaybackException { + // Do nothing. + } + + /** + * Called when the position is reset. This occurs when the renderer is enabled after + * {@link #onRendererOffsetChanged(long)} has been called, and also when a position + * discontinuity is encountered. + *

          + * The default implementation is a no-op. + * + * @param positionUs The new playback position in microseconds. + * @param joining Whether this renderer is being enabled to join an ongoing playback. + * @throws ExoPlaybackException If an error occurs. + */ + protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException { + // Do nothing. + } + + /** + * Called when the renderer is started. + *

          + * The default implementation is a no-op. + * + * @throws ExoPlaybackException If an error occurs. + */ + protected void onStarted() throws ExoPlaybackException { + // Do nothing. + } + + /** + * Called when the renderer is stopped. + *

          + * The default implementation is a no-op. + * + * @throws ExoPlaybackException If an error occurs. + */ + protected void onStopped() throws ExoPlaybackException { + // Do nothing. + } + + /** + * Called when the renderer is disabled. + *

          + * The default implementation is a no-op. + */ + protected void onDisabled() { + // Do nothing. + } + + // Methods to be called by subclasses. + + /** + * Returns the configuration set when the renderer was most recently enabled. + */ + protected final RendererConfiguration getConfiguration() { + return configuration; + } + + /** + * Returns the index of the renderer within the player. + */ + protected final int getIndex() { + return index; + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java index d518b5a6be..5d120990fc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java @@ -573,6 +573,8 @@ public abstract class MappingTrackSelector extends TrackSelector { } } + boolean[] rendererEnabled = determineEnabledRenderers(rendererCapabilities, trackSelections); + // Package up the track information and selections. MappedTrackInfo mappedTrackInfo = new MappedTrackInfo(rendererTrackTypes, rendererTrackGroupArrays, mixedMimeTypeAdaptationSupport, rendererFormatSupports, @@ -583,14 +585,26 @@ public abstract class MappingTrackSelector extends TrackSelector { RendererConfiguration[] rendererConfigurations = new RendererConfiguration[rendererCapabilities.length]; for (int i = 0; i < rendererCapabilities.length; i++) { - rendererConfigurations[i] = trackSelections[i] != null ? RendererConfiguration.DEFAULT : null; + rendererConfigurations[i] = rendererEnabled[i] ? RendererConfiguration.DEFAULT : null; } // Configure audio and video renderers to use tunneling if appropriate. maybeConfigureRenderersForTunneling(rendererCapabilities, rendererTrackGroupArrays, rendererFormatSupports, rendererConfigurations, trackSelections, tunnelingAudioSessionId); - return new TrackSelectorResult(trackGroups, new TrackSelectionArray(trackSelections), - mappedTrackInfo, rendererConfigurations); + return new TrackSelectorResult(trackGroups, rendererEnabled, + new TrackSelectionArray(trackSelections), mappedTrackInfo, rendererConfigurations); + } + + private boolean[] determineEnabledRenderers(RendererCapabilities[] rendererCapabilities, + TrackSelection[] trackSelections) { + boolean[] rendererEnabled = new boolean[trackSelections.length]; + for (int i = 0; i < rendererEnabled.length; i++) { + boolean forceRendererDisabled = rendererDisabledFlags.get(i); + rendererEnabled[i] = !forceRendererDisabled + && (rendererCapabilities[i].getTrackType() == C.TRACK_TYPE_NONE + || trackSelections[i] != null); + } + return rendererEnabled; } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java index cab9a689be..801f5b9584 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java @@ -28,6 +28,10 @@ public final class TrackSelectorResult { * The track groups that were provided to the {@link TrackSelector}. */ public final TrackGroupArray groups; + /** + * An array containing whether each renderer is enabled after the track selection operation. + */ + public final boolean[] renderersEnabled; /** * A {@link TrackSelectionArray} containing the track selection for each renderer. */ @@ -38,21 +42,25 @@ public final class TrackSelectorResult { */ public final Object info; /** - * A {@link RendererConfiguration} for each renderer, to be used with the selections. + * A {@link RendererConfiguration} for each enabled renderer, to be used with the selections. */ public final RendererConfiguration[] rendererConfigurations; /** * @param groups The track groups provided to the {@link TrackSelector}. + * @param renderersEnabled An array containing whether each renderer is enabled after the track + * selection operation. * @param selections A {@link TrackSelectionArray} containing the selection for each renderer. * @param info An opaque object that will be returned to * {@link TrackSelector#onSelectionActivated(Object)} should the selection be activated. - * @param rendererConfigurations A {@link RendererConfiguration} for each renderer, to be used - * with the selections. + * @param rendererConfigurations A {@link RendererConfiguration} for each enabled renderer, + * to be used with the selections. */ - public TrackSelectorResult(TrackGroupArray groups, TrackSelectionArray selections, Object info, + public TrackSelectorResult(TrackGroupArray groups, boolean[] renderersEnabled, + TrackSelectionArray selections, Object info, RendererConfiguration[] rendererConfigurations) { this.groups = groups; + this.renderersEnabled = renderersEnabled; this.selections = selections; this.info = info; this.rendererConfigurations = rendererConfigurations; @@ -79,8 +87,8 @@ public final class TrackSelectorResult { /** * Returns whether this result is equivalent to {@code other} for the renderer at the given index. - * The results are equivalent if they have equal track selections and configurations for the - * renderer. + * The results are equivalent if they have equal renderersEnabled array, track selections, and + * configurations for the renderer. * * @param other The other {@link TrackSelectorResult}. May be null, in which case {@code false} * will be returned. @@ -92,7 +100,8 @@ public final class TrackSelectorResult { if (other == null) { return false; } - return Util.areEqual(selections.get(index), other.selections.get(index)) + return renderersEnabled[index] == other.renderersEnabled[index] + && Util.areEqual(selections.get(index), other.selections.get(index)) && Util.areEqual(rendererConfigurations[index], other.rendererConfigurations[index]); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/MappingTrackSelectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/MappingTrackSelectorTest.java index cffc530354..b9ea0087c7 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/MappingTrackSelectorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/MappingTrackSelectorTest.java @@ -15,15 +15,18 @@ */ package com.google.android.exoplayer2.trackselection; +import static com.google.android.exoplayer2.RendererConfiguration.DEFAULT; import static com.google.common.truth.Truth.assertThat; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.RendererCapabilities; +import com.google.android.exoplayer2.RendererConfiguration; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.util.MimeTypes; +import java.util.Arrays; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; @@ -40,9 +43,15 @@ public final class MappingTrackSelectorTest { new FakeRendererCapabilities(C.TRACK_TYPE_VIDEO); private static final RendererCapabilities AUDIO_CAPABILITIES = new FakeRendererCapabilities(C.TRACK_TYPE_AUDIO); + private static final RendererCapabilities NO_SAMPLE_CAPABILITIES = + new FakeRendererCapabilities(C.TRACK_TYPE_NONE); private static final RendererCapabilities[] RENDERER_CAPABILITIES = new RendererCapabilities[] { VIDEO_CAPABILITIES, AUDIO_CAPABILITIES }; + private static final RendererCapabilities[] RENDERER_CAPABILITIES_WITH_NO_SAMPLE_RENDERER = + new RendererCapabilities[] { + VIDEO_CAPABILITIES, AUDIO_CAPABILITIES, NO_SAMPLE_CAPABILITIES + }; private static final TrackGroup VIDEO_TRACK_GROUP = new TrackGroup( Format.createVideoSampleFormat("video", MimeTypes.VIDEO_H264, null, Format.NO_VALUE, @@ -58,6 +67,13 @@ public final class MappingTrackSelectorTest { new FixedTrackSelection(AUDIO_TRACK_GROUP, 0) }; + private static final TrackSelection[] TRACK_SELECTIONS_WITH_NO_SAMPLE_RENDERER = + new TrackSelection[] { + new FixedTrackSelection(VIDEO_TRACK_GROUP, 0), + new FixedTrackSelection(AUDIO_TRACK_GROUP, 0), + null + }; + /** * Tests that the video and audio track groups are mapped onto the correct renderers. */ @@ -104,10 +120,14 @@ public final class MappingTrackSelectorTest { */ @Test public void testSelectTracks() throws ExoPlaybackException { - FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector(TRACK_SELECTIONS); + FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector( + TRACK_SELECTIONS); TrackSelectorResult result = trackSelector.selectTracks(RENDERER_CAPABILITIES, TRACK_GROUPS); assertThat(result.selections.get(0)).isEqualTo(TRACK_SELECTIONS[0]); assertThat(result.selections.get(1)).isEqualTo(TRACK_SELECTIONS[1]); + assertThat(new boolean[] {true, true}).isEqualTo(result.renderersEnabled); + assertThat(new RendererConfiguration[] {DEFAULT, DEFAULT}) + .isEqualTo(result.rendererConfigurations); } /** @@ -115,11 +135,15 @@ public final class MappingTrackSelectorTest { */ @Test public void testSelectTracksWithNullOverride() throws ExoPlaybackException { - FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector(TRACK_SELECTIONS); + FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector( + TRACK_SELECTIONS); trackSelector.setSelectionOverride(0, new TrackGroupArray(VIDEO_TRACK_GROUP), null); TrackSelectorResult result = trackSelector.selectTracks(RENDERER_CAPABILITIES, TRACK_GROUPS); assertThat(result.selections.get(0)).isNull(); assertThat(result.selections.get(1)).isEqualTo(TRACK_SELECTIONS[1]); + assertThat(new boolean[] {false, true}).isEqualTo(result.renderersEnabled); + assertThat(new RendererConfiguration[] {null, DEFAULT}) + .isEqualTo(result.rendererConfigurations); } /** @@ -127,12 +151,16 @@ public final class MappingTrackSelectorTest { */ @Test public void testSelectTracksWithClearedNullOverride() throws ExoPlaybackException { - FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector(TRACK_SELECTIONS); + FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector( + TRACK_SELECTIONS); trackSelector.setSelectionOverride(0, new TrackGroupArray(VIDEO_TRACK_GROUP), null); trackSelector.clearSelectionOverride(0, new TrackGroupArray(VIDEO_TRACK_GROUP)); TrackSelectorResult result = trackSelector.selectTracks(RENDERER_CAPABILITIES, TRACK_GROUPS); assertThat(result.selections.get(0)).isEqualTo(TRACK_SELECTIONS[0]); assertThat(result.selections.get(1)).isEqualTo(TRACK_SELECTIONS[1]); + assertThat(new boolean[] {true, true}).isEqualTo(result.renderersEnabled); + assertThat(new RendererConfiguration[] {DEFAULT, DEFAULT}) + .isEqualTo(result.rendererConfigurations); } /** @@ -140,12 +168,133 @@ public final class MappingTrackSelectorTest { */ @Test public void testSelectTracksWithNullOverrideForDifferentTracks() throws ExoPlaybackException { - FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector(TRACK_SELECTIONS); + FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector( + TRACK_SELECTIONS); trackSelector.setSelectionOverride(0, new TrackGroupArray(VIDEO_TRACK_GROUP), null); TrackSelectorResult result = trackSelector.selectTracks(RENDERER_CAPABILITIES, new TrackGroupArray(VIDEO_TRACK_GROUP, AUDIO_TRACK_GROUP, VIDEO_TRACK_GROUP)); assertThat(result.selections.get(0)).isEqualTo(TRACK_SELECTIONS[0]); assertThat(result.selections.get(1)).isEqualTo(TRACK_SELECTIONS[1]); + assertThat(new boolean[] {true, true}).isEqualTo(result.renderersEnabled); + assertThat(new RendererConfiguration[] {DEFAULT, DEFAULT}) + .isEqualTo(result.rendererConfigurations); + } + + /** + * Tests the result of {@link MappingTrackSelector#selectTracks(RendererCapabilities[], + * TrackGroupArray[], int[][][])} is propagated correctly to the result of + * {@link MappingTrackSelector#selectTracks(RendererCapabilities[], TrackGroupArray)} + * when there is no-sample renderer. + */ + @Test + public void testSelectTracksWithNoSampleRenderer() throws ExoPlaybackException { + TrackSelection[] expectedTrackSelection = TRACK_SELECTIONS_WITH_NO_SAMPLE_RENDERER; + FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector(expectedTrackSelection); + TrackSelectorResult result = trackSelector.selectTracks( + RENDERER_CAPABILITIES_WITH_NO_SAMPLE_RENDERER, TRACK_GROUPS); + assertThat(result.selections.get(0)).isEqualTo(expectedTrackSelection[0]); + assertThat(result.selections.get(1)).isEqualTo(expectedTrackSelection[1]); + assertThat(result.selections.get(2)).isNull(); + assertThat(new boolean[] {true, true, true}).isEqualTo(result.renderersEnabled); + assertThat(new RendererConfiguration[] {DEFAULT, DEFAULT, DEFAULT}) + .isEqualTo(result.rendererConfigurations); + } + + /** + * Tests that a null override clears a track selection when there is no-sample renderer. + */ + @Test + public void testSelectTracksWithNoSampleRendererWithNullOverride() throws ExoPlaybackException { + TrackSelection[] expectedTrackSelection = TRACK_SELECTIONS_WITH_NO_SAMPLE_RENDERER; + FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector(expectedTrackSelection); + trackSelector.setSelectionOverride(0, new TrackGroupArray(VIDEO_TRACK_GROUP), null); + TrackSelectorResult result = trackSelector.selectTracks( + RENDERER_CAPABILITIES_WITH_NO_SAMPLE_RENDERER, TRACK_GROUPS); + assertThat(result.selections.get(0)).isNull(); + assertThat(result.selections.get(1)).isEqualTo(expectedTrackSelection[1]); + assertThat(result.selections.get(2)).isNull(); + assertThat(new boolean[] {false, true, true}).isEqualTo(result.renderersEnabled); + assertThat(new RendererConfiguration[] {null, DEFAULT, DEFAULT}) + .isEqualTo(result.rendererConfigurations); + } + + /** + * Tests that a null override can be cleared when there is no-sample renderer. + */ + @Test + public void testSelectTracksWithNoSampleRendererWithClearedNullOverride() + throws ExoPlaybackException { + TrackSelection[] expectedTrackSelection = TRACK_SELECTIONS_WITH_NO_SAMPLE_RENDERER; + FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector(expectedTrackSelection); + trackSelector.setSelectionOverride(0, new TrackGroupArray(VIDEO_TRACK_GROUP), null); + trackSelector.clearSelectionOverride(0, new TrackGroupArray(VIDEO_TRACK_GROUP)); + TrackSelectorResult result = trackSelector.selectTracks( + RENDERER_CAPABILITIES_WITH_NO_SAMPLE_RENDERER, TRACK_GROUPS); + assertThat(result.selections.get(0)).isEqualTo(expectedTrackSelection[0]); + assertThat(result.selections.get(1)).isEqualTo(expectedTrackSelection[1]); + assertThat(result.selections.get(2)).isNull(); + assertThat(new boolean[] {true, true, true}).isEqualTo(result.renderersEnabled); + assertThat(new RendererConfiguration[] {DEFAULT, DEFAULT, DEFAULT}) + .isEqualTo(result.rendererConfigurations); + } + + /** + * Tests that an override is not applied for a different set of available track groups + * when there is no-sample renderer. + */ + @Test + public void testSelectTracksWithNoSampleRendererWithNullOverrideForDifferentTracks() + throws ExoPlaybackException { + TrackSelection[] expectedTrackSelection = TRACK_SELECTIONS_WITH_NO_SAMPLE_RENDERER; + FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector(expectedTrackSelection); + trackSelector.setSelectionOverride(0, new TrackGroupArray(VIDEO_TRACK_GROUP), null); + TrackSelectorResult result = trackSelector.selectTracks( + RENDERER_CAPABILITIES_WITH_NO_SAMPLE_RENDERER, + new TrackGroupArray(VIDEO_TRACK_GROUP, AUDIO_TRACK_GROUP, VIDEO_TRACK_GROUP)); + assertThat(result.selections.get(0)).isEqualTo(expectedTrackSelection[0]); + assertThat(result.selections.get(1)).isEqualTo(expectedTrackSelection[1]); + assertThat(result.selections.get(2)).isNull(); + assertThat(new boolean[] {true, true, true}).isEqualTo(result.renderersEnabled); + assertThat(new RendererConfiguration[] {DEFAULT, DEFAULT, DEFAULT}) + .isEqualTo(result.rendererConfigurations); + } + + /** + * Tests that disabling another renderer works when there is no-sample renderer. + */ + @Test + public void testSelectTracksDisablingNormalRendererWithNoSampleRenderer() + throws ExoPlaybackException { + TrackSelection[] expectedTrackSelection = TRACK_SELECTIONS_WITH_NO_SAMPLE_RENDERER; + FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector(expectedTrackSelection); + trackSelector.setRendererDisabled(0, true); + TrackSelectorResult result = trackSelector.selectTracks( + RENDERER_CAPABILITIES_WITH_NO_SAMPLE_RENDERER, TRACK_GROUPS); + assertThat(result.selections.get(0)).isNull(); + assertThat(result.selections.get(1)).isEqualTo(expectedTrackSelection[1]); + assertThat(result.selections.get(2)).isNull(); + assertThat(new boolean[] {false, true, true}).isEqualTo(result.renderersEnabled); + assertThat(new RendererConfiguration[] {null, DEFAULT, DEFAULT}) + .isEqualTo(result.rendererConfigurations); + } + + /** + * Tests that disabling no-sample renderer work. + */ + @Test + public void testSelectTracksDisablingNoSampleRenderer() + throws ExoPlaybackException { + TrackSelection[] expectedTrackSelection = TRACK_SELECTIONS_WITH_NO_SAMPLE_RENDERER; + FakeMappingTrackSelector trackSelector = new FakeMappingTrackSelector(expectedTrackSelection); + trackSelector.setRendererDisabled(2, true); + TrackSelectorResult result = trackSelector.selectTracks( + RENDERER_CAPABILITIES_WITH_NO_SAMPLE_RENDERER, TRACK_GROUPS); + assertThat(result.selections.get(0)).isEqualTo(expectedTrackSelection[0]); + assertThat(result.selections.get(1)).isEqualTo(expectedTrackSelection[1]); + assertThat(result.selections.get(2)).isNull(); + assertThat(new boolean[] {true, true, false}).isEqualTo(result.renderersEnabled); + assertThat(new RendererConfiguration[] {DEFAULT, DEFAULT, null}) + .isEqualTo(result.rendererConfigurations); } /** @@ -166,7 +315,12 @@ public final class MappingTrackSelectorTest { TrackGroupArray[] rendererTrackGroupArrays, int[][][] rendererFormatSupports) throws ExoPlaybackException { lastRendererTrackGroupArrays = rendererTrackGroupArrays; - return result == null ? new TrackSelection[rendererCapabilities.length] : result; + TrackSelection[] trackSelectionResult = new TrackSelection[rendererCapabilities.length]; + return result == null ? trackSelectionResult + // return a copy of the provided result, because MappingTrackSelector + // might modify the returned array here, and we don't want that to affect + // the original array. + : Arrays.asList(result).toArray(trackSelectionResult); } public void assertMappedTrackGroups(int rendererIndex, TrackGroup... expected) { From 872cbec9e13391da31135bf6e1fad280f41c5458 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 13 Sep 2017 09:08:43 -0700 Subject: [PATCH 0435/2472] Add TrimmingAudioProcessor for gapless Remove gapless functionality that relies on MediaCodec, and implement this in an AudioProcessor instead. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=168547487 --- .../com/google/android/exoplayer2/Format.java | 2 - .../android/exoplayer2/audio/AudioTrack.java | 60 +++--- .../audio/ChannelMappingAudioProcessor.java | 2 +- .../audio/MediaCodecAudioRenderer.java | 9 +- .../audio/SimpleDecoderAudioRenderer.java | 7 +- .../audio/TrimmingAudioProcessor.java | 180 ++++++++++++++++++ 6 files changed, 221 insertions(+), 39 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/audio/TrimmingAudioProcessor.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Format.java b/library/core/src/main/java/com/google/android/exoplayer2/Format.java index c6be2e2eba..ba68d6de33 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Format.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Format.java @@ -529,8 +529,6 @@ public final class Format implements Parcelable { maybeSetIntegerV16(format, "rotation-degrees", rotationDegrees); maybeSetIntegerV16(format, MediaFormat.KEY_CHANNEL_COUNT, channelCount); maybeSetIntegerV16(format, MediaFormat.KEY_SAMPLE_RATE, sampleRate); - maybeSetIntegerV16(format, "encoder-delay", encoderDelay); - maybeSetIntegerV16(format, "encoder-padding", encoderPadding); for (int i = 0; i < initializationData.size(); i++) { format.setByteBuffer("csd-" + i, ByteBuffer.wrap(initializationData.get(i))); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java index d7ebd69fbf..a2d061ac84 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java @@ -40,21 +40,23 @@ import java.util.LinkedList; * playback position smoothing, non-blocking writes and reconfiguration. *

          * Before starting playback, specify the input format by calling - * {@link #configure(String, int, int, int, int)}. Optionally call {@link #setAudioSessionId(int)}, - * {@link #setAudioAttributes(AudioAttributes)}, {@link #enableTunnelingV21(int)} and - * {@link #disableTunneling()} to configure audio playback. These methods may be called after - * writing data to the track, in which case it will be reinitialized as required. + * {@link #configure(String, int, int, int, int, int[], int, int)}. Optionally call + * {@link #setAudioSessionId(int)}, {@link #setAudioAttributes(AudioAttributes)}, + * {@link #enableTunnelingV21(int)} and {@link #disableTunneling()} to configure audio playback. + * These methods may be called after writing data to the track, in which case it will be + * reinitialized as required. *

          * Call {@link #handleBuffer(ByteBuffer, long)} to write data, and {@link #handleDiscontinuity()} * when the data being fed is discontinuous. Call {@link #play()} to start playing the written data. *

          - * Call {@link #configure(String, int, int, int, int)} whenever the input format changes. The track - * will be reinitialized on the next call to {@link #handleBuffer(ByteBuffer, long)}. + * Call {@link #configure(String, int, int, int, int, int[], int, int)} whenever the input format + * changes. The track will be reinitialized on the next call to + * {@link #handleBuffer(ByteBuffer, long)}. *

          * Calling {@link #reset()} releases the underlying {@link android.media.AudioTrack} (and so does - * calling {@link #configure(String, int, int, int, int)} unless the format is unchanged). It is - * safe to call {@link #handleBuffer(ByteBuffer, long)} after {@link #reset()} without calling - * {@link #configure(String, int, int, int, int)}. + * calling {@link #configure(String, int, int, int, int, int[], int, int)} unless the format is + * unchanged). It is safe to call {@link #handleBuffer(ByteBuffer, long)} after {@link #reset()} + * without calling {@link #configure(String, int, int, int, int, int[], int, int)}. *

          * Call {@link #playToEndOfStream()} repeatedly to play out all data when no more input buffers will * be provided via {@link #handleBuffer(ByteBuffer, long)} until the next {@link #reset}. Call @@ -280,6 +282,7 @@ public final class AudioTrack { @Nullable private final AudioCapabilities audioCapabilities; private final ChannelMappingAudioProcessor channelMappingAudioProcessor; + private final TrimmingAudioProcessor trimmingAudioProcessor; private final SonicAudioProcessor sonicAudioProcessor; private final AudioProcessor[] availableAudioProcessors; private final Listener listener; @@ -375,12 +378,14 @@ public final class AudioTrack { audioTrackUtil = new AudioTrackUtil(); } channelMappingAudioProcessor = new ChannelMappingAudioProcessor(); + trimmingAudioProcessor = new TrimmingAudioProcessor(); sonicAudioProcessor = new SonicAudioProcessor(); - availableAudioProcessors = new AudioProcessor[3 + audioProcessors.length]; + availableAudioProcessors = new AudioProcessor[4 + audioProcessors.length]; availableAudioProcessors[0] = new ResamplingAudioProcessor(); availableAudioProcessors[1] = channelMappingAudioProcessor; - System.arraycopy(audioProcessors, 0, availableAudioProcessors, 2, audioProcessors.length); - availableAudioProcessors[2 + audioProcessors.length] = sonicAudioProcessor; + availableAudioProcessors[2] = trimmingAudioProcessor; + System.arraycopy(audioProcessors, 0, availableAudioProcessors, 3, audioProcessors.length); + availableAudioProcessors[3 + audioProcessors.length] = sonicAudioProcessor; playheadOffsets = new long[MAX_PLAYHEAD_OFFSET_COUNT]; volume = 1.0f; startMediaTimeState = START_NOT_SET; @@ -450,24 +455,6 @@ public final class AudioTrack { return startMediaTimeUs + applySpeedup(positionUs); } - /** - * Configures (or reconfigures) the audio track. - * - * @param mimeType The mime type. - * @param channelCount The number of channels. - * @param sampleRate The sample rate in Hz. - * @param pcmEncoding For PCM formats, the encoding used. One of {@link C#ENCODING_PCM_16BIT}, - * {@link C#ENCODING_PCM_16BIT}, {@link C#ENCODING_PCM_24BIT} and - * {@link C#ENCODING_PCM_32BIT}. - * @param specifiedBufferSize A specific size for the playback buffer in bytes, or 0 to infer a - * suitable buffer size automatically. - * @throws ConfigurationException If an error occurs configuring the track. - */ - public void configure(String mimeType, int channelCount, int sampleRate, - @C.PcmEncoding int pcmEncoding, int specifiedBufferSize) throws ConfigurationException { - configure(mimeType, channelCount, sampleRate, pcmEncoding, specifiedBufferSize, null); - } - /** * Configures (or reconfigures) the audio track. * @@ -484,16 +471,22 @@ public final class AudioTrack { * input unchanged. Otherwise, the element at index {@code i} specifies index of the input * channel to map to output channel {@code i} when preprocessing input buffers. After the * map is applied the audio data will have {@code outputChannels.length} channels. + * @param trimStartSamples The number of audio samples to trim from the start of data written to + * the track after this call. + * @param trimEndSamples The number of audio samples to trim from data written to the track + * immediately preceding the next call to {@link #reset()} or + * {@link #configure(String, int, int, int, int, int[], int, int)}. * @throws ConfigurationException If an error occurs configuring the track. */ public void configure(String mimeType, int channelCount, int sampleRate, - @C.PcmEncoding int pcmEncoding, int specifiedBufferSize, int[] outputChannels) - throws ConfigurationException { + @C.PcmEncoding int pcmEncoding, int specifiedBufferSize, @Nullable int[] outputChannels, + int trimStartSamples, int trimEndSamples) throws ConfigurationException { boolean passthrough = !MimeTypes.AUDIO_RAW.equals(mimeType); @C.Encoding int encoding = passthrough ? getEncodingForMimeType(mimeType) : pcmEncoding; boolean flush = false; if (!passthrough) { pcmFrameSize = Util.getPcmFrameSize(pcmEncoding, channelCount); + trimmingAudioProcessor.setTrimSampleCount(trimStartSamples, trimEndSamples); channelMappingAudioProcessor.setChannelMap(outputChannels); for (AudioProcessor audioProcessor : availableAudioProcessors) { try { @@ -689,7 +682,8 @@ public final class AudioTrack { * Returns whether the data was handled in full. If the data was not handled in full then the same * {@link ByteBuffer} must be provided to subsequent calls until it has been fully consumed, * except in the case of an interleaving call to {@link #reset()} (or an interleaving call to - * {@link #configure(String, int, int, int, int)} that caused the track to be reset). + * {@link #configure(String, int, int, int, int, int[], int, int)} that caused the track to be + * reset). * * @param buffer The buffer containing audio data. * @param presentationTimeUs The presentation timestamp of the buffer in microseconds. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java index b755776f1e..ef85985f1f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java @@ -52,7 +52,7 @@ import java.util.Arrays; * Resets the channel mapping. After calling this method, call {@link #configure(int, int, int)} * to start using the new channel map. * - * @see AudioTrack#configure(String, int, int, int, int, int[]) + * @see AudioTrack#configure(String, int, int, int, int, int[], int, int) */ public void setChannelMap(int[] outputChannels) { pendingOutputChannels = outputChannels; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index 7f157e5866..cbb3a4944d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java @@ -53,6 +53,8 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media private android.media.MediaFormat passthroughMediaFormat; private int pcmEncoding; private int channelCount; + private int encoderDelay; + private int encoderPadding; private long currentPositionUs; private boolean allowPositionDiscontinuity; @@ -134,8 +136,8 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media @Nullable AudioRendererEventListener eventListener, @Nullable AudioCapabilities audioCapabilities, AudioProcessor... audioProcessors) { super(C.TRACK_TYPE_AUDIO, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys); - audioTrack = new AudioTrack(audioCapabilities, audioProcessors, new AudioTrackListener()); eventDispatcher = new EventDispatcher(eventHandler, eventListener); + audioTrack = new AudioTrack(audioCapabilities, audioProcessors, new AudioTrackListener()); } @Override @@ -240,6 +242,8 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media pcmEncoding = MimeTypes.AUDIO_RAW.equals(newFormat.sampleMimeType) ? newFormat.pcmEncoding : C.ENCODING_PCM_16BIT; channelCount = newFormat.channelCount; + encoderDelay = newFormat.encoderDelay != Format.NO_VALUE ? newFormat.encoderDelay : 0; + encoderPadding = newFormat.encoderPadding != Format.NO_VALUE ? newFormat.encoderPadding : 0; } @Override @@ -262,7 +266,8 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } try { - audioTrack.configure(mimeType, channelCount, sampleRate, pcmEncoding, 0, channelMap); + audioTrack.configure(mimeType, channelCount, sampleRate, pcmEncoding, 0, channelMap, + encoderDelay, encoderPadding); } catch (AudioTrack.ConfigurationException e) { throw ExoPlaybackException.createForRenderer(e, getIndex()); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java index 012f06da39..557421e4b3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java @@ -78,6 +78,8 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements private DecoderCounters decoderCounters; private Format inputFormat; + private int encoderDelay; + private int encoderPadding; private SimpleDecoder decoder; private DecoderInputBuffer inputBuffer; @@ -308,7 +310,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements if (audioTrackNeedsConfigure) { Format outputFormat = getOutputFormat(); audioTrack.configure(outputFormat.sampleMimeType, outputFormat.channelCount, - outputFormat.sampleRate, outputFormat.pcmEncoding, 0); + outputFormat.sampleRate, outputFormat.pcmEncoding, 0, null, encoderDelay, encoderPadding); audioTrackNeedsConfigure = false; } @@ -587,6 +589,9 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements audioTrackNeedsConfigure = true; } + encoderDelay = newFormat.encoderDelay == Format.NO_VALUE ? 0 : newFormat.encoderDelay; + encoderPadding = newFormat.encoderPadding == Format.NO_VALUE ? 0 : newFormat.encoderPadding; + eventDispatcher.inputFormatChanged(newFormat); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/TrimmingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/TrimmingAudioProcessor.java new file mode 100644 index 0000000000..c66cbf4882 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/TrimmingAudioProcessor.java @@ -0,0 +1,180 @@ +/* + * 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; + +/** + * Audio processor for trimming samples from the start/end of data. + */ +/* package */ final class TrimmingAudioProcessor implements AudioProcessor { + + private boolean isActive; + private int trimStartSamples; + private int trimEndSamples; + private int channelCount; + + private int pendingTrimStartBytes; + private ByteBuffer buffer; + private ByteBuffer outputBuffer; + private byte[] endBuffer; + private int endBufferSize; + private boolean inputEnded; + + /** + * Creates a new audio processor for trimming samples from the start/end of data. + */ + public TrimmingAudioProcessor() { + buffer = EMPTY_BUFFER; + outputBuffer = EMPTY_BUFFER; + channelCount = Format.NO_VALUE; + } + + /** + * Sets the number of audio samples to trim from the start and end of audio passed to this + * processor. After calling this method, call {@link #configure(int, int, int)} to apply the new + * trimming sample counts. + * + * @param trimStartSamples The number of audio samples to trim from the start of audio. + * @param trimEndSamples The number of audio samples to trim from the end of audio. + * @see AudioTrack#configure(String, int, int, int, int, int[], int, int) + */ + public void setTrimSampleCount(int trimStartSamples, int trimEndSamples) { + this.trimStartSamples = trimStartSamples; + this.trimEndSamples = trimEndSamples; + } + + @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); + } + this.channelCount = channelCount; + endBuffer = new byte[trimEndSamples * channelCount * 2]; + endBufferSize = 0; + pendingTrimStartBytes = trimStartSamples * channelCount * 2; + boolean wasActive = isActive; + isActive = trimStartSamples != 0 || trimEndSamples != 0; + return wasActive != isActive; + } + + @Override + public boolean isActive() { + return isActive; + } + + @Override + public int getOutputChannelCount() { + return channelCount; + } + + @Override + public int getOutputEncoding() { + return C.ENCODING_PCM_16BIT; + } + + @Override + public void queueInput(ByteBuffer inputBuffer) { + int position = inputBuffer.position(); + int limit = inputBuffer.limit(); + int remaining = limit - position; + + // Trim any pending start bytes from the input buffer. + int trimBytes = Math.min(remaining, pendingTrimStartBytes); + pendingTrimStartBytes -= trimBytes; + inputBuffer.position(position + trimBytes); + if (pendingTrimStartBytes > 0) { + // Nothing to output yet. + return; + } + remaining -= trimBytes; + + // endBuffer must be kept as full as possible, so that we trim the right amount of media if we + // don't receive any more input. After taking into account the number of bytes needed to keep + // endBuffer as full as possible, the output should be any surplus bytes currently in endBuffer + // followed by any surplus bytes in the new inputBuffer. + int remainingBytesToOutput = endBufferSize + remaining - endBuffer.length; + if (buffer.capacity() < remainingBytesToOutput) { + buffer = ByteBuffer.allocateDirect(remainingBytesToOutput).order(ByteOrder.nativeOrder()); + } else { + buffer.clear(); + } + + // Output from endBuffer. + int endBufferBytesToOutput = Util.constrainValue(remainingBytesToOutput, 0, endBufferSize); + buffer.put(endBuffer, 0, endBufferBytesToOutput); + remainingBytesToOutput -= endBufferBytesToOutput; + + // Output from inputBuffer, restoring its limit afterwards. + int inputBufferBytesToOutput = Util.constrainValue(remainingBytesToOutput, 0, remaining); + inputBuffer.limit(inputBuffer.position() + inputBufferBytesToOutput); + buffer.put(inputBuffer); + inputBuffer.limit(limit); + remaining -= inputBufferBytesToOutput; + + // Compact endBuffer, then repopulate it using the new input. + endBufferSize -= endBufferBytesToOutput; + System.arraycopy(endBuffer, endBufferBytesToOutput, endBuffer, 0, endBufferSize); + inputBuffer.get(endBuffer, endBufferSize, remaining); + endBufferSize += remaining; + + buffer.flip(); + outputBuffer = buffer; + } + + @Override + public void queueEndOfStream() { + inputEnded = true; + } + + @Override + public ByteBuffer getOutput() { + ByteBuffer outputBuffer = this.outputBuffer; + this.outputBuffer = EMPTY_BUFFER; + return outputBuffer; + } + + @SuppressWarnings("ReferenceEquality") + @Override + public boolean isEnded() { + return inputEnded && outputBuffer == EMPTY_BUFFER; + } + + @Override + public void flush() { + outputBuffer = EMPTY_BUFFER; + inputEnded = false; + // It's no longer necessary to trim any media from the start, but it is necessary to clear the + // end buffer and refill it. + pendingTrimStartBytes = 0; + endBufferSize = 0; + } + + @Override + public void reset() { + flush(); + buffer = EMPTY_BUFFER; + channelCount = Format.NO_VALUE; + endBuffer = null; + } + +} From 7d59383cc442d8e443e08732c985b886f8d033a0 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 13 Sep 2017 09:25:46 -0700 Subject: [PATCH 0436/2472] Add reason to onPositionDiscontinuity. This allows listeners to easily determine the source of the discontinuity. Reasons can be period transitions, seeks, and internal reasons. Listeners still using the deprecated ExoPlayer.EventListener interface were updated to Player.EventListener. GitHub: #3252 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=168549612 --- .../android/exoplayer2/demo/EventLogger.java | 17 ++++++++++-- .../exoplayer2/demo/PlayerActivity.java | 2 +- .../exoplayer2/ext/cast/CastPlayer.java | 4 +-- .../exoplayer2/ext/flac/FlacPlaybackTest.java | 2 +- .../exoplayer2/ext/ima/ImaAdsLoader.java | 2 +- .../ext/leanback/LeanbackPlayerAdapter.java | 3 ++- .../mediasession/MediaSessionConnector.java | 2 +- .../exoplayer2/ext/opus/OpusPlaybackTest.java | 2 +- .../exoplayer2/ext/vp9/VpxPlaybackTest.java | 2 +- .../android/exoplayer2/ExoPlayerImpl.java | 6 ++--- .../com/google/android/exoplayer2/Player.java | 27 +++++++++++++++++-- .../exoplayer2/ui/DebugTextViewHelper.java | 2 +- .../exoplayer2/ui/PlaybackControlView.java | 3 +-- .../exoplayer2/ui/SimpleExoPlayerView.java | 2 +- .../android/exoplayer2/testutil/Action.java | 4 +-- .../exoplayer2/testutil/ExoHostedTest.java | 2 +- .../testutil/ExoPlayerTestRunner.java | 2 +- 17 files changed, 60 insertions(+), 24 deletions(-) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java index 533306e0a2..83ba61fff1 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java @@ -105,8 +105,8 @@ import java.util.Locale; } @Override - public void onPositionDiscontinuity() { - Log.d(TAG, "positionDiscontinuity"); + public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { + Log.d(TAG, "positionDiscontinuity [" + getDiscontinuityReasonString(reason) + "]"); } @Override @@ -484,4 +484,17 @@ import java.util.Locale; return "?"; } } + + private static String getDiscontinuityReasonString(@Player.DiscontinuityReason int reason) { + switch (reason) { + case Player.DISCONTINUITY_REASON_PERIOD_TRANSITION: + return "PERIOD_TRANSITION"; + case Player.DISCONTINUITY_REASON_SEEK: + return "SEEK"; + case Player.DISCONTINUITY_REASON_INTERNAL: + return "INTERNAL"; + default: + return "?"; + } + } } diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index c2c4df9ea8..071e724053 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -510,7 +510,7 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi } @Override - public void onPositionDiscontinuity() { + public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { if (inErrorState) { // This will only occur if the user has performed a seek whilst in the error state. Update the // resume position so that if the user then retries, playback will resume from the position to diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java index 234b8384f9..9d3636f8ac 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java @@ -336,7 +336,7 @@ public final class CastPlayer implements Player { pendingSeekWindowIndex = windowIndex; pendingSeekPositionMs = positionMs; for (EventListener listener : listeners) { - listener.onPositionDiscontinuity(); + listener.onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK); } } } @@ -539,7 +539,7 @@ public final class CastPlayer implements Player { if (this.currentWindowIndex != currentWindowIndex) { this.currentWindowIndex = currentWindowIndex; for (EventListener listener : listeners) { - listener.onPositionDiscontinuity(); + listener.onPositionDiscontinuity(DISCONTINUITY_REASON_PERIOD_TRANSITION); } } if (updateTracksAndSelections()) { 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 1257b652eb..8e1926ab3b 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 @@ -100,7 +100,7 @@ public class FlacPlaybackTest extends InstrumentationTestCase { } @Override - public void onPositionDiscontinuity() { + public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { // Do nothing. } diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index 87033173de..3ce4202450 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -583,7 +583,7 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, } @Override - public void onPositionDiscontinuity() { + public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { if (adsManager == null) { return; } diff --git a/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java b/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java index f5ef8b2ca4..93583f7d24 100644 --- a/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java +++ b/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java @@ -32,6 +32,7 @@ import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Player.DiscontinuityReason; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.TrackGroupArray; @@ -278,7 +279,7 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { } @Override - public void onPositionDiscontinuity() { + public void onPositionDiscontinuity(@DiscontinuityReason int reason) { Callback callback = getCallback(); callback.onCurrentPositionChanged(LeanbackPlayerAdapter.this); callback.onBufferedPositionChanged(LeanbackPlayerAdapter.this); diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java index 7304d9cdb6..61e3772750 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java @@ -686,7 +686,7 @@ public final class MediaSessionConnector { } @Override - public void onPositionDiscontinuity() { + public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { if (currentWindowIndex != player.getCurrentWindowIndex()) { if (queueNavigator != null) { queueNavigator.onCurrentWindowIndexChanged(player); 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 6eeebaef4b..8f82a9fdc0 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 @@ -100,7 +100,7 @@ public class OpusPlaybackTest extends InstrumentationTestCase { } @Override - public void onPositionDiscontinuity() { + public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { // Do nothing. } 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 50f4bf394d..52b6670c09 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 @@ -132,7 +132,7 @@ public class VpxPlaybackTest extends InstrumentationTestCase { } @Override - public void onPositionDiscontinuity() { + public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { // Do nothing. } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index e574bfc1ee..75e08aadc6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -257,7 +257,7 @@ import java.util.concurrent.CopyOnWriteArraySet; maskingWindowPositionMs = positionMs; internalPlayer.seekTo(timeline, windowIndex, C.msToUs(positionMs)); for (Player.EventListener listener : listeners) { - listener.onPositionDiscontinuity(); + listener.onPositionDiscontinuity(DISCONTINUITY_REASON_SEEK); } } } @@ -484,7 +484,7 @@ import java.util.concurrent.CopyOnWriteArraySet; } if (msg.arg1 != 0) { for (Player.EventListener listener : listeners) { - listener.onPositionDiscontinuity(); + listener.onPositionDiscontinuity(DISCONTINUITY_REASON_SEEK); } } } @@ -494,7 +494,7 @@ import java.util.concurrent.CopyOnWriteArraySet; if (pendingSeekAcks == 0) { playbackInfo = (ExoPlayerImplInternal.PlaybackInfo) msg.obj; for (Player.EventListener listener : listeners) { - listener.onPositionDiscontinuity(); + listener.onPositionDiscontinuity(DISCONTINUITY_REASON_PERIOD_TRANSITION); } } break; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Player.java b/library/core/src/main/java/com/google/android/exoplayer2/Player.java index ae2785f6f8..795b7249c8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Player.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Player.java @@ -55,7 +55,7 @@ public interface Player { * Note that if the timeline has changed then a position discontinuity may also have occurred. * For example, the current period index may have changed as a result of periods being added or * removed from the timeline. This will not be reported via a separate call to - * {@link #onPositionDiscontinuity()}. + * {@link #onPositionDiscontinuity(int)}. * * @param timeline The latest timeline. Never null, but may be empty. * @param manifest The latest manifest. May be null. @@ -119,8 +119,10 @@ public interface Player { *

          * When a position discontinuity occurs as a result of a change to the timeline this method is * not called. {@link #onTimelineChanged(Timeline, Object)} is called in this case. + * + * @param reason The {@link DiscontinuityReason} responsible for the discontinuity. */ - void onPositionDiscontinuity(); + void onPositionDiscontinuity(@DiscontinuityReason int reason); /** * Called when the current playback parameters change. The playback parameters may change due to @@ -172,6 +174,27 @@ public interface Player { */ int REPEAT_MODE_ALL = 2; + /** + * Reasons for position discontinuities. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({DISCONTINUITY_REASON_PERIOD_TRANSITION, DISCONTINUITY_REASON_SEEK, + DISCONTINUITY_REASON_INTERNAL}) + public @interface DiscontinuityReason {} + /** + * Automatic playback transition from one period in the timeline to the next. The period index may + * be the same as it was before the discontinuity in case the current period is repeated. + */ + int DISCONTINUITY_REASON_PERIOD_TRANSITION = 0; + /** + * Seek within the current period or to another period. + */ + int DISCONTINUITY_REASON_SEEK = 1; + /** + * Discontinuity introduced internally by the source. + */ + int DISCONTINUITY_REASON_INTERNAL = 2; + /** * Register a listener to receive events from the player. The listener's methods will be called on * the thread that was used to construct the player. However, if the thread used to construct the diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java index be04ce2fe0..cff860b671 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java @@ -99,7 +99,7 @@ public final class DebugTextViewHelper implements Runnable, Player.EventListener } @Override - public void onPositionDiscontinuity() { + public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { updateAndPost(); } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java index 16f555ffbc..848aabc258 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java @@ -1090,7 +1090,7 @@ public class PlaybackControlView extends FrameLayout { } @Override - public void onPositionDiscontinuity() { + public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { updateNavigation(); updateProgress(); } @@ -1150,4 +1150,3 @@ public class PlaybackControlView extends FrameLayout { } } - diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index 9bc4bb5b87..b7e162d748 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -927,7 +927,7 @@ public final class SimpleExoPlayerView extends FrameLayout { } @Override - public void onPositionDiscontinuity() { + public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { // Do nothing. } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java index bc16e105da..b41d44d016 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java @@ -368,7 +368,7 @@ public abstract class Action { final ActionNode nextAction) { player.addListener(new PlayerListener() { @Override - public void onPositionDiscontinuity() { + public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { player.removeListener(this); nextAction.schedule(player, trackSelector, surface, handler); } @@ -445,7 +445,7 @@ public abstract class Action { } @Override - public void onPositionDiscontinuity() { + public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java index c039dd3283..d3186e475d 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java @@ -248,7 +248,7 @@ public abstract class ExoHostedTest implements HostedTest, Player.EventListener, } @Override - public final void onPositionDiscontinuity() { + public final void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { // Do nothing. } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java index f65cb39bfc..b8c0846f8e 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java @@ -374,7 +374,7 @@ public final class ExoPlayerTestRunner implements Player.EventListener { } @Override - public void onPositionDiscontinuity() { + public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { positionDiscontinuityCount++; periodIndices.add(player.getCurrentPeriodIndex()); } From b3004ab1c302bf9a0e195fc4234b982f2ffee255 Mon Sep 17 00:00:00 2001 From: hoangtc Date: Thu, 14 Sep 2017 04:28:56 -0700 Subject: [PATCH 0437/2472] Do not apply SampleStream skip-ahead for NoSampleRenderer. Currently, to make transition to next media period seamless, after the renderer has read until the end of the current SampleStream, we may send it the next SampleStream so the renderer may read from the next SampleStream ahead of the transition. For NoSampleRenderer, we should avoid doing this: skipping ahead for such renderer doesn't have any benefit (the renderer does not consume data from SampleStream), and it will change the provided rendererOffsetUs while the renderer is still rendering from the playing media period. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=168669800 --- .../android/exoplayer2/ExoPlayerImplInternal.java | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index c0fc36964c..61c5b01cf7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -1355,18 +1355,25 @@ import java.io.IOException; } else if (!renderer.isCurrentStreamFinal()) { TrackSelection newSelection = newTrackSelectorResult.selections.get(i); boolean newRendererEnabled = newTrackSelectorResult.renderersEnabled[i]; + boolean isNoSampleRenderer = rendererCapabilities[i].getTrackType() == C.TRACK_TYPE_NONE; RendererConfiguration oldConfig = oldTrackSelectorResult.rendererConfigurations[i]; RendererConfiguration newConfig = newTrackSelectorResult.rendererConfigurations[i]; - if (newRendererEnabled && newConfig.equals(oldConfig)) { + if (newRendererEnabled && newConfig.equals(oldConfig) && !isNoSampleRenderer) { // Replace the renderer's SampleStream so the transition to playing the next period can // be seamless. + // This should be avoided for no-sample renderer, because skipping ahead for such + // renderer doesn't have any benefit (the renderer does not consume the sample stream), + // and it will change the provided rendererOffsetUs while the renderer is still + // rendering from the playing media period. Format[] formats = getFormats(newSelection); renderer.replaceStream(formats, readingPeriodHolder.sampleStreams[i], readingPeriodHolder.getRendererOffset()); } else { - // The renderer will be disabled when transitioning to playing the next period, either - // because there's no new selection or because a configuration change is required. Mark - // the SampleStream as final to play out any remaining data. + // The renderer will be disabled when transitioning to playing the next period, because + // there's no new selection, or because a configuration change is required, or because + // it's a no-sample renderer for which rendererOffsetUs should be updated only when + // starting to play the next period. Mark the SampleStream as final to play out any + // remaining data. renderer.setCurrentStreamFinal(); } } From f2aed7186e9c47ddc55456c5f5bbeb5cb3100988 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 14 Sep 2017 04:31:17 -0700 Subject: [PATCH 0438/2472] Workaround the skip ad button not being focused Issue: #3258 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=168669969 --- .../exoplayer2/ext/ima/ImaAdsLoader.java | 26 +++++++++++++++++++ .../exoplayer2/ui/SimpleExoPlayerView.java | 7 +++++ 2 files changed, 33 insertions(+) diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index 3ce4202450..e9641fc4d3 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -20,6 +20,7 @@ import android.net.Uri; import android.os.SystemClock; import android.util.Log; import android.view.ViewGroup; +import android.webkit.WebView; import com.google.ads.interactivemedia.v3.api.Ad; import com.google.ads.interactivemedia.v3.api.AdDisplayContainer; import com.google.ads.interactivemedia.v3.api.AdErrorEvent; @@ -112,6 +113,14 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, */ private static final long END_OF_CONTENT_POSITION_THRESHOLD_MS = 5000; + /** + * The "Skip ad" button rendered in the IMA WebView does not gain focus by default and cannot be + * clicked via a keypress event. Workaround this issue by calling focus() on the HTML element in + * the WebView directly when an ad starts. See [Internal: b/62371030]. + */ + private static final String FOCUS_SKIP_BUTTON_WORKAROUND_JS = "javascript:" + + "try{ document.getElementsByClassName(\"videoAdUiSkipButton\")[0].focus(); } catch (e) {}"; + private final Uri adTagUri; private final Timeline.Period period; private final List adCallbacks; @@ -121,6 +130,7 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, private EventListener eventListener; private Player player; + private ViewGroup adUiViewGroup; private VideoProgressUpdate lastContentProgress; private VideoProgressUpdate lastAdProgress; @@ -249,6 +259,7 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, ViewGroup adUiViewGroup) { this.player = player; this.eventListener = eventListener; + this.adUiViewGroup = adUiViewGroup; lastAdProgress = null; lastContentProgress = null; adDisplayContainer.setAdContainer(adUiViewGroup); @@ -278,6 +289,7 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, player.removeListener(this); player = null; eventListener = null; + adUiViewGroup = null; } /** @@ -363,6 +375,11 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, imaPausedContent = true; pauseContentInternal(); break; + case STARTED: + if (ad.isSkippable()) { + focusSkipButton(); + } + break; case TAPPED: if (eventListener != null) { eventListener.onAdTapped(); @@ -732,4 +749,13 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, return adGroupTimesUs; } + private void focusSkipButton() { + if (playingAd && adUiViewGroup != null && adUiViewGroup.getChildCount() > 0 + && adUiViewGroup.getChildAt(0) instanceof WebView) { + WebView webView = (WebView) (adUiViewGroup.getChildAt(0)); + webView.requestFocus(); + webView.loadUrl(FOCUS_SKIP_BUTTON_WORKAROUND_JS); + } + } + } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index b7e162d748..b6f0677871 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -516,6 +516,13 @@ public final class SimpleExoPlayerView extends FrameLayout { @Override public boolean dispatchKeyEvent(KeyEvent event) { + if (player != null && player.isPlayingAd()) { + // Focus any overlay UI now, in case it's provided by a WebView whose contents may update + // dynamically. This is needed to make the "Skip ad" button focused on Android TV when using + // IMA [Internal: b/62371030]. + overlayFrameLayout.requestFocus(); + return super.dispatchKeyEvent(event); + } maybeShowController(true); return dispatchMediaKeyEvent(event) || super.dispatchKeyEvent(event); } From 0ad39c642d8ff7881d7b3623a82669461c6deb48 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 14 Sep 2017 09:35:08 -0700 Subject: [PATCH 0439/2472] Relax test termination for API level 23 and above This allows test runs to continue when the activity is paused (but not stopped), which is in line with what we do in the demo app's PlayerActivity. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=168699521 --- .../google/android/exoplayer2/testutil/HostActivity.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/HostActivity.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/HostActivity.java index 299cb10815..1ef1acd80b 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/HostActivity.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/HostActivity.java @@ -31,6 +31,7 @@ import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.Window; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; /** * A host activity for performing playback tests. @@ -179,12 +180,17 @@ public final class HostActivity extends Activity implements SurfaceHolder.Callba @Override public void onPause() { super.onPause(); - maybeStopHostedTest(); + if (Util.SDK_INT <= 23) { + maybeStopHostedTest(); + } } @Override public void onStop() { super.onStop(); + if (Util.SDK_INT > 23) { + maybeStopHostedTest(); + } wakeLock.release(); wakeLock = null; wifiLock.release(); From 58293abc11a92eb90a0b634e879129d001eec15f Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 14 Sep 2017 10:34:22 -0700 Subject: [PATCH 0440/2472] Remove IMA dependency and add AdsMediaSource AdsMediaSource lives in the core library so only ImaAdsLoader remains in the ima extension. AdsMediaSource takes an AdsLoader implementation. ImaAdsMediaSource is deprecated rather than removed for now. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=168707921 --- .../exoplayer2/demo/PlayerActivity.java | 36 +- .../exoplayer2/ext/ima/ImaAdsLoader.java | 66 +--- .../exoplayer2/ext/ima/ImaAdsMediaSource.java | 282 +------------- .../source/ads}/AdPlaybackState.java | 4 +- .../exoplayer2/source/ads/AdsLoader.java | 96 +++++ .../exoplayer2/source/ads/AdsMediaSource.java | 349 ++++++++++++++++++ .../source/ads}/SinglePeriodAdTimeline.java | 2 +- 7 files changed, 486 insertions(+), 349 deletions(-) rename {extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima => library/core/src/main/java/com/google/android/exoplayer2/source/ads}/AdPlaybackState.java (98%) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsLoader.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java rename {extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima => library/core/src/main/java/com/google/android/exoplayer2/source/ads}/SinglePeriodAdTimeline.java (98%) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index 071e724053..0efb04782d 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -56,6 +56,8 @@ import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.source.ads.AdsLoader; +import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.source.dash.DashMediaSource; import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource; import com.google.android.exoplayer2.source.hls.HlsMediaSource; @@ -73,8 +75,6 @@ import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.util.Util; -import java.lang.reflect.Constructor; -import java.lang.reflect.Method; import java.net.CookieHandler; import java.net.CookieManager; import java.net.CookiePolicy; @@ -129,9 +129,9 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi // Fields used only for ad playback. The ads loader is loaded via reflection. - private Object imaAdsLoader; // com.google.android.exoplayer2.ext.ima.ImaAdsLoader + private AdsLoader adsLoader; private Uri loadedAdTagUri; - private ViewGroup adOverlayViewGroup; + private ViewGroup adUiViewGroup; // Activity lifecycle @@ -453,32 +453,20 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi // Load the extension source using reflection so the demo app doesn't have to depend on it. // The ads loader is reused for multiple playbacks, so that ad playback can resume. Class loaderClass = Class.forName("com.google.android.exoplayer2.ext.ima.ImaAdsLoader"); - if (imaAdsLoader == null) { - imaAdsLoader = loaderClass.getConstructor(Context.class, Uri.class) + if (adsLoader == null) { + adsLoader = (AdsLoader) loaderClass.getConstructor(Context.class, Uri.class) .newInstance(this, adTagUri); - adOverlayViewGroup = new FrameLayout(this); + adUiViewGroup = new FrameLayout(this); // The demo app has a non-null overlay frame layout. - simpleExoPlayerView.getOverlayFrameLayout().addView(adOverlayViewGroup); + simpleExoPlayerView.getOverlayFrameLayout().addView(adUiViewGroup); } - Class sourceClass = - Class.forName("com.google.android.exoplayer2.ext.ima.ImaAdsMediaSource"); - Constructor constructor = sourceClass.getConstructor(MediaSource.class, - DataSource.Factory.class, loaderClass, ViewGroup.class); - return (MediaSource) constructor.newInstance(mediaSource, mediaDataSourceFactory, imaAdsLoader, - adOverlayViewGroup); + return new AdsMediaSource(mediaSource, mediaDataSourceFactory, adsLoader, adUiViewGroup); } private void releaseAdsLoader() { - if (imaAdsLoader != null) { - try { - Class loaderClass = Class.forName("com.google.android.exoplayer2.ext.ima.ImaAdsLoader"); - Method releaseMethod = loaderClass.getMethod("release"); - releaseMethod.invoke(imaAdsLoader); - } catch (Exception e) { - // Should never happen. - throw new IllegalStateException(e); - } - imaAdsLoader = null; + if (adsLoader != null) { + adsLoader.release(); + adsLoader = null; loadedAdTagUri = null; simpleExoPlayerView.getOverlayFrameLayout().removeAllViews(); } diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index e9641fc4d3..7e5912ed28 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -29,7 +29,6 @@ import com.google.ads.interactivemedia.v3.api.AdEvent; import com.google.ads.interactivemedia.v3.api.AdEvent.AdEventListener; import com.google.ads.interactivemedia.v3.api.AdEvent.AdEventType; import com.google.ads.interactivemedia.v3.api.AdPodInfo; -import com.google.ads.interactivemedia.v3.api.AdsLoader; import com.google.ads.interactivemedia.v3.api.AdsLoader.AdsLoadedListener; import com.google.ads.interactivemedia.v3.api.AdsManager; import com.google.ads.interactivemedia.v3.api.AdsManagerLoadedEvent; @@ -48,6 +47,8 @@ import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.source.ads.AdPlaybackState; +import com.google.android.exoplayer2.source.ads.AdsLoader; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; @@ -58,40 +59,9 @@ import java.util.Map; /** * Loads ads using the IMA SDK. All methods are called on the main thread. */ -public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, +public final class ImaAdsLoader implements AdsLoader, Player.EventListener, VideoAdPlayer, ContentProgressProvider, AdErrorListener, AdsLoadedListener, AdEventListener { - /** - * Listener for ad loader events. All methods are called on the main thread. - */ - /* package */ interface EventListener { - - /** - * Called when the ad playback state has been updated. - * - * @param adPlaybackState The new ad playback state. - */ - void onAdPlaybackState(AdPlaybackState adPlaybackState); - - /** - * Called when there was an error loading ads. - * - * @param error The error. - */ - void onLoadError(IOException error); - - /** - * Called when the user clicks through an ad (for example, following a 'learn more' link). - */ - void onAdClicked(); - - /** - * Called when the user taps a non-clickthrough part of an ad. - */ - void onAdTapped(); - - } - static { ExoPlayerLibraryInfo.registerModule("goog.exo.ima"); } @@ -126,7 +96,7 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, private final List adCallbacks; private final ImaSdkFactory imaSdkFactory; private final AdDisplayContainer adDisplayContainer; - private final AdsLoader adsLoader; + private final com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader; private EventListener eventListener; private Player player; @@ -160,7 +130,8 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, */ private boolean imaPausedInAd; /** - * Whether {@link AdsLoader#contentComplete()} has been called since starting ad playback. + * Whether {@link com.google.ads.interactivemedia.v3.api.AdsLoader#contentComplete()} has been + * called since starting ad playback. */ private boolean sentContentComplete; @@ -248,15 +219,8 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, contentDurationMs = C.TIME_UNSET; } - /** - * Attaches a player that will play ads loaded using this instance. - * - * @param player The player instance that will play the loaded ads. - * @param eventListener Listener for ads loader events. - * @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI. - */ - /* package */ void attachPlayer(ExoPlayer player, EventListener eventListener, - ViewGroup adUiViewGroup) { + @Override + public void attachPlayer(ExoPlayer player, EventListener eventListener, ViewGroup adUiViewGroup) { this.player = player; this.eventListener = eventListener; this.adUiViewGroup = adUiViewGroup; @@ -265,7 +229,7 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, adDisplayContainer.setAdContainer(adUiViewGroup); player.addListener(this); if (adPlaybackState != null) { - eventListener.onAdPlaybackState(adPlaybackState); + eventListener.onAdPlaybackState(adPlaybackState.copy()); if (imaPausedContent) { adsManager.resume(); } @@ -274,12 +238,8 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, } } - /** - * Detaches the attached player and event listener. To attach a new player, call - * {@link #attachPlayer(ExoPlayer, EventListener, ViewGroup)}. Call {@link #release()} to release - * all resources associated with this instance. - */ - /* package */ void detachPlayer() { + @Override + public void detachPlayer() { if (adsManager != null && imaPausedContent) { adPlaybackState.setAdResumePositionUs(C.msToUs(player.getCurrentPosition())); adsManager.pause(); @@ -292,9 +252,7 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, adUiViewGroup = null; } - /** - * Releases the loader. Must be called when the instance is no longer needed. - */ + @Override public void release() { released = true; if (adsManager != null) { diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java index d56a3ad41f..c3574c414b 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java @@ -16,83 +16,25 @@ package com.google.android.exoplayer2.ext.ima; import android.os.Handler; -import android.os.Looper; import android.support.annotation.Nullable; -import android.util.Log; import android.view.ViewGroup; -import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; -import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; -import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; /** - * A {@link MediaSource} that inserts ads linearly with a provided content media source using the - * Interactive Media Ads SDK for ad loading and tracking. + * A {@link MediaSource} that inserts ads linearly with a provided content media source. + * + * @deprecated Use com.google.android.exoplayer2.source.ads.AdsMediaSource with ImaAdsLoader. */ +@Deprecated public final class ImaAdsMediaSource implements MediaSource { - /** - * Listener for events relating to ad loading. - */ - public interface AdsListener { - - /** - * Called if there was an error loading ads. The media source will load the content without ads - * if ads can't be loaded, so listen for this event if you need to implement additional handling - * (for example, stopping the player). - * - * @param error The error. - */ - void onAdLoadError(IOException error); - - /** - * Called when the user clicks through an ad (for example, following a 'learn more' link). - */ - void onAdClicked(); - - /** - * Called when the user taps a non-clickthrough part of an ad. - */ - void onAdTapped(); - - } - - private static final String TAG = "ImaAdsMediaSource"; - - private final MediaSource contentMediaSource; - private final DataSource.Factory dataSourceFactory; - private final ImaAdsLoader imaAdsLoader; - private final ViewGroup adUiViewGroup; - private final Handler mainHandler; - private final AdsLoaderListener adsLoaderListener; - private final Map adMediaSourceByMediaPeriod; - private final Timeline.Period period; - @Nullable - private final Handler eventHandler; - @Nullable - private final AdsListener eventListener; - - private Handler playerHandler; - private ExoPlayer player; - private volatile boolean released; - - // Accessed on the player thread. - private Timeline contentTimeline; - private Object contentManifest; - private AdPlaybackState adPlaybackState; - private MediaSource[][] adGroupMediaSources; - private long[][] adDurationsUs; - private MediaSource.Listener listener; + private final AdsMediaSource adsMediaSource; /** * Constructs a new source that inserts ads linearly with the content specified by @@ -121,230 +63,34 @@ public final class ImaAdsMediaSource implements MediaSource { */ public ImaAdsMediaSource(MediaSource contentMediaSource, DataSource.Factory dataSourceFactory, ImaAdsLoader imaAdsLoader, ViewGroup adUiViewGroup, @Nullable Handler eventHandler, - @Nullable AdsListener eventListener) { - this.contentMediaSource = contentMediaSource; - this.dataSourceFactory = dataSourceFactory; - this.imaAdsLoader = imaAdsLoader; - this.adUiViewGroup = adUiViewGroup; - this.eventHandler = eventHandler; - this.eventListener = eventListener; - mainHandler = new Handler(Looper.getMainLooper()); - adsLoaderListener = new AdsLoaderListener(); - adMediaSourceByMediaPeriod = new HashMap<>(); - period = new Timeline.Period(); - adGroupMediaSources = new MediaSource[0][]; - adDurationsUs = new long[0][]; + @Nullable AdsMediaSource.AdsListener eventListener) { + adsMediaSource = new AdsMediaSource(contentMediaSource, dataSourceFactory, imaAdsLoader, + adUiViewGroup, eventHandler, eventListener); } @Override public void prepareSource(final ExoPlayer player, boolean isTopLevelSource, Listener listener) { - Assertions.checkArgument(isTopLevelSource); - this.listener = listener; - this.player = player; - playerHandler = new Handler(); - contentMediaSource.prepareSource(player, false, new Listener() { - @Override - public void onSourceInfoRefreshed(Timeline timeline, Object manifest) { - ImaAdsMediaSource.this.onContentSourceInfoRefreshed(timeline, manifest); - } - }); - mainHandler.post(new Runnable() { - @Override - public void run() { - imaAdsLoader.attachPlayer(player, adsLoaderListener, adUiViewGroup); - } - }); + adsMediaSource.prepareSource(player, isTopLevelSource, listener); } @Override public void maybeThrowSourceInfoRefreshError() throws IOException { - contentMediaSource.maybeThrowSourceInfoRefreshError(); - for (MediaSource[] mediaSources : adGroupMediaSources) { - for (MediaSource mediaSource : mediaSources) { - if (mediaSource != null) { - mediaSource.maybeThrowSourceInfoRefreshError(); - } - } - } + adsMediaSource.maybeThrowSourceInfoRefreshError(); } @Override public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { - if (adPlaybackState.adGroupCount > 0 && id.isAd()) { - final int adGroupIndex = id.adGroupIndex; - final int adIndexInAdGroup = id.adIndexInAdGroup; - if (adGroupMediaSources[adGroupIndex].length <= adIndexInAdGroup) { - MediaSource adMediaSource = new ExtractorMediaSource( - adPlaybackState.adUris[id.adGroupIndex][id.adIndexInAdGroup], dataSourceFactory, - new DefaultExtractorsFactory(), mainHandler, adsLoaderListener); - int oldAdCount = adGroupMediaSources[id.adGroupIndex].length; - if (adIndexInAdGroup >= oldAdCount) { - int adCount = adIndexInAdGroup + 1; - adGroupMediaSources[adGroupIndex] = - Arrays.copyOf(adGroupMediaSources[adGroupIndex], adCount); - adDurationsUs[adGroupIndex] = Arrays.copyOf(adDurationsUs[adGroupIndex], adCount); - Arrays.fill(adDurationsUs[adGroupIndex], oldAdCount, adCount, C.TIME_UNSET); - } - adGroupMediaSources[adGroupIndex][adIndexInAdGroup] = adMediaSource; - adMediaSource.prepareSource(player, false, new Listener() { - @Override - public void onSourceInfoRefreshed(Timeline timeline, Object manifest) { - onAdSourceInfoRefreshed(adGroupIndex, adIndexInAdGroup, timeline); - } - }); - } - MediaSource mediaSource = adGroupMediaSources[adGroupIndex][adIndexInAdGroup]; - MediaPeriod mediaPeriod = mediaSource.createPeriod(new MediaPeriodId(0), allocator); - adMediaSourceByMediaPeriod.put(mediaPeriod, mediaSource); - return mediaPeriod; - } else { - return contentMediaSource.createPeriod(id, allocator); - } + return adsMediaSource.createPeriod(id, allocator); } @Override public void releasePeriod(MediaPeriod mediaPeriod) { - if (adMediaSourceByMediaPeriod.containsKey(mediaPeriod)) { - adMediaSourceByMediaPeriod.remove(mediaPeriod).releasePeriod(mediaPeriod); - } else { - contentMediaSource.releasePeriod(mediaPeriod); - } + adsMediaSource.releasePeriod(mediaPeriod); } @Override public void releaseSource() { - released = true; - contentMediaSource.releaseSource(); - for (MediaSource[] mediaSources : adGroupMediaSources) { - for (MediaSource mediaSource : mediaSources) { - if (mediaSource != null) { - mediaSource.releaseSource(); - } - } - } - mainHandler.post(new Runnable() { - @Override - public void run() { - imaAdsLoader.detachPlayer(); - } - }); - } - - // Internal methods. - - private void onAdPlaybackState(AdPlaybackState adPlaybackState) { - if (this.adPlaybackState == null) { - adGroupMediaSources = new MediaSource[adPlaybackState.adGroupCount][]; - Arrays.fill(adGroupMediaSources, new MediaSource[0]); - adDurationsUs = new long[adPlaybackState.adGroupCount][]; - Arrays.fill(adDurationsUs, new long[0]); - } - this.adPlaybackState = adPlaybackState; - maybeUpdateSourceInfo(); - } - - private void onLoadError(final IOException error) { - Log.w(TAG, "Ad load error", error); - if (eventHandler != null && eventListener != null) { - eventHandler.post(new Runnable() { - @Override - public void run() { - if (!released) { - eventListener.onAdLoadError(error); - } - } - }); - } - } - - private void onContentSourceInfoRefreshed(Timeline timeline, Object manifest) { - contentTimeline = timeline; - contentManifest = manifest; - maybeUpdateSourceInfo(); - } - - private void onAdSourceInfoRefreshed(int adGroupIndex, int adIndexInAdGroup, Timeline timeline) { - Assertions.checkArgument(timeline.getPeriodCount() == 1); - adDurationsUs[adGroupIndex][adIndexInAdGroup] = timeline.getPeriod(0, period).getDurationUs(); - maybeUpdateSourceInfo(); - } - - private void maybeUpdateSourceInfo() { - if (adPlaybackState != null && contentTimeline != null) { - Timeline timeline = adPlaybackState.adGroupCount == 0 ? contentTimeline - : new SinglePeriodAdTimeline(contentTimeline, adPlaybackState.adGroupTimesUs, - adPlaybackState.adCounts, adPlaybackState.adsLoadedCounts, - adPlaybackState.adsPlayedCounts, adDurationsUs, adPlaybackState.adResumePositionUs); - listener.onSourceInfoRefreshed(timeline, contentManifest); - } - } - - /** - * Listener for ad loading events. All methods are called on the main thread. - */ - private final class AdsLoaderListener implements ImaAdsLoader.EventListener, - ExtractorMediaSource.EventListener { - - @Override - public void onAdPlaybackState(final AdPlaybackState adPlaybackState) { - if (released) { - return; - } - playerHandler.post(new Runnable() { - @Override - public void run() { - if (released) { - return; - } - ImaAdsMediaSource.this.onAdPlaybackState(adPlaybackState); - } - }); - } - - @Override - public void onLoadError(final IOException error) { - if (released) { - return; - } - playerHandler.post(new Runnable() { - @Override - public void run() { - if (released) { - return; - } - ImaAdsMediaSource.this.onLoadError(error); - } - }); - } - - @Override - public void onAdClicked() { - if (eventHandler != null && eventListener != null) { - eventHandler.post(new Runnable() { - @Override - public void run() { - if (!released) { - eventListener.onAdClicked(); - } - } - }); - } - } - - @Override - public void onAdTapped() { - if (eventHandler != null && eventListener != null) { - eventHandler.post(new Runnable() { - @Override - public void run() { - if (!released) { - eventListener.onAdTapped(); - } - } - }); - } - } - + adsMediaSource.releaseSource(); } } diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/AdPlaybackState.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java similarity index 98% rename from extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/AdPlaybackState.java rename to library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java index 0edd7d6558..97c97dec8f 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/AdPlaybackState.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.ext.ima; +package com.google.android.exoplayer2.source.ads; import android.net.Uri; import com.google.android.exoplayer2.C; @@ -22,7 +22,7 @@ import java.util.Arrays; /** * Represents the structure of ads to play and the state of loaded/played ads. */ -/* package */ final class AdPlaybackState { +public final class AdPlaybackState { /** * The number of ad groups. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsLoader.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsLoader.java new file mode 100644 index 0000000000..241750a21f --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsLoader.java @@ -0,0 +1,96 @@ +/* + * 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.ads; + +import android.view.ViewGroup; +import com.google.android.exoplayer2.ExoPlayer; +import java.io.IOException; + +/** + * Interface for loaders of ads, which can be used with {@link AdsMediaSource}. + *

          + * Ad loaders notify the {@link AdsMediaSource} about events via {@link EventListener}. In + * particular, implementations must call {@link EventListener#onAdPlaybackState(AdPlaybackState)} + * with a new copy of the current {@link AdPlaybackState} whenever further information about ads + * becomes known (for example, when an ad media URI is available, or an ad has played to the end). + *

          + * {@link #attachPlayer(ExoPlayer, EventListener, ViewGroup)} will be called when the ads media + * source first initializes, at which point the loader can request ads. If the player enters the + * background, {@link #detachPlayer()} will be called. Loaders should maintain any ad playback state + * in preparation for a later call to {@link #attachPlayer(ExoPlayer, EventListener, ViewGroup)}. If + * an ad is playing when the player is detached, store the current playback position via + * {@link AdPlaybackState#setAdResumePositionUs(long)}. + *

          + * If {@link EventListener#onAdPlaybackState(AdPlaybackState)} has been called, the implementation + * of {@link #attachPlayer(ExoPlayer, EventListener, ViewGroup)} should invoke the same listener to + * provide the existing playback state to the new player. + */ +public interface AdsLoader { + + /** + * Listener for ad loader events. All methods are called on the main thread. + */ + interface EventListener { + + /** + * Called when the ad playback state has been updated. + * + * @param adPlaybackState The new ad playback state. + */ + void onAdPlaybackState(AdPlaybackState adPlaybackState); + + /** + * Called when there was an error loading ads. + * + * @param error The error. + */ + void onLoadError(IOException error); + + /** + * Called when the user clicks through an ad (for example, following a 'learn more' link). + */ + void onAdClicked(); + + /** + * Called when the user taps a non-clickthrough part of an ad. + */ + void onAdTapped(); + + } + + /** + * Attaches a player that will play ads loaded using this instance. Called on the main thread by + * {@link AdsMediaSource}. + * + * @param player The player instance that will play the loaded ads. + * @param eventListener Listener for ads loader events. + * @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI. + */ + void attachPlayer(ExoPlayer player, EventListener eventListener, ViewGroup adUiViewGroup); + + /** + * Detaches the attached player and event listener. Called on the main thread by + * {@link AdsMediaSource}. + */ + void detachPlayer(); + + /** + * Releases the loader. Called by the application on the main thread when the instance is no + * longer needed. + */ + void release(); + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java new file mode 100644 index 0000000000..41a856f83f --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java @@ -0,0 +1,349 @@ +/* + * 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.ads; + +import android.os.Handler; +import android.os.Looper; +import android.support.annotation.Nullable; +import android.util.Log; +import android.view.ViewGroup; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; +import com.google.android.exoplayer2.source.ExtractorMediaSource; +import com.google.android.exoplayer2.source.MediaPeriod; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.upstream.Allocator; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.util.Assertions; +import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +/** + * A {@link MediaSource} that inserts ads linearly with a provided content media source. + */ +public final class AdsMediaSource implements MediaSource { + + /** + * Listener for events relating to ad loading. + */ + public interface AdsListener { + + /** + * Called if there was an error loading ads. The media source will load the content without ads + * if ads can't be loaded, so listen for this event if you need to implement additional handling + * (for example, stopping the player). + * + * @param error The error. + */ + void onAdLoadError(IOException error); + + /** + * Called when the user clicks through an ad (for example, following a 'learn more' link). + */ + void onAdClicked(); + + /** + * Called when the user taps a non-clickthrough part of an ad. + */ + void onAdTapped(); + + } + + private static final String TAG = "AdsMediaSource"; + + private final MediaSource contentMediaSource; + private final DataSource.Factory dataSourceFactory; + private final AdsLoader adsLoader; + private final ViewGroup adUiViewGroup; + private final Handler mainHandler; + private final ComponentListener componentListener; + private final Map adMediaSourceByMediaPeriod; + private final Timeline.Period period; + @Nullable + private final Handler eventHandler; + @Nullable + private final AdsListener eventListener; + + private Handler playerHandler; + private ExoPlayer player; + private volatile boolean released; + + // Accessed on the player thread. + private Timeline contentTimeline; + private Object contentManifest; + private AdPlaybackState adPlaybackState; + private MediaSource[][] adGroupMediaSources; + private long[][] adDurationsUs; + private MediaSource.Listener listener; + + /** + * Constructs a new source that inserts ads linearly with the content specified by + * {@code contentMediaSource}. + * + * @param contentMediaSource The {@link MediaSource} providing the content to play. + * @param dataSourceFactory Factory for data sources used to load ad media. + * @param adsLoader The loader for ads. + * @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI. + */ + public AdsMediaSource(MediaSource contentMediaSource, DataSource.Factory dataSourceFactory, + AdsLoader adsLoader, ViewGroup adUiViewGroup) { + this(contentMediaSource, dataSourceFactory, adsLoader, adUiViewGroup, null, null); + } + + /** + * Constructs a new source that inserts ads linearly with the content specified by + * {@code contentMediaSource}. + * + * @param contentMediaSource The {@link MediaSource} providing the content to play. + * @param dataSourceFactory Factory for data sources used to load ad media. + * @param adsLoader The loader for ads. + * @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI. + * @param eventHandler A handler for events. May be null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + */ + public AdsMediaSource(MediaSource contentMediaSource, DataSource.Factory dataSourceFactory, + AdsLoader adsLoader, ViewGroup adUiViewGroup, @Nullable Handler eventHandler, + @Nullable AdsListener eventListener) { + this.contentMediaSource = contentMediaSource; + this.dataSourceFactory = dataSourceFactory; + this.adsLoader = adsLoader; + this.adUiViewGroup = adUiViewGroup; + this.eventHandler = eventHandler; + this.eventListener = eventListener; + mainHandler = new Handler(Looper.getMainLooper()); + componentListener = new ComponentListener(); + adMediaSourceByMediaPeriod = new HashMap<>(); + period = new Timeline.Period(); + adGroupMediaSources = new MediaSource[0][]; + adDurationsUs = new long[0][]; + } + + @Override + public void prepareSource(final ExoPlayer player, boolean isTopLevelSource, Listener listener) { + Assertions.checkArgument(isTopLevelSource); + this.listener = listener; + this.player = player; + playerHandler = new Handler(); + contentMediaSource.prepareSource(player, false, new Listener() { + @Override + public void onSourceInfoRefreshed(Timeline timeline, Object manifest) { + AdsMediaSource.this.onContentSourceInfoRefreshed(timeline, manifest); + } + }); + mainHandler.post(new Runnable() { + @Override + public void run() { + adsLoader.attachPlayer(player, componentListener, adUiViewGroup); + } + }); + } + + @Override + public void maybeThrowSourceInfoRefreshError() throws IOException { + contentMediaSource.maybeThrowSourceInfoRefreshError(); + for (MediaSource[] mediaSources : adGroupMediaSources) { + for (MediaSource mediaSource : mediaSources) { + if (mediaSource != null) { + mediaSource.maybeThrowSourceInfoRefreshError(); + } + } + } + } + + @Override + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { + if (adPlaybackState.adGroupCount > 0 && id.isAd()) { + final int adGroupIndex = id.adGroupIndex; + final int adIndexInAdGroup = id.adIndexInAdGroup; + if (adGroupMediaSources[adGroupIndex].length <= adIndexInAdGroup) { + MediaSource adMediaSource = new ExtractorMediaSource( + adPlaybackState.adUris[id.adGroupIndex][id.adIndexInAdGroup], dataSourceFactory, + new DefaultExtractorsFactory(), mainHandler, componentListener); + int oldAdCount = adGroupMediaSources[id.adGroupIndex].length; + if (adIndexInAdGroup >= oldAdCount) { + int adCount = adIndexInAdGroup + 1; + adGroupMediaSources[adGroupIndex] = + Arrays.copyOf(adGroupMediaSources[adGroupIndex], adCount); + adDurationsUs[adGroupIndex] = Arrays.copyOf(adDurationsUs[adGroupIndex], adCount); + Arrays.fill(adDurationsUs[adGroupIndex], oldAdCount, adCount, C.TIME_UNSET); + } + adGroupMediaSources[adGroupIndex][adIndexInAdGroup] = adMediaSource; + adMediaSource.prepareSource(player, false, new Listener() { + @Override + public void onSourceInfoRefreshed(Timeline timeline, Object manifest) { + onAdSourceInfoRefreshed(adGroupIndex, adIndexInAdGroup, timeline); + } + }); + } + MediaSource mediaSource = adGroupMediaSources[adGroupIndex][adIndexInAdGroup]; + MediaPeriod mediaPeriod = mediaSource.createPeriod(new MediaPeriodId(0), allocator); + adMediaSourceByMediaPeriod.put(mediaPeriod, mediaSource); + return mediaPeriod; + } else { + return contentMediaSource.createPeriod(id, allocator); + } + } + + @Override + public void releasePeriod(MediaPeriod mediaPeriod) { + if (adMediaSourceByMediaPeriod.containsKey(mediaPeriod)) { + adMediaSourceByMediaPeriod.remove(mediaPeriod).releasePeriod(mediaPeriod); + } else { + contentMediaSource.releasePeriod(mediaPeriod); + } + } + + @Override + public void releaseSource() { + released = true; + contentMediaSource.releaseSource(); + for (MediaSource[] mediaSources : adGroupMediaSources) { + for (MediaSource mediaSource : mediaSources) { + if (mediaSource != null) { + mediaSource.releaseSource(); + } + } + } + mainHandler.post(new Runnable() { + @Override + public void run() { + adsLoader.detachPlayer(); + } + }); + } + + // Internal methods. + + private void onAdPlaybackState(AdPlaybackState adPlaybackState) { + if (this.adPlaybackState == null) { + adGroupMediaSources = new MediaSource[adPlaybackState.adGroupCount][]; + Arrays.fill(adGroupMediaSources, new MediaSource[0]); + adDurationsUs = new long[adPlaybackState.adGroupCount][]; + Arrays.fill(adDurationsUs, new long[0]); + } + this.adPlaybackState = adPlaybackState; + maybeUpdateSourceInfo(); + } + + private void onLoadError(final IOException error) { + Log.w(TAG, "Ad load error", error); + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + if (!released) { + eventListener.onAdLoadError(error); + } + } + }); + } + } + + private void onContentSourceInfoRefreshed(Timeline timeline, Object manifest) { + contentTimeline = timeline; + contentManifest = manifest; + maybeUpdateSourceInfo(); + } + + private void onAdSourceInfoRefreshed(int adGroupIndex, int adIndexInAdGroup, Timeline timeline) { + Assertions.checkArgument(timeline.getPeriodCount() == 1); + adDurationsUs[adGroupIndex][adIndexInAdGroup] = timeline.getPeriod(0, period).getDurationUs(); + maybeUpdateSourceInfo(); + } + + private void maybeUpdateSourceInfo() { + if (adPlaybackState != null && contentTimeline != null) { + Timeline timeline = adPlaybackState.adGroupCount == 0 ? contentTimeline + : new SinglePeriodAdTimeline(contentTimeline, adPlaybackState.adGroupTimesUs, + adPlaybackState.adCounts, adPlaybackState.adsLoadedCounts, + adPlaybackState.adsPlayedCounts, adDurationsUs, adPlaybackState.adResumePositionUs); + listener.onSourceInfoRefreshed(timeline, contentManifest); + } + } + + /** + * Listener for component events. All methods are called on the main thread. + */ + private final class ComponentListener implements AdsLoader.EventListener, + ExtractorMediaSource.EventListener { + + @Override + public void onAdPlaybackState(final AdPlaybackState adPlaybackState) { + if (released) { + return; + } + playerHandler.post(new Runnable() { + @Override + public void run() { + if (released) { + return; + } + AdsMediaSource.this.onAdPlaybackState(adPlaybackState); + } + }); + } + + @Override + public void onAdClicked() { + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + if (!released) { + eventListener.onAdClicked(); + } + } + }); + } + } + + @Override + public void onAdTapped() { + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + if (!released) { + eventListener.onAdTapped(); + } + } + }); + } + } + + @Override + public void onLoadError(final IOException error) { + if (released) { + return; + } + playerHandler.post(new Runnable() { + @Override + public void run() { + if (released) { + return; + } + AdsMediaSource.this.onLoadError(error); + } + }); + } + + } + +} diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/SinglePeriodAdTimeline.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/SinglePeriodAdTimeline.java similarity index 98% rename from extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/SinglePeriodAdTimeline.java rename to library/core/src/main/java/com/google/android/exoplayer2/source/ads/SinglePeriodAdTimeline.java index 0162d22c34..c2974681db 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/SinglePeriodAdTimeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/SinglePeriodAdTimeline.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.ext.ima; +package com.google.android.exoplayer2.source.ads; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Timeline; From a479afff5f45f21810e0345f705fb07a14657ca8 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 14 Sep 2017 10:34:30 -0700 Subject: [PATCH 0441/2472] Fix potential NPE/ThreadSafety issues with MediaDrm listener - MediaDrmEventListener.onEvent is typically called on the app's main thread. mediaDrmHandler is instantiated on the playback thread. Hence mediaDrmHandler should be volatile to ensure visibility. - Nulling mediaDrmHandler could result in a NPE in onEvent. Instantiate mediaDrmHandler (and don't null it again) to avoid this. MediaDrmHandler.handleMessage will correctly discard any events for sessions that are now closed. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=168707938 --- .../drm/DefaultDrmSessionManager.java | 28 ++++++++----------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java index 029b41fde8..cff9f9da0f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java @@ -98,21 +98,22 @@ public class DefaultDrmSessionManager implements DrmSe /** Releases an existing offline license. */ public static final int MODE_RELEASE = 3; + private final UUID uuid; + private final ExoMediaDrm mediaDrm; + private final MediaDrmCallback callback; + private final HashMap optionalKeyRequestParameters; private final Handler eventHandler; private final EventListener eventListener; - private final ExoMediaDrm mediaDrm; - private final HashMap optionalKeyRequestParameters; - private final MediaDrmCallback callback; - private final UUID uuid; private final boolean multiSession; + private final List> sessions; + private final AtomicBoolean provisioningInProgress; + private Looper playbackLooper; private int mode; private byte[] offlineLicenseKeySetId; - private final List> sessions; - private final AtomicBoolean provisioningInProgress; - /* package */ MediaDrmHandler mediaDrmHandler; + /* package */ volatile MediaDrmHandler mediaDrmHandler; /** * Instantiates a new instance using the Widevine scheme. @@ -226,6 +227,7 @@ public class DefaultDrmSessionManager implements DrmSe if (multiSession) { mediaDrm.setPropertyString("sessionSharing", "enable"); } + mediaDrm.setOnEventListener(new MediaDrmEventListener()); } /** @@ -334,8 +336,9 @@ public class DefaultDrmSessionManager implements DrmSe Assertions.checkState(this.playbackLooper == null || this.playbackLooper == playbackLooper); if (sessions.isEmpty()) { this.playbackLooper = playbackLooper; - mediaDrmHandler = new MediaDrmHandler(playbackLooper); - mediaDrm.setOnEventListener(new MediaDrmEventListener()); + if (mediaDrmHandler == null) { + mediaDrmHandler = new MediaDrmHandler(playbackLooper); + } } DefaultDrmSession session = null; @@ -383,13 +386,6 @@ public class DefaultDrmSessionManager implements DrmSe if (drmSession.release()) { sessions.remove(drmSession); } - - if (sessions.isEmpty()) { - mediaDrm.setOnEventListener(null); - mediaDrmHandler.removeCallbacksAndMessages(null); - mediaDrmHandler = null; - playbackLooper = null; - } } @Override From a3a2fb506ce30bd8a3e02eeea5b4503af2d22d44 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 14 Sep 2017 11:09:18 -0700 Subject: [PATCH 0442/2472] Provisioning: Fix some (admittedly quite theoretical) issues: 1. Only tell sessions that want provisioning when provisioning occurs. 2. Also propagate failure to provision to these sessions. 3. If a session responsible for provisioning is released, start provisioning using another session instead. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=168713918 --- .../exoplayer2/drm/DefaultDrmSession.java | 93 +++++++++++-------- .../drm/DefaultDrmSessionManager.java | 54 ++++++++--- 2 files changed, 97 insertions(+), 50 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java index 4f187264a6..e82c7e46b2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java @@ -32,7 +32,6 @@ import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.UUID; -import java.util.concurrent.atomic.AtomicBoolean; /** * A {@link DrmSession} that supports playbacks using {@link ExoMediaDrm}. @@ -41,12 +40,29 @@ import java.util.concurrent.atomic.AtomicBoolean; /* package */ class DefaultDrmSession implements DrmSession { /** - * Listener of {@link DefaultDrmSession} events. + * Manages provisioning requests. */ - public interface EventListener { + public interface ProvisioningManager { /** - * Called each time provision is completed. + * Called when a session requires provisioning. The manager may call + * {@link #provision()} to have this session perform the provisioning operation. The manager + * will call {@link DefaultDrmSession#onProvisionCompleted()} when provisioning has + * completed, or {@link DefaultDrmSession#onProvisionError} if provisioning fails. + * + * @param session The session. + */ + void provisionRequired(DefaultDrmSession session); + + /** + * Called by a session when it fails to perform a provisioning operation. + * + * @param error The error that occurred. + */ + void onProvisionError(Exception error); + + /** + * Called by a session when it successfully completes a provisioning operation. */ void onProvisionCompleted(); @@ -59,14 +75,13 @@ import java.util.concurrent.atomic.AtomicBoolean; private static final int MAX_LICENSE_DURATION_TO_RENEW = 60; private final ExoMediaDrm mediaDrm; + private final ProvisioningManager provisioningManager; private final byte[] initData; private final String mimeType; private final @DefaultDrmSessionManager.Mode int mode; private final HashMap optionalKeyRequestParameters; private final Handler eventHandler; private final DefaultDrmSessionManager.EventListener eventListener; - private final AtomicBoolean provisioningInProgress; - private final EventListener sessionEventListener; /* package */ final MediaDrmCallback callback; /* package */ final UUID uuid; @@ -87,6 +102,7 @@ import java.util.concurrent.atomic.AtomicBoolean; * * @param uuid The UUID of the drm scheme. * @param mediaDrm The media DRM. + * @param provisioningManager The manager for provisioning. * @param initData The DRM init data. * @param mode The DRM mode. * @param offlineLicenseKeySetId The offlineLicense KeySetId. @@ -96,13 +112,14 @@ import java.util.concurrent.atomic.AtomicBoolean; * @param eventHandler The handler to post listener events. * @param eventListener The DRM session manager event listener. */ - public DefaultDrmSession(UUID uuid, ExoMediaDrm mediaDrm, byte[] initData, String mimeType, + public DefaultDrmSession(UUID uuid, ExoMediaDrm mediaDrm, + ProvisioningManager provisioningManager, byte[] initData, String mimeType, @DefaultDrmSessionManager.Mode int mode, byte[] offlineLicenseKeySetId, HashMap optionalKeyRequestParameters, MediaDrmCallback callback, Looper playbackLooper, Handler eventHandler, - DefaultDrmSessionManager.EventListener eventListener, AtomicBoolean provisioningInProgress, - EventListener sessionEventListener) { + DefaultDrmSessionManager.EventListener eventListener) { this.uuid = uuid; + this.provisioningManager = provisioningManager; this.mediaDrm = mediaDrm; this.mode = mode; this.offlineLicenseKeySetId = offlineLicenseKeySetId; @@ -111,8 +128,6 @@ import java.util.concurrent.atomic.AtomicBoolean; this.eventHandler = eventHandler; this.eventListener = eventListener; - this.provisioningInProgress = provisioningInProgress; - this.sessionEventListener = sessionEventListener; state = STATE_OPENING; postResponseHandler = new PostResponseHandler(playbackLooper); @@ -164,7 +179,7 @@ import java.util.concurrent.atomic.AtomicBoolean; return false; } - public boolean canReuse(byte[] initData) { + public boolean hasInitData(byte[] initData) { return Arrays.equals(this.initData, initData); } @@ -172,7 +187,24 @@ import java.util.concurrent.atomic.AtomicBoolean; return Arrays.equals(this.sessionId, sessionId); } - // DrmSession Implementation. + // Provisioning implementation. + + public void provision() { + ProvisionRequest request = mediaDrm.getProvisionRequest(); + postRequestHandler.obtainMessage(MSG_PROVISION, request).sendToTarget(); + } + + public void onProvisionCompleted() { + if (openInternal(false)) { + doLicense(); + } + } + + public void onProvisionError(Exception error) { + onError(error); + } + + // DrmSession implementation. @Override @DrmSession.State @@ -221,7 +253,7 @@ import java.util.concurrent.atomic.AtomicBoolean; return true; } catch (NotProvisionedException e) { if (allowProvisioning) { - postProvisionRequest(); + provisioningManager.provisionRequired(this); } else { onError(e); } @@ -232,42 +264,25 @@ import java.util.concurrent.atomic.AtomicBoolean; return false; } - private void postProvisionRequest() { - if (provisioningInProgress.getAndSet(true)) { + private void onProvisionResponse(Object response) { + if (state != STATE_OPENING && !isOpen()) { + // This event is stale. return; } - ProvisionRequest request = mediaDrm.getProvisionRequest(); - postRequestHandler.obtainMessage(MSG_PROVISION, request).sendToTarget(); - } - private void onProvisionResponse(Object response) { - provisioningInProgress.set(false); if (response instanceof Exception) { - onError((Exception) response); + provisioningManager.onProvisionError((Exception) response); return; } try { mediaDrm.provideProvisionResponse((byte[]) response); } catch (DeniedByServerException e) { - onError(e); + provisioningManager.onProvisionError(e); return; } - if (sessionEventListener != null) { - sessionEventListener.onProvisionCompleted(); - } - } - - public void onProvisionCompleted() { - if (state != STATE_OPENING && !isOpen()) { - // This event is stale. - return; - } - - if (openInternal(false)) { - doLicense(); - } + provisioningManager.onProvisionCompleted(); } private void doLicense() { @@ -405,7 +420,7 @@ import java.util.concurrent.atomic.AtomicBoolean; private void onKeysError(Exception e) { if (e instanceof NotProvisionedException) { - postProvisionRequest(); + provisioningManager.provisionRequired(this); } else { onError(e); } @@ -447,7 +462,7 @@ import java.util.concurrent.atomic.AtomicBoolean; break; case ExoMediaDrm.EVENT_PROVISION_REQUIRED: state = STATE_OPENED; - postProvisionRequest(); + provisioningManager.provisionRequired(this); break; default: break; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java index cff9f9da0f..a0d5a932f2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java @@ -24,6 +24,7 @@ import android.support.annotation.IntDef; import android.support.annotation.NonNull; import android.text.TextUtils; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.drm.DefaultDrmSession.ProvisioningManager; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; import com.google.android.exoplayer2.drm.ExoMediaDrm.OnEventListener; import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil; @@ -36,14 +37,13 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.UUID; -import java.util.concurrent.atomic.AtomicBoolean; /** * A {@link DrmSessionManager} that supports playbacks using {@link ExoMediaDrm}. */ @TargetApi(18) public class DefaultDrmSessionManager implements DrmSessionManager, - DefaultDrmSession.EventListener { + ProvisioningManager { /** * Listener of {@link DefaultDrmSessionManager} events. @@ -107,7 +107,7 @@ public class DefaultDrmSessionManager implements DrmSe private final boolean multiSession; private final List> sessions; - private final AtomicBoolean provisioningInProgress; + private final List> provisioningSessions; private Looper playbackLooper; private int mode; @@ -223,7 +223,7 @@ public class DefaultDrmSessionManager implements DrmSe this.multiSession = multiSession; mode = MODE_PLAYBACK; sessions = new ArrayList<>(); - provisioningInProgress = new AtomicBoolean(false); + provisioningSessions = new ArrayList<>(); if (multiSession) { mediaDrm.setPropertyString("sessionSharing", "enable"); } @@ -363,17 +363,21 @@ public class DefaultDrmSessionManager implements DrmSe } } - for (DefaultDrmSession s : sessions) { - if (!multiSession || s.canReuse(initData)) { - session = s; - break; + if (!multiSession) { + // Look for an existing session to use. + for (DefaultDrmSession existingSession : sessions) { + if (existingSession.hasInitData(initData)) { + session = existingSession; + break; + } } } if (session == null) { - session = new DefaultDrmSession(uuid, mediaDrm, initData, mimeType, mode, + // Create a new session. + session = new DefaultDrmSession<>(uuid, mediaDrm, this, initData, mimeType, mode, offlineLicenseKeySetId, optionalKeyRequestParameters, callback, playbackLooper, - eventHandler, eventListener, provisioningInProgress, this); + eventHandler, eventListener); sessions.add(session); } session.acquire(); @@ -385,16 +389,44 @@ public class DefaultDrmSessionManager implements DrmSe DefaultDrmSession drmSession = (DefaultDrmSession) session; if (drmSession.release()) { sessions.remove(drmSession); + if (provisioningSessions.size() > 1 && provisioningSessions.get(0) == drmSession) { + // Other sessions were waiting for the released session to complete a provision operation. + // We need to have one of those sessions perform the provision operation instead. + provisioningSessions.get(1).provision(); + } + provisioningSessions.remove(drmSession); + } + } + + // ProvisioningManager implementation. + + @Override + public void provisionRequired(DefaultDrmSession session) { + provisioningSessions.add(session); + if (provisioningSessions.size() == 1) { + // This is the first session requesting provisioning, so have it perform the operation. + session.provision(); } } @Override public void onProvisionCompleted() { - for (DefaultDrmSession session : sessions) { + for (DefaultDrmSession session : provisioningSessions) { session.onProvisionCompleted(); } + provisioningSessions.clear(); } + @Override + public void onProvisionError(Exception error) { + for (DefaultDrmSession session : provisioningSessions) { + session.onProvisionError(error); + } + provisioningSessions.clear(); + } + + // Internal methods. + /** * Extracts {@link SchemeData} suitable for the given DRM scheme {@link UUID}. * From 6592a6474e99d0710b0b03454bde750221a73d64 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Fri, 15 Sep 2017 03:44:41 -0700 Subject: [PATCH 0443/2472] Improve documentation for SCTE35-related metadata Also expose break_durations in microseconds instead of 90kHz. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=168816992 --- .../metadata/scte35/PrivateCommand.java | 9 +++ .../metadata/scte35/SpliceInsertCommand.java | 65 ++++++++++++++++--- .../scte35/SpliceScheduleCommand.java | 62 +++++++++++++++--- .../metadata/scte35/TimeSignalCommand.java | 6 ++ .../scte35/SpliceInfoDecoderTest.java | 4 +- 5 files changed, 127 insertions(+), 19 deletions(-) diff --git a/library/core/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 index beb4cb9b88..4334fa99cb 100644 --- a/library/core/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 @@ -24,8 +24,17 @@ import com.google.android.exoplayer2.util.ParsableByteArray; */ public final class PrivateCommand extends SpliceCommand { + /** + * The {@code pts_adjustment} as defined in SCTE35, Section 9.2. + */ public final long ptsAdjustment; + /** + * The identifier as defined in SCTE35, Section 9.3.6. + */ public final long identifier; + /** + * The private bytes as defined in SCTE35, Section 9.3.6. + */ public final byte[] commandBytes; private PrivateCommand(long identifier, byte[] commandBytes, long ptsAdjustment) { diff --git a/library/core/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 index 7ce8b47e2a..6f56d3b68c 100644 --- a/library/core/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 @@ -29,24 +29,72 @@ import java.util.List; */ public final class SpliceInsertCommand extends SpliceCommand { + /** + * The splice event id. + */ public final long spliceEventId; + /** + * True if the event with id {@link #spliceEventId} has been canceled. + */ public final boolean spliceEventCancelIndicator; + /** + * If true, the splice event is an opportunity to exit from the network feed. If false, indicates + * an opportunity to return to the network feed. + */ public final boolean outOfNetworkIndicator; + /** + * Whether the splice mode is program splice mode, whereby all PIDs/components are to be spliced. + * If false, splicing is done per PID/component. + */ public final boolean programSpliceFlag; + /** + * Whether splicing should be done at the nearest opportunity. If false, splicing should be done + * at the moment indicated by {@link #programSplicePlaybackPositionUs} or + * {@link ComponentSplice#componentSplicePlaybackPositionUs}, depending on + * {@link #programSpliceFlag}. + */ public final boolean spliceImmediateFlag; + /** + * If {@link #programSpliceFlag} is true, the PTS at which the program splice should occur. + * {@link C#TIME_UNSET} otherwise. + */ public final long programSplicePts; + /** + * Equivalent to {@link #programSplicePts} but in the playback timebase. + */ public final long programSplicePlaybackPositionUs; + /** + * If {@link #programSpliceFlag} is false, a non-empty list containing the + * {@link ComponentSplice}s. Otherwise, an empty list. + */ public final List componentSpliceList; + /** + * If {@link #breakDurationUs} is not {@link C#TIME_UNSET}, defines whether + * {@link #breakDurationUs} should be used to know when to return to the network feed. If + * {@link #breakDurationUs} is {@link C#TIME_UNSET}, the value is undefined. + */ public final boolean autoReturn; - public final long breakDuration; + /** + * The duration of the splice in microseconds, or {@link C#TIME_UNSET} if no duration is present. + */ + public final long breakDurationUs; + /** + * The unique program id as defined in SCTE35, Section 9.3.3. + */ public final int uniqueProgramId; + /** + * Holds the value of {@code avail_num} as defined in SCTE35, Section 9.3.3. + */ public final int availNum; + /** + * Holds the value of {@code avails_expected} as defined in SCTE35, Section 9.3.3. + */ public final int availsExpected; private SpliceInsertCommand(long spliceEventId, boolean spliceEventCancelIndicator, boolean outOfNetworkIndicator, boolean programSpliceFlag, boolean spliceImmediateFlag, long programSplicePts, long programSplicePlaybackPositionUs, - List componentSpliceList, boolean autoReturn, long breakDuration, + List componentSpliceList, boolean autoReturn, long breakDurationUs, int uniqueProgramId, int availNum, int availsExpected) { this.spliceEventId = spliceEventId; this.spliceEventCancelIndicator = spliceEventCancelIndicator; @@ -57,7 +105,7 @@ public final class SpliceInsertCommand extends SpliceCommand { this.programSplicePlaybackPositionUs = programSplicePlaybackPositionUs; this.componentSpliceList = Collections.unmodifiableList(componentSpliceList); this.autoReturn = autoReturn; - this.breakDuration = breakDuration; + this.breakDurationUs = breakDurationUs; this.uniqueProgramId = uniqueProgramId; this.availNum = availNum; this.availsExpected = availsExpected; @@ -78,7 +126,7 @@ public final class SpliceInsertCommand extends SpliceCommand { } this.componentSpliceList = Collections.unmodifiableList(componentSpliceList); autoReturn = in.readByte() == 1; - breakDuration = in.readLong(); + breakDurationUs = in.readLong(); uniqueProgramId = in.readInt(); availNum = in.readInt(); availsExpected = in.readInt(); @@ -98,7 +146,7 @@ public final class SpliceInsertCommand extends SpliceCommand { int availNum = 0; int availsExpected = 0; boolean autoReturn = false; - long duration = C.TIME_UNSET; + long breakDurationUs = C.TIME_UNSET; if (!spliceEventCancelIndicator) { int headerByte = sectionData.readUnsignedByte(); outOfNetworkIndicator = (headerByte & 0x80) != 0; @@ -124,7 +172,8 @@ public final class SpliceInsertCommand extends SpliceCommand { if (durationFlag) { long firstByte = sectionData.readUnsignedByte(); autoReturn = (firstByte & 0x80) != 0; - duration = ((firstByte & 0x01) << 32) | sectionData.readUnsignedInt(); + long breakDuration90khz = ((firstByte & 0x01) << 32) | sectionData.readUnsignedInt(); + breakDurationUs = breakDuration90khz * 1000 / 90; } uniqueProgramId = sectionData.readUnsignedShort(); availNum = sectionData.readUnsignedByte(); @@ -133,7 +182,7 @@ public final class SpliceInsertCommand extends SpliceCommand { return new SpliceInsertCommand(spliceEventId, spliceEventCancelIndicator, outOfNetworkIndicator, programSpliceFlag, spliceImmediateFlag, programSplicePts, timestampAdjuster.adjustTsTimestamp(programSplicePts), componentSplices, autoReturn, - duration, uniqueProgramId, availNum, availsExpected); + breakDurationUs, uniqueProgramId, availNum, availsExpected); } /** @@ -181,7 +230,7 @@ public final class SpliceInsertCommand extends SpliceCommand { componentSpliceList.get(i).writeToParcel(dest); } dest.writeByte((byte) (autoReturn ? 1 : 0)); - dest.writeLong(breakDuration); + dest.writeLong(breakDurationUs); dest.writeInt(uniqueProgramId); dest.writeInt(availNum); dest.writeInt(availsExpected); diff --git a/library/core/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 index 9b391cea6c..8696909c97 100644 --- a/library/core/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 @@ -33,22 +33,62 @@ public final class SpliceScheduleCommand extends SpliceCommand { */ public static final class Event { + /** + * The splice event id. + */ public final long spliceEventId; + /** + * True if the event with id {@link #spliceEventId} has been canceled. + */ public final boolean spliceEventCancelIndicator; + /** + * If true, the splice event is an opportunity to exit from the network feed. If false, + * indicates an opportunity to return to the network feed. + */ public final boolean outOfNetworkIndicator; + /** + * Whether the splice mode is program splice mode, whereby all PIDs/components are to be + * spliced. If false, splicing is done per PID/component. + */ public final boolean programSpliceFlag; + /** + * Represents the time of the signaled splice event as the number of seconds since 00 hours UTC, + * January 6th, 1980, with the count of intervening leap seconds included. + */ public final long utcSpliceTime; + /** + * If {@link #programSpliceFlag} is false, a non-empty list containing the + * {@link ComponentSplice}s. Otherwise, an empty list. + */ public final List componentSpliceList; + /** + * If {@link #breakDurationUs} is not {@link C#TIME_UNSET}, defines whether + * {@link #breakDurationUs} should be used to know when to return to the network feed. If + * {@link #breakDurationUs} is {@link C#TIME_UNSET}, the value is undefined. + */ public final boolean autoReturn; - public final long breakDuration; + /** + * The duration of the splice in microseconds, or {@link C#TIME_UNSET} if no duration is + * present. + */ + public final long breakDurationUs; + /** + * The unique program id as defined in SCTE35, Section 9.3.2. + */ public final int uniqueProgramId; + /** + * Holds the value of {@code avail_num} as defined in SCTE35, Section 9.3.2. + */ public final int availNum; + /** + * Holds the value of {@code avails_expected} as defined in SCTE35, Section 9.3.2. + */ public final int availsExpected; private Event(long spliceEventId, boolean spliceEventCancelIndicator, boolean outOfNetworkIndicator, boolean programSpliceFlag, List componentSpliceList, long utcSpliceTime, boolean autoReturn, - long breakDuration, int uniqueProgramId, int availNum, int availsExpected) { + long breakDurationUs, int uniqueProgramId, int availNum, int availsExpected) { this.spliceEventId = spliceEventId; this.spliceEventCancelIndicator = spliceEventCancelIndicator; this.outOfNetworkIndicator = outOfNetworkIndicator; @@ -56,7 +96,7 @@ public final class SpliceScheduleCommand extends SpliceCommand { this.componentSpliceList = Collections.unmodifiableList(componentSpliceList); this.utcSpliceTime = utcSpliceTime; this.autoReturn = autoReturn; - this.breakDuration = breakDuration; + this.breakDurationUs = breakDurationUs; this.uniqueProgramId = uniqueProgramId; this.availNum = availNum; this.availsExpected = availsExpected; @@ -75,7 +115,7 @@ public final class SpliceScheduleCommand extends SpliceCommand { this.componentSpliceList = Collections.unmodifiableList(componentSpliceList); this.utcSpliceTime = in.readLong(); this.autoReturn = in.readByte() == 1; - this.breakDuration = in.readLong(); + this.breakDurationUs = in.readLong(); this.uniqueProgramId = in.readInt(); this.availNum = in.readInt(); this.availsExpected = in.readInt(); @@ -93,7 +133,7 @@ public final class SpliceScheduleCommand extends SpliceCommand { int availNum = 0; int availsExpected = 0; boolean autoReturn = false; - long duration = C.TIME_UNSET; + long breakDurationUs = C.TIME_UNSET; if (!spliceEventCancelIndicator) { int headerByte = sectionData.readUnsignedByte(); outOfNetworkIndicator = (headerByte & 0x80) != 0; @@ -114,15 +154,16 @@ public final class SpliceScheduleCommand extends SpliceCommand { if (durationFlag) { long firstByte = sectionData.readUnsignedByte(); autoReturn = (firstByte & 0x80) != 0; - duration = ((firstByte & 0x01) << 32) | sectionData.readUnsignedInt(); + long breakDuration90khz = ((firstByte & 0x01) << 32) | sectionData.readUnsignedInt(); + breakDurationUs = breakDuration90khz * 1000 / 90; } uniqueProgramId = sectionData.readUnsignedShort(); availNum = sectionData.readUnsignedByte(); availsExpected = sectionData.readUnsignedByte(); } return new Event(spliceEventId, spliceEventCancelIndicator, outOfNetworkIndicator, - programSpliceFlag, componentSplices, utcSpliceTime, autoReturn, duration, uniqueProgramId, - availNum, availsExpected); + programSpliceFlag, componentSplices, utcSpliceTime, autoReturn, breakDurationUs, + uniqueProgramId, availNum, availsExpected); } private void writeToParcel(Parcel dest) { @@ -137,7 +178,7 @@ public final class SpliceScheduleCommand extends SpliceCommand { } dest.writeLong(utcSpliceTime); dest.writeByte((byte) (autoReturn ? 1 : 0)); - dest.writeLong(breakDuration); + dest.writeLong(breakDurationUs); dest.writeInt(uniqueProgramId); dest.writeInt(availNum); dest.writeInt(availsExpected); @@ -173,6 +214,9 @@ public final class SpliceScheduleCommand extends SpliceCommand { } + /** + * The list of scheduled events. + */ public final List events; private SpliceScheduleCommand(List events) { diff --git a/library/core/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 index f756b72d6d..e233a276ed 100644 --- a/library/core/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 @@ -25,7 +25,13 @@ import com.google.android.exoplayer2.util.TimestampAdjuster; */ public final class TimeSignalCommand extends SpliceCommand { + /** + * A PTS value, as defined in SCTE35, Section 9.3.4. + */ public final long ptsTime; + /** + * Equivalent to {@link #ptsTime} but in the playback timebase. + */ public final long playbackPositionUs; private TimeSignalCommand(long ptsTime, long playbackPositionUs) { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoderTest.java index 15cb9b23c5..8cd90c7a64 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/scte35/SpliceInfoDecoderTest.java @@ -109,7 +109,7 @@ public final class SpliceInfoDecoderTest { assertThat(command.programSpliceFlag).isTrue(); assertThat(command.spliceImmediateFlag).isFalse(); assertThat(command.programSplicePlaybackPositionUs).isEqualTo(3000000); - assertThat(command.breakDuration).isEqualTo(TIME_UNSET); + assertThat(command.breakDurationUs).isEqualTo(TIME_UNSET); assertThat(command.uniqueProgramId).isEqualTo(16); assertThat(command.availNum).isEqualTo(1); assertThat(command.availsExpected).isEqualTo(2); @@ -155,7 +155,7 @@ public final class SpliceInfoDecoderTest { assertThat(command.programSpliceFlag).isFalse(); assertThat(command.spliceImmediateFlag).isFalse(); assertThat(command.programSplicePlaybackPositionUs).isEqualTo(TIME_UNSET); - assertThat(command.breakDuration).isEqualTo(TIME_UNSET); + assertThat(command.breakDurationUs).isEqualTo(TIME_UNSET); List componentSplices = command.componentSpliceList; assertThat(componentSplices).hasSize(2); assertThat(componentSplices.get(0).componentTag).isEqualTo(16); From b2bc4450df84c8c6f6720a508dc08d5249761886 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Fri, 15 Sep 2017 07:51:51 -0700 Subject: [PATCH 0444/2472] Run cbc1/cbcs tests from API 25 onwards ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=168834998 --- .../playbacktests/gts/CommonEncryptionDrmTest.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/CommonEncryptionDrmTest.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/CommonEncryptionDrmTest.java index c6dd72debd..15acae96fd 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/CommonEncryptionDrmTest.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/CommonEncryptionDrmTest.java @@ -74,7 +74,9 @@ public final class CommonEncryptionDrmTest extends ActivityInstrumentationTestCa } public void testCbc1SchemeType() { - if (Util.SDK_INT < 24) { + if (Util.SDK_INT < 25) { + // cbc1 support was added in API 24, but it is stable from API 25 onwards. + // See [internal: b/65634809]. // Pass. return; } @@ -82,7 +84,9 @@ public final class CommonEncryptionDrmTest extends ActivityInstrumentationTestCa } public void testCbcsSchemeType() { - if (Util.SDK_INT < 24) { + if (Util.SDK_INT < 25) { + // cbcs support was added in API 24, but it is stable from API 25 onwards. + // See [internal: b/65634809]. // Pass. return; } From 457d0ba1b9b9b4995e25d028655867fb4e5821e7 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 15 Sep 2017 09:28:28 -0700 Subject: [PATCH 0445/2472] Work around broken AAC decoders on Galaxy S6 Issue: #3249 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=168844850 --- .../exoplayer2/mediacodec/MediaCodecUtil.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java index 2cd336e9ef..f75ce5a9e5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java @@ -325,6 +325,21 @@ public final class MediaCodecUtil { return false; } + // Work around https://github.com/google/ExoPlayer/issues/3249. + if (Util.SDK_INT < 24 + && ("OMX.SEC.aac.dec".equals(name) || "OMX.Exynos.AAC.Decoder".equals(name)) + && Util.MANUFACTURER.equals("samsung") + && (Util.DEVICE.startsWith("zeroflte") // Galaxy S6 + || Util.DEVICE.startsWith("zerolte") // Galaxy S6 Edge + || Util.DEVICE.startsWith("zenlte") // Galaxy S6 Edge+ + || Util.DEVICE.equals("SC-05G") // Galaxy S6 + || Util.DEVICE.equals("marinelteatt") // Galaxy S6 Active + || Util.DEVICE.equals("404SC") // Galaxy S6 Edge + || Util.DEVICE.equals("SC-04G") + || Util.DEVICE.equals("SCV31"))) { + return false; + } + // Work around https://github.com/google/ExoPlayer/issues/548. // VP8 decoder on Samsung Galaxy S3/S4/S4 Mini/Tab 3/Note 2 does not render video. if (Util.SDK_INT <= 19 From 8d739067ece566284c76e2e0ac7248c790a602bb Mon Sep 17 00:00:00 2001 From: eguven Date: Mon, 18 Sep 2017 02:42:26 -0700 Subject: [PATCH 0446/2472] Add HlsDownloadTest ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=169064003 --- .../exoplayer2/playbacktests/gts/DashDownloadTest.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashDownloadTest.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashDownloadTest.java index 66884f3e5b..7818ddcf12 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashDownloadTest.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashDownloadTest.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.playbacktests.gts; import android.net.Uri; import android.test.ActivityInstrumentationTestCase2; +import android.util.Log; import com.google.android.exoplayer2.offline.Downloader; import com.google.android.exoplayer2.offline.Downloader.ProgressListener; import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; @@ -178,8 +179,9 @@ public final class DashDownloadTest extends ActivityInstrumentationTestCase2= stopAt) { Thread.currentThread().interrupt(); } From 5e2c7d967a74fe0ce67b9c57bb391c40935566d1 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Mon, 18 Sep 2017 05:44:31 -0700 Subject: [PATCH 0447/2472] Fix 2 CEA decoder bugs 1- Avoid dropped buffers by using a PriorityQueue instead of a set. 2- Process the end of stream after non-EOS buffers. Issue:#3250 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=169077365 --- .../exoplayer2/text/SubtitleInputBuffer.java | 3 +++ .../android/exoplayer2/text/TextRenderer.java | 8 ++++---- .../android/exoplayer2/text/cea/CeaDecoder.java | 14 ++++++-------- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleInputBuffer.java b/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleInputBuffer.java index 28e67e8623..4b3b61bddf 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleInputBuffer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleInputBuffer.java @@ -37,6 +37,9 @@ public final class SubtitleInputBuffer extends DecoderInputBuffer @Override public int compareTo(@NonNull SubtitleInputBuffer other) { + if (isEndOfStream() != other.isEndOfStream()) { + return isEndOfStream() ? 1 : -1; + } long delta = timeUs - other.timeUs; if (delta == 0) { return 0; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java index c3dc2383ef..c6d7f6f163 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java @@ -37,7 +37,7 @@ import java.util.List; *

          * {@link Subtitle}s are decoded from sample data using {@link SubtitleDecoder} instances obtained * from a {@link SubtitleDecoderFactory}. The actual rendering of the subtitle {@link Cue}s is - * delegated to an {@link TextOutput}. + * delegated to a {@link TextOutput}. */ public final class TextRenderer extends BaseRenderer implements Callback { @@ -291,9 +291,9 @@ public final class TextRenderer extends BaseRenderer implements Callback { } private long getNextEventTime() { - return ((nextSubtitleEventIndex == C.INDEX_UNSET) - || (nextSubtitleEventIndex >= subtitle.getEventTimeCount())) ? Long.MAX_VALUE - : (subtitle.getEventTime(nextSubtitleEventIndex)); + return nextSubtitleEventIndex == C.INDEX_UNSET + || nextSubtitleEventIndex >= subtitle.getEventTimeCount() + ? Long.MAX_VALUE : subtitle.getEventTime(nextSubtitleEventIndex); } private void updateOutput(List cues) { diff --git a/library/core/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 index fac0982e65..bb13a7d143 100644 --- a/library/core/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 @@ -24,7 +24,7 @@ import com.google.android.exoplayer2.text.SubtitleInputBuffer; import com.google.android.exoplayer2.text.SubtitleOutputBuffer; import com.google.android.exoplayer2.util.Assertions; import java.util.LinkedList; -import java.util.TreeSet; +import java.util.PriorityQueue; /** * Base class for subtitle parsers for CEA captions. @@ -36,7 +36,7 @@ import java.util.TreeSet; private final LinkedList availableInputBuffers; private final LinkedList availableOutputBuffers; - private final TreeSet queuedInputBuffers; + private final PriorityQueue queuedInputBuffers; private SubtitleInputBuffer dequeuedInputBuffer; private long playbackPositionUs; @@ -50,7 +50,7 @@ import java.util.TreeSet; for (int i = 0; i < NUM_OUTPUT_BUFFERS; i++) { availableOutputBuffers.add(new CeaOutputBuffer(this)); } - queuedInputBuffers = new TreeSet<>(); + queuedInputBuffers = new PriorityQueue<>(); } @Override @@ -73,7 +73,6 @@ import java.util.TreeSet; @Override public void queueInputBuffer(SubtitleInputBuffer inputBuffer) throws SubtitleDecoderException { - Assertions.checkArgument(inputBuffer != null); Assertions.checkArgument(inputBuffer == dequeuedInputBuffer); if (inputBuffer.isDecodeOnly()) { // We can drop this buffer early (i.e. before it would be decoded) as the CEA formats allow @@ -90,13 +89,12 @@ import java.util.TreeSet; if (availableOutputBuffers.isEmpty()) { return null; } - // iterate through all available input buffers whose timestamps are less than or equal // to the current playback position; processing input buffers for future content should // be deferred until they would be applicable while (!queuedInputBuffers.isEmpty() - && queuedInputBuffers.first().timeUs <= playbackPositionUs) { - SubtitleInputBuffer inputBuffer = queuedInputBuffers.pollFirst(); + && queuedInputBuffers.peek().timeUs <= playbackPositionUs) { + SubtitleInputBuffer inputBuffer = queuedInputBuffers.poll(); // If the input buffer indicates we've reached the end of the stream, we can // return immediately with an output buffer propagating that @@ -142,7 +140,7 @@ import java.util.TreeSet; public void flush() { playbackPositionUs = 0; while (!queuedInputBuffers.isEmpty()) { - releaseInputBuffer(queuedInputBuffers.pollFirst()); + releaseInputBuffer(queuedInputBuffers.poll()); } if (dequeuedInputBuffer != null) { releaseInputBuffer(dequeuedInputBuffer); From 9bdf1ee944ad8d021cf5a03bbdb399ff768d57d3 Mon Sep 17 00:00:00 2001 From: eguven Date: Mon, 18 Sep 2017 09:39:09 -0700 Subject: [PATCH 0448/2472] Check if the cache is already empty before trying to evict more span This case may happen if the max span size is more than the max size the evictor is configured. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=169101093 --- .../cache/LeastRecentlyUsedCacheEvictor.java | 2 +- .../LeastRecentlyUsedCacheEvictorTest.java | 46 +++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/LeastRecentlyUsedCacheEvictorTest.java diff --git a/library/core/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 index d2a84f65f4..79d23dd1b0 100644 --- a/library/core/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 @@ -74,7 +74,7 @@ public final class LeastRecentlyUsedCacheEvictor implements CacheEvictor, Compar } private void evictCache(Cache cache, long requiredSpace) { - while (currentSize + requiredSpace > maxBytes) { + while (currentSize + requiredSpace > maxBytes && !leastRecentlyUsed.isEmpty()) { try { cache.removeSpan(leastRecentlyUsed.first()); } catch (CacheException e) { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/LeastRecentlyUsedCacheEvictorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/LeastRecentlyUsedCacheEvictorTest.java new file mode 100644 index 0000000000..6f7f567ae7 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/LeastRecentlyUsedCacheEvictorTest.java @@ -0,0 +1,46 @@ +/* + * 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 org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Unit tests for {@link LeastRecentlyUsedCacheEvictor}. + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public class LeastRecentlyUsedCacheEvictorTest { + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + } + + @Test + public void testContentBiggerThanMaxSizeDoesNotThrowException() throws Exception { + int maxBytes = 100; + LeastRecentlyUsedCacheEvictor evictor = new LeastRecentlyUsedCacheEvictor(maxBytes); + evictor.onCacheInitialized(); + evictor.onStartFile(Mockito.mock(Cache.class), "key", 0, maxBytes + 1); + } + +} From 5b0e693419c189f76cd0822fb6168311c660658b Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 19 Sep 2017 07:44:46 -0700 Subject: [PATCH 0449/2472] Fix DefaultDashChunkSource.updateLiveEdgeTimeUs Very subtle, but lastAvailableSegmentNum is shifted by RepresentationHolder.segmentNumShift. When accessing the index directly it's necessary to unshift. The easiest way to do this is to call through the holder, which does this for you. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=169239928 --- .../source/dash/DefaultDashChunkSource.java | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/library/dash/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 index cd7ef6a2bf..eba36e9057 100644 --- a/library/dash/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 @@ -317,15 +317,8 @@ public class DefaultDashChunkSource implements DashChunkSource { private void updateLiveEdgeTimeUs(RepresentationHolder representationHolder, int lastAvailableSegmentNum) { - if (manifest.dynamic) { - DashSegmentIndex segmentIndex = representationHolder.representation.getIndex(); - long lastSegmentDurationUs = segmentIndex.getDurationUs(lastAvailableSegmentNum, - manifest.getPeriodDurationUs(periodIndex)); - liveEdgeTimeUs = segmentIndex.getTimeUs(lastAvailableSegmentNum) - + lastSegmentDurationUs; - } else { - liveEdgeTimeUs = C.TIME_UNSET; - } + liveEdgeTimeUs = manifest.dynamic + ? representationHolder.getSegmentEndTimeUs(lastAvailableSegmentNum) : C.TIME_UNSET; } private long getNowUnixTimeUs() { From fed2a1a6eaf8025bc6ff8ff92c78ce7dcabc17d1 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 19 Sep 2017 08:46:04 -0700 Subject: [PATCH 0450/2472] Fix PTS wraparound in HLS+Webvtt Issue:#2928 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=169246424 --- .../android/exoplayer2/source/hls/WebvttExtractor.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/library/hls/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 index 12ea2c16c7..0b8f7f36a6 100644 --- a/library/hls/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 @@ -141,8 +141,7 @@ import java.util.regex.Pattern; throw new ParserException("X-TIMESTAMP-MAP doesn't contain media timestamp: " + line); } vttTimestampUs = WebvttParserUtil.parseTimestampUs(localTimestampMatcher.group(1)); - tsTimestampUs = TimestampAdjuster.ptsToUs( - Long.parseLong(mediaTimestampMatcher.group(1))); + tsTimestampUs = TimestampAdjuster.ptsToUs(Long.parseLong(mediaTimestampMatcher.group(1))); } } @@ -155,8 +154,8 @@ import java.util.regex.Pattern; } long firstCueTimeUs = WebvttParserUtil.parseTimestampUs(cueHeaderMatcher.group(1)); - long sampleTimeUs = timestampAdjuster.adjustSampleTimestamp( - firstCueTimeUs + tsTimestampUs - vttTimestampUs); + long sampleTimeUs = timestampAdjuster.adjustTsTimestamp( + TimestampAdjuster.usToPts(firstCueTimeUs + tsTimestampUs - vttTimestampUs)); long subsampleOffsetUs = sampleTimeUs - firstCueTimeUs; // Output the track. TrackOutput trackOutput = buildTrackOutput(subsampleOffsetUs); From 8a0e148041ad4368276c407afcc841c6848f8099 Mon Sep 17 00:00:00 2001 From: eguven Date: Tue, 19 Sep 2017 09:07:04 -0700 Subject: [PATCH 0451/2472] Workaround a cipher issue in Android 4.3 [] Issue: #2755 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=169249093 --- .../upstream/cache/CachedContentIndex.java | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/library/core/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 index e1c2c13865..f043771e30 100644 --- a/library/core/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 @@ -99,7 +99,7 @@ import javax.crypto.spec.SecretKeySpec; if (secretKey != null) { Assertions.checkArgument(secretKey.length == 16); try { - cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING"); + cipher = getCipher(); secretKeySpec = new SecretKeySpec(secretKey, "AES"); } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { throw new IllegalStateException(e); // Should never happen. @@ -354,6 +354,18 @@ import javax.crypto.spec.SecretKeySpec; return cachedContent; } + private static Cipher getCipher() throws NoSuchPaddingException, NoSuchAlgorithmException { + // Workaround for https://issuetracker.google.com/issues/36976726 + if (Util.SDK_INT == 18) { + try { + return Cipher.getInstance("AES/CBC/PKCS5PADDING", "BC"); + } catch (Throwable ignored) { + // ignored + } + } + return Cipher.getInstance("AES/CBC/PKCS5PADDING"); + } + /** * Returns an id which isn't used in the given array. If the maximum id in the array is smaller * than {@link java.lang.Integer#MAX_VALUE} it just returns the next bigger integer. Otherwise it From 09248608c08ebd349b5805a9e4685b147c41e55f Mon Sep 17 00:00:00 2001 From: eguven Date: Tue, 19 Sep 2017 09:10:35 -0700 Subject: [PATCH 0452/2472] Notify span listeners even if index store fails in SimpleCache.removeSpan This fixes infinite loop in LeastRecentlyUsedCacheEvictor.evictCache when index store fails. Also made CachedContentIndex not final so it can be mocked and added a package protected SimpleCache constructor so mock index can be injected. Issue: #3260 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=169249517 --- .../upstream/cache/CachedContentIndex.java | 2 +- .../upstream/cache/SimpleCache.java | 27 ++++++++--- .../upstream/cache/SimpleCacheTest.java | 45 ++++++++++++++++++- 3 files changed, 66 insertions(+), 8 deletions(-) diff --git a/library/core/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 index f043771e30..5d92c51dcc 100644 --- a/library/core/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 @@ -49,7 +49,7 @@ import javax.crypto.spec.SecretKeySpec; /** * This class maintains the index of cached content. */ -/*package*/ final class CachedContentIndex { +/*package*/ class CachedContentIndex { public static final String FILE_NAME = "cached_content_index.exi"; diff --git a/library/core/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 index bb1ac83698..2fe16287db 100644 --- a/library/core/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 @@ -47,7 +47,7 @@ public final class SimpleCache implements Cache { * @param evictor The evictor to be used. */ public SimpleCache(File cacheDir, CacheEvictor evictor) { - this(cacheDir, evictor, null); + this(cacheDir, evictor, null, false); } /** @@ -74,10 +74,22 @@ public final class SimpleCache implements Cache { * @param encrypt When false, a plaintext index will be written. */ public SimpleCache(File cacheDir, CacheEvictor evictor, byte[] secretKey, boolean encrypt) { + this(cacheDir, evictor, new CachedContentIndex(cacheDir, secretKey, encrypt)); + } + + /** + * Constructs the cache. The cache will delete any unrecognized files from the directory. Hence + * the directory cannot be used to store other files. + * + * @param cacheDir A dedicated cache directory. + * @param evictor The evictor to be used. + * @param index The CachedContentIndex to be used. + */ + /*package*/ SimpleCache(File cacheDir, CacheEvictor evictor, CachedContentIndex index) { this.cacheDir = cacheDir; this.evictor = evictor; this.lockedSpans = new HashMap<>(); - this.index = new CachedContentIndex(cacheDir, secretKey, encrypt); + this.index = index; this.listeners = new HashMap<>(); // Start cache initialization. final ConditionVariable conditionVariable = new ConditionVariable(); @@ -304,11 +316,14 @@ public final class SimpleCache implements Cache { return; } totalSpace -= span.length; - if (removeEmptyCachedContent && cachedContent.isEmpty()) { - index.removeEmpty(cachedContent.key); - index.store(); + try { + if (removeEmptyCachedContent && cachedContent.isEmpty()) { + index.removeEmpty(cachedContent.key); + index.store(); + } + } finally { + notifySpanRemoved(span); } - notifySpanRemoved(span); } @Override diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java index ed55045835..d5894895b1 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java @@ -18,8 +18,10 @@ package com.google.android.exoplayer2.upstream.cache; import static com.google.android.exoplayer2.C.LENGTH_UNSET; import static com.google.android.exoplayer2.util.Util.toByteArray; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.doAnswer; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.upstream.cache.Cache.CacheException; import com.google.android.exoplayer2.util.Util; import java.io.File; import java.io.FileInputStream; @@ -29,9 +31,14 @@ import java.util.NavigableSet; import java.util.Random; import java.util.Set; import org.junit.After; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; @@ -49,6 +56,7 @@ public class SimpleCacheTest { @Before public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); cacheDir = Util.createTempDirectory(RuntimeEnvironment.application, "ExoPlayerTest"); } @@ -209,7 +217,6 @@ public class SimpleCacheTest { assertThat(cacheDir.listFiles()).hasLength(0); } - @Test public void testGetCachedBytes() throws Exception { SimpleCache simpleCache = getSimpleCache(); @@ -245,6 +252,42 @@ public class SimpleCacheTest { simpleCache.releaseHoleSpan(cacheSpan); } + /* Tests https://github.com/google/ExoPlayer/issues/3260 case. */ + @Test + public void testExceptionDuringEvictionByLeastRecentlyUsedCacheEvictorNotHang() throws Exception { + CachedContentIndex index = Mockito.spy(new CachedContentIndex(cacheDir)); + SimpleCache simpleCache = + new SimpleCache(cacheDir, new LeastRecentlyUsedCacheEvictor(20), index); + + // Add some content. + CacheSpan cacheSpan = simpleCache.startReadWrite(KEY_1, 0); + addCache(simpleCache, KEY_1, 0, 15); + + // Make index.store() throw exception from now on. + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + throw new Cache.CacheException("SimpleCacheTest"); + } + }).when(index).store(); + + // Adding more content will make LeastRecentlyUsedCacheEvictor evict previous content. + try { + addCache(simpleCache, KEY_1, 15, 15); + Assert.fail("Exception was expected"); + } catch (CacheException e) { + // do nothing. + } + + simpleCache.releaseHoleSpan(cacheSpan); + + // Although store() has failed, it should remove the first span and add the new one. + NavigableSet cachedSpans = simpleCache.getCachedSpans(KEY_1); + assertThat(cachedSpans).isNotNull(); + assertThat(cachedSpans).hasSize(1); + assertThat(cachedSpans.pollFirst().position).isEqualTo(15); + } + private SimpleCache getSimpleCache() { return new SimpleCache(cacheDir, new NoOpCacheEvictor()); } From 89f66924d514732ffa74689d22c97b0fe3c16a38 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 19 Sep 2017 09:48:28 -0700 Subject: [PATCH 0453/2472] Use IntDef for MediaCodecRenderer internal states ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=169254794 --- .../exoplayer2/mediacodec/MediaCodecRenderer.java | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java index d6725e373a..761b23fe58 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java @@ -23,6 +23,7 @@ import android.media.MediaCrypto; import android.media.MediaFormat; import android.os.Looper; import android.os.SystemClock; +import android.support.annotation.IntDef; import android.support.annotation.Nullable; import android.util.Log; import com.google.android.exoplayer2.BaseRenderer; @@ -42,6 +43,8 @@ import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.NalUnitUtil; import com.google.android.exoplayer2.util.TraceUtil; import com.google.android.exoplayer2.util.Util; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; @@ -126,6 +129,10 @@ public abstract class MediaCodecRenderer extends BaseRenderer { */ private static final long MAX_CODEC_HOTSWAP_TIME_MS = 1000; + @Retention(RetentionPolicy.SOURCE) + @IntDef({RECONFIGURATION_STATE_NONE, RECONFIGURATION_STATE_WRITE_PENDING, + RECONFIGURATION_STATE_QUEUE_PENDING}) + private @interface ReconfigurationState {} /** * There is no pending adaptive reconfiguration work. */ @@ -140,6 +147,10 @@ public abstract class MediaCodecRenderer extends BaseRenderer { */ private static final int RECONFIGURATION_STATE_QUEUE_PENDING = 2; + @Retention(RetentionPolicy.SOURCE) + @IntDef({REINITIALIZATION_STATE_NONE, REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM, + REINITIALIZATION_STATE_WAIT_END_OF_STREAM}) + private @interface ReinitializationState {} /** * The codec does not need to be re-initialized. */ @@ -198,8 +209,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer { private int outputIndex; private boolean shouldSkipOutputBuffer; private boolean codecReconfigured; - private int codecReconfigurationState; - private int codecReinitializationState; + private @ReconfigurationState int codecReconfigurationState; + private @ReinitializationState int codecReinitializationState; private boolean codecReceivedBuffers; private boolean codecReceivedEos; From d346266dc9631a985c662c6c1e444f47bbb12caa Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 19 Sep 2017 09:57:07 -0700 Subject: [PATCH 0454/2472] Workaround Samsung tablet reboot playing adaptive secure content ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=169256059 --- .../mediacodec/MediaCodecRenderer.java | 57 ++++++++++++++----- 1 file changed, 42 insertions(+), 15 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java index 761b23fe58..1ad6c1eed7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java @@ -168,9 +168,26 @@ public abstract class MediaCodecRenderer extends BaseRenderer { */ private static final int REINITIALIZATION_STATE_WAIT_END_OF_STREAM = 2; + @Retention(RetentionPolicy.SOURCE) + @IntDef({ADAPTATION_WORKAROUND_MODE_NEVER, ADAPTATION_WORKAROUND_MODE_SAME_RESOLUTION, + ADAPTATION_WORKAROUND_MODE_ALWAYS}) + private @interface AdaptationWorkaroundMode {} + /** + * The adaptation workaround is never used. + */ + private static final int ADAPTATION_WORKAROUND_MODE_NEVER = 0; + /** + * The adaptation workaround is used when adapting between formats of the same resolution only. + */ + private static final int ADAPTATION_WORKAROUND_MODE_SAME_RESOLUTION = 1; + /** + * The adaptation workaround is always used when adapting between formats. + */ + private static final int ADAPTATION_WORKAROUND_MODE_ALWAYS = 2; + /** * H.264/AVC buffer to queue when using the adaptation workaround (see - * {@link #codecNeedsAdaptationWorkaround(String)}. Consists of three NAL units with start codes: + * {@link #codecAdaptationWorkaroundMode(String)}. Consists of three NAL units with start codes: * Baseline sequence/picture parameter sets and a 32 * 32 pixel IDR slice. This stream can be * queued to force a resolution change when adapting to a new format. */ @@ -193,9 +210,9 @@ public abstract class MediaCodecRenderer extends BaseRenderer { private DrmSession pendingDrmSession; private MediaCodec codec; private MediaCodecInfo codecInfo; + private @AdaptationWorkaroundMode int codecAdaptationWorkaroundMode; private boolean codecNeedsDiscardToSpsWorkaround; private boolean codecNeedsFlushWorkaround; - private boolean codecNeedsAdaptationWorkaround; private boolean codecNeedsEosPropagationWorkaround; private boolean codecNeedsEosFlushWorkaround; private boolean codecNeedsEosOutputExceptionWorkaround; @@ -361,9 +378,9 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } String codecName = codecInfo.name; + codecAdaptationWorkaroundMode = codecAdaptationWorkaroundMode(codecName); codecNeedsDiscardToSpsWorkaround = codecNeedsDiscardToSpsWorkaround(codecName, format); codecNeedsFlushWorkaround = codecNeedsFlushWorkaround(codecName); - codecNeedsAdaptationWorkaround = codecNeedsAdaptationWorkaround(codecName); codecNeedsEosPropagationWorkaround = codecNeedsEosPropagationWorkaround(codecName); codecNeedsEosFlushWorkaround = codecNeedsEosFlushWorkaround(codecName); codecNeedsEosOutputExceptionWorkaround = codecNeedsEosOutputExceptionWorkaround(codecName); @@ -464,7 +481,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { codecReceivedBuffers = false; codecNeedsDiscardToSpsWorkaround = false; codecNeedsFlushWorkaround = false; - codecNeedsAdaptationWorkaround = false; + codecAdaptationWorkaroundMode = ADAPTATION_WORKAROUND_MODE_NEVER; codecNeedsEosPropagationWorkaround = false; codecNeedsEosFlushWorkaround = false; codecNeedsMonoChannelCountWorkaround = false; @@ -808,8 +825,10 @@ public abstract class MediaCodecRenderer extends BaseRenderer { && canReconfigureCodec(codec, codecInfo.adaptive, oldFormat, format)) { codecReconfigured = true; codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING; - codecNeedsAdaptationWorkaroundBuffer = codecNeedsAdaptationWorkaround - && format.width == oldFormat.width && format.height == oldFormat.height; + codecNeedsAdaptationWorkaroundBuffer = + codecAdaptationWorkaroundMode == ADAPTATION_WORKAROUND_MODE_ALWAYS + || (codecAdaptationWorkaroundMode == ADAPTATION_WORKAROUND_MODE_SAME_RESOLUTION + && format.width == oldFormat.width && format.height == oldFormat.height); } else { if (codecReceivedBuffers) { // Signal end of stream and wait for any final output buffers before re-initialization. @@ -995,7 +1014,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { */ private void processOutputFormat() throws ExoPlaybackException { MediaFormat format = codec.getOutputFormat(); - if (codecNeedsAdaptationWorkaround + if (codecAdaptationWorkaroundMode != ADAPTATION_WORKAROUND_MODE_NEVER && format.getInteger(MediaFormat.KEY_WIDTH) == ADAPTATION_WORKAROUND_SLICE_WIDTH_HEIGHT && format.getInteger(MediaFormat.KEY_HEIGHT) == ADAPTATION_WORKAROUND_SLICE_WIDTH_HEIGHT) { // We assume this format changed event was caused by the adaptation workaround. @@ -1109,22 +1128,30 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } /** - * Returns whether the decoder is known to get stuck during some adaptations where the resolution - * does not change. + * Returns a mode that specifies when the adaptation workaround should be enabled. *

          - * If true is returned, the renderer will work around the issue by queueing and discarding a blank - * frame at a different resolution, which resets the codec's internal state. + * When enabled, the workaround queues and discards a blank frame with a resolution whose width + * and height both equal {@link #ADAPTATION_WORKAROUND_SLICE_WIDTH_HEIGHT}, to reset the codec's + * internal state when a format change occurs. *

          * See [Internal: b/27807182]. + * See GitHub issue #3257. * * @param name The name of the decoder. - * @return True if the decoder is known to get stuck during some adaptations. + * @return The mode specifying when the adaptation workaround should be enabled. */ - private static boolean codecNeedsAdaptationWorkaround(String name) { - return Util.SDK_INT < 24 + private @AdaptationWorkaroundMode int codecAdaptationWorkaroundMode(String name) { + if (Util.SDK_INT <= 24 && "OMX.Exynos.avc.dec.secure".equals(name) + && Util.MODEL.startsWith("SM-T585")) { + return ADAPTATION_WORKAROUND_MODE_ALWAYS; + } else if (Util.SDK_INT < 24 && ("OMX.Nvidia.h264.decode".equals(name) || "OMX.Nvidia.h264.decode.secure".equals(name)) && ("flounder".equals(Util.DEVICE) || "flounder_lte".equals(Util.DEVICE) - || "grouper".equals(Util.DEVICE) || "tilapia".equals(Util.DEVICE)); + || "grouper".equals(Util.DEVICE) || "tilapia".equals(Util.DEVICE))) { + return ADAPTATION_WORKAROUND_MODE_SAME_RESOLUTION; + } else { + return ADAPTATION_WORKAROUND_MODE_NEVER; + } } /** From ed76882271e45f1098ad69f8e796f836d6d193f0 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 19 Sep 2017 10:04:50 -0700 Subject: [PATCH 0455/2472] Bump version + release notes ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=169257339 --- RELEASENOTES.md | 17 +++++++++++++++++ constants.gradle | 2 +- demos/cast/src/main/AndroidManifest.xml | 4 ++-- demos/main/src/main/AndroidManifest.xml | 4 ++-- .../exoplayer2/ExoPlayerLibraryInfo.java | 6 +++--- 5 files changed, 25 insertions(+), 8 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index b694143542..db57428701 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,22 @@ # Release notes # +### r2.5.3 ### + +* IMA extension: Support skipping of skippable ads on AndroidTV and other + non-touch devices ([#3258](https://github.com/google/ExoPlayer/issues/3258)). +* HLS: Fix broken WebVTT captions when PTS wraps around + ([#2928](https://github.com/google/ExoPlayer/issues/2928)). +* Captions: Fix issues rendering CEA-608 captions + ([#3250](https://github.com/google/ExoPlayer/issues/3250)). +* Fix adaptive switching for live playbacks close to the live edge + ([#3017](https://github.com/google/ExoPlayer/issues/3017)). +* Workaround broken AAC decoders on Galaxy S6 + ([#3249](https://github.com/google/ExoPlayer/issues/3249)). +* Caching: Fix infinite loop when cache eviction fails + ([#3260](https://github.com/google/ExoPlayer/issues/3260)). +* Caching: Force use of BouncyCastle on JellyBean to fix decryption issue + ([#2755](https://github.com/google/ExoPlayer/issues/2755)). + ### r2.5.2 ### * IMA extension: Fix issue where ad playback could end prematurely for some diff --git a/constants.gradle b/constants.gradle index 78dd343a94..73d6aa5c56 100644 --- a/constants.gradle +++ b/constants.gradle @@ -28,7 +28,7 @@ project.ext { junitVersion = '4.12' truthVersion = '0.35' robolectricVersion = '3.4.2' - releaseVersion = 'r2.5.2' + releaseVersion = 'r2.5.3' modulePrefix = ':' if (gradle.ext.has('exoplayerModulePrefix')) { modulePrefix += gradle.ext.exoplayerModulePrefix diff --git a/demos/cast/src/main/AndroidManifest.xml b/demos/cast/src/main/AndroidManifest.xml index ee3177323e..95dfb5f8e0 100644 --- a/demos/cast/src/main/AndroidManifest.xml +++ b/demos/cast/src/main/AndroidManifest.xml @@ -15,8 +15,8 @@ --> + android:versionCode="2503" + android:versionName="2.5.3"> diff --git a/demos/main/src/main/AndroidManifest.xml b/demos/main/src/main/AndroidManifest.xml index 50a39f11a6..4a9eec4002 100644 --- a/demos/main/src/main/AndroidManifest.xml +++ b/demos/main/src/main/AndroidManifest.xml @@ -16,8 +16,8 @@ + android:versionCode="2503" + android:versionName="2.5.3"> diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java index 98eeb99ad8..90385ed6c0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java @@ -31,13 +31,13 @@ public final class ExoPlayerLibraryInfo { * The version of the library expressed as a string, for example "1.2.3". */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. - public static final String VERSION = "2.5.2"; + public static final String VERSION = "2.5.3"; /** * The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final String VERSION_SLASHY = "ExoPlayerLib/2.5.2"; + public static final String VERSION_SLASHY = "ExoPlayerLib/2.5.3"; /** * The version of the library expressed as an integer, for example 1002003. @@ -47,7 +47,7 @@ public final class ExoPlayerLibraryInfo { * integer version 123045006 (123-045-006). */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final int VERSION_INT = 2005002; + public static final int VERSION_INT = 2005003; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} From aacfb37dff4671f801872e429074447a466eaa04 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 12 Sep 2017 08:21:43 -0700 Subject: [PATCH 0456/2472] De-dupe ACTION_DOWN events Issue: #3259 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=168378650 --- .../exoplayer2/ui/PlaybackControlView.java | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java index 123b3051e5..fb97343c38 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java @@ -996,30 +996,30 @@ public class PlaybackControlView extends FrameLayout { return false; } if (event.getAction() == KeyEvent.ACTION_DOWN) { - switch (keyCode) { - case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: - fastForward(); - break; - case KeyEvent.KEYCODE_MEDIA_REWIND: - rewind(); - break; - case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: - controlDispatcher.dispatchSetPlayWhenReady(player, !player.getPlayWhenReady()); - break; - case KeyEvent.KEYCODE_MEDIA_PLAY: - controlDispatcher.dispatchSetPlayWhenReady(player, true); - break; - case KeyEvent.KEYCODE_MEDIA_PAUSE: - controlDispatcher.dispatchSetPlayWhenReady(player, false); - break; - case KeyEvent.KEYCODE_MEDIA_NEXT: - next(); - break; - case KeyEvent.KEYCODE_MEDIA_PREVIOUS: - previous(); - break; - default: - break; + if (keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD) { + fastForward(); + } else if (keyCode == KeyEvent.KEYCODE_MEDIA_REWIND) { + rewind(); + } else if (event.getRepeatCount() == 0) { + switch (keyCode) { + case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: + controlDispatcher.dispatchSetPlayWhenReady(player, !player.getPlayWhenReady()); + break; + case KeyEvent.KEYCODE_MEDIA_PLAY: + controlDispatcher.dispatchSetPlayWhenReady(player, true); + break; + case KeyEvent.KEYCODE_MEDIA_PAUSE: + controlDispatcher.dispatchSetPlayWhenReady(player, false); + break; + case KeyEvent.KEYCODE_MEDIA_NEXT: + next(); + break; + case KeyEvent.KEYCODE_MEDIA_PREVIOUS: + previous(); + break; + default: + break; + } } } return true; From 67567ffa6c5d3b5c532eaf2d78874a7ab9932209 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 20 Sep 2017 03:36:32 -0700 Subject: [PATCH 0457/2472] TsExtractor: Do less work if payload reader does not exist There's no reason to perform the discontinuity check or skip the adaptation field if we don't have a payload reader for the packet. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=169374609 --- .../exoplayer2/extractor/ts/TsExtractor.java | 32 ++++++++----------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/library/core/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 index 90506ab2f6..213d30d47d 100644 --- a/library/core/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 @@ -263,20 +263,24 @@ public final class TsExtractor implements Extractor { boolean adaptationFieldExists = (tsPacketHeader & 0x20) != 0; boolean payloadExists = (tsPacketHeader & 0x10) != 0; + TsPayloadReader payloadReader = payloadExists ? tsPayloadReaders.get(pid) : null; + if (payloadReader == null) { + tsPacketBuffer.setPosition(endOfPacket); + return RESULT_CONTINUE; + } + // Discontinuity check. - boolean discontinuityFound = false; if (mode != MODE_HLS) { int continuityCounter = tsPacketHeader & 0xF; int previousCounter = continuityCounters.get(pid, continuityCounter - 1); continuityCounters.put(pid, continuityCounter); if (previousCounter == continuityCounter) { - if (payloadExists) { - // Duplicate packet found. - tsPacketBuffer.setPosition(endOfPacket); - return RESULT_CONTINUE; - } + // Duplicate packet found. + tsPacketBuffer.setPosition(endOfPacket); + return RESULT_CONTINUE; } else if (continuityCounter != ((previousCounter + 1) & 0xF)) { - discontinuityFound = true; + // Discontinuity found. + payloadReader.seek(); } } @@ -287,17 +291,9 @@ public final class TsExtractor implements Extractor { } // Read the payload. - if (payloadExists) { - TsPayloadReader payloadReader = tsPayloadReaders.get(pid); - if (payloadReader != null) { - if (discontinuityFound) { - payloadReader.seek(); - } - tsPacketBuffer.setLimit(endOfPacket); - payloadReader.consume(tsPacketBuffer, payloadUnitStartIndicator); - tsPacketBuffer.setLimit(limit); - } - } + tsPacketBuffer.setLimit(endOfPacket); + payloadReader.consume(tsPacketBuffer, payloadUnitStartIndicator); + tsPacketBuffer.setLimit(limit); tsPacketBuffer.setPosition(endOfPacket); return RESULT_CONTINUE; From 26d789e6d3e1160d14cde0d06a7cb0ddbe82a4c7 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 20 Sep 2017 03:37:59 -0700 Subject: [PATCH 0458/2472] Tweak release notes for 2.5.3 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=169374725 --- RELEASENOTES.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index db57428701..c63a20ba94 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -8,8 +8,6 @@ ([#2928](https://github.com/google/ExoPlayer/issues/2928)). * Captions: Fix issues rendering CEA-608 captions ([#3250](https://github.com/google/ExoPlayer/issues/3250)). -* Fix adaptive switching for live playbacks close to the live edge - ([#3017](https://github.com/google/ExoPlayer/issues/3017)). * Workaround broken AAC decoders on Galaxy S6 ([#3249](https://github.com/google/ExoPlayer/issues/3249)). * Caching: Fix infinite loop when cache eviction fails From 642e95beaaeb57421ee4da43e0b72c3293c3191c Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 14 Sep 2017 04:31:17 -0700 Subject: [PATCH 0459/2472] Workaround the skip ad button not being focused Issue: #3258 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=168669969 --- .../exoplayer2/ext/ima/ImaAdsLoader.java | 26 +++++++++++++++++++ .../exoplayer2/ui/SimpleExoPlayerView.java | 7 +++++ 2 files changed, 33 insertions(+) diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index 11aab906e0..b6d9280579 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -20,6 +20,7 @@ import android.net.Uri; import android.os.SystemClock; import android.util.Log; import android.view.ViewGroup; +import android.webkit.WebView; import com.google.ads.interactivemedia.v3.api.Ad; import com.google.ads.interactivemedia.v3.api.AdDisplayContainer; import com.google.ads.interactivemedia.v3.api.AdErrorEvent; @@ -112,6 +113,14 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, */ private static final long END_OF_CONTENT_POSITION_THRESHOLD_MS = 5000; + /** + * The "Skip ad" button rendered in the IMA WebView does not gain focus by default and cannot be + * clicked via a keypress event. Workaround this issue by calling focus() on the HTML element in + * the WebView directly when an ad starts. See [Internal: b/62371030]. + */ + private static final String FOCUS_SKIP_BUTTON_WORKAROUND_JS = "javascript:" + + "try{ document.getElementsByClassName(\"videoAdUiSkipButton\")[0].focus(); } catch (e) {}"; + private final Uri adTagUri; private final Timeline.Period period; private final List adCallbacks; @@ -121,6 +130,7 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, private EventListener eventListener; private Player player; + private ViewGroup adUiViewGroup; private VideoProgressUpdate lastContentProgress; private VideoProgressUpdate lastAdProgress; @@ -249,6 +259,7 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, ViewGroup adUiViewGroup) { this.player = player; this.eventListener = eventListener; + this.adUiViewGroup = adUiViewGroup; lastAdProgress = null; lastContentProgress = null; adDisplayContainer.setAdContainer(adUiViewGroup); @@ -278,6 +289,7 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, player.removeListener(this); player = null; eventListener = null; + adUiViewGroup = null; } /** @@ -363,6 +375,11 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, imaPausedContent = true; pauseContentInternal(); break; + case STARTED: + if (ad.isSkippable()) { + focusSkipButton(); + } + break; case TAPPED: if (eventListener != null) { eventListener.onAdTapped(); @@ -727,4 +744,13 @@ public final class ImaAdsLoader implements Player.EventListener, VideoAdPlayer, return adGroupTimesUs; } + private void focusSkipButton() { + if (playingAd && adUiViewGroup != null && adUiViewGroup.getChildCount() > 0 + && adUiViewGroup.getChildAt(0) instanceof WebView) { + WebView webView = (WebView) (adUiViewGroup.getChildAt(0)); + webView.requestFocus(); + webView.loadUrl(FOCUS_SKIP_BUTTON_WORKAROUND_JS); + } + } + } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index a4fb539175..de28eb2f93 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -515,6 +515,13 @@ public final class SimpleExoPlayerView extends FrameLayout { @Override public boolean dispatchKeyEvent(KeyEvent event) { + if (player != null && player.isPlayingAd()) { + // Focus any overlay UI now, in case it's provided by a WebView whose contents may update + // dynamically. This is needed to make the "Skip ad" button focused on Android TV when using + // IMA [Internal: b/62371030]. + overlayFrameLayout.requestFocus(); + return super.dispatchKeyEvent(event); + } maybeShowController(true); return dispatchMediaKeyEvent(event) || super.dispatchKeyEvent(event); } From 30a04fd14b15f9c329faa1ae137332b7c3de5f0c Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 15 Sep 2017 09:28:28 -0700 Subject: [PATCH 0460/2472] Work around broken AAC decoders on Galaxy S6 Issue: #3249 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=168844850 --- .../exoplayer2/mediacodec/MediaCodecUtil.java | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java index 1073e8d9c1..904ce2f0bd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java @@ -325,7 +325,22 @@ public final class MediaCodecUtil { return false; } - // Work around https://github.com/google/ExoPlayer/issues/548 + // Work around https://github.com/google/ExoPlayer/issues/3249. + if (Util.SDK_INT < 24 + && ("OMX.SEC.aac.dec".equals(name) || "OMX.Exynos.AAC.Decoder".equals(name)) + && Util.MANUFACTURER.equals("samsung") + && (Util.DEVICE.startsWith("zeroflte") // Galaxy S6 + || Util.DEVICE.startsWith("zerolte") // Galaxy S6 Edge + || Util.DEVICE.startsWith("zenlte") // Galaxy S6 Edge+ + || Util.DEVICE.equals("SC-05G") // Galaxy S6 + || Util.DEVICE.equals("marinelteatt") // Galaxy S6 Active + || Util.DEVICE.equals("404SC") // Galaxy S6 Edge + || Util.DEVICE.equals("SC-04G") + || Util.DEVICE.equals("SCV31"))) { + return false; + } + + // Work around https://github.com/google/ExoPlayer/issues/548. // 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) From 961f01a850f2879d5eda461f4f431e04347a5e34 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Mon, 18 Sep 2017 05:44:31 -0700 Subject: [PATCH 0461/2472] Fix 2 CEA decoder bugs 1- Avoid dropped buffers by using a PriorityQueue instead of a set. 2- Process the end of stream after non-EOS buffers. Issue:#3250 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=169077365 --- .../exoplayer2/text/SubtitleInputBuffer.java | 3 +++ .../android/exoplayer2/text/TextRenderer.java | 6 +++--- .../android/exoplayer2/text/cea/CeaDecoder.java | 14 ++++++-------- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleInputBuffer.java b/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleInputBuffer.java index 28e67e8623..4b3b61bddf 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleInputBuffer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleInputBuffer.java @@ -37,6 +37,9 @@ public final class SubtitleInputBuffer extends DecoderInputBuffer @Override public int compareTo(@NonNull SubtitleInputBuffer other) { + if (isEndOfStream() != other.isEndOfStream()) { + return isEndOfStream() ? 1 : -1; + } long delta = timeUs - other.timeUs; if (delta == 0) { return 0; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java index 1820d43e75..700fc0cb4b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java @@ -294,9 +294,9 @@ public final class TextRenderer extends BaseRenderer implements Callback { } private long getNextEventTime() { - return ((nextSubtitleEventIndex == C.INDEX_UNSET) - || (nextSubtitleEventIndex >= subtitle.getEventTimeCount())) ? Long.MAX_VALUE - : (subtitle.getEventTime(nextSubtitleEventIndex)); + return nextSubtitleEventIndex == C.INDEX_UNSET + || nextSubtitleEventIndex >= subtitle.getEventTimeCount() + ? Long.MAX_VALUE : subtitle.getEventTime(nextSubtitleEventIndex); } private void updateOutput(List cues) { diff --git a/library/core/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 index fac0982e65..bb13a7d143 100644 --- a/library/core/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 @@ -24,7 +24,7 @@ import com.google.android.exoplayer2.text.SubtitleInputBuffer; import com.google.android.exoplayer2.text.SubtitleOutputBuffer; import com.google.android.exoplayer2.util.Assertions; import java.util.LinkedList; -import java.util.TreeSet; +import java.util.PriorityQueue; /** * Base class for subtitle parsers for CEA captions. @@ -36,7 +36,7 @@ import java.util.TreeSet; private final LinkedList availableInputBuffers; private final LinkedList availableOutputBuffers; - private final TreeSet queuedInputBuffers; + private final PriorityQueue queuedInputBuffers; private SubtitleInputBuffer dequeuedInputBuffer; private long playbackPositionUs; @@ -50,7 +50,7 @@ import java.util.TreeSet; for (int i = 0; i < NUM_OUTPUT_BUFFERS; i++) { availableOutputBuffers.add(new CeaOutputBuffer(this)); } - queuedInputBuffers = new TreeSet<>(); + queuedInputBuffers = new PriorityQueue<>(); } @Override @@ -73,7 +73,6 @@ import java.util.TreeSet; @Override public void queueInputBuffer(SubtitleInputBuffer inputBuffer) throws SubtitleDecoderException { - Assertions.checkArgument(inputBuffer != null); Assertions.checkArgument(inputBuffer == dequeuedInputBuffer); if (inputBuffer.isDecodeOnly()) { // We can drop this buffer early (i.e. before it would be decoded) as the CEA formats allow @@ -90,13 +89,12 @@ import java.util.TreeSet; if (availableOutputBuffers.isEmpty()) { return null; } - // iterate through all available input buffers whose timestamps are less than or equal // to the current playback position; processing input buffers for future content should // be deferred until they would be applicable while (!queuedInputBuffers.isEmpty() - && queuedInputBuffers.first().timeUs <= playbackPositionUs) { - SubtitleInputBuffer inputBuffer = queuedInputBuffers.pollFirst(); + && queuedInputBuffers.peek().timeUs <= playbackPositionUs) { + SubtitleInputBuffer inputBuffer = queuedInputBuffers.poll(); // If the input buffer indicates we've reached the end of the stream, we can // return immediately with an output buffer propagating that @@ -142,7 +140,7 @@ import java.util.TreeSet; public void flush() { playbackPositionUs = 0; while (!queuedInputBuffers.isEmpty()) { - releaseInputBuffer(queuedInputBuffers.pollFirst()); + releaseInputBuffer(queuedInputBuffers.poll()); } if (dequeuedInputBuffer != null) { releaseInputBuffer(dequeuedInputBuffer); From 06bba08be78bbb8a4c44853eafcf3c099578fecf Mon Sep 17 00:00:00 2001 From: eguven Date: Mon, 18 Sep 2017 09:39:09 -0700 Subject: [PATCH 0462/2472] Check if the cache is already empty before trying to evict more span This case may happen if the max span size is more than the max size the evictor is configured. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=169101093 --- .../cache/LeastRecentlyUsedCacheEvictor.java | 2 +- .../LeastRecentlyUsedCacheEvictorTest.java | 46 +++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/LeastRecentlyUsedCacheEvictorTest.java diff --git a/library/core/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 index d2a84f65f4..79d23dd1b0 100644 --- a/library/core/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 @@ -74,7 +74,7 @@ public final class LeastRecentlyUsedCacheEvictor implements CacheEvictor, Compar } private void evictCache(Cache cache, long requiredSpace) { - while (currentSize + requiredSpace > maxBytes) { + while (currentSize + requiredSpace > maxBytes && !leastRecentlyUsed.isEmpty()) { try { cache.removeSpan(leastRecentlyUsed.first()); } catch (CacheException e) { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/LeastRecentlyUsedCacheEvictorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/LeastRecentlyUsedCacheEvictorTest.java new file mode 100644 index 0000000000..6f7f567ae7 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/LeastRecentlyUsedCacheEvictorTest.java @@ -0,0 +1,46 @@ +/* + * 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 org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Unit tests for {@link LeastRecentlyUsedCacheEvictor}. + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public class LeastRecentlyUsedCacheEvictorTest { + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + } + + @Test + public void testContentBiggerThanMaxSizeDoesNotThrowException() throws Exception { + int maxBytes = 100; + LeastRecentlyUsedCacheEvictor evictor = new LeastRecentlyUsedCacheEvictor(maxBytes); + evictor.onCacheInitialized(); + evictor.onStartFile(Mockito.mock(Cache.class), "key", 0, maxBytes + 1); + } + +} From ca4d482b59d74bd4a88e13c5d32558b993b1c966 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 19 Sep 2017 08:46:04 -0700 Subject: [PATCH 0463/2472] Fix PTS wraparound in HLS+Webvtt Issue:#2928 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=169246424 --- .../android/exoplayer2/source/hls/WebvttExtractor.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/library/hls/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 index 12ea2c16c7..0b8f7f36a6 100644 --- a/library/hls/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 @@ -141,8 +141,7 @@ import java.util.regex.Pattern; throw new ParserException("X-TIMESTAMP-MAP doesn't contain media timestamp: " + line); } vttTimestampUs = WebvttParserUtil.parseTimestampUs(localTimestampMatcher.group(1)); - tsTimestampUs = TimestampAdjuster.ptsToUs( - Long.parseLong(mediaTimestampMatcher.group(1))); + tsTimestampUs = TimestampAdjuster.ptsToUs(Long.parseLong(mediaTimestampMatcher.group(1))); } } @@ -155,8 +154,8 @@ import java.util.regex.Pattern; } long firstCueTimeUs = WebvttParserUtil.parseTimestampUs(cueHeaderMatcher.group(1)); - long sampleTimeUs = timestampAdjuster.adjustSampleTimestamp( - firstCueTimeUs + tsTimestampUs - vttTimestampUs); + long sampleTimeUs = timestampAdjuster.adjustTsTimestamp( + TimestampAdjuster.usToPts(firstCueTimeUs + tsTimestampUs - vttTimestampUs)); long subsampleOffsetUs = sampleTimeUs - firstCueTimeUs; // Output the track. TrackOutput trackOutput = buildTrackOutput(subsampleOffsetUs); From 8b43d896f1441c89bf7cec9b79eb8674ddb511e0 Mon Sep 17 00:00:00 2001 From: eguven Date: Tue, 19 Sep 2017 09:07:04 -0700 Subject: [PATCH 0464/2472] Workaround a cipher issue in Android 4.3 [] Issue: #2755 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=169249093 --- .../upstream/cache/CachedContentIndex.java | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/library/core/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 index 809f15b5a6..d0a9bc13c1 100644 --- a/library/core/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 @@ -99,7 +99,7 @@ import javax.crypto.spec.SecretKeySpec; if (secretKey != null) { Assertions.checkArgument(secretKey.length == 16); try { - cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING"); + cipher = getCipher(); secretKeySpec = new SecretKeySpec(secretKey, "AES"); } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { throw new IllegalStateException(e); // Should never happen. @@ -354,6 +354,18 @@ import javax.crypto.spec.SecretKeySpec; return cachedContent; } + private static Cipher getCipher() throws NoSuchPaddingException, NoSuchAlgorithmException { + // Workaround for https://issuetracker.google.com/issues/36976726 + if (Util.SDK_INT == 18) { + try { + return Cipher.getInstance("AES/CBC/PKCS5PADDING", "BC"); + } catch (Throwable ignored) { + // ignored + } + } + return Cipher.getInstance("AES/CBC/PKCS5PADDING"); + } + /** * Returns an id which isn't used in the given array. If the maximum id in the array is smaller * than {@link java.lang.Integer#MAX_VALUE} it just returns the next bigger integer. Otherwise it From c127a577d7af8e419f0d9308d66951717bbaedeb Mon Sep 17 00:00:00 2001 From: eguven Date: Tue, 19 Sep 2017 09:10:35 -0700 Subject: [PATCH 0465/2472] Notify span listeners even if index store fails in SimpleCache.removeSpan This fixes infinite loop in LeastRecentlyUsedCacheEvictor.evictCache when index store fails. Also made CachedContentIndex not final so it can be mocked and added a package protected SimpleCache constructor so mock index can be injected. Issue: #3260 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=169249517 --- .../upstream/cache/CachedContentIndex.java | 2 +- .../upstream/cache/SimpleCache.java | 27 ++++++++++++++----- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/library/core/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 index d0a9bc13c1..758da3548a 100644 --- a/library/core/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 @@ -49,7 +49,7 @@ import javax.crypto.spec.SecretKeySpec; /** * This class maintains the index of cached content. */ -/*package*/ final class CachedContentIndex { +/*package*/ class CachedContentIndex { public static final String FILE_NAME = "cached_content_index.exi"; diff --git a/library/core/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 index 15a5673a4e..10df7c802c 100644 --- a/library/core/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 @@ -48,7 +48,7 @@ public final class SimpleCache implements Cache { * @param evictor The evictor to be used. */ public SimpleCache(File cacheDir, CacheEvictor evictor) { - this(cacheDir, evictor, null); + this(cacheDir, evictor, null, false); } /** @@ -75,10 +75,22 @@ public final class SimpleCache implements Cache { * @param encrypt When false, a plaintext index will be written. */ public SimpleCache(File cacheDir, CacheEvictor evictor, byte[] secretKey, boolean encrypt) { + this(cacheDir, evictor, new CachedContentIndex(cacheDir, secretKey, encrypt)); + } + + /** + * Constructs the cache. The cache will delete any unrecognized files from the directory. Hence + * the directory cannot be used to store other files. + * + * @param cacheDir A dedicated cache directory. + * @param evictor The evictor to be used. + * @param index The CachedContentIndex to be used. + */ + /*package*/ SimpleCache(File cacheDir, CacheEvictor evictor, CachedContentIndex index) { this.cacheDir = cacheDir; this.evictor = evictor; this.lockedSpans = new HashMap<>(); - this.index = new CachedContentIndex(cacheDir, secretKey, encrypt); + this.index = index; this.listeners = new HashMap<>(); // Start cache initialization. final ConditionVariable conditionVariable = new ConditionVariable(); @@ -305,11 +317,14 @@ public final class SimpleCache implements Cache { return; } totalSpace -= span.length; - if (removeEmptyCachedContent && cachedContent.isEmpty()) { - index.removeEmpty(cachedContent.key); - index.store(); + try { + if (removeEmptyCachedContent && cachedContent.isEmpty()) { + index.removeEmpty(cachedContent.key); + index.store(); + } + } finally { + notifySpanRemoved(span); } - notifySpanRemoved(span); } @Override From 48bc420d02d118e7638075e7e41015fa8ae9cf07 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 19 Sep 2017 10:04:50 -0700 Subject: [PATCH 0466/2472] Bump version + release notes ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=169257339 --- RELEASENOTES.md | 17 +++++++++++++++++ constants.gradle | 2 +- demo/src/main/AndroidManifest.xml | 4 ++-- .../exoplayer2/ExoPlayerLibraryInfo.java | 6 +++--- 4 files changed, 23 insertions(+), 6 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index b694143542..db57428701 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,22 @@ # Release notes # +### r2.5.3 ### + +* IMA extension: Support skipping of skippable ads on AndroidTV and other + non-touch devices ([#3258](https://github.com/google/ExoPlayer/issues/3258)). +* HLS: Fix broken WebVTT captions when PTS wraps around + ([#2928](https://github.com/google/ExoPlayer/issues/2928)). +* Captions: Fix issues rendering CEA-608 captions + ([#3250](https://github.com/google/ExoPlayer/issues/3250)). +* Fix adaptive switching for live playbacks close to the live edge + ([#3017](https://github.com/google/ExoPlayer/issues/3017)). +* Workaround broken AAC decoders on Galaxy S6 + ([#3249](https://github.com/google/ExoPlayer/issues/3249)). +* Caching: Fix infinite loop when cache eviction fails + ([#3260](https://github.com/google/ExoPlayer/issues/3260)). +* Caching: Force use of BouncyCastle on JellyBean to fix decryption issue + ([#2755](https://github.com/google/ExoPlayer/issues/2755)). + ### r2.5.2 ### * IMA extension: Fix issue where ad playback could end prematurely for some diff --git a/constants.gradle b/constants.gradle index 7391228853..db7b12acf0 100644 --- a/constants.gradle +++ b/constants.gradle @@ -24,7 +24,7 @@ project.ext { supportLibraryVersion = '25.4.0' dexmakerVersion = '1.2' mockitoVersion = '1.9.5' - releaseVersion = 'r2.5.2' + releaseVersion = 'r2.5.3' modulePrefix = ':' if (gradle.ext.has('exoplayerModulePrefix')) { modulePrefix += gradle.ext.exoplayerModulePrefix diff --git a/demo/src/main/AndroidManifest.xml b/demo/src/main/AndroidManifest.xml index 081ca00077..612044762f 100644 --- a/demo/src/main/AndroidManifest.xml +++ b/demo/src/main/AndroidManifest.xml @@ -16,8 +16,8 @@ + android:versionCode="2503" + android:versionName="2.5.3"> diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java index 98eeb99ad8..90385ed6c0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java @@ -31,13 +31,13 @@ public final class ExoPlayerLibraryInfo { * The version of the library expressed as a string, for example "1.2.3". */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. - public static final String VERSION = "2.5.2"; + public static final String VERSION = "2.5.3"; /** * The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final String VERSION_SLASHY = "ExoPlayerLib/2.5.2"; + public static final String VERSION_SLASHY = "ExoPlayerLib/2.5.3"; /** * The version of the library expressed as an integer, for example 1002003. @@ -47,7 +47,7 @@ public final class ExoPlayerLibraryInfo { * integer version 123045006 (123-045-006). */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final int VERSION_INT = 2005002; + public static final int VERSION_INT = 2005003; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} From eb3cc0b3502a2b4c6ff283ec23aeac07f20e0c6d Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 20 Sep 2017 03:37:59 -0700 Subject: [PATCH 0467/2472] Tweak release notes for 2.5.3 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=169374725 --- RELEASENOTES.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index db57428701..c63a20ba94 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -8,8 +8,6 @@ ([#2928](https://github.com/google/ExoPlayer/issues/2928)). * Captions: Fix issues rendering CEA-608 captions ([#3250](https://github.com/google/ExoPlayer/issues/3250)). -* Fix adaptive switching for live playbacks close to the live edge - ([#3017](https://github.com/google/ExoPlayer/issues/3017)). * Workaround broken AAC decoders on Galaxy S6 ([#3249](https://github.com/google/ExoPlayer/issues/3249)). * Caching: Fix infinite loop when cache eviction fails From a8136cbb991ec3de9407a72e36fb48a1e8f966de Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 20 Sep 2017 14:49:23 +0100 Subject: [PATCH 0468/2472] Rm test class from release --- .../LeastRecentlyUsedCacheEvictorTest.java | 46 ------------------- 1 file changed, 46 deletions(-) delete mode 100644 library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/LeastRecentlyUsedCacheEvictorTest.java diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/LeastRecentlyUsedCacheEvictorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/LeastRecentlyUsedCacheEvictorTest.java deleted file mode 100644 index 6f7f567ae7..0000000000 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/LeastRecentlyUsedCacheEvictorTest.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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 org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; - -/** - * Unit tests for {@link LeastRecentlyUsedCacheEvictor}. - */ -@RunWith(RobolectricTestRunner.class) -@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) -public class LeastRecentlyUsedCacheEvictorTest { - - @Before - public void setUp() throws Exception { - MockitoAnnotations.initMocks(this); - } - - @Test - public void testContentBiggerThanMaxSizeDoesNotThrowException() throws Exception { - int maxBytes = 100; - LeastRecentlyUsedCacheEvictor evictor = new LeastRecentlyUsedCacheEvictor(maxBytes); - evictor.onCacheInitialized(); - evictor.onStartFile(Mockito.mock(Cache.class), "key", 0, maxBytes + 1); - } - -} From 6314a0ec82343ee79075e7f8794a69471599b019 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 20 Sep 2017 05:38:24 -0700 Subject: [PATCH 0469/2472] Add support for Widevine encrypted HLS This includes both cbcs and cenc. Will only work for streams that require a single pssh. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=169382884 --- .../android/exoplayer2/drm/DrmInitData.java | 10 ++- .../extractor/mp4/FragmentedMp4Extractor.java | 16 +++-- .../playlist/HlsMediaPlaylistParserTest.java | 17 ++--- .../exoplayer2/source/hls/HlsChunkSource.java | 8 +-- .../exoplayer2/source/hls/HlsMediaChunk.java | 36 +++++----- .../source/hls/offline/HlsDownloader.java | 5 +- .../source/hls/playlist/HlsMediaPlaylist.java | 45 ++++++------- .../hls/playlist/HlsPlaylistParser.java | 65 ++++++++++++++----- .../smoothstreaming/DefaultSsChunkSource.java | 2 +- 9 files changed, 122 insertions(+), 82 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java index c8e76ec291..e346ab800f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java @@ -58,7 +58,15 @@ public final class DrmInitData implements Comparator, Parcelable { * @param schemeDatas Scheme initialization data for possibly multiple DRM schemes. */ public DrmInitData(SchemeData... schemeDatas) { - this(null, true, schemeDatas); + this(null, schemeDatas); + } + + /** + * @param schemeType The protection scheme type, or null if not applicable or unknown. + * @param schemeDatas Scheme initialization data for possibly multiple DRM schemes. + */ + public DrmInitData(@Nullable String schemeType, SchemeData... schemeDatas) { + this(schemeType, true, schemeDatas); } private DrmInitData(@Nullable String schemeType, boolean cloneSchemeDatas, diff --git a/library/core/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 index d9ab47546a..4807e05277 100644 --- a/library/core/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 @@ -120,6 +120,9 @@ public final class FragmentedMp4Extractor implements Extractor { @Flags private final int flags; private final Track sideloadedTrack; + // Manifest DRM data. + private final DrmInitData sideloadedDrmInitData; + // Track-linked data bundle, accessible as a whole through trackID. private final SparseArray trackBundles; @@ -179,7 +182,7 @@ public final class FragmentedMp4Extractor implements Extractor { * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed. */ public FragmentedMp4Extractor(@Flags int flags, TimestampAdjuster timestampAdjuster) { - this(flags, timestampAdjuster, null); + this(flags, timestampAdjuster, null, null); } /** @@ -187,12 +190,14 @@ public final class FragmentedMp4Extractor implements Extractor { * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed. * @param sideloadedTrack Sideloaded track information, in the case that the extractor * will not receive a moov box in the input data. + * @param sideloadedDrmInitData The {@link DrmInitData} to use for encrypted tracks. */ public FragmentedMp4Extractor(@Flags int flags, TimestampAdjuster timestampAdjuster, - Track sideloadedTrack) { + Track sideloadedTrack, DrmInitData sideloadedDrmInitData) { this.flags = flags | (sideloadedTrack != null ? FLAG_SIDELOADED : 0); this.timestampAdjuster = timestampAdjuster; this.sideloadedTrack = sideloadedTrack; + this.sideloadedDrmInitData = sideloadedDrmInitData; atomHeader = new ParsableByteArray(Atom.LONG_HEADER_SIZE); nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE); nalPrefix = new ParsableByteArray(5); @@ -402,7 +407,8 @@ public final class FragmentedMp4Extractor implements Extractor { private void onMoovContainerAtomRead(ContainerAtom moov) throws ParserException { Assertions.checkState(sideloadedTrack == null, "Unexpected moov box."); - DrmInitData drmInitData = getDrmInitDataFromAtoms(moov.leafChildren); + DrmInitData drmInitData = sideloadedDrmInitData != null ? sideloadedDrmInitData + : getDrmInitDataFromAtoms(moov.leafChildren); // Read declaration of track fragments in the Moov box. ContainerAtom mvex = moov.getContainerAtomOfType(Atom.TYPE_mvex); @@ -456,7 +462,9 @@ public final class FragmentedMp4Extractor implements Extractor { private void onMoofContainerAtomRead(ContainerAtom moof) throws ParserException { parseMoof(moof, trackBundles, flags, extendedTypeScratch); - DrmInitData drmInitData = getDrmInitDataFromAtoms(moof.leafChildren); + // If drm init data is sideloaded, we ignore pssh boxes. + DrmInitData drmInitData = sideloadedDrmInitData != null ? null + : getDrmInitDataFromAtoms(moof.leafChildren); if (drmInitData != null) { int trackCount = trackBundles.size(); for (int i = 0; i < trackCount; i++) { diff --git a/library/hls/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 index e2eb173df8..b036f13a0f 100644 --- a/library/hls/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 @@ -85,9 +85,8 @@ public class HlsMediaPlaylistParserTest extends TestCase { Segment segment = segments.get(0); assertEquals(4, mediaPlaylist.discontinuitySequence + segment.relativeDiscontinuitySequence); assertEquals(7975000, segment.durationUs); - assertFalse(segment.isEncrypted); - assertEquals(null, segment.encryptionKeyUri); - assertEquals(null, segment.encryptionIV); + assertNull(segment.fullSegmentEncryptionKeyUri); + assertNull(segment.encryptionIV); assertEquals(51370, segment.byterangeLength); assertEquals(0, segment.byterangeOffset); assertEquals("https://priv.example.com/fileSequence2679.ts", segment.url); @@ -95,8 +94,7 @@ public class HlsMediaPlaylistParserTest extends TestCase { segment = segments.get(1); assertEquals(0, segment.relativeDiscontinuitySequence); assertEquals(7975000, segment.durationUs); - assertTrue(segment.isEncrypted); - assertEquals("https://priv.example.com/key.php?r=2680", segment.encryptionKeyUri); + assertEquals("https://priv.example.com/key.php?r=2680", segment.fullSegmentEncryptionKeyUri); assertEquals("0x1566B", segment.encryptionIV); assertEquals(51501, segment.byterangeLength); assertEquals(2147483648L, segment.byterangeOffset); @@ -105,8 +103,7 @@ public class HlsMediaPlaylistParserTest extends TestCase { segment = segments.get(2); assertEquals(0, segment.relativeDiscontinuitySequence); assertEquals(7941000, segment.durationUs); - assertFalse(segment.isEncrypted); - assertEquals(null, segment.encryptionKeyUri); + assertNull(segment.fullSegmentEncryptionKeyUri); assertEquals(null, segment.encryptionIV); assertEquals(51501, segment.byterangeLength); assertEquals(2147535149L, segment.byterangeOffset); @@ -115,8 +112,7 @@ public class HlsMediaPlaylistParserTest extends TestCase { segment = segments.get(3); assertEquals(1, segment.relativeDiscontinuitySequence); assertEquals(7975000, segment.durationUs); - assertTrue(segment.isEncrypted); - assertEquals("https://priv.example.com/key.php?r=2682", segment.encryptionKeyUri); + assertEquals("https://priv.example.com/key.php?r=2682", segment.fullSegmentEncryptionKeyUri); // 0xA7A == 2682. assertNotNull(segment.encryptionIV); assertEquals("A7A", segment.encryptionIV.toUpperCase(Locale.getDefault())); @@ -127,8 +123,7 @@ public class HlsMediaPlaylistParserTest extends TestCase { segment = segments.get(4); assertEquals(1, segment.relativeDiscontinuitySequence); assertEquals(7975000, segment.durationUs); - assertTrue(segment.isEncrypted); - assertEquals("https://priv.example.com/key.php?r=2682", segment.encryptionKeyUri); + assertEquals("https://priv.example.com/key.php?r=2682", segment.fullSegmentEncryptionKeyUri); // 0xA7B == 2683. assertNotNull(segment.encryptionIV); assertEquals("A7B", segment.encryptionIV.toUpperCase(Locale.getDefault())); diff --git a/library/hls/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 index 0ad9dd1a6e..a5688e8bc5 100644 --- a/library/hls/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 @@ -276,9 +276,9 @@ import java.util.List; // Handle encryption. HlsMediaPlaylist.Segment segment = mediaPlaylist.segments.get(chunkIndex); - // Check if encryption is specified. - if (segment.isEncrypted) { - Uri keyUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.encryptionKeyUri); + // Check if the segment is completely encrypted using the identity key format. + if (segment.fullSegmentEncryptionKeyUri != null) { + Uri keyUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.fullSegmentEncryptionKeyUri); if (!keyUri.equals(encryptionKeyUri)) { // Encryption is specified and the key has changed. out.chunk = newEncryptionKeyChunk(keyUri, segment.encryptionIV, selectedVariantIndex, @@ -314,7 +314,7 @@ import java.util.List; out.chunk = new HlsMediaChunk(mediaDataSource, dataSpec, initDataSpec, selectedUrl, muxedCaptionFormats, trackSelection.getSelectionReason(), trackSelection.getSelectionData(), startTimeUs, startTimeUs + segment.durationUs, chunkMediaSequence, discontinuitySequence, - isTimestampMaster, timestampAdjuster, previous, segment.keyFormat, encryptionKey, + isTimestampMaster, timestampAdjuster, previous, mediaPlaylist.drmInitData, encryptionKey, encryptionIv); } diff --git a/library/hls/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 index e3e1bab48d..91513b536e 100644 --- a/library/hls/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 @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.source.hls; import android.text.TextUtils; 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.DefaultExtractorInput; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; @@ -32,7 +33,6 @@ import com.google.android.exoplayer2.metadata.id3.Id3Decoder; import com.google.android.exoplayer2.metadata.id3.PrivFrame; import com.google.android.exoplayer2.source.chunk.MediaChunk; import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl; -import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.util.MimeTypes; @@ -88,6 +88,7 @@ import java.util.concurrent.atomic.AtomicInteger; private final boolean shouldSpliceIn; private final boolean needNewExtractor; private final List muxedCaptionFormats; + private final DrmInitData drmInitData; private final boolean isPackedAudio; private final Id3Decoder id3Decoder; @@ -117,20 +118,21 @@ import java.util.concurrent.atomic.AtomicInteger; * @param isMasterTimestampSource True if the chunk can initialize the timestamp adjuster. * @param timestampAdjuster Adjuster corresponding to the provided discontinuity sequence number. * @param previousChunk The {@link HlsMediaChunk} that preceded this one. May be null. - * @param keyFormat A string describing the format for {@code keyData}, or null if the chunk is - * not encrypted. - * @param keyData Data specifying how to obtain the keys to decrypt the chunk, or null if the - * chunk is not encrypted. - * @param encryptionIv The AES initialization vector, or null if the chunk is not encrypted. + * @param drmInitData A {@link DrmInitData} to sideload to the extractor. + * @param fullSegmentEncryptionKey The key to decrypt the full segment, or null if the segment is + * not fully encrypted. + * @param encryptionIv The AES initialization vector, or null if the segment is not fully + * encrypted. */ public HlsMediaChunk(DataSource dataSource, DataSpec dataSpec, DataSpec initDataSpec, HlsUrl hlsUrl, List muxedCaptionFormats, int trackSelectionReason, Object trackSelectionData, long startTimeUs, long endTimeUs, int chunkIndex, int discontinuitySequenceNumber, boolean isMasterTimestampSource, - TimestampAdjuster timestampAdjuster, HlsMediaChunk previousChunk, String keyFormat, - byte[] keyData, byte[] encryptionIv) { - super(buildDataSource(dataSource, keyFormat, keyData, encryptionIv), dataSpec, hlsUrl.format, - trackSelectionReason, trackSelectionData, startTimeUs, endTimeUs, chunkIndex); + TimestampAdjuster timestampAdjuster, HlsMediaChunk previousChunk, DrmInitData drmInitData, + byte[] fullSegmentEncryptionKey, byte[] encryptionIv) { + super(buildDataSource(dataSource, fullSegmentEncryptionKey, encryptionIv), dataSpec, + hlsUrl.format, trackSelectionReason, trackSelectionData, startTimeUs, endTimeUs, + chunkIndex); this.discontinuitySequenceNumber = discontinuitySequenceNumber; this.initDataSpec = initDataSpec; this.hlsUrl = hlsUrl; @@ -139,6 +141,7 @@ import java.util.concurrent.atomic.AtomicInteger; this.timestampAdjuster = timestampAdjuster; // Note: this.dataSource and dataSource may be different. this.isEncrypted = this.dataSource instanceof Aes128DataSource; + this.drmInitData = drmInitData; lastPathSegment = dataSpec.uri.getLastPathSegment(); isPackedAudio = lastPathSegment.endsWith(AAC_FILE_EXTENSION) || lastPathSegment.endsWith(AC3_FILE_EXTENSION) @@ -331,14 +334,13 @@ import java.util.concurrent.atomic.AtomicInteger; // Internal factory methods. /** - * If the content is encrypted using the "identity" key format, returns an - * {@link Aes128DataSource} that wraps the original in order to decrypt the loaded data. Else - * returns the original. + * If the segment is fully encrypted, returns an {@link Aes128DataSource} that wraps the original + * in order to decrypt the loaded data. Else returns the original. */ - private static DataSource buildDataSource(DataSource dataSource, String keyFormat, byte[] keyData, + private static DataSource buildDataSource(DataSource dataSource, byte[] fullSegmentEncryptionKey, byte[] encryptionIv) { - if (HlsMediaPlaylist.KEYFORMAT_IDENTITY.equals(keyFormat)) { - return new Aes128DataSource(dataSource, keyData, encryptionIv); + if (fullSegmentEncryptionKey != null) { + return new Aes128DataSource(dataSource, fullSegmentEncryptionKey, encryptionIv); } return dataSource; } @@ -357,7 +359,7 @@ import java.util.concurrent.atomic.AtomicInteger; extractor = previousExtractor; } else if (lastPathSegment.endsWith(MP4_FILE_EXTENSION) || lastPathSegment.startsWith(M4_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 4)) { - extractor = new FragmentedMp4Extractor(0, timestampAdjuster); + extractor = new FragmentedMp4Extractor(0, timestampAdjuster, null, drmInitData); } else { // MPEG-2 TS segments, but we need a new extractor. // This flag ensures the change of pid between streams does not affect the sample queues. diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java index ac8ec5ee5e..5ac61294a4 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java @@ -117,8 +117,9 @@ public final class HlsDownloader extends SegmentDownloader encryptionKeyUris) throws IOException, InterruptedException { long startTimeUs = mediaPlaylist.startTimeUs + hlsSegment.relativeStartTimeUs; - if (hlsSegment.isEncrypted) { - Uri keyUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, hlsSegment.encryptionKeyUri); + if (hlsSegment.fullSegmentEncryptionKeyUri != null) { + Uri keyUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, + hlsSegment.fullSegmentEncryptionKeyUri); if (encryptionKeyUris.add(keyUri)) { segments.add(new Segment(startTimeUs, new DataSpec(keyUri))); } diff --git a/library/hls/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 index 1b573f41c2..b21ecb02d5 100644 --- a/library/hls/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 @@ -18,6 +18,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 com.google.android.exoplayer2.drm.DrmInitData; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Collections; @@ -50,17 +51,10 @@ public final class HlsMediaPlaylist extends HlsPlaylist { */ public final long relativeStartTimeUs; /** - * Whether the segment is encrypted, as defined by #EXT-X-KEY. + * The encryption identity key uri as defined by #EXT-X-KEY, or null if the segment does not use + * full segment encryption with identity key. */ - public final boolean isEncrypted; - /** - * The key format as defined by #EXT-X-KEY, or null if the segment is not encrypted. - */ - public final String keyFormat; - /** - * The encryption key uri as defined by #EXT-X-KEY, or null if the segment is not encrypted. - */ - public final String encryptionKeyUri; + public final String fullSegmentEncryptionKeyUri; /** * The encryption initialization vector as defined by #EXT-X-KEY, or null if the segment is not * encrypted. @@ -77,7 +71,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { public final long byterangeLength; public Segment(String uri, long byterangeOffset, long byterangeLength) { - this(uri, 0, -1, C.TIME_UNSET, false, null, null, null, byterangeOffset, byterangeLength); + this(uri, 0, -1, C.TIME_UNSET, null, null, byterangeOffset, byterangeLength); } /** @@ -85,23 +79,19 @@ public final class HlsMediaPlaylist extends HlsPlaylist { * @param durationUs See {@link #durationUs}. * @param relativeDiscontinuitySequence See {@link #relativeDiscontinuitySequence}. * @param relativeStartTimeUs See {@link #relativeStartTimeUs}. - * @param isEncrypted See {@link #isEncrypted}. - * @param keyFormat See {@link #keyFormat}. - * @param encryptionKeyUri See {@link #encryptionKeyUri}. + * @param fullSegmentEncryptionKeyUri See {@link #fullSegmentEncryptionKeyUri}. * @param encryptionIV See {@link #encryptionIV}. * @param byterangeOffset See {@link #byterangeOffset}. * @param byterangeLength See {@link #byterangeLength}. */ public Segment(String url, long durationUs, int relativeDiscontinuitySequence, - long relativeStartTimeUs, boolean isEncrypted, String keyFormat, String encryptionKeyUri, + long relativeStartTimeUs, String fullSegmentEncryptionKeyUri, String encryptionIV, long byterangeOffset, long byterangeLength) { this.url = url; this.durationUs = durationUs; this.relativeDiscontinuitySequence = relativeDiscontinuitySequence; this.relativeStartTimeUs = relativeStartTimeUs; - this.isEncrypted = isEncrypted; - this.keyFormat = keyFormat; - this.encryptionKeyUri = encryptionKeyUri; + this.fullSegmentEncryptionKeyUri = fullSegmentEncryptionKeyUri; this.encryptionIV = encryptionIV; this.byterangeOffset = byterangeOffset; this.byterangeLength = byterangeLength; @@ -115,11 +105,6 @@ public final class HlsMediaPlaylist extends HlsPlaylist { } - /** - * The identity key format, as defined by #EXT-X-KEY. - */ - public static final String KEYFORMAT_IDENTITY = "identity"; - /** * Type of the playlist, as defined by #EXT-X-PLAYLIST-TYPE. */ @@ -176,6 +161,11 @@ public final class HlsMediaPlaylist extends HlsPlaylist { * Whether the playlist contains a #EXT-X-PROGRAM-DATE-TIME tag. */ public final boolean hasProgramDateTime; + /** + * DRM initialization data for sample decryption, or null if none of the segment uses sample + * encryption. + */ + public final DrmInitData drmInitData; /** * The initialization segment, as defined by #EXT-X-MAP. */ @@ -203,6 +193,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { * @param hasIndependentSegmentsTag See {@link #hasIndependentSegmentsTag}. * @param hasEndTag See {@link #hasEndTag}. * @param hasProgramDateTime See {@link #hasProgramDateTime}. + * @param drmInitData See {@link #drmInitData}. * @param initializationSegment See {@link #initializationSegment}. * @param segments See {@link #segments}. */ @@ -210,7 +201,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { long startOffsetUs, long startTimeUs, boolean hasDiscontinuitySequence, int discontinuitySequence, int mediaSequence, int version, long targetDurationUs, boolean hasIndependentSegmentsTag, boolean hasEndTag, boolean hasProgramDateTime, - Segment initializationSegment, List segments) { + DrmInitData drmInitData, Segment initializationSegment, List segments) { super(baseUri, tags); this.playlistType = playlistType; this.startTimeUs = startTimeUs; @@ -222,6 +213,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { this.hasIndependentSegmentsTag = hasIndependentSegmentsTag; this.hasEndTag = hasEndTag; this.hasProgramDateTime = hasProgramDateTime; + this.drmInitData = drmInitData; this.initializationSegment = initializationSegment; this.segments = Collections.unmodifiableList(segments); if (!segments.isEmpty()) { @@ -273,7 +265,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { public HlsMediaPlaylist copyWith(long startTimeUs, int discontinuitySequence) { return new HlsMediaPlaylist(playlistType, baseUri, tags, startOffsetUs, startTimeUs, true, discontinuitySequence, mediaSequence, version, targetDurationUs, hasIndependentSegmentsTag, - hasEndTag, hasProgramDateTime, initializationSegment, segments); + hasEndTag, hasProgramDateTime, drmInitData, initializationSegment, segments); } /** @@ -288,7 +280,8 @@ public final class HlsMediaPlaylist extends HlsPlaylist { } return new HlsMediaPlaylist(playlistType, baseUri, tags, startOffsetUs, startTimeUs, hasDiscontinuitySequence, discontinuitySequence, mediaSequence, version, targetDurationUs, - hasIndependentSegmentsTag, true, hasProgramDateTime, initializationSegment, segments); + hasIndependentSegmentsTag, true, hasProgramDateTime, drmInitData, initializationSegment, + segments); } } diff --git a/library/hls/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 index dc5fd96f35..c18f31f47e 100644 --- a/library/hls/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 @@ -16,9 +16,12 @@ package com.google.android.exoplayer2.source.hls.playlist; import android.net.Uri; +import android.util.Base64; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.drm.DrmInitData; +import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; import com.google.android.exoplayer2.source.UnrecognizedInputFormatException; import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment; import com.google.android.exoplayer2.upstream.ParsingLoadable; @@ -28,6 +31,7 @@ import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; @@ -71,6 +75,11 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser Date: Wed, 20 Sep 2017 11:21:15 -0700 Subject: [PATCH 0470/2472] Handle bracket params on the end of SmoothStreaming URLs Issue: #3230 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=169421873 --- .../com/google/android/exoplayer2/util/Util.java | 3 +-- .../google/android/exoplayer2/util/UtilTest.java | 13 +++++++++++++ .../source/smoothstreaming/SsMediaSource.java | 4 ++-- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java index 519919f129..5d93f413d5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -900,8 +900,7 @@ public final class Util { return C.TYPE_DASH; } else if (fileName.endsWith(".m3u8")) { return C.TYPE_HLS; - } else if (fileName.endsWith(".ism") || fileName.endsWith(".isml") - || fileName.endsWith(".ism/manifest") || fileName.endsWith(".isml/manifest")) { + } else if (fileName.matches(".*\\.ism(l)?(/manifest(\\(.+\\))?)?")) { return C.TYPE_SS; } else { return C.TYPE_OTHER; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/UtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/UtilTest.java index 70caff9bf1..1afe380483 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/util/UtilTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/UtilTest.java @@ -23,6 +23,7 @@ import static com.google.android.exoplayer2.util.Util.parseXsDuration; import static com.google.android.exoplayer2.util.Util.unescapeFileName; import static com.google.common.truth.Truth.assertThat; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.testutil.TestUtil; import java.util.ArrayList; import java.util.List; @@ -39,6 +40,18 @@ import org.robolectric.annotation.Config; @Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) public class UtilTest { + @Test + public void testInferContentType() { + assertThat(Util.inferContentType("http://a.b/c.ism")).isEqualTo(C.TYPE_SS); + assertThat(Util.inferContentType("http://a.b/c.isml")).isEqualTo(C.TYPE_SS); + assertThat(Util.inferContentType("http://a.b/c.ism/Manifest")).isEqualTo(C.TYPE_SS); + assertThat(Util.inferContentType("http://a.b/c.isml/manifest")).isEqualTo(C.TYPE_SS); + assertThat(Util.inferContentType("http://a.b/c.isml/manifest(filter=x)")).isEqualTo(C.TYPE_SS); + + assertThat(Util.inferContentType("http://a.b/c.ism/prefix-manifest")).isEqualTo(C.TYPE_OTHER); + assertThat(Util.inferContentType("http://a.b/c.ism/manifest-suffix")).isEqualTo(C.TYPE_OTHER); + } + @Test public void testArrayBinarySearchFloor() { long[] values = new long[0]; diff --git a/library/smoothstreaming/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 index 885d5bd227..236bee83ab 100644 --- a/library/smoothstreaming/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 @@ -193,8 +193,8 @@ public final class SsMediaSource implements MediaSource, Assertions.checkState(manifest == null || !manifest.isLive); this.manifest = manifest; this.manifestUri = manifestUri == null ? null - : Util.toLowerInvariant(manifestUri.getLastPathSegment()).equals("manifest") ? manifestUri - : Uri.withAppendedPath(manifestUri, "Manifest"); + : Util.toLowerInvariant(manifestUri.getLastPathSegment()).matches("manifest(\\(.+\\))?") + ? manifestUri : Uri.withAppendedPath(manifestUri, "Manifest"); this.manifestDataSourceFactory = manifestDataSourceFactory; this.manifestParser = manifestParser; this.chunkSourceFactory = chunkSourceFactory; From ff2ece56dd9afa7eaf03bdb994e1ef9387fe44a6 Mon Sep 17 00:00:00 2001 From: yqritc Date: Thu, 21 Sep 2017 12:01:25 +0900 Subject: [PATCH 0471/2472] fix primarySnapshotAccessAgeMs --- .../exoplayer2/source/hls/playlist/HlsPlaylistTracker.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/hls/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 index 567dbd4af6..dfa544dfcd 100644 --- a/library/hls/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 @@ -351,7 +351,7 @@ public final class HlsPlaylistTracker implements Loader.Callback PRIMARY_URL_KEEPALIVE_MS) { primaryHlsUrl = url; playlistBundles.get(primaryHlsUrl).loadPlaylist(); From cac16f1647be29828c1b404e5de0b6e2577d2e61 Mon Sep 17 00:00:00 2001 From: yqritc Date: Thu, 21 Sep 2017 14:18:59 +0900 Subject: [PATCH 0472/2472] remove keep alive check for updating primary url to avoid redundant playlist loading --- .../source/hls/playlist/HlsPlaylistTracker.java | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/library/hls/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 index dfa544dfcd..9b777bccf8 100644 --- a/library/hls/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 @@ -112,11 +112,6 @@ public final class HlsPlaylistTracker implements Loader.Callback PRIMARY_URL_KEEPALIVE_MS) { - primaryHlsUrl = url; - playlistBundles.get(primaryHlsUrl).loadPlaylist(); - } + + primaryHlsUrl = url; + playlistBundles.get(primaryHlsUrl).loadPlaylist(); } private void createBundles(List urls) { From 455f9fb9f0fb422652ed3e1771f843db0c1383a1 Mon Sep 17 00:00:00 2001 From: yqritc Date: Thu, 21 Sep 2017 16:01:32 +0900 Subject: [PATCH 0473/2472] remove space --- .../exoplayer2/source/hls/playlist/HlsPlaylistTracker.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/hls/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 index 9b777bccf8..d9ab410fb9 100644 --- a/library/hls/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 @@ -200,7 +200,7 @@ public final class HlsPlaylistTracker implements Loader.Callback Date: Thu, 21 Sep 2017 03:42:48 -0700 Subject: [PATCH 0474/2472] Add DashDownloadActionTest ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=169522830 --- library/dash/build.gradle | 5 ++++ .../dash/manifest/RepresentationKey.java | 23 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/library/dash/build.gradle b/library/dash/build.gradle index 48e9b9b97e..99441a2849 100644 --- a/library/dash/build.gradle +++ b/library/dash/build.gradle @@ -39,6 +39,11 @@ dependencies { androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion androidTestCompile 'org.mockito:mockito-core:' + mockitoVersion + testCompile project(modulePrefix + 'testutils') + testCompile 'com.google.truth:truth:' + truthVersion + testCompile 'junit:junit:' + junitVersion + testCompile 'org.mockito:mockito-core:' + mockitoVersion + testCompile 'org.robolectric:robolectric:' + robolectricVersion } ext { diff --git a/library/dash/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 index cf17a081d7..4ce1d06700 100644 --- a/library/dash/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 @@ -80,4 +80,27 @@ public final class RepresentationKey implements Parcelable, Comparable Date: Tue, 7 Mar 2017 05:42:33 +0000 Subject: [PATCH 0475/2472] Tweak live-streaming track selection logic. Follow-up on the update to ABR logic in AdaptiveTrackSelection for live streaming case: - Do not reset liveEdgeTimeUs when user seek to a different position. - For HlsChunkSource, for non-independent segments, currently the bufferedDuration calculate will subtract previousChunk's duration. So to make it work with live-streaming ABR logic, we subtract timeToLiveEdgeUs a similar amount to compensate for that operation. - Minor update to DefaultSSChunkSource, only perform TrackSelection when needed (after checking necessary conditions). ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=169531275 --- .../source/dash/DefaultDashChunkSource.java | 7 +++-- .../exoplayer2/source/hls/HlsChunkSource.java | 27 ++++++++++++------- .../smoothstreaming/DefaultSsChunkSource.java | 12 +++------ 3 files changed, 25 insertions(+), 21 deletions(-) diff --git a/library/dash/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 index eba36e9057..732934515b 100644 --- a/library/dash/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 @@ -181,7 +181,7 @@ public class DefaultDashChunkSource implements DashChunkSource { } long bufferedDurationUs = previous != null ? (previous.endTimeUs - playbackPositionUs) : 0; - long timeToLiveEdgeUs = resolveTimeToLiveEdgeUs(playbackPositionUs, previous == null); + long timeToLiveEdgeUs = resolveTimeToLiveEdgeUs(playbackPositionUs); trackSelection.updateSelectedTrack(bufferedDurationUs, timeToLiveEdgeUs); RepresentationHolder representationHolder = @@ -385,9 +385,8 @@ public class DefaultDashChunkSource implements DashChunkSource { } } - private long resolveTimeToLiveEdgeUs(long playbackPositionUs, boolean isAfterPositionReset) { - boolean resolveTimeToLiveEdgePossible = manifest.dynamic - && !isAfterPositionReset && liveEdgeTimeUs != C.TIME_UNSET; + private long resolveTimeToLiveEdgeUs(long playbackPositionUs) { + boolean resolveTimeToLiveEdgePossible = manifest.dynamic && liveEdgeTimeUs != C.TIME_UNSET; return resolveTimeToLiveEdgePossible ? liveEdgeTimeUs - playbackPositionUs : C.TIME_UNSET; } diff --git a/library/hls/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 index a5688e8bc5..8aa4e057a2 100644 --- a/library/hls/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 @@ -209,14 +209,24 @@ import java.util.List; int oldVariantIndex = previous == null ? C.INDEX_UNSET : trackGroup.indexOf(previous.trackFormat); expectedPlaylistUrl = null; - // Unless segments are known to be independent, switching variant will require downloading - // overlapping segments. Hence we use the start time of the previous chunk rather than its end - // time for this case. - long bufferedDurationUs = previous == null ? 0 : Math.max(0, - (independentSegments ? previous.endTimeUs : previous.startTimeUs) - playbackPositionUs); + long bufferedDurationUs = previous == null ? 0 : previous.endTimeUs - playbackPositionUs; + + long timeToLiveEdgeUs = resolveTimeToLiveEdgeUs(playbackPositionUs); + if (previous != null && !independentSegments) { + // Unless segments are known to be independent, switching variant will require downloading + // overlapping segments. Hence we will subtract previous chunk's duration from buffered + // duration. + // This may affect the live-streaming adaptive track selection logic, when we are comparing + // buffered duration to time to live edge to decide whether to switch. Therefore, + // we will subtract this same amount from timeToLiveEdgeUs as well. + long subtractedDurationUs = previous.getDurationUs(); + bufferedDurationUs = Math.max(0, bufferedDurationUs - subtractedDurationUs); + if (timeToLiveEdgeUs != C.TIME_UNSET) { + timeToLiveEdgeUs = Math.max(0, timeToLiveEdgeUs - subtractedDurationUs); + } + } // Select the variant. - long timeToLiveEdgeUs = resolveTimeToLiveEdgeUs(playbackPositionUs, previous == null); trackSelection.updateSelectedTrack(bufferedDurationUs, timeToLiveEdgeUs); int selectedVariantIndex = trackSelection.getSelectedIndexInTrackGroup(); @@ -365,9 +375,8 @@ import java.util.List; // Private methods. - private long resolveTimeToLiveEdgeUs(long playbackPositionUs, boolean isAfterPositionReset) { - final boolean resolveTimeToLiveEdgePossible = !isAfterPositionReset - && liveEdgeTimeUs != C.TIME_UNSET; + private long resolveTimeToLiveEdgeUs(long playbackPositionUs) { + final boolean resolveTimeToLiveEdgePossible = liveEdgeTimeUs != C.TIME_UNSET; return resolveTimeToLiveEdgePossible ? liveEdgeTimeUs - playbackPositionUs : C.TIME_UNSET; } diff --git a/library/smoothstreaming/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 index cbd6db04af..d9743649c5 100644 --- a/library/smoothstreaming/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 @@ -154,10 +154,6 @@ public class DefaultSsChunkSource implements SsChunkSource { return; } - long bufferedDurationUs = previous != null ? (previous.endTimeUs - playbackPositionUs) : 0; - long timeToLiveEdgeUs = resolveTimeToLiveEdgeUs(playbackPositionUs); - trackSelection.updateSelectedTrack(bufferedDurationUs, timeToLiveEdgeUs); - StreamElement streamElement = manifest.streamElements[elementIndex]; if (streamElement.chunkCount == 0) { // There aren't any chunks for us to load. @@ -183,6 +179,10 @@ public class DefaultSsChunkSource implements SsChunkSource { return; } + long bufferedDurationUs = previous != null ? (previous.endTimeUs - playbackPositionUs) : 0; + long timeToLiveEdgeUs = resolveTimeToLiveEdgeUs(playbackPositionUs); + trackSelection.updateSelectedTrack(bufferedDurationUs, timeToLiveEdgeUs); + long chunkStartTimeUs = streamElement.getStartTimeUs(chunkIndex); long chunkEndTimeUs = chunkStartTimeUs + streamElement.getChunkDurationUs(chunkIndex); int currentAbsoluteChunkIndex = chunkIndex + currentManifestChunkOffset; @@ -229,10 +229,6 @@ public class DefaultSsChunkSource implements SsChunkSource { } StreamElement currentElement = manifest.streamElements[elementIndex]; - if (currentElement.chunkCount == 0) { - return C.TIME_UNSET; - } - int lastChunkIndex = currentElement.chunkCount - 1; long lastChunkEndTimeUs = currentElement.getStartTimeUs(lastChunkIndex) + currentElement.getChunkDurationUs(lastChunkIndex); From 25a9177ce36aee20dfa95447695969e98a52b0c0 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 21 Sep 2017 09:18:43 -0700 Subject: [PATCH 0476/2472] Deduplicate DefaultTrackSelector javadocs ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=169552239 --- .../trackselection/DefaultTrackSelector.java | 55 ++++--------------- 1 file changed, 11 insertions(+), 44 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index ba0f63b0bb..6c6d02a1fb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -193,11 +193,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * Returns an instance with the provided preferred language for audio and forced text tracks. - * - * @param preferredAudioLanguage The preferred language as defined by RFC 5646. {@code null} to - * select the default track, or first track if there's no default. - * @return An instance with the provided preferred language for audio and forced text tracks. + * Returns an instance with the provided {@link #preferredAudioLanguage}. */ public Parameters withPreferredAudioLanguage(String preferredAudioLanguage) { preferredAudioLanguage = Util.normalizeLanguageCode(preferredAudioLanguage); @@ -211,11 +207,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * Returns an instance with the provided preferred language for text tracks. - * - * @param preferredTextLanguage The preferred language as defined by RFC 5646. {@code null} to - * select the default track, or no track if there's no default. - * @return An instance with the provided preferred language for text tracks. + * Returns an instance with the provided {@link #preferredTextLanguage}. */ public Parameters withPreferredTextLanguage(String preferredTextLanguage) { preferredTextLanguage = Util.normalizeLanguageCode(preferredTextLanguage); @@ -229,10 +221,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * Returns an instance with the provided mixed mime adaptiveness allowance. - * - * @param allowMixedMimeAdaptiveness Whether to allow selections to contain mixed mime types. - * @return An instance with the provided mixed mime adaptiveness allowance. + * Returns an instance with the provided {@link #allowMixedMimeAdaptiveness}. */ public Parameters withAllowMixedMimeAdaptiveness(boolean allowMixedMimeAdaptiveness) { if (allowMixedMimeAdaptiveness == this.allowMixedMimeAdaptiveness) { @@ -245,10 +234,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * Returns an instance with the provided seamless adaptiveness allowance. - * - * @param allowNonSeamlessAdaptiveness Whether non-seamless adaptation is allowed. - * @return An instance with the provided seamless adaptiveness allowance. + * Returns an instance with the provided {@link #allowNonSeamlessAdaptiveness}. */ public Parameters withAllowNonSeamlessAdaptiveness(boolean allowNonSeamlessAdaptiveness) { if (allowNonSeamlessAdaptiveness == this.allowNonSeamlessAdaptiveness) { @@ -261,11 +247,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * Returns an instance with the provided max video size. - * - * @param maxVideoWidth The max video width. - * @param maxVideoHeight The max video width. - * @return An instance with the provided max video size. + * Returns an instance with the provided {@link #maxVideoWidth} and {@link #maxVideoHeight}. */ public Parameters withMaxVideoSize(int maxVideoWidth, int maxVideoHeight) { if (maxVideoWidth == this.maxVideoWidth && maxVideoHeight == this.maxVideoHeight) { @@ -278,10 +260,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * Returns an instance with the provided max video bitrate. - * - * @param maxVideoBitrate The max video bitrate. - * @return An instance with the provided max video bitrate. + * Returns an instance with the provided {@link #maxVideoBitrate}. */ public Parameters withMaxVideoBitrate(int maxVideoBitrate) { if (maxVideoBitrate == this.maxVideoBitrate) { @@ -312,11 +291,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * Returns an instance with the provided {@code exceedVideoConstraintsIfNecessary} value. - * - * @param exceedVideoConstraintsIfNecessary Whether to exceed video constraints when no - * selection can be made otherwise. - * @return An instance with the provided {@code exceedVideoConstraintsIfNecessary} value. + * Returns an instance with the provided {@link #exceedVideoConstraintsIfNecessary}. */ public Parameters withExceedVideoConstraintsIfNecessary( boolean exceedVideoConstraintsIfNecessary) { @@ -330,11 +305,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * Returns an instance with the provided {@code exceedRendererCapabilitiesIfNecessary} value. - * - * @param exceedRendererCapabilitiesIfNecessary Whether to exceed renderer capabilities when no - * selection can be made otherwise. - * @return An instance with the provided {@code exceedRendererCapabilitiesIfNecessary} value. + * Returns an instance with the provided {@link #exceedRendererCapabilitiesIfNecessary}. */ public Parameters withExceedRendererCapabilitiesIfNecessary( boolean exceedRendererCapabilitiesIfNecessary) { @@ -348,12 +319,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * Returns an instance with the provided viewport size. - * - * @param viewportWidth Viewport width in pixels. - * @param viewportHeight Viewport height in pixels. - * @param viewportOrientationMayChange Whether orientation may change during playback. - * @return An instance with the provided viewport size. + * Returns an instance with the provided {@link #viewportWidth}, {@link #viewportHeight} and + * {@link #viewportOrientationMayChange}. */ public Parameters withViewportSize(int viewportWidth, int viewportHeight, boolean viewportOrientationMayChange) { @@ -371,7 +338,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { * Returns an instance where the viewport size is obtained from the provided {@link Context}. * * @param context The context to obtain the viewport size from. - * @param viewportOrientationMayChange Whether orientation may change during playback. + * @param viewportOrientationMayChange See {@link #viewportOrientationMayChange}. * @return An instance where the viewport size is obtained from the provided {@link Context}. */ public Parameters withViewportSizeFromContext(Context context, From 4fb18453e1dd02e301fa99e73b01c19b58c862a2 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Fri, 22 Sep 2017 03:22:02 -0700 Subject: [PATCH 0477/2472] Add a "forceLowestBitrate" option to DefaultTrackSelector ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=169668371 --- .../trackselection/DefaultTrackSelector.java | 90 ++++++++++++------- 1 file changed, 60 insertions(+), 30 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index 6c6d02a1fb..0ab4f62866 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -127,6 +127,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { public final boolean viewportOrientationMayChange; // General + /** + * Whether to force selection of the single lowest bitrate audio and video tracks that comply + * with all other constraints. + */ + public final boolean forceLowestBitrate; /** * Whether to allow adaptive selections containing mixed mime types. */ @@ -145,6 +150,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { *

            *
          • No preferred audio language is set.
          • *
          • No preferred text language is set.
          • + *
          • Lowest bitrate track selections are not forced.
          • *
          • Adaptation between different mime types is not allowed.
          • *
          • Non seamless adaptation is allowed.
          • *
          • No max limit for video width/height.
          • @@ -155,13 +161,14 @@ public class DefaultTrackSelector extends MappingTrackSelector { *
          */ public Parameters() { - this(null, null, false, true, Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE, true, - true, Integer.MAX_VALUE, Integer.MAX_VALUE, true); + this(null, null, false, false, true, Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE, + true, true, Integer.MAX_VALUE, Integer.MAX_VALUE, true); } /** * @param preferredAudioLanguage See {@link #preferredAudioLanguage} * @param preferredTextLanguage See {@link #preferredTextLanguage} + * @param forceLowestBitrate See {@link #forceLowestBitrate}. * @param allowMixedMimeAdaptiveness See {@link #allowMixedMimeAdaptiveness} * @param allowNonSeamlessAdaptiveness See {@link #allowNonSeamlessAdaptiveness} * @param maxVideoWidth See {@link #maxVideoWidth} @@ -174,12 +181,14 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @param viewportOrientationMayChange See {@link #viewportOrientationMayChange} */ public Parameters(String preferredAudioLanguage, String preferredTextLanguage, - boolean allowMixedMimeAdaptiveness, boolean allowNonSeamlessAdaptiveness, - int maxVideoWidth, int maxVideoHeight, int maxVideoBitrate, - boolean exceedVideoConstraintsIfNecessary, boolean exceedRendererCapabilitiesIfNecessary, - int viewportWidth, int viewportHeight, boolean viewportOrientationMayChange) { + boolean forceLowestBitrate, boolean allowMixedMimeAdaptiveness, + boolean allowNonSeamlessAdaptiveness, int maxVideoWidth, int maxVideoHeight, + int maxVideoBitrate, boolean exceedVideoConstraintsIfNecessary, + boolean exceedRendererCapabilitiesIfNecessary, int viewportWidth, int viewportHeight, + boolean viewportOrientationMayChange) { this.preferredAudioLanguage = preferredAudioLanguage; this.preferredTextLanguage = preferredTextLanguage; + this.forceLowestBitrate = forceLowestBitrate; this.allowMixedMimeAdaptiveness = allowMixedMimeAdaptiveness; this.allowNonSeamlessAdaptiveness = allowNonSeamlessAdaptiveness; this.maxVideoWidth = maxVideoWidth; @@ -200,7 +209,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (TextUtils.equals(preferredAudioLanguage, this.preferredAudioLanguage)) { return this; } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, + return new Parameters(preferredAudioLanguage, preferredTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, viewportHeight, viewportOrientationMayChange); @@ -214,7 +223,20 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (TextUtils.equals(preferredTextLanguage, this.preferredTextLanguage)) { return this; } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, + return new Parameters(preferredAudioLanguage, preferredTextLanguage, forceLowestBitrate, + allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, + maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, + viewportWidth, viewportHeight, viewportOrientationMayChange); + } + + /** + * Returns an instance with the provided {@link #forceLowestBitrate}. + */ + public Parameters withForceLowestBitrate(boolean forceLowestBitrate) { + if (forceLowestBitrate == this.forceLowestBitrate) { + return this; + } + return new Parameters(preferredAudioLanguage, preferredTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, viewportHeight, viewportOrientationMayChange); @@ -227,7 +249,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (allowMixedMimeAdaptiveness == this.allowMixedMimeAdaptiveness) { return this; } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, + return new Parameters(preferredAudioLanguage, preferredTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, viewportHeight, viewportOrientationMayChange); @@ -240,7 +262,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (allowNonSeamlessAdaptiveness == this.allowNonSeamlessAdaptiveness) { return this; } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, + return new Parameters(preferredAudioLanguage, preferredTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, viewportHeight, viewportOrientationMayChange); @@ -253,7 +275,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (maxVideoWidth == this.maxVideoWidth && maxVideoHeight == this.maxVideoHeight) { return this; } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, + return new Parameters(preferredAudioLanguage, preferredTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, viewportHeight, viewportOrientationMayChange); @@ -266,7 +288,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (maxVideoBitrate == this.maxVideoBitrate) { return this; } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, + return new Parameters(preferredAudioLanguage, preferredTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, viewportHeight, viewportOrientationMayChange); @@ -298,7 +320,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (exceedVideoConstraintsIfNecessary == this.exceedVideoConstraintsIfNecessary) { return this; } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, + return new Parameters(preferredAudioLanguage, preferredTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, viewportHeight, viewportOrientationMayChange); @@ -312,7 +334,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (exceedRendererCapabilitiesIfNecessary == this.exceedRendererCapabilitiesIfNecessary) { return this; } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, + return new Parameters(preferredAudioLanguage, preferredTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, viewportHeight, viewportOrientationMayChange); @@ -328,7 +350,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { && viewportOrientationMayChange == this.viewportOrientationMayChange) { return this; } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, + return new Parameters(preferredAudioLanguage, preferredTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, viewportHeight, viewportOrientationMayChange); @@ -366,7 +388,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { return false; } Parameters other = (Parameters) obj; - return allowMixedMimeAdaptiveness == other.allowMixedMimeAdaptiveness + return forceLowestBitrate == other.forceLowestBitrate + && allowMixedMimeAdaptiveness == other.allowMixedMimeAdaptiveness && allowNonSeamlessAdaptiveness == other.allowNonSeamlessAdaptiveness && maxVideoWidth == other.maxVideoWidth && maxVideoHeight == other.maxVideoHeight && exceedVideoConstraintsIfNecessary == other.exceedVideoConstraintsIfNecessary @@ -382,6 +405,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { public int hashCode() { int result = preferredAudioLanguage.hashCode(); result = 31 * result + preferredTextLanguage.hashCode(); + result = 31 * result + (forceLowestBitrate ? 1 : 0); result = 31 * result + (allowMixedMimeAdaptiveness ? 1 : 0); result = 31 * result + (allowNonSeamlessAdaptiveness ? 1 : 0); result = 31 * result + maxVideoWidth; @@ -535,7 +559,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { TrackGroupArray groups, int[][] formatSupport, Parameters params, TrackSelection.Factory adaptiveTrackSelectionFactory) throws ExoPlaybackException { TrackSelection selection = null; - if (adaptiveTrackSelectionFactory != null) { + if (!params.forceLowestBitrate && adaptiveTrackSelectionFactory != null) { selection = selectAdaptiveVideoTrack(rendererCapabilities, groups, formatSupport, params, adaptiveTrackSelectionFactory); } @@ -676,19 +700,21 @@ public class DefaultTrackSelector extends MappingTrackSelector { } boolean selectTrack = trackScore > selectedTrackScore; if (trackScore == selectedTrackScore) { - // Use the pixel count as a tie breaker (or bitrate if pixel counts are tied). If we're - // within constraints prefer a higher pixel count (or bitrate), else prefer a lower - // count (or bitrate). If still tied then prefer the first track (i.e. the one that's - // already selected). - int comparisonResult; - int formatPixelCount = format.getPixelCount(); - if (formatPixelCount != selectedPixelCount) { - comparisonResult = compareFormatValues(format.getPixelCount(), selectedPixelCount); + if (params.forceLowestBitrate) { + // Use bitrate as a tie breaker, preferring the lower bitrate. + selectTrack = compareFormatValues(format.bitrate, selectedBitrate) < 0; } else { - comparisonResult = compareFormatValues(format.bitrate, selectedBitrate); + // Use the pixel count as a tie breaker (or bitrate if pixel counts are tied). If + // we're within constraints prefer a higher pixel count (or bitrate), else prefer a + // lower count (or bitrate). If still tied then prefer the first track (i.e. the one + // that's already selected). + int formatPixelCount = format.getPixelCount(); + int comparisonResult = formatPixelCount != selectedPixelCount + ? compareFormatValues(formatPixelCount, selectedPixelCount) + : compareFormatValues(format.bitrate, selectedBitrate); + selectTrack = isWithinCapabilities && isWithinConstraints + ? comparisonResult > 0 : comparisonResult < 0; } - selectTrack = isWithinCapabilities && isWithinConstraints - ? comparisonResult > 0 : comparisonResult < 0; } if (selectTrack) { selectedGroup = trackGroup; @@ -739,6 +765,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { int selectedGroupIndex = C.INDEX_UNSET; int selectedTrackIndex = C.INDEX_UNSET; int selectedTrackScore = 0; + int selectedBitrate = Format.NO_VALUE; for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) { TrackGroup trackGroup = groups.get(groupIndex); int[] trackFormatSupport = formatSupport[groupIndex]; @@ -748,10 +775,13 @@ public class DefaultTrackSelector extends MappingTrackSelector { Format format = trackGroup.getFormat(trackIndex); int trackScore = getAudioTrackScore(trackFormatSupport[trackIndex], params.preferredAudioLanguage, format); - if (trackScore > selectedTrackScore) { + if (trackScore > selectedTrackScore + || (trackScore == selectedTrackScore && params.forceLowestBitrate + && compareFormatValues(format.bitrate, selectedBitrate) < 0)) { selectedGroupIndex = groupIndex; selectedTrackIndex = trackIndex; selectedTrackScore = trackScore; + selectedBitrate = format.bitrate; } } } @@ -762,7 +792,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { } TrackGroup selectedGroup = groups.get(selectedGroupIndex); - if (adaptiveTrackSelectionFactory != null) { + if (!params.forceLowestBitrate && adaptiveTrackSelectionFactory != null) { // If the group of the track with the highest score allows it, try to enable adaptation. int[] adaptiveTracks = getAdaptiveAudioTracks(selectedGroup, formatSupport[selectedGroupIndex], params.allowMixedMimeAdaptiveness); From 1f8a8dbfa39d8b50a2465d6b04b5508d799f0d68 Mon Sep 17 00:00:00 2001 From: eguven Date: Fri, 22 Sep 2017 06:27:22 -0700 Subject: [PATCH 0478/2472] Add version number to serialized DownloadAction data ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=169681768 --- .../exoplayer2/offline/ProgressiveDownloadActionTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/ProgressiveDownloadActionTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/ProgressiveDownloadActionTest.java index 95dd218092..44fe6c069b 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/offline/ProgressiveDownloadActionTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/ProgressiveDownloadActionTest.java @@ -135,7 +135,8 @@ public class ProgressiveDownloadActionTest { ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); DataInputStream input = new DataInputStream(in); - DownloadAction action2 = ProgressiveDownloadAction.DESERIALIZER.readFromStream(input); + DownloadAction action2 = + ProgressiveDownloadAction.DESERIALIZER.readFromStream(action1.getVersion(), input); assertThat(action2).isEqualTo(action1); } From 505d5cd0a4953e43094759efdd4c6cbc0749bee7 Mon Sep 17 00:00:00 2001 From: olly Date: Sun, 24 Sep 2017 04:57:51 -0700 Subject: [PATCH 0479/2472] Fix a few lint warnings ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=169830938 --- .../com/google/android/exoplayer2/castdemo/MainActivity.java | 4 +++- .../com/google/android/exoplayer2/castdemo/PlayerManager.java | 4 +--- .../google/android/exoplayer2/demo/TrackSelectionHelper.java | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java index e1c7519a05..9a2b76c941 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.castdemo; import android.graphics.Color; import android.os.Bundle; +import android.support.annotation.NonNull; import android.support.v7.app.AppCompatActivity; import android.view.KeyEvent; import android.view.Menu; @@ -129,7 +130,8 @@ public class MainActivity extends AppCompatActivity { } @Override - public View getView(int position, View convertView, ViewGroup parent) { + @NonNull + public View getView(int position, View convertView, @NonNull ViewGroup parent) { View view = super.getView(position, convertView, parent); view.setBackgroundColor(Color.WHITE); return view; diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java index 8b461ec65c..2538b03102 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java @@ -57,7 +57,6 @@ import com.google.android.gms.cast.framework.CastContext; private final SimpleExoPlayerView exoPlayerView; private final PlaybackControlView castControlView; - private final CastContext castContext; private final SimpleExoPlayer exoPlayer; private final CastPlayer castPlayer; @@ -73,14 +72,13 @@ import com.google.android.gms.cast.framework.CastContext; Context context) { this.exoPlayerView = exoPlayerView; this.castControlView = castControlView; - castContext = CastContext.getSharedInstance(context); DefaultTrackSelector trackSelector = new DefaultTrackSelector(BANDWIDTH_METER); RenderersFactory renderersFactory = new DefaultRenderersFactory(context, null); exoPlayer = ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector); exoPlayerView.setPlayer(exoPlayer); - castPlayer = new CastPlayer(castContext); + castPlayer = new CastPlayer(CastContext.getSharedInstance(context)); castPlayer.setSessionAvailabilityListener(this); castControlView.setPlayer(castPlayer); diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionHelper.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionHelper.java index fb7217f8fd..e033b91eef 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionHelper.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionHelper.java @@ -109,7 +109,7 @@ import java.util.Arrays; private View buildView(Context context) { LayoutInflater inflater = LayoutInflater.from(context); View view = inflater.inflate(R.layout.track_selection_dialog, null); - ViewGroup root = (ViewGroup) view.findViewById(R.id.root); + ViewGroup root = view.findViewById(R.id.root); TypedArray attributeArray = context.getTheme().obtainStyledAttributes( new int[] {android.R.attr.selectableItemBackground}); From 06f7b6be6ae5e085606cc11e122b861c1bec9d19 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 25 Sep 2017 02:20:14 -0700 Subject: [PATCH 0480/2472] Use IntDef for AudioTrack.startMediaTimeState ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=169880369 --- .../android/exoplayer2/audio/AudioTrack.java | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java index a2d061ac84..25813aefc0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java @@ -22,6 +22,7 @@ import android.media.AudioManager; import android.media.AudioTimestamp; import android.os.ConditionVariable; import android.os.SystemClock; +import android.support.annotation.IntDef; import android.support.annotation.Nullable; import android.util.Log; import com.google.android.exoplayer2.C; @@ -29,6 +30,8 @@ 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; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.lang.reflect.Method; import java.nio.ByteBuffer; import java.nio.ByteOrder; @@ -247,6 +250,12 @@ public final class AudioTrack { */ private static final long MAX_LATENCY_US = 5 * C.MICROS_PER_SECOND; + /** + * Represents states of the {@link #startMediaTimeUs} value. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({START_NOT_SET, START_IN_SYNC, START_NEED_SYNC}) + private @interface StartMediaTimeState {} private static final int START_NOT_SET = 0; private static final int START_IN_SYNC = 1; private static final int START_NEED_SYNC = 2; @@ -299,10 +308,8 @@ public final class AudioTrack { private android.media.AudioTrack audioTrack; private int sampleRate; private int channelConfig; - @C.Encoding - private int encoding; - @C.Encoding - private int outputEncoding; + private @C.Encoding int encoding; + private @C.Encoding int outputEncoding; private AudioAttributes audioAttributes; private boolean passthrough; private int bufferSize; @@ -331,7 +338,7 @@ public final class AudioTrack { private long writtenPcmBytes; private long writtenEncodedFrames; private int framesPerEncodedSample; - private int startMediaTimeState; + private @StartMediaTimeState int startMediaTimeState; private long startMediaTimeUs; private long resumeSystemTimeUs; private long latencyUs; From c6bc30bab73781c3d02f66d4d636045328fd190f Mon Sep 17 00:00:00 2001 From: strobe Date: Mon, 25 Sep 2017 09:41:58 -0700 Subject: [PATCH 0481/2472] Add NEON-accelerated HDR conversion routines to VPX. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=169919087 --- extensions/vp9/src/main/jni/vpx_jni.cc | 265 +++++++++++++++++++++---- 1 file changed, 223 insertions(+), 42 deletions(-) diff --git a/extensions/vp9/src/main/jni/vpx_jni.cc b/extensions/vp9/src/main/jni/vpx_jni.cc index f0b93b1dc2..5c480d1525 100644 --- a/extensions/vp9/src/main/jni/vpx_jni.cc +++ b/extensions/vp9/src/main/jni/vpx_jni.cc @@ -15,6 +15,9 @@ */ #include +#ifdef __ARM_NEON__ +#include +#endif #include #include @@ -70,6 +73,216 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) { return JNI_VERSION_1_6; } +#ifdef __ARM_NEON__ +static int convert_16_to_8_neon(const vpx_image_t* const img, jbyte* const data, + const int32_t uvHeight, const int32_t yLength, + const int32_t uvLength) { + if (!(android_getCpuFeatures() & ANDROID_CPU_ARM_FEATURE_NEON)) return 0; + uint32x2_t lcg_val = vdup_n_u32(random()); + lcg_val = vset_lane_u32(random(), lcg_val, 1); + // LCG values recommended in good ol' "Numerical Recipes" + const uint32x2_t LCG_MULT = vdup_n_u32(1664525); + const uint32x2_t LCG_INCR = vdup_n_u32(1013904223); + + const uint16_t* srcBase = + reinterpret_cast(img->planes[VPX_PLANE_Y]); + uint8_t* dstBase = reinterpret_cast(data); + // In units of uint16_t, so /2 from raw stride + const int srcStride = img->stride[VPX_PLANE_Y] / 2; + const int dstStride = img->stride[VPX_PLANE_Y]; + + for (int y = 0; y < img->d_h; y++) { + const uint16_t* src = srcBase; + uint8_t* dst = dstBase; + + // Each read consumes 4 2-byte samples, but to reduce branches and + // random steps we unroll to four rounds, so each loop consumes 16 + // samples. + const int imax = img->d_w & ~15; + int i; + for (i = 0; i < imax; i += 16) { + // Run a round of the RNG. + lcg_val = vmla_u32(LCG_INCR, lcg_val, LCG_MULT); + + // The lower two bits of this LCG parameterization are garbage, + // leaving streaks on the image. We access the upper bits of each + // 16-bit lane by shifting. (We use this both as an 8- and 16-bit + // vector, so the choice of which one to keep it as is arbitrary.) + uint8x8_t randvec = + vreinterpret_u8_u16(vshr_n_u16(vreinterpret_u16_u32(lcg_val), 8)); + + // We retrieve the values and shift them so that the bits we'll + // shift out (after biasing) are in the upper 8 bits of each 16-bit + // lane. + uint16x4_t values = vshl_n_u16(vld1_u16(src), 6); + src += 4; + + // We add the bias bits in the lower 8 to the shifted values to get + // the final values in the upper 8 bits. + uint16x4_t added1 = vqadd_u16(values, vreinterpret_u16_u8(randvec)); + + // Shifting the randvec bits left by 2 bits, as an 8-bit vector, + // should leave us with enough bias to get the needed rounding + // operation. + randvec = vshl_n_u8(randvec, 2); + + // Retrieve and sum the next 4 pixels. + values = vshl_n_u16(vld1_u16(src), 6); + src += 4; + uint16x4_t added2 = vqadd_u16(values, vreinterpret_u16_u8(randvec)); + + // Reinterpret the two added vectors as 8x8, zip them together, and + // discard the lower portions. + uint8x8_t zipped = + vuzp_u8(vreinterpret_u8_u16(added1), vreinterpret_u8_u16(added2)) + .val[1]; + vst1_u8(dst, zipped); + dst += 8; + + // Run it again with the next two rounds using the remaining + // entropy in randvec. + randvec = vshl_n_u8(randvec, 2); + values = vshl_n_u16(vld1_u16(src), 6); + src += 4; + added1 = vqadd_u16(values, vreinterpret_u16_u8(randvec)); + randvec = vshl_n_u8(randvec, 2); + values = vshl_n_u16(vld1_u16(src), 6); + src += 4; + added2 = vqadd_u16(values, vreinterpret_u16_u8(randvec)); + zipped = vuzp_u8(vreinterpret_u8_u16(added1), vreinterpret_u8_u16(added2)) + .val[1]; + vst1_u8(dst, zipped); + dst += 8; + } + + uint32_t randval = 0; + // For the remaining pixels in each row - usually none, as most + // standard sizes are divisible by 32 - convert them "by hand". + while (i < img->d_w) { + if (!randval) randval = random(); + dstBase[i] = (srcBase[i] + (randval & 3)) >> 2; + i++; + randval >>= 2; + } + + srcBase += srcStride; + dstBase += dstStride; + } + + const uint16_t* srcUBase = + reinterpret_cast(img->planes[VPX_PLANE_U]); + const uint16_t* srcVBase = + reinterpret_cast(img->planes[VPX_PLANE_V]); + const int32_t uvWidth = (img->d_w + 1) / 2; + uint8_t* dstUBase = reinterpret_cast(data + yLength); + uint8_t* dstVBase = reinterpret_cast(data + yLength + uvLength); + const int srcUVStride = img->stride[VPX_PLANE_V] / 2; + const int dstUVStride = img->stride[VPX_PLANE_V]; + + for (int y = 0; y < uvHeight; y++) { + const uint16_t* srcU = srcUBase; + const uint16_t* srcV = srcVBase; + uint8_t* dstU = dstUBase; + uint8_t* dstV = dstVBase; + + // As before, each i++ consumes 4 samples (8 bytes). For simplicity we + // don't unroll these loops more than we have to, which is 8 samples. + const int imax = uvWidth & ~7; + int i; + for (i = 0; i < imax; i += 8) { + lcg_val = vmla_u32(LCG_INCR, lcg_val, LCG_MULT); + uint8x8_t randvec = + vreinterpret_u8_u16(vshr_n_u16(vreinterpret_u16_u32(lcg_val), 8)); + uint16x4_t uVal1 = vqadd_u16(vshl_n_u16(vld1_u16(srcU), 6), + vreinterpret_u16_u8(randvec)); + srcU += 4; + randvec = vshl_n_u8(randvec, 2); + uint16x4_t vVal1 = vqadd_u16(vshl_n_u16(vld1_u16(srcV), 6), + vreinterpret_u16_u8(randvec)); + srcV += 4; + randvec = vshl_n_u8(randvec, 2); + uint16x4_t uVal2 = vqadd_u16(vshl_n_u16(vld1_u16(srcU), 6), + vreinterpret_u16_u8(randvec)); + srcU += 4; + randvec = vshl_n_u8(randvec, 2); + uint16x4_t vVal2 = vqadd_u16(vshl_n_u16(vld1_u16(srcV), 6), + vreinterpret_u16_u8(randvec)); + srcV += 4; + vst1_u8(dstU, + vuzp_u8(vreinterpret_u8_u16(uVal1), vreinterpret_u8_u16(uVal2)) + .val[1]); + dstU += 8; + vst1_u8(dstV, + vuzp_u8(vreinterpret_u8_u16(vVal1), vreinterpret_u8_u16(vVal2)) + .val[1]); + dstV += 8; + } + + i *= 4; + uint32_t randval = 0; + while (i < uvWidth) { + if (!randval) randval = random(); + dstUBase[i] = (srcUBase[i] + (randval & 3)) >> 2; + randval >>= 2; + dstVBase[i] = (srcVBase[i] + (randval & 3)) >> 2; + randval >>= 2; + i++; + } + + srcUBase += srcUVStride; + srcVBase += srcUVStride; + dstUBase += dstUVStride; + dstVBase += dstUVStride; + } + + return 1; +} + +#endif // __ARM_NEON__ + +static void convert_16_to_8_standard(const vpx_image_t* const img, + jbyte* const data, const int32_t uvHeight, + const int32_t yLength, + const int32_t uvLength) { + // Y + int sampleY = 0; + for (int y = 0; y < img->d_h; y++) { + const uint16_t* srcBase = reinterpret_cast( + img->planes[VPX_PLANE_Y] + img->stride[VPX_PLANE_Y] * y); + int8_t* destBase = data + img->stride[VPX_PLANE_Y] * y; + for (int x = 0; x < img->d_w; x++) { + // Lightweight dither. Carryover the remainder of each 10->8 bit + // conversion to the next pixel. + sampleY += *srcBase++; + *destBase++ = sampleY >> 2; + sampleY = sampleY & 3; // Remainder. + } + } + // UV + int sampleU = 0; + int sampleV = 0; + const int32_t uvWidth = (img->d_w + 1) / 2; + for (int y = 0; y < uvHeight; y++) { + const uint16_t* srcUBase = reinterpret_cast( + img->planes[VPX_PLANE_U] + img->stride[VPX_PLANE_U] * y); + const uint16_t* srcVBase = reinterpret_cast( + img->planes[VPX_PLANE_V] + img->stride[VPX_PLANE_V] * y); + int8_t* destUBase = data + yLength + img->stride[VPX_PLANE_U] * y; + int8_t* destVBase = + data + yLength + uvLength + img->stride[VPX_PLANE_V] * y; + for (int x = 0; x < uvWidth; x++) { + // Lightweight dither. Carryover the remainder of each 10->8 bit + // conversion to the next pixel. + sampleU += *srcUBase++; + *destUBase++ = sampleU >> 2; + sampleU = sampleU & 3; // Remainder. + sampleV += *srcVBase++; + *destVBase++ = sampleV >> 2; + sampleV = sampleV & 3; // Remainder. + } + } +} + DECODER_FUNC(jlong, vpxInit) { vpx_codec_ctx_t* context = new vpx_codec_ctx_t(); vpx_codec_dec_cfg_t cfg = {0, 0, 0}; @@ -201,47 +414,17 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) { // Note: The stride for BT2020 is twice of what we use so this is wasting // memory. The long term goal however is to upload half-float/short so // it's not important to optimize the stride at this time. - // Y - int sampleY = 0; - for (int y = 0; y < img->d_h; y++) { - const uint16_t* srcBase = reinterpret_cast( - img->planes[VPX_PLANE_Y] + img->stride[VPX_PLANE_Y] * y); - int8_t* destBase = data + img->stride[VPX_PLANE_Y] * y; - for (int x = 0; x < img->d_w; x++) { - // Lightweight dither. Carryover the remainder of each 10->8 bit - // conversion to the next pixel. - sampleY += *srcBase++; - *destBase++ = sampleY >> 2; - sampleY = sampleY & 3; // Remainder. - } - } - // UV - int sampleU = 0; - int sampleV = 0; - const int32_t uvWidth = (img->d_w + 1) / 2; - for (int y = 0; y < uvHeight; y++) { - const uint16_t* srcUBase = reinterpret_cast( - img->planes[VPX_PLANE_U] + img->stride[VPX_PLANE_U] * y); - const uint16_t* srcVBase = reinterpret_cast( - img->planes[VPX_PLANE_V] + img->stride[VPX_PLANE_V] * y); - int8_t* destUBase = data + yLength + img->stride[VPX_PLANE_U] * y; - int8_t* destVBase = data + yLength + uvLength - + img->stride[VPX_PLANE_V] * y; - for (int x = 0; x < uvWidth; x++) { - // Lightweight dither. Carryover the remainder of each 10->8 bit - // conversion to the next pixel. - sampleU += *srcUBase++; - *destUBase++ = sampleU >> 2; - sampleU = sampleU & 3; // Remainder. - sampleV += *srcVBase++; - *destVBase++ = sampleV >> 2; - sampleV = sampleV & 3; // Remainder. - } + int converted = 0; +#ifdef __ARM_NEON__ + converted = convert_16_to_8_neon(img, data, uvHeight, yLength, uvLength); +#endif // __ARM_NEON__ + if (!converted) { + convert_16_to_8_standard(img, data, uvHeight, yLength, uvLength); } } else { - // TODO: This copy can be eliminated by using external frame buffers. This - // is insignificant for smaller videos but takes ~1.5ms for 1080p clips. - // So this should eventually be gotten rid of. + // TODO: This copy can be eliminated by using external frame + // buffers. This is insignificant for smaller videos but takes ~1.5ms + // for 1080p clips. So this should eventually be gotten rid of. memcpy(data, img->planes[VPX_PLANE_Y], yLength); memcpy(data + yLength, img->planes[VPX_PLANE_U], uvLength); memcpy(data + yLength + uvLength, img->planes[VPX_PLANE_V], uvLength); @@ -255,9 +438,7 @@ DECODER_FUNC(jstring, vpxGetErrorMessage, jlong jContext) { return env->NewStringUTF(vpx_codec_error(context)); } -DECODER_FUNC(jint, vpxGetErrorCode, jlong jContext) { - return errorCode; -} +DECODER_FUNC(jint, vpxGetErrorCode, jlong jContext) { return errorCode; } LIBRARY_FUNC(jstring, vpxIsSecureDecodeSupported) { // Doesn't support From b17a6ba51dbdfc5808330ebf0b370e920beee085 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 25 Sep 2017 10:44:28 -0700 Subject: [PATCH 0482/2472] Add TODO to respect FLAG_ALLOW_GZIP in CronetDataSource ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=169927989 --- .../android/exoplayer2/ext/cronet/CronetDataSource.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java index 204a2756bb..cdc8eb7b35 100644 --- a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java +++ b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java @@ -460,6 +460,12 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou } requestBuilder.addHeader("Range", rangeValue.toString()); } + // TODO: Uncomment when https://bugs.chromium.org/p/chromium/issues/detail?id=767025 is fixed + // (adjusting the code as necessary). + // Force identity encoding unless gzip is allowed. + // if (!dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP)) { + // requestBuilder.addHeader("Accept-Encoding", "identity"); + // } // Set the method and (if non-empty) the body. if (dataSpec.postBody != null) { requestBuilder.setHttpMethod("POST"); From 792f046c02b45fee7a4f6f0f8ce8de41e081334d Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 26 Sep 2017 17:06:36 +0100 Subject: [PATCH 0483/2472] Clean up HlsPlaylistTracker --- .../hls/playlist/HlsPlaylistTracker.java | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/library/hls/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 index d9ab410fb9..f259a63420 100644 --- a/library/hls/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 @@ -200,7 +200,7 @@ public final class HlsPlaylistTracker implements Loader.Callback urls) { int listSize = urls.size(); - long currentTimeMs = SystemClock.elapsedRealtime(); for (int i = 0; i < listSize; i++) { HlsUrl url = urls.get(i); - MediaPlaylistBundle bundle = new MediaPlaylistBundle(url, currentTimeMs); + MediaPlaylistBundle bundle = new MediaPlaylistBundle(url); playlistBundles.put(url, bundle); } } @@ -470,14 +469,12 @@ public final class HlsPlaylistTracker implements Loader.Callback( dataSourceFactory.createDataSource(C.DATA_TYPE_MANIFEST), @@ -486,7 +483,6 @@ public final class HlsPlaylistTracker implements Loader.Callback Date: Tue, 26 Sep 2017 11:09:45 -0700 Subject: [PATCH 0484/2472] Prevent unnecessary consecutive playlist loads ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=170078933 --- .../hls/playlist/HlsPlaylistTracker.java | 69 ++++++++++++------- 1 file changed, 43 insertions(+), 26 deletions(-) diff --git a/library/hls/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 index f259a63420..d3682217d8 100644 --- a/library/hls/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 @@ -363,9 +363,8 @@ public final class HlsPlaylistTracker implements Loader.Callback C.usToMs(playlistSnapshot.targetDurationUs) * PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT) { - // The playlist seems to be stuck, we blacklist it. + // The playlist seems to be stuck. Blacklist it. playlistError = new PlaylistStuckException(playlistUrl.url); blacklistPlaylist(); - } else if (loadedPlaylist.mediaSequence + loadedPlaylist.segments.size() - < playlistSnapshot.mediaSequence) { - // The media sequence has jumped backwards. The server has likely reset. - playlistError = new PlaylistResetException(playlistUrl.url); } - refreshDelayUs = playlistSnapshot.targetDurationUs / 2; } - if (refreshDelayUs != C.TIME_UNSET) { - // See HLS spec v20, section 6.3.4 for more information on media playlist refreshing. - pendingRefresh = playlistRefreshHandler.postDelayed(this, C.usToMs(refreshDelayUs)); + // Do not allow the playlist to load again within the target duration if we obtained a new + // snapshot, or half the target duration otherwise. + earliestNextLoadTimeMs = currentTimeMs + C.usToMs(playlistSnapshot != oldPlaylist + ? playlistSnapshot.targetDurationUs : (playlistSnapshot.targetDurationUs / 2)); + // Schedule a load if this is the primary playlist and it doesn't have an end tag. Else the + // next load will be scheduled when refreshPlaylist is called, or when this playlist becomes + // the primary. + if (playlistUrl == primaryHlsUrl && !playlistSnapshot.hasEndTag) { + loadPlaylist(); } } - private void blacklistPlaylist() { + /** + * Blacklists the playlist. + * + * @return Whether the playlist is the primary, despite being blacklisted. + */ + private boolean blacklistPlaylist() { blacklistUntilMs = SystemClock.elapsedRealtime() + ChunkedTrackBlacklistUtil.DEFAULT_TRACK_BLACKLIST_MS; notifyPlaylistBlacklisting(playlistUrl, ChunkedTrackBlacklistUtil.DEFAULT_TRACK_BLACKLIST_MS); + return primaryHlsUrl == playlistUrl && !maybeSelectNewPrimaryUrl(); } } From 55f696e11d6aa3fef22539d2eb998bb19bd0eeb5 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 27 Sep 2017 03:14:20 -0700 Subject: [PATCH 0485/2472] Fix spurious failures due to late decoding. By default, if a codec is instantiated during an ongoing playback, ExoPlayer will render the first frame that it receives (so that there's "something other than black" drawn to the surface). This frame is the key-frame before the current playback position, and may be as much as 5 seconds behind the current position. ExoPlayer then drops subsequent frames that are late until it's caught up to the current position again. For GTS tests that are counting dropped frames, this is not desirable behavior, since it will cause spurious test failures in cases where DummySurface is not supported. This change overrides the default behavior so that the player instead skips (rather than drops) frames until it's caught up to the current playback position, and only then renders the first frame. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=170175944 --- .../testutil/DebugRenderersFactory.java | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java index af7c1a3e2a..b63afd3984 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java @@ -17,6 +17,8 @@ package com.google.android.exoplayer2.testutil; import android.annotation.TargetApi; import android.content.Context; +import android.media.MediaCodec; +import android.media.MediaCrypto; import android.os.Handler; import com.google.android.exoplayer2.DefaultRenderersFactory; import com.google.android.exoplayer2.ExoPlaybackException; @@ -25,9 +27,12 @@ import com.google.android.exoplayer2.Renderer; 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.MediaCodecInfo; import com.google.android.exoplayer2.mediacodec.MediaCodecSelector; +import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; import com.google.android.exoplayer2.video.VideoRendererEventListener; +import java.nio.ByteBuffer; import java.util.ArrayList; /** @@ -66,6 +71,7 @@ public class DebugRenderersFactory extends DefaultRenderersFactory { private int queueSize; private int bufferCount; private int minimumInsertIndex; + private boolean skipToPositionBeforeRenderingFirstFrame; public DebugMediaCodecVideoRenderer(Context context, MediaCodecSelector mediaCodecSelector, long allowedJoiningTimeMs, DrmSessionManager drmSessionManager, @@ -75,10 +81,23 @@ public class DebugRenderersFactory extends DefaultRenderersFactory { eventHandler, eventListener, maxDroppedFrameCountToNotify); } + @Override + protected void configureCodec(MediaCodecInfo codecInfo, MediaCodec codec, Format format, + MediaCrypto crypto) throws DecoderQueryException { + // If the codec is being initialized whilst the renderer is started, default behavior is to + // render the first frame (i.e. the keyframe before the current position), then drop frames up + // to the current playback position. For test runs that place a maximum limit on the number of + // dropped frames allowed, this is not desired behavior. Hence we skip (rather than drop) + // frames up to the current playback position [Internal: b/66494991]. + skipToPositionBeforeRenderingFirstFrame = getState() == Renderer.STATE_STARTED; + super.configureCodec(codecInfo, codec, format, crypto); + } + @Override protected void releaseCodec() { super.releaseCodec(); clearTimestamps(); + skipToPositionBeforeRenderingFirstFrame = false; } @Override @@ -102,6 +121,34 @@ public class DebugRenderersFactory extends DefaultRenderersFactory { maybeShiftTimestampsList(); } + @Override + protected boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs, MediaCodec codec, + ByteBuffer buffer, int bufferIndex, int bufferFlags, long bufferPresentationTimeUs, + boolean shouldSkip) { + if (skipToPositionBeforeRenderingFirstFrame && bufferPresentationTimeUs < positionUs) { + // After the codec has been initialized, don't render the first frame until we've caught up + // to the playback position. Else test runs on devices that do not support dummy surface + // will drop frames between rendering the first one and catching up [Internal: b/66494991]. + shouldSkip = true; + } + return super.processOutputBuffer(positionUs, elapsedRealtimeUs, codec, buffer, bufferIndex, + bufferFlags, bufferPresentationTimeUs, shouldSkip); + } + + @Override + protected void renderOutputBuffer(MediaCodec codec, int index, long presentationTimeUs) { + skipToPositionBeforeRenderingFirstFrame = false; + super.renderOutputBuffer(codec, index, presentationTimeUs); + } + + @TargetApi(21) + @Override + protected void renderOutputBufferV21(MediaCodec codec, int index, long presentationTimeUs, + long releaseTimeNs) { + skipToPositionBeforeRenderingFirstFrame = false; + super.renderOutputBufferV21(codec, index, presentationTimeUs, releaseTimeNs); + } + @Override protected void onProcessedOutputBuffer(long presentationTimeUs) { super.onProcessedOutputBuffer(presentationTimeUs); From 52de36c5ebd52b61f01347ca258fe5336b04b815 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 27 Sep 2017 03:43:18 -0700 Subject: [PATCH 0486/2472] Add abstract, default Player event listener. This allows simplified listener implementations as most listeners will not listen to all possible notifications. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=170177821 --- .../exoplayer2/demo/PlayerActivity.java | 192 ++++++++---------- .../exoplayer2/ext/flac/FlacPlaybackTest.java | 59 +----- .../exoplayer2/ext/ima/ImaAdsLoader.java | 32 +-- .../ext/leanback/LeanbackPlayerAdapter.java | 30 +-- .../mediasession/MediaSessionConnector.java | 14 +- .../exoplayer2/ext/opus/OpusPlaybackTest.java | 59 +----- .../exoplayer2/ext/vp9/VpxPlaybackTest.java | 59 +----- .../com/google/android/exoplayer2/Player.java | 52 +++++ .../exoplayer2/ui/DebugTextViewHelper.java | 42 +--- .../exoplayer2/ui/PlaybackControlView.java | 28 +-- .../exoplayer2/ui/SimpleExoPlayerView.java | 42 +--- .../android/exoplayer2/testutil/Action.java | 60 +----- .../exoplayer2/testutil/ExoHostedTest.java | 41 +--- .../testutil/ExoPlayerTestRunner.java | 23 +-- 14 files changed, 176 insertions(+), 557 deletions(-) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index 0efb04782d..c0838390ed 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -37,11 +37,8 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.DefaultRenderersFactory; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayerFactory; -import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.Player.EventListener; import com.google.android.exoplayer2.SimpleExoPlayer; -import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; @@ -83,7 +80,7 @@ import java.util.UUID; /** * An activity that plays media using {@link SimpleExoPlayer}. */ -public class PlayerActivity extends Activity implements OnClickListener, EventListener, +public class PlayerActivity extends Activity implements OnClickListener, PlaybackControlView.VisibilityListener { public static final String DRM_SCHEME_UUID_EXTRA = "drm_scheme_uuid"; @@ -294,7 +291,7 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi drmSessionManager, extensionRendererMode); player = ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector); - player.addListener(this); + player.addListener(new PlayerEventListener()); player.addListener(eventLogger); player.addMetadataOutput(eventLogger); player.setAudioDebugListener(eventLogger); @@ -472,110 +469,6 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi } } - // Player.EventListener implementation - - @Override - public void onLoadingChanged(boolean isLoading) { - // Do nothing. - } - - @Override - public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { - if (playbackState == Player.STATE_ENDED) { - showControls(); - } - updateButtonVisibilities(); - } - - @Override - public void onRepeatModeChanged(int repeatMode) { - // Do nothing. - } - - @Override - public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { - // Do nothing. - } - - @Override - public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { - if (inErrorState) { - // This will only occur if the user has performed a seek whilst in the error state. Update the - // resume position so that if the user then retries, playback will resume from the position to - // which they seeked. - updateResumePosition(); - } - } - - @Override - public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { - // Do nothing. - } - - @Override - public void onTimelineChanged(Timeline timeline, Object manifest) { - // Do nothing. - } - - @Override - public void onPlayerError(ExoPlaybackException e) { - String errorString = null; - if (e.type == ExoPlaybackException.TYPE_RENDERER) { - Exception cause = e.getRendererException(); - if (cause instanceof DecoderInitializationException) { - // Special case for decoder initialization failures. - DecoderInitializationException decoderInitializationException = - (DecoderInitializationException) cause; - if (decoderInitializationException.decoderName == null) { - if (decoderInitializationException.getCause() instanceof DecoderQueryException) { - errorString = getString(R.string.error_querying_decoders); - } else if (decoderInitializationException.secureDecoderRequired) { - errorString = getString(R.string.error_no_secure_decoder, - decoderInitializationException.mimeType); - } else { - errorString = getString(R.string.error_no_decoder, - decoderInitializationException.mimeType); - } - } else { - errorString = getString(R.string.error_instantiating_decoder, - decoderInitializationException.decoderName); - } - } - } - if (errorString != null) { - showToast(errorString); - } - inErrorState = true; - if (isBehindLiveWindow(e)) { - clearResumePosition(); - initializePlayer(); - } else { - updateResumePosition(); - updateButtonVisibilities(); - showControls(); - } - } - - @Override - @SuppressWarnings("ReferenceEquality") - public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { - updateButtonVisibilities(); - 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; - } - } - // User controls private void updateButtonVisibilities() { @@ -645,4 +538,85 @@ public class PlayerActivity extends Activity implements OnClickListener, EventLi return false; } + private class PlayerEventListener extends Player.DefaultEventListener { + + @Override + public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + if (playbackState == Player.STATE_ENDED) { + showControls(); + } + updateButtonVisibilities(); + } + + @Override + public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { + if (inErrorState) { + // This will only occur if the user has performed a seek whilst in the error state. Update + // the resume position so that if the user then retries, playback will resume from the + // position to which they seeked. + updateResumePosition(); + } + } + + @Override + public void onPlayerError(ExoPlaybackException e) { + String errorString = null; + if (e.type == ExoPlaybackException.TYPE_RENDERER) { + Exception cause = e.getRendererException(); + if (cause instanceof DecoderInitializationException) { + // Special case for decoder initialization failures. + DecoderInitializationException decoderInitializationException = + (DecoderInitializationException) cause; + if (decoderInitializationException.decoderName == null) { + if (decoderInitializationException.getCause() instanceof DecoderQueryException) { + errorString = getString(R.string.error_querying_decoders); + } else if (decoderInitializationException.secureDecoderRequired) { + errorString = getString(R.string.error_no_secure_decoder, + decoderInitializationException.mimeType); + } else { + errorString = getString(R.string.error_no_decoder, + decoderInitializationException.mimeType); + } + } else { + errorString = getString(R.string.error_instantiating_decoder, + decoderInitializationException.decoderName); + } + } + } + if (errorString != null) { + showToast(errorString); + } + inErrorState = true; + if (isBehindLiveWindow(e)) { + clearResumePosition(); + initializePlayer(); + } else { + updateResumePosition(); + updateButtonVisibilities(); + showControls(); + } + } + + @Override + @SuppressWarnings("ReferenceEquality") + public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { + updateButtonVisibilities(); + 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/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 8e1926ab3b..65fb4c8195 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,15 +22,11 @@ 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.Player; import com.google.android.exoplayer2.Renderer; -import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; import com.google.android.exoplayer2.source.ExtractorMediaSource; -import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; -import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; /** @@ -45,20 +41,22 @@ public class FlacPlaybackTest extends InstrumentationTestCase { } private void playUri(String uri) throws ExoPlaybackException { - TestPlaybackThread thread = new TestPlaybackThread(Uri.parse(uri), + TestPlaybackRunnable testPlaybackRunnable = new TestPlaybackRunnable(Uri.parse(uri), getInstrumentation().getContext()); + Thread thread = new Thread(testPlaybackRunnable); thread.start(); try { thread.join(); } catch (InterruptedException e) { fail(); // Should never happen. } - if (thread.playbackException != null) { - throw thread.playbackException; + if (testPlaybackRunnable.playbackException != null) { + throw testPlaybackRunnable.playbackException; } } - private static class TestPlaybackThread extends Thread implements Player.EventListener { + private static class TestPlaybackRunnable extends Player.DefaultEventListener + implements Runnable { private final Context context; private final Uri uri; @@ -66,7 +64,7 @@ public class FlacPlaybackTest extends InstrumentationTestCase { private ExoPlayer player; private ExoPlaybackException playbackException; - public TestPlaybackThread(Uri uri, Context context) { + public TestPlaybackRunnable(Uri uri, Context context) { this.uri = uri; this.context = context; } @@ -89,31 +87,6 @@ public class FlacPlaybackTest extends InstrumentationTestCase { Looper.loop(); } - @Override - public void onLoadingChanged(boolean isLoading) { - // Do nothing. - } - - @Override - public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { - // Do nothing. - } - - @Override - public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { - // Do nothing. - } - - @Override - public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { - // Do nothing. - } - - @Override - public void onTimelineChanged(Timeline timeline, Object manifest) { - // Do nothing. - } - @Override public void onPlayerError(ExoPlaybackException error) { playbackException = error; @@ -123,25 +96,11 @@ public class FlacPlaybackTest extends InstrumentationTestCase { public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { if (playbackState == Player.STATE_ENDED || (playbackState == Player.STATE_IDLE && playbackException != null)) { - releasePlayerAndQuitLooper(); + player.release(); + Looper.myLooper().quit(); } } - @Override - public void onRepeatModeChanged(int repeatMode) { - // Do nothing. - } - - @Override - public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { - // Do nothing. - } - - private void releasePlayerAndQuitLooper() { - player.release(); - Looper.myLooper().quit(); - } - } } diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index 7e5912ed28..a411da0133 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -43,13 +43,10 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; -import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.ads.AdPlaybackState; import com.google.android.exoplayer2.source.ads.AdsLoader; -import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; import java.util.ArrayList; @@ -59,8 +56,8 @@ import java.util.Map; /** * Loads ads using the IMA SDK. All methods are called on the main thread. */ -public final class ImaAdsLoader implements AdsLoader, Player.EventListener, VideoAdPlayer, - ContentProgressProvider, AdErrorListener, AdsLoadedListener, AdEventListener { +public final class ImaAdsLoader extends Player.DefaultEventListener implements AdsLoader, + VideoAdPlayer, ContentProgressProvider, AdErrorListener, AdsLoadedListener, AdEventListener { static { ExoPlayerLibraryInfo.registerModule("goog.exo.ima"); @@ -511,16 +508,6 @@ public final class ImaAdsLoader implements AdsLoader, Player.EventListener, Vide updateImaStateForPlayerState(); } - @Override - public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { - // Do nothing. - } - - @Override - public void onLoadingChanged(boolean isLoading) { - // Do nothing. - } - @Override public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { if (adsManager == null) { @@ -538,16 +525,6 @@ public final class ImaAdsLoader implements AdsLoader, Player.EventListener, Vide } } - @Override - public void onRepeatModeChanged(int repeatMode) { - // Do nothing. - } - - @Override - public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { - // Do nothing. - } - @Override public void onPlayerError(ExoPlaybackException error) { if (playingAd) { @@ -584,11 +561,6 @@ public final class ImaAdsLoader implements AdsLoader, Player.EventListener, Vide } } - @Override - public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { - // Do nothing. - } - // Internal methods. private void requestAds() { diff --git a/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java b/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java index 93583f7d24..510ed9cf4f 100644 --- a/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java +++ b/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java @@ -30,13 +30,10 @@ import com.google.android.exoplayer2.ControlDispatcher; import com.google.android.exoplayer2.DefaultControlDispatcher; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; -import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player.DiscontinuityReason; import com.google.android.exoplayer2.SimpleExoPlayer; 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.ErrorMessageProvider; /** @@ -221,7 +218,7 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { } } - private final class ComponentListener implements Player.EventListener, + private final class ComponentListener extends Player.DefaultEventListener implements SimpleExoPlayer.VideoListener, SurfaceHolder.Callback { // SurfaceHolder.Callback implementation. @@ -260,11 +257,6 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { } } - @Override - public void onLoadingChanged(boolean isLoading) { - // Do nothing. - } - @Override public void onTimelineChanged(Timeline timeline, Object manifest) { Callback callback = getCallback(); @@ -273,11 +265,6 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { callback.onBufferedPositionChanged(LeanbackPlayerAdapter.this); } - @Override - public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { - // Do nothing. - } - @Override public void onPositionDiscontinuity(@DiscontinuityReason int reason) { Callback callback = getCallback(); @@ -285,21 +272,6 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { callback.onBufferedPositionChanged(LeanbackPlayerAdapter.this); } - @Override - public void onPlaybackParametersChanged(PlaybackParameters params) { - // Do nothing. - } - - @Override - public void onRepeatModeChanged(int repeatMode) { - // Do nothing. - } - - @Override - public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { - // Do nothing. - } - // SimpleExoplayerView.Callback implementation. @Override diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java index 61e3772750..565b9c0084 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java @@ -36,8 +36,6 @@ import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.util.ErrorMessageProvider; import com.google.android.exoplayer2.util.RepeatModeUtil; import java.util.Collections; @@ -624,7 +622,7 @@ public final class MediaSessionConnector { & QueueEditor.ACTIONS & action) != 0; } - private class ExoPlayerEventListener implements Player.EventListener { + private class ExoPlayerEventListener extends Player.DefaultEventListener { private int currentWindowIndex; private int currentWindowCount; @@ -649,16 +647,6 @@ public final class MediaSessionConnector { updateMediaSessionMetadata(); } - @Override - public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { - // Do nothing. - } - - @Override - public void onLoadingChanged(boolean isLoading) { - // Do nothing. - } - @Override public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { updateMediaSessionPlaybackState(); 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 8f82a9fdc0..591f43f38a 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,15 +22,11 @@ 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.Player; import com.google.android.exoplayer2.Renderer; -import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; import com.google.android.exoplayer2.source.ExtractorMediaSource; -import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; -import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; /** @@ -45,20 +41,22 @@ public class OpusPlaybackTest extends InstrumentationTestCase { } private void playUri(String uri) throws ExoPlaybackException { - TestPlaybackThread thread = new TestPlaybackThread(Uri.parse(uri), + TestPlaybackRunnable testPlaybackRunnable = new TestPlaybackRunnable(Uri.parse(uri), getInstrumentation().getContext()); + Thread thread = new Thread(testPlaybackRunnable); thread.start(); try { thread.join(); } catch (InterruptedException e) { fail(); // Should never happen. } - if (thread.playbackException != null) { - throw thread.playbackException; + if (testPlaybackRunnable.playbackException != null) { + throw testPlaybackRunnable.playbackException; } } - private static class TestPlaybackThread extends Thread implements Player.EventListener { + private static class TestPlaybackRunnable extends Player.DefaultEventListener + implements Runnable { private final Context context; private final Uri uri; @@ -66,7 +64,7 @@ public class OpusPlaybackTest extends InstrumentationTestCase { private ExoPlayer player; private ExoPlaybackException playbackException; - public TestPlaybackThread(Uri uri, Context context) { + public TestPlaybackRunnable(Uri uri, Context context) { this.uri = uri; this.context = context; } @@ -89,31 +87,6 @@ public class OpusPlaybackTest extends InstrumentationTestCase { Looper.loop(); } - @Override - public void onLoadingChanged(boolean isLoading) { - // Do nothing. - } - - @Override - public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { - // Do nothing. - } - - @Override - public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { - // Do nothing. - } - - @Override - public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { - // Do nothing. - } - - @Override - public void onTimelineChanged(Timeline timeline, Object manifest) { - // Do nothing. - } - @Override public void onPlayerError(ExoPlaybackException error) { playbackException = error; @@ -123,25 +96,11 @@ public class OpusPlaybackTest extends InstrumentationTestCase { public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { if (playbackState == Player.STATE_ENDED || (playbackState == Player.STATE_IDLE && playbackException != null)) { - releasePlayerAndQuitLooper(); + player.release(); + Looper.myLooper().quit(); } } - @Override - public void onRepeatModeChanged(int repeatMode) { - // Do nothing. - } - - @Override - public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { - // Do nothing. - } - - private void releasePlayerAndQuitLooper() { - player.release(); - Looper.myLooper().quit(); - } - } } 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 52b6670c09..c2c1867a90 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,15 +23,11 @@ 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.Player; import com.google.android.exoplayer2.Renderer; -import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; import com.google.android.exoplayer2.source.ExtractorMediaSource; -import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; -import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; /** @@ -74,20 +70,22 @@ public class VpxPlaybackTest extends InstrumentationTestCase { } private void playUri(String uri) throws ExoPlaybackException { - TestPlaybackThread thread = new TestPlaybackThread(Uri.parse(uri), + TestPlaybackRunnable testPlaybackRunnable = new TestPlaybackRunnable(Uri.parse(uri), getInstrumentation().getContext()); + Thread thread = new Thread(testPlaybackRunnable); thread.start(); try { thread.join(); } catch (InterruptedException e) { fail(); // Should never happen. } - if (thread.playbackException != null) { - throw thread.playbackException; + if (testPlaybackRunnable.playbackException != null) { + throw testPlaybackRunnable.playbackException; } } - private static class TestPlaybackThread extends Thread implements Player.EventListener { + private static class TestPlaybackRunnable extends Player.DefaultEventListener + implements Runnable { private final Context context; private final Uri uri; @@ -95,7 +93,7 @@ public class VpxPlaybackTest extends InstrumentationTestCase { private ExoPlayer player; private ExoPlaybackException playbackException; - public TestPlaybackThread(Uri uri, Context context) { + public TestPlaybackRunnable(Uri uri, Context context) { this.uri = uri; this.context = context; } @@ -121,31 +119,6 @@ public class VpxPlaybackTest extends InstrumentationTestCase { Looper.loop(); } - @Override - public void onLoadingChanged(boolean isLoading) { - // Do nothing. - } - - @Override - public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { - // Do nothing. - } - - @Override - public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { - // Do nothing. - } - - @Override - public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { - // Do nothing. - } - - @Override - public void onTimelineChanged(Timeline timeline, Object manifest) { - // Do nothing. - } - @Override public void onPlayerError(ExoPlaybackException error) { playbackException = error; @@ -155,25 +128,11 @@ public class VpxPlaybackTest extends InstrumentationTestCase { public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { if (playbackState == Player.STATE_ENDED || (playbackState == Player.STATE_IDLE && playbackException != null)) { - releasePlayerAndQuitLooper(); + player.release(); + Looper.myLooper().quit(); } } - @Override - public void onRepeatModeChanged(int repeatMode) { - // Do nothing. - } - - @Override - public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { - // Do nothing. - } - - private void releasePlayerAndQuitLooper() { - player.release(); - Looper.myLooper().quit(); - } - } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Player.java b/library/core/src/main/java/com/google/android/exoplayer2/Player.java index 795b7249c8..3dd702b85f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Player.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Player.java @@ -136,6 +136,58 @@ public interface Player { } + /** + * {@link EventListener} allowing selective overrides. All methods are implemented as no-ops. + */ + abstract class DefaultEventListener implements EventListener { + + @Override + public void onTimelineChanged(Timeline timeline, Object manifest) { + // Do nothing. + } + + @Override + public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { + // Do nothing. + } + + @Override + public void onLoadingChanged(boolean isLoading) { + // Do nothing. + } + + @Override + public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + // Do nothing. + } + + @Override + public void onRepeatModeChanged(int repeatMode) { + // Do nothing. + } + + @Override + public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { + // Do nothing. + } + + @Override + public void onPlayerError(ExoPlaybackException error) { + // Do nothing. + } + + @Override + public void onPositionDiscontinuity(int reason) { + // Do nothing. + } + + @Override + public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { + // Do nothing. + } + + } + /** * The player does not have any media to play. */ diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java index cff860b671..f443c2a06f 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java @@ -17,22 +17,17 @@ package com.google.android.exoplayer2.ui; import android.annotation.SuppressLint; import android.widget.TextView; -import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.SimpleExoPlayer; -import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.decoder.DecoderCounters; -import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import java.util.Locale; /** * A helper class for periodically updating a {@link TextView} with debug information obtained from * a {@link SimpleExoPlayer}. */ -public final class DebugTextViewHelper implements Runnable, Player.EventListener { +public final class DebugTextViewHelper extends Player.DefaultEventListener implements Runnable { private static final int REFRESH_INTERVAL_MS = 1000; @@ -78,51 +73,16 @@ public final class DebugTextViewHelper implements Runnable, Player.EventListener // Player.EventListener implementation. - @Override - public void onLoadingChanged(boolean isLoading) { - // Do nothing. - } - @Override public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { updateAndPost(); } - @Override - public void onRepeatModeChanged(int repeatMode) { - // Do nothing. - } - - @Override - public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { - // Do nothing. - } - @Override public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { updateAndPost(); } - @Override - public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { - // Do nothing. - } - - @Override - public void onTimelineChanged(Timeline timeline, Object manifest) { - // Do nothing. - } - - @Override - public void onPlayerError(ExoPlaybackException error) { - // Do nothing. - } - - @Override - public void onTracksChanged(TrackGroupArray tracks, TrackSelectionArray selections) { - // Do nothing. - } - // Runnable implementation. @Override diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java index 848aabc258..f47ac8695c 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java @@ -30,13 +30,9 @@ import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.TextView; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; -import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.RepeatModeUtil; import com.google.android.exoplayer2.util.Util; @@ -1046,8 +1042,8 @@ public class PlaybackControlView extends FrameLayout { return true; } - private final class ComponentListener implements Player.EventListener, TimeBar.OnScrubListener, - OnClickListener { + private final class ComponentListener extends Player.DefaultEventListener implements + TimeBar.OnScrubListener, OnClickListener { @Override public void onScrubStart(TimeBar timeBar, long position) { @@ -1095,11 +1091,6 @@ public class PlaybackControlView extends FrameLayout { updateProgress(); } - @Override - public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { - // Do nothing. - } - @Override public void onTimelineChanged(Timeline timeline, Object manifest) { updateNavigation(); @@ -1107,21 +1098,6 @@ public class PlaybackControlView extends FrameLayout { updateProgress(); } - @Override - public void onLoadingChanged(boolean isLoading) { - // Do nothing. - } - - @Override - public void onTracksChanged(TrackGroupArray tracks, TrackSelectionArray selections) { - // Do nothing. - } - - @Override - public void onPlayerError(ExoPlaybackException error) { - // Do nothing. - } - @Override public void onClick(View view) { if (player != null) { diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index b6f0677871..053bc26a6e 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -36,11 +36,8 @@ import android.widget.ImageView; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ControlDispatcher; import com.google.android.exoplayer2.DefaultControlDispatcher; -import com.google.android.exoplayer2.ExoPlaybackException; -import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.SimpleExoPlayer; -import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.id3.ApicFrame; import com.google.android.exoplayer2.source.TrackGroupArray; @@ -871,8 +868,8 @@ public final class SimpleExoPlayerView extends FrameLayout { aspectRatioFrame.setResizeMode(resizeMode); } - private final class ComponentListener implements TextOutput, SimpleExoPlayer.VideoListener, - Player.EventListener { + private final class ComponentListener extends Player.DefaultEventListener implements TextOutput, + SimpleExoPlayer.VideoListener { // TextOutput implementation @@ -908,46 +905,11 @@ public final class SimpleExoPlayerView extends FrameLayout { // Player.EventListener implementation - @Override - public void onLoadingChanged(boolean isLoading) { - // Do nothing. - } - @Override public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { maybeShowController(false); } - @Override - public void onRepeatModeChanged(int repeatMode) { - // Do nothing. - } - - @Override - public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { - // Do nothing. - } - - @Override - public void onPlayerError(ExoPlaybackException e) { - // Do nothing. - } - - @Override - public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { - // Do nothing. - } - - @Override - public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { - // Do nothing. - } - - @Override - public void onTimelineChanged(Timeline timeline, Object manifest) { - // Do nothing. - } - } } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java index b41d44d016..76e39525b4 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java @@ -18,17 +18,13 @@ package com.google.android.exoplayer2.testutil; import android.os.Handler; import android.util.Log; import android.view.Surface; -import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; -import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.testutil.ActionSchedule.ActionNode; import com.google.android.exoplayer2.trackselection.MappingTrackSelector; -import com.google.android.exoplayer2.trackselection.TrackSelectionArray; /** * Base class for actions to perform during playback tests. @@ -326,7 +322,7 @@ public abstract class Action { protected void doActionAndScheduleNextImpl(final SimpleExoPlayer player, final MappingTrackSelector trackSelector, final Surface surface, final Handler handler, final ActionNode nextAction) { - PlayerListener listener = new PlayerListener() { + Player.EventListener listener = new Player.DefaultEventListener() { @Override public void onTimelineChanged(Timeline timeline, Object manifest) { if (timeline.equals(expectedTimeline)) { @@ -351,7 +347,7 @@ public abstract class Action { } /** - * Waits for {@link Player.EventListener#onPositionDiscontinuity()}. + * Waits for {@link Player.EventListener#onPositionDiscontinuity(int)}. */ public static final class WaitForPositionDiscontinuity extends Action { @@ -366,7 +362,7 @@ public abstract class Action { protected void doActionAndScheduleNextImpl(final SimpleExoPlayer player, final MappingTrackSelector trackSelector, final Surface surface, final Handler handler, final ActionNode nextAction) { - player.addListener(new PlayerListener() { + player.addListener(new Player.DefaultEventListener() { @Override public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { player.removeListener(this); @@ -406,54 +402,4 @@ public abstract class Action { } - /** Listener implementation used for overriding. Does nothing. */ - private static class PlayerListener implements Player.EventListener { - - @Override - public void onTimelineChanged(Timeline timeline, Object manifest) { - - } - - @Override - public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { - - } - - @Override - public void onLoadingChanged(boolean isLoading) { - - } - - @Override - public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { - - } - - @Override - public void onRepeatModeChanged(@ExoPlayer.RepeatMode int repeatMode) { - - } - - @Override - public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { - - } - - @Override - public void onPlayerError(ExoPlaybackException error) { - - } - - @Override - public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { - - } - - @Override - public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { - - } - - } - } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java index d3186e475d..142c5060b5 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java @@ -25,23 +25,19 @@ 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.Player; 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; import com.google.android.exoplayer2.audio.AudioTrack; 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.source.MediaSource; -import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.testutil.HostActivity.HostedTest; import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.MappingTrackSelector; -import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.upstream.BandwidthMeter; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; @@ -53,7 +49,7 @@ import junit.framework.Assert; /** * A {@link HostedTest} for {@link ExoPlayer} playback tests. */ -public abstract class ExoHostedTest implements HostedTest, Player.EventListener, +public abstract class ExoHostedTest extends Player.DefaultEventListener implements HostedTest, AudioRendererEventListener, VideoRendererEventListener { static { @@ -203,16 +199,6 @@ public abstract class ExoHostedTest implements HostedTest, Player.EventListener, // Player.EventListener - @Override - public void onLoadingChanged(boolean isLoading) { - // Do nothing. - } - - @Override - public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { - // Do nothing. - } - @Override public final void onPlayerStateChanged(boolean playWhenReady, int playbackState) { Log.d(tag, "state [" + playWhenReady + ", " + playbackState + "]"); @@ -230,16 +216,6 @@ public abstract class ExoHostedTest implements HostedTest, Player.EventListener, this.playing = playing; } - @Override - public void onRepeatModeChanged(int repeatMode) { - // Do nothing. - } - - @Override - public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { - // Do nothing. - } - @Override public final void onPlayerError(ExoPlaybackException error) { playerWasPrepared = true; @@ -247,21 +223,6 @@ public abstract class ExoHostedTest implements HostedTest, Player.EventListener, onPlayerErrorInternal(error); } - @Override - public final void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { - // Do nothing. - } - - @Override - public final void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { - // Do nothing. - } - - @Override - public final void onTimelineChanged(Timeline timeline, Object manifest) { - // Do nothing. - } - // AudioRendererEventListener @Override diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java index b8c0846f8e..a87066415d 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java @@ -22,7 +22,6 @@ import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.LoadControl; -import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.RenderersFactory; @@ -49,7 +48,7 @@ import junit.framework.Assert; /** * Helper class to run an ExoPlayer test. */ -public final class ExoPlayerTestRunner implements Player.EventListener { +public final class ExoPlayerTestRunner extends Player.DefaultEventListener { /** * Builder to set-up a {@link ExoPlayerTestRunner}. Default fake implementations will be used for @@ -341,11 +340,6 @@ public final class ExoPlayerTestRunner implements Player.EventListener { this.trackGroups = trackGroups; } - @Override - public void onLoadingChanged(boolean isLoading) { - // Do nothing. - } - @Override public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { if (periodIndices.isEmpty() && playbackState == Player.STATE_READY) { @@ -358,16 +352,6 @@ public final class ExoPlayerTestRunner implements Player.EventListener { } } - @Override - public void onRepeatModeChanged(@Player.RepeatMode int repeatMode) { - // Do nothing. - } - - @Override - public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { - // Do nothing. - } - @Override public void onPlayerError(ExoPlaybackException error) { handleException(exception); @@ -379,9 +363,4 @@ public final class ExoPlayerTestRunner implements Player.EventListener { periodIndices.add(player.getCurrentPeriodIndex()); } - @Override - public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { - // Do nothing. - } - } From b14b3d43de673398bc18581aaa646e73166b73d7 Mon Sep 17 00:00:00 2001 From: zhihuichen Date: Wed, 27 Sep 2017 16:11:01 -0700 Subject: [PATCH 0487/2472] Expose OnKeyStatusChange events, this is required to learn the usabilities of the keys. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=170268043 --- .../android/exoplayer2/drm/ExoMediaDrm.java | 32 +++++++++++++ .../exoplayer2/drm/FrameworkMediaDrm.java | 48 ++++++++++++++++++- 2 files changed, 78 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java index 25b065c543..1930e11f06 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java @@ -21,7 +21,9 @@ import android.media.MediaCryptoException; import android.media.MediaDrm; import android.media.MediaDrmException; import android.media.NotProvisionedException; +import android.os.Handler; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.UUID; @@ -80,6 +82,31 @@ public interface ExoMediaDrm { byte[] data); } + /** + * @see android.media.MediaDrm.KeyStatus + */ + interface ExoKeyStatus { + int getStatusCode(); + byte[] getKeyId(); + } + + /** + * @see android.media.MediaDrm.OnKeyStatusChangeListener + */ + interface OnKeyStatusChangeListener { + /** + * Called when the keys in a session change status, such as when the license is renewed or + * expires. + * + * @param mediaDrm the {@link ExoMediaDrm} object on which the event occurred. + * @param sessionId the DRM session ID on which the event occurred. + * @param exoKeyInfo a list of {@link ExoKeyStatus} that contains key ID and status. + * @param hasNewUsableKey true if new key becomes usable. + */ + void onKeyStatusChange(ExoMediaDrm mediaDrm, byte[] sessionId, + List exoKeyInfo, boolean hasNewUsableKey); + } + /** * @see android.media.MediaDrm.KeyRequest */ @@ -101,6 +128,11 @@ public interface ExoMediaDrm { */ void setOnEventListener(OnEventListener listener); + /** + * @see MediaDrm#setOnKeyStatusChangeListener(MediaDrm.OnKeyStatusChangeListener, Handler) + */ + void setOnKeyStatusChangeListener(OnKeyStatusChangeListener listener); + /** * @see MediaDrm#openSession() */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java index 2edd6ff254..c3ab3462d9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java @@ -20,6 +20,7 @@ import android.media.DeniedByServerException; import android.media.MediaCrypto; import android.media.MediaCryptoException; import android.media.MediaDrm; +import android.media.MediaDrm.KeyStatus; import android.media.MediaDrmException; import android.media.NotProvisionedException; import android.media.UnsupportedSchemeException; @@ -27,14 +28,16 @@ import android.support.annotation.NonNull; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.UUID; /** * An {@link ExoMediaDrm} implementation that wraps the framework {@link MediaDrm}. */ -@TargetApi(18) +@TargetApi(23) public final class FrameworkMediaDrm implements ExoMediaDrm { private final UUID uuid; @@ -71,13 +74,36 @@ public final class FrameworkMediaDrm implements ExoMediaDrm listener) { mediaDrm.setOnEventListener(listener == null ? null : new MediaDrm.OnEventListener() { @Override - public void onEvent(@NonNull MediaDrm md, byte[] sessionId, int event, int extra, + public void onEvent(@NonNull MediaDrm md, @NonNull byte[] sessionId, int event, int extra, byte[] data) { listener.onEvent(FrameworkMediaDrm.this, sessionId, event, extra, data); } }); } + @Override + public void setOnKeyStatusChangeListener( + final ExoMediaDrm.OnKeyStatusChangeListener listener) { + if (Util.SDK_INT < 23) { + throw new UnsupportedOperationException(); + } + + mediaDrm.setOnKeyStatusChangeListener(listener == null ? null + : new MediaDrm.OnKeyStatusChangeListener() { + @Override + public void onKeyStatusChange(@NonNull MediaDrm md, @NonNull byte[] sessionId, + @NonNull List keyInfo, boolean hasNewUsableKey) { + List exoKeyInfo = new ArrayList<>(); + for (KeyStatus keyStatus : keyInfo) { + exoKeyInfo.add(new FrameworkKeyStatus(keyStatus)); + } + + listener.onKeyStatusChange(FrameworkMediaDrm.this, sessionId, exoKeyInfo, + hasNewUsableKey); + } + }, null); + } + @Override public byte[] openSession() throws MediaDrmException { return mediaDrm.openSession(); @@ -178,4 +204,22 @@ public final class FrameworkMediaDrm implements ExoMediaDrm Date: Thu, 28 Sep 2017 00:56:03 -0700 Subject: [PATCH 0488/2472] Add AudioSink interface and use it from audio renderers This change allows applications to provide custom AudioSinks, which could be based on android.media.AudioTrack like AudioTrackAudioSink, or could be completely custom. The refactoring is mostly mechanical and shouldn't result in any functionality changes. Some android.media.AudioTrack-specific details have to appear in the AudioSink interface so this change modifies the javadoc on the AudioTrack (now AudioSink) to note that some methods will have no effect. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=170311083 --- .../android/exoplayer2/demo/EventLogger.java | 2 +- .../android/exoplayer2/SimpleExoPlayer.java | 4 +- .../audio/AudioRendererEventListener.java | 14 +- .../android/exoplayer2/audio/AudioSink.java | 332 ++++++++++++++ .../audio/ChannelMappingAudioProcessor.java | 2 +- ...{AudioTrack.java => DefaultAudioSink.java} | 422 ++++-------------- .../audio/MediaCodecAudioRenderer.java | 89 ++-- .../audio/SimpleDecoderAudioRenderer.java | 94 ++-- .../audio/TrimmingAudioProcessor.java | 2 +- .../exoplayer2/testutil/ExoHostedTest.java | 12 +- 10 files changed, 560 insertions(+), 413 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java rename library/core/src/main/java/com/google/android/exoplayer2/audio/{AudioTrack.java => DefaultAudioSink.java} (76%) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java index 83ba61fff1..5c2b40e630 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java @@ -248,7 +248,7 @@ import java.util.Locale; } @Override - public void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) { + public void onAudioSinkUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) { printInternalError("audioTrackUnderrun [" + bufferSize + ", " + bufferSizeMs + ", " + elapsedSinceLastFeedMs + "]", null); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 1c35adb917..5a5a948d58 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -976,10 +976,10 @@ public class SimpleExoPlayer implements ExoPlayer { } @Override - public void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs, + public void onAudioSinkUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) { if (audioDebugListener != null) { - audioDebugListener.onAudioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs); + audioDebugListener.onAudioSinkUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioRendererEventListener.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioRendererEventListener.java index 5f9f599f01..7a4958a61a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioRendererEventListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioRendererEventListener.java @@ -63,15 +63,15 @@ public interface AudioRendererEventListener { void onAudioInputFormatChanged(Format format); /** - * Called when an {@link AudioTrack} underrun occurs. + * Called when an {@link AudioSink} underrun occurs. * - * @param bufferSize The size of the {@link AudioTrack}'s buffer, in bytes. - * @param bufferSizeMs The size of the {@link AudioTrack}'s buffer, in milliseconds, if it is + * @param bufferSize The size of the {@link AudioSink}'s buffer, in bytes. + * @param bufferSizeMs The size of the {@link AudioSink}'s buffer, in milliseconds, if it is * configured for PCM output. {@link C#TIME_UNSET} if it is configured for passthrough output, * as the buffered media can have a variable bitrate so the duration may be unknown. - * @param elapsedSinceLastFeedMs The time since the {@link AudioTrack} was last fed data. + * @param elapsedSinceLastFeedMs The time since the {@link AudioSink} was last fed data. */ - void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs); + void onAudioSinkUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs); /** * Called when the renderer is disabled. @@ -144,7 +144,7 @@ public interface AudioRendererEventListener { } /** - * Invokes {@link AudioRendererEventListener#onAudioTrackUnderrun(int, long, long)}. + * Invokes {@link AudioRendererEventListener#onAudioSinkUnderrun(int, long, long)}. */ public void audioTrackUnderrun(final int bufferSize, final long bufferSizeMs, final long elapsedSinceLastFeedMs) { @@ -152,7 +152,7 @@ public interface AudioRendererEventListener { handler.post(new Runnable() { @Override public void run() { - listener.onAudioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs); + listener.onAudioSinkUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs); } }); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java new file mode 100644 index 0000000000..879769b0e2 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java @@ -0,0 +1,332 @@ +/* + * 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 android.media.AudioTrack; +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.PlaybackParameters; +import java.nio.ByteBuffer; + +/** + * A sink that consumes audio data. + *

          + * Before starting playback, specify the input audio format by calling + * {@link #configure(String, int, int, int, int, int[], int, int)}. + *

          + * Call {@link #handleBuffer(ByteBuffer, long)} to write data, and {@link #handleDiscontinuity()} + * when the data being fed is discontinuous. Call {@link #play()} to start playing the written data. + *

          + * Call {@link #configure(String, int, int, int, int, int[], int, int)} whenever the input format + * changes. The sink will be reinitialized on the next call to + * {@link #handleBuffer(ByteBuffer, long)}. + *

          + * Call {@link #reset()} to prepare the sink to receive audio data from a new playback position. + *

          + * Call {@link #playToEndOfStream()} repeatedly to play out all data when no more input buffers will + * be provided via {@link #handleBuffer(ByteBuffer, long)} until the next {@link #reset()}. Call + * {@link #release()} when the instance is no longer required. + *

          + * The implementation may be backed by a platform {@link AudioTrack}. In this case, + * {@link #setAudioSessionId(int)}, {@link #setAudioAttributes(AudioAttributes)}, + * {@link #enableTunnelingV21(int)} and/or {@link #disableTunneling()} may be called before writing + * data to the sink. These methods may also be called after writing data to the sink, in which case + * it will be reinitialized as required. For implementations that are not based on platform + * {@link AudioTrack}s, calling methods relating to audio sessions, audio attributes, and tunneling + * may have no effect. + */ +public interface AudioSink { + + /** + * Listener for audio sink events. + */ + interface Listener { + + /** + * Called if the audio sink has started rendering audio to a new platform audio session. + * + * @param audioSessionId The newly generated audio session's identifier. + */ + void onAudioSessionId(int audioSessionId); + + /** + * Called when the audio sink handles a buffer whose timestamp is discontinuous with the last + * buffer handled since it was reset. + */ + void onPositionDiscontinuity(); + + /** + * Called when the audio sink runs out of data. + *

          + * An audio sink implementation may never call this method (for example, if audio data is + * consumed in batches rather than based on the sink's own clock). + * + * @param bufferSize The size of the sink's buffer, in bytes. + * @param bufferSizeMs The size of the sink's buffer, in milliseconds, if it is configured for + * PCM output. {@link C#TIME_UNSET} if it is configured for passthrough output, as the + * buffered media can have a variable bitrate so the duration may be unknown. + * @param elapsedSinceLastFeedMs The time since the sink was last fed data, in milliseconds. + */ + void onUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs); + + } + + /** + * Thrown when a failure occurs configuring the sink. + */ + final class ConfigurationException extends Exception { + + /** + * Creates a new configuration exception with the specified {@code cause} and no message. + */ + public ConfigurationException(Throwable cause) { + super(cause); + } + + /** + * Creates a new configuration exception with the specified {@code message} and no cause. + */ + public ConfigurationException(String message) { + super(message); + } + + } + + /** + * Thrown when a failure occurs initializing the sink. + */ + final class InitializationException extends Exception { + + /** + * The underlying {@link AudioTrack}'s state, if applicable. + */ + public final int audioTrackState; + + /** + * @param audioTrackState The underlying {@link AudioTrack}'s state, if applicable. + * @param sampleRate The requested sample rate in Hz. + * @param channelConfig The requested channel configuration. + * @param bufferSize The requested buffer size in bytes. + */ + public InitializationException(int audioTrackState, int sampleRate, int channelConfig, + int bufferSize) { + super("AudioTrack init failed: " + audioTrackState + ", Config(" + sampleRate + ", " + + channelConfig + ", " + bufferSize + ")"); + this.audioTrackState = audioTrackState; + } + + } + + /** + * Thrown when a failure occurs writing to the sink. + */ + final class WriteException extends Exception { + + /** + * The error value returned from the sink implementation. If the sink writes to a platform + * {@link AudioTrack}, this will be the error value returned from + * {@link AudioTrack#write(byte[], int, int)} or {@link AudioTrack#write(ByteBuffer, int, int)}. + * Otherwise, the meaning of the error code depends on the sink implementation. + */ + public final int errorCode; + + /** + * @param errorCode The error value returned from the sink implementation. + */ + public WriteException(int errorCode) { + super("AudioTrack write failed: " + errorCode); + this.errorCode = errorCode; + } + + } + + /** + * Returned by {@link #getCurrentPositionUs(boolean)} when the position is not set. + */ + long CURRENT_POSITION_NOT_SET = Long.MIN_VALUE; + + /** + * Sets the listener for sink events, which should be the audio renderer. + * + * @param listener The listener for sink events, which should be the audio renderer. + */ + void setListener(Listener listener); + + /** + * Returns whether it's possible to play audio in the specified format using encoded audio + * passthrough. + * + * @param mimeType The format mime type. + * @return Whether it's possible to play audio in the format using encoded audio passthrough. + */ + boolean isPassthroughSupported(String mimeType); + + /** + * Returns the playback position in the stream starting at zero, in microseconds, or + * {@link #CURRENT_POSITION_NOT_SET} if it is not yet available. + * + * @param sourceEnded Specify {@code true} if no more input buffers will be provided. + * @return The playback position relative to the start of playback, in microseconds. + */ + long getCurrentPositionUs(boolean sourceEnded); + + /** + * Configures (or reconfigures) the sink. + * + * @param mimeType The MIME type of audio data provided in the input buffers. + * @param channelCount The number of channels. + * @param sampleRate The sample rate in Hz. + * @param pcmEncoding For PCM formats, the encoding used. One of {@link C#ENCODING_PCM_16BIT}, + * {@link C#ENCODING_PCM_16BIT}, {@link C#ENCODING_PCM_24BIT} and + * {@link C#ENCODING_PCM_32BIT}. + * @param specifiedBufferSize A specific size for the playback buffer in bytes, or 0 to infer a + * suitable buffer size. + * @param outputChannels A mapping from input to output channels that is applied to this sink's + * input as a preprocessing step, if handling PCM input. Specify {@code null} to leave the + * input unchanged. Otherwise, the element at index {@code i} specifies index of the input + * channel to map to output channel {@code i} when preprocessing input buffers. After the + * map is applied the audio data will have {@code outputChannels.length} channels. + * @param trimStartSamples The number of audio samples to trim from the start of data written to + * the sink after this call. + * @param trimEndSamples The number of audio samples to trim from data written to the sink + * immediately preceding the next call to {@link #reset()} or + * {@link #configure(String, int, int, int, int, int[], int, int)}. + * @throws ConfigurationException If an error occurs configuring the sink. + */ + void configure(String mimeType, int channelCount, int sampleRate, @C.PcmEncoding int pcmEncoding, + int specifiedBufferSize, @Nullable int[] outputChannels, int trimStartSamples, + int trimEndSamples) throws ConfigurationException; + + /** + * Starts or resumes consuming audio if initialized. + */ + void play(); + + /** + * Signals to the sink that the next buffer is discontinuous with the previous buffer. + */ + void handleDiscontinuity(); + + /** + * Attempts to process data from a {@link ByteBuffer}, starting from its current position and + * ending at its limit (exclusive). The position of the {@link ByteBuffer} is advanced by the + * number of bytes that were handled. {@link Listener#onPositionDiscontinuity()} will be called if + * {@code presentationTimeUs} is discontinuous with the last buffer handled since the last reset. + *

          + * Returns whether the data was handled in full. If the data was not handled in full then the same + * {@link ByteBuffer} must be provided to subsequent calls until it has been fully consumed, + * except in the case of an intervening call to {@link #reset()} (or to + * {@link #configure(String, int, int, int, int, int[], int, int)} that causes the sink to be + * reset). + * + * @param buffer The buffer containing audio data. + * @param presentationTimeUs The presentation timestamp of the buffer in microseconds. + * @return Whether the buffer was handled fully. + * @throws InitializationException If an error occurs initializing the sink. + * @throws WriteException If an error occurs writing the audio data. + */ + boolean handleBuffer(ByteBuffer buffer, long presentationTimeUs) + throws InitializationException, WriteException; + + /** + * Processes any remaining data. {@link #isEnded()} will return {@code true} when no data remains. + * + * @throws WriteException If an error occurs draining data to the sink. + */ + void playToEndOfStream() throws WriteException; + + /** + * Returns whether {@link #playToEndOfStream} has been called and all buffers have been processed. + */ + boolean isEnded(); + + /** + * Returns whether the sink has data pending that has not been consumed yet. + */ + boolean hasPendingData(); + + /** + * Attempts to set the playback parameters and returns the active playback parameters, which may + * differ from those passed in. + * + * @param playbackParameters The new playback parameters to attempt to set. + * @return The active playback parameters. + */ + PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters); + + /** + * Gets the active {@link PlaybackParameters}. + */ + PlaybackParameters getPlaybackParameters(); + + /** + * Sets attributes for audio playback. If the attributes have changed and if the sink is not + * configured for use with tunneling, then it is reset and the audio session id is cleared. + *

          + * If the sink is configured for use with tunneling then the audio attributes are ignored. The + * sink is not reset and the audio session id is not cleared. The passed attributes will be used + * if the sink is later re-configured into non-tunneled mode. + * + * @param audioAttributes The attributes for audio playback. + */ + void setAudioAttributes(AudioAttributes audioAttributes); + + /** + * Sets the audio session id. + */ + void setAudioSessionId(int audioSessionId); + + /** + * Enables tunneling, if possible. The sink is reset if tunneling was previously disabled or if + * the audio session id has changed. Enabling tunneling is only possible if the sink is based on a + * platform {@link AudioTrack}, and requires platform API version 21 onwards. + * + * @param tunnelingAudioSessionId The audio session id to use. + * @throws IllegalStateException Thrown if enabling tunneling on platform API version < 21. + */ + void enableTunnelingV21(int tunnelingAudioSessionId); + + /** + * Disables tunneling. If tunneling was previously enabled then the sink is reset and any audio + * session id is cleared. + */ + void disableTunneling(); + + /** + * Sets the playback volume. + * + * @param volume A volume in the range [0.0, 1.0]. + */ + void setVolume(float volume); + + /** + * Pauses playback. + */ + void pause(); + + /** + * Resets the sink, after which it is ready to receive buffers from a new playback position. + *

          + * The audio session may remain active until {@link #release()} is called. + */ + void reset(); + + /** + * Releases any resources associated with this instance. + */ + void release(); + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java index ef85985f1f..03bbd5817b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java @@ -52,7 +52,7 @@ import java.util.Arrays; * Resets the channel mapping. After calling this method, call {@link #configure(int, int, int)} * to start using the new channel map. * - * @see AudioTrack#configure(String, int, int, int, int, int[], int, int) + * @see AudioSink#configure(String, int, int, int, int, int[], int, int) */ public void setChannelMap(int[] outputChannels) { pendingOutputChannels = outputChannels; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java similarity index 76% rename from library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java rename to library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java index 25813aefc0..34ea173deb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrack.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java @@ -20,6 +20,7 @@ import android.annotation.TargetApi; import android.media.AudioFormat; import android.media.AudioManager; import android.media.AudioTimestamp; +import android.media.AudioTrack; import android.os.ConditionVariable; import android.os.SystemClock; import android.support.annotation.IntDef; @@ -39,131 +40,19 @@ import java.util.ArrayList; import java.util.LinkedList; /** - * Plays audio data. The implementation delegates to an {@link android.media.AudioTrack} and handles - * playback position smoothing, non-blocking writes and reconfiguration. + * Plays audio data. The implementation delegates to an {@link AudioTrack} and handles playback + * position smoothing, non-blocking writes and reconfiguration. *

          - * Before starting playback, specify the input format by calling - * {@link #configure(String, int, int, int, int, int[], int, int)}. Optionally call - * {@link #setAudioSessionId(int)}, {@link #setAudioAttributes(AudioAttributes)}, - * {@link #enableTunnelingV21(int)} and {@link #disableTunneling()} to configure audio playback. - * These methods may be called after writing data to the track, in which case it will be - * reinitialized as required. - *

          - * Call {@link #handleBuffer(ByteBuffer, long)} to write data, and {@link #handleDiscontinuity()} - * when the data being fed is discontinuous. Call {@link #play()} to start playing the written data. - *

          - * Call {@link #configure(String, int, int, int, int, int[], int, int)} whenever the input format - * changes. The track will be reinitialized on the next call to - * {@link #handleBuffer(ByteBuffer, long)}. - *

          - * Calling {@link #reset()} releases the underlying {@link android.media.AudioTrack} (and so does - * calling {@link #configure(String, int, int, int, int, int[], int, int)} unless the format is - * unchanged). It is safe to call {@link #handleBuffer(ByteBuffer, long)} after {@link #reset()} - * without calling {@link #configure(String, int, int, int, int, int[], int, int)}. - *

          - * Call {@link #playToEndOfStream()} repeatedly to play out all data when no more input buffers will - * be provided via {@link #handleBuffer(ByteBuffer, long)} until the next {@link #reset}. Call - * {@link #release()} when the instance is no longer required. + * If tunneling mode is enabled, care must be taken that audio processors do not output buffers with + * a different duration than their input, and buffer processors must produce output corresponding to + * their last input immediately after that input is queued. This means that, for example, speed + * adjustment is not possible while using tunneling. */ -public final class AudioTrack { +public final class DefaultAudioSink implements AudioSink { /** - * Listener for audio track events. - */ - public interface Listener { - - /** - * Called when the audio track has been initialized with a newly generated audio session id. - * - * @param audioSessionId The newly generated audio session id. - */ - void onAudioSessionId(int audioSessionId); - - /** - * Called when the audio track handles a buffer whose timestamp is discontinuous with the last - * buffer handled since it was reset. - */ - void onPositionDiscontinuity(); - - /** - * Called when the audio track underruns. - * - * @param bufferSize The size of the track's buffer, in bytes. - * @param bufferSizeMs The size of the track's buffer, in milliseconds, if it is configured for - * PCM output. {@link C#TIME_UNSET} if it is configured for passthrough output, as the - * buffered media can have a variable bitrate so the duration may be unknown. - * @param elapsedSinceLastFeedMs The time since the track was last fed data, in milliseconds. - */ - void onUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs); - - } - - /** - * Thrown when a failure occurs configuring the track. - */ - public static final class ConfigurationException extends Exception { - - public ConfigurationException(Throwable cause) { - super(cause); - } - - public ConfigurationException(String message) { - super(message); - } - - } - - /** - * Thrown when a failure occurs initializing an {@link android.media.AudioTrack}. - */ - public static final class InitializationException extends Exception { - - /** - * The state as reported by {@link android.media.AudioTrack#getState()}. - */ - public final int audioTrackState; - - /** - * @param audioTrackState The state as reported by {@link android.media.AudioTrack#getState()}. - * @param sampleRate The requested sample rate in Hz. - * @param channelConfig The requested channel configuration. - * @param bufferSize The requested buffer size in bytes. - */ - public InitializationException(int audioTrackState, int sampleRate, int channelConfig, - int bufferSize) { - super("AudioTrack init failed: " + audioTrackState + ", Config(" + sampleRate + ", " - + channelConfig + ", " + bufferSize + ")"); - this.audioTrackState = audioTrackState; - } - - } - - /** - * Thrown when a failure occurs writing to an {@link android.media.AudioTrack}. - */ - public static final class WriteException extends Exception { - - /** - * The error value returned from {@link android.media.AudioTrack#write(byte[], int, int)} or - * {@link android.media.AudioTrack#write(ByteBuffer, int, int)}. - */ - public final int errorCode; - - /** - * @param errorCode The error value returned from - * {@link android.media.AudioTrack#write(byte[], int, int)} or - * {@link android.media.AudioTrack#write(ByteBuffer, int, int)}. - */ - public WriteException(int errorCode) { - super("AudioTrack write failed: " + errorCode); - this.errorCode = errorCode; - } - - } - - /** - * Thrown when {@link android.media.AudioTrack#getTimestamp} returns a spurious timestamp, if - * {@code AudioTrack#failOnSpuriousAudioTimestamp} is set. + * Thrown when {@link AudioTrack#getTimestamp} returns a spurious timestamp, if + * {@link #failOnSpuriousAudioTimestamp} is set. */ public static final class InvalidAudioTrackTimestampException extends RuntimeException { @@ -177,61 +66,56 @@ public final class AudioTrack { } /** - * Returned by {@link #getCurrentPositionUs(boolean)} when the position is not set. - */ - public static final long CURRENT_POSITION_NOT_SET = Long.MIN_VALUE; - - /** - * A minimum length for the {@link android.media.AudioTrack} buffer, in microseconds. + * A minimum length for the {@link AudioTrack} buffer, in microseconds. */ private static final long MIN_BUFFER_DURATION_US = 250000; /** - * A maximum length for the {@link android.media.AudioTrack} buffer, in microseconds. + * A maximum length for the {@link AudioTrack} buffer, in microseconds. */ private static final long MAX_BUFFER_DURATION_US = 750000; /** - * The length for passthrough {@link android.media.AudioTrack} buffers, in microseconds. + * The length for passthrough {@link AudioTrack} buffers, in microseconds. */ private static final long PASSTHROUGH_BUFFER_DURATION_US = 250000; /** * A multiplication factor to apply to the minimum buffer size requested by the underlying - * {@link android.media.AudioTrack}. + * {@link AudioTrack}. */ private static final int BUFFER_MULTIPLICATION_FACTOR = 4; /** - * @see android.media.AudioTrack#PLAYSTATE_STOPPED + * @see AudioTrack#PLAYSTATE_STOPPED */ - private static final int PLAYSTATE_STOPPED = android.media.AudioTrack.PLAYSTATE_STOPPED; + private static final int PLAYSTATE_STOPPED = AudioTrack.PLAYSTATE_STOPPED; /** - * @see android.media.AudioTrack#PLAYSTATE_PAUSED + * @see AudioTrack#PLAYSTATE_PAUSED */ - private static final int PLAYSTATE_PAUSED = android.media.AudioTrack.PLAYSTATE_PAUSED; + private static final int PLAYSTATE_PAUSED = AudioTrack.PLAYSTATE_PAUSED; /** - * @see android.media.AudioTrack#PLAYSTATE_PLAYING + * @see AudioTrack#PLAYSTATE_PLAYING */ - private static final int PLAYSTATE_PLAYING = android.media.AudioTrack.PLAYSTATE_PLAYING; + private static final int PLAYSTATE_PLAYING = AudioTrack.PLAYSTATE_PLAYING; /** - * @see android.media.AudioTrack#ERROR_BAD_VALUE + * @see AudioTrack#ERROR_BAD_VALUE */ - private static final int ERROR_BAD_VALUE = android.media.AudioTrack.ERROR_BAD_VALUE; + private static final int ERROR_BAD_VALUE = AudioTrack.ERROR_BAD_VALUE; /** - * @see android.media.AudioTrack#MODE_STATIC + * @see AudioTrack#MODE_STATIC */ - private static final int MODE_STATIC = android.media.AudioTrack.MODE_STATIC; + private static final int MODE_STATIC = AudioTrack.MODE_STATIC; /** - * @see android.media.AudioTrack#MODE_STREAM + * @see AudioTrack#MODE_STREAM */ - private static final int MODE_STREAM = android.media.AudioTrack.MODE_STREAM; + private static final int MODE_STREAM = AudioTrack.MODE_STREAM; /** - * @see android.media.AudioTrack#STATE_INITIALIZED + * @see AudioTrack#STATE_INITIALIZED */ - private static final int STATE_INITIALIZED = android.media.AudioTrack.STATE_INITIALIZED; + private static final int STATE_INITIALIZED = AudioTrack.STATE_INITIALIZED; /** - * @see android.media.AudioTrack#WRITE_NON_BLOCKING + * @see AudioTrack#WRITE_NON_BLOCKING */ @SuppressLint("InlinedApi") - private static final int WRITE_NON_BLOCKING = android.media.AudioTrack.WRITE_NON_BLOCKING; + private static final int WRITE_NON_BLOCKING = AudioTrack.WRITE_NON_BLOCKING; private static final String TAG = "AudioTrack"; @@ -282,7 +166,7 @@ public final class AudioTrack { /** * Whether to throw an {@link InvalidAudioTrackTimestampException} when a spurious timestamp is - * reported from {@link android.media.AudioTrack#getTimestamp}. + * reported from {@link AudioTrack#getTimestamp}. *

          * The flag must be set before creating a player. Should be set to {@code true} for testing and * debugging purposes only. @@ -294,18 +178,17 @@ public final class AudioTrack { private final TrimmingAudioProcessor trimmingAudioProcessor; 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; + @Nullable private Listener listener; /** * Used to keep the audio session active on pre-V21 builds (see {@link #initialize()}). */ - private android.media.AudioTrack keepSessionIdAudioTrack; - - private android.media.AudioTrack audioTrack; + private AudioTrack keepSessionIdAudioTrack; + private AudioTrack audioTrack; private int sampleRate; private int channelConfig; private @C.Encoding int encoding; @@ -364,17 +247,15 @@ public final class AudioTrack { * default capabilities (no encoded audio passthrough support) should be assumed. * @param audioProcessors An array of {@link AudioProcessor}s that will process PCM audio before * output. May be empty. - * @param listener Listener for audio track events. */ - public AudioTrack(@Nullable AudioCapabilities audioCapabilities, AudioProcessor[] audioProcessors, - Listener listener) { + public DefaultAudioSink(@Nullable AudioCapabilities audioCapabilities, + AudioProcessor[] audioProcessors) { this.audioCapabilities = audioCapabilities; - this.listener = listener; releasingConditionVariable = new ConditionVariable(true); if (Util.SDK_INT >= 18) { try { getLatencyMethod = - android.media.AudioTrack.class.getMethod("getLatency", (Class[]) null); + AudioTrack.class.getMethod("getLatency", (Class[]) null); } catch (NoSuchMethodException e) { // There's no guarantee this method exists. Do nothing. } @@ -405,29 +286,21 @@ public final class AudioTrack { playbackParametersCheckpoints = new LinkedList<>(); } - /** - * Returns whether it's possible to play audio in the specified format using encoded passthrough. - * - * @param mimeType The format mime type. - * @return Whether it's possible to play audio in the format using encoded passthrough. - */ + @Override + public void setListener(Listener listener) { + this.listener = listener; + } + + @Override public boolean isPassthroughSupported(String mimeType) { return audioCapabilities != null && audioCapabilities.supportsEncoding(getEncodingForMimeType(mimeType)); } - /** - * Returns the playback position in the stream starting at zero, in microseconds, or - * {@link #CURRENT_POSITION_NOT_SET} if it is not yet available. - * - *

          If the device supports it, the method uses the playback timestamp from - * {@link android.media.AudioTrack#getTimestamp}. Otherwise, it derives a smoothed position by - * sampling the {@link android.media.AudioTrack}'s frame position. - * - * @param sourceEnded Specify {@code true} if no more input buffers will be provided. - * @return The playback position relative to the start of playback, in microseconds. - */ + @Override public long getCurrentPositionUs(boolean sourceEnded) { + // If the device supports it, use the playback timestamp from AudioTrack.getTimestamp. + // Otherwise, derive a smoothed position by sampling the track's frame position. if (!hasCurrentPositionUs()) { return CURRENT_POSITION_NOT_SET; } @@ -462,29 +335,7 @@ public final class AudioTrack { return startMediaTimeUs + applySpeedup(positionUs); } - /** - * Configures (or reconfigures) the audio track. - * - * @param mimeType The mime type. - * @param channelCount The number of channels. - * @param sampleRate The sample rate in Hz. - * @param pcmEncoding For PCM formats, the encoding used. One of {@link C#ENCODING_PCM_16BIT}, - * {@link C#ENCODING_PCM_16BIT}, {@link C#ENCODING_PCM_24BIT} and - * {@link C#ENCODING_PCM_32BIT}. - * @param specifiedBufferSize A specific size for the playback buffer in bytes, or 0 to infer a - * suitable buffer size automatically. - * @param outputChannels A mapping from input to output channels that is applied to this track's - * input as a preprocessing step, if handling PCM input. Specify {@code null} to leave the - * input unchanged. Otherwise, the element at index {@code i} specifies index of the input - * channel to map to output channel {@code i} when preprocessing input buffers. After the - * map is applied the audio data will have {@code outputChannels.length} channels. - * @param trimStartSamples The number of audio samples to trim from the start of data written to - * the track after this call. - * @param trimEndSamples The number of audio samples to trim from data written to the track - * immediately preceding the next call to {@link #reset()} or - * {@link #configure(String, int, int, int, int, int[], int, int)}. - * @throws ConfigurationException If an error occurs configuring the track. - */ + @Override public void configure(String mimeType, int channelCount, int sampleRate, @C.PcmEncoding int pcmEncoding, int specifiedBufferSize, @Nullable int[] outputChannels, int trimStartSamples, int trimEndSamples) throws ConfigurationException { @@ -590,8 +441,7 @@ public final class AudioTrack { bufferSize = (int) (PASSTHROUGH_BUFFER_DURATION_US * 192 * 1024 / C.MICROS_PER_SECOND); } } else { - int minBufferSize = - android.media.AudioTrack.getMinBufferSize(sampleRate, channelConfig, outputEncoding); + int minBufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, outputEncoding); Assertions.checkState(minBufferSize != ERROR_BAD_VALUE); int multipliedBufferSize = minBufferSize * BUFFER_MULTIPLICATION_FACTOR; int minAppBufferSize = (int) durationUsToFrames(MIN_BUFFER_DURATION_US) * outputPcmFrameSize; @@ -651,7 +501,9 @@ public final class AudioTrack { } if (this.audioSessionId != audioSessionId) { this.audioSessionId = audioSessionId; - listener.onAudioSessionId(audioSessionId); + if (listener != null) { + listener.onAudioSessionId(audioSessionId); + } } audioTrackUtil.reconfigure(audioTrack, needsPassthroughWorkarounds()); @@ -659,9 +511,7 @@ public final class AudioTrack { hasData = false; } - /** - * Starts or resumes playing audio if the audio track has been initialized. - */ + @Override public void play() { playing = true; if (isInitialized()) { @@ -670,9 +520,7 @@ public final class AudioTrack { } } - /** - * Signals to the audio track that the next buffer is discontinuous with the previous buffer. - */ + @Override public void handleDiscontinuity() { // Force resynchronization after a skipped buffer. if (startMediaTimeState == START_IN_SYNC) { @@ -680,24 +528,7 @@ public final class AudioTrack { } } - /** - * Attempts to process data from a {@link ByteBuffer}, starting from its current position and - * ending at its limit (exclusive). The position of the {@link ByteBuffer} is advanced by the - * number of bytes that were handled. {@link Listener#onPositionDiscontinuity()} will be called if - * {@code presentationTimeUs} is discontinuous with the last buffer handled since the last reset. - *

          - * Returns whether the data was handled in full. If the data was not handled in full then the same - * {@link ByteBuffer} must be provided to subsequent calls until it has been fully consumed, - * except in the case of an interleaving call to {@link #reset()} (or an interleaving call to - * {@link #configure(String, int, int, int, int, int[], int, int)} that caused the track to be - * reset). - * - * @param buffer The buffer containing audio data. - * @param presentationTimeUs The presentation timestamp of the buffer in microseconds. - * @return Whether the buffer was handled fully. - * @throws InitializationException If an error occurs initializing the track. - * @throws WriteException If an error occurs writing the audio data. - */ + @Override @SuppressWarnings("ReferenceEquality") public boolean handleBuffer(ByteBuffer buffer, long presentationTimeUs) throws InitializationException, WriteException { @@ -729,7 +560,7 @@ public final class AudioTrack { boolean hadData = hasData; hasData = hasPendingData(); - if (hadData && !hasData && audioTrack.getPlayState() != PLAYSTATE_STOPPED) { + if (hadData && !hasData && audioTrack.getPlayState() != PLAYSTATE_STOPPED && listener != null) { long elapsedSinceLastFeedMs = SystemClock.elapsedRealtime() - lastFeedElapsedRealtimeMs; listener.onUnderrun(bufferSize, C.usToMs(bufferSizeUs), elapsedSinceLastFeedMs); } @@ -779,7 +610,9 @@ public final class AudioTrack { // number of bytes submitted. startMediaTimeUs += (presentationTimeUs - expectedPresentationTimeUs); startMediaTimeState = START_IN_SYNC; - listener.onPositionDiscontinuity(); + if (listener != null) { + listener.onPositionDiscontinuity(); + } } } @@ -899,11 +732,7 @@ public final class AudioTrack { return false; } - /** - * Plays out remaining audio. {@link #isEnded()} will return {@code true} when playback has ended. - * - * @throws WriteException If an error occurs draining data to the track. - */ + @Override public void playToEndOfStream() throws WriteException { if (handledEndOfStream || !isInitialized()) { return; @@ -947,30 +776,19 @@ public final class AudioTrack { return true; } - /** - * Returns whether all buffers passed to {@link #handleBuffer(ByteBuffer, long)} have been - * completely processed and played. - */ + @Override public boolean isEnded() { return !isInitialized() || (handledEndOfStream && !hasPendingData()); } - /** - * Returns whether the audio track has more data pending that will be played back. - */ + @Override public boolean hasPendingData() { return isInitialized() && (getWrittenFrames() > audioTrackUtil.getPlaybackHeadPosition() || overrideHasPendingData()); } - /** - * Attempts to set the playback parameters and returns the active playback parameters, which may - * differ from those passed in. - * - * @param playbackParameters The new playback parameters to attempt to set. - * @return The active playback parameters. - */ + @Override public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) { if (passthrough) { // The playback parameters are always the default in passthrough mode. @@ -997,24 +815,12 @@ public final class AudioTrack { return this.playbackParameters; } - /** - * Gets the {@link PlaybackParameters}. - */ + @Override public PlaybackParameters getPlaybackParameters() { return playbackParameters; } - /** - * Sets the attributes for audio playback. If the attributes have changed and if the audio track - * is not configured for use with tunneling, then the audio track is reset and the audio session - * id is cleared. - *

          - * If the audio track is configured for use with tunneling then the audio attributes are ignored. - * The audio track is not reset and the audio session id is not cleared. The passed attributes - * will be used if the audio track is later re-configured into non-tunneled mode. - * - * @param audioAttributes The attributes for audio playback. - */ + @Override public void setAudioAttributes(AudioAttributes audioAttributes) { if (this.audioAttributes.equals(audioAttributes)) { return; @@ -1028,9 +834,7 @@ public final class AudioTrack { audioSessionId = C.AUDIO_SESSION_ID_UNSET; } - /** - * Sets the audio session id. The audio track is reset if the audio session id has changed. - */ + @Override public void setAudioSessionId(int audioSessionId) { if (this.audioSessionId != audioSessionId) { this.audioSessionId = audioSessionId; @@ -1038,18 +842,7 @@ public final class AudioTrack { } } - /** - * Enables tunneling. The audio track is reset if tunneling was previously disabled or if the - * audio session id has changed. Enabling tunneling requires platform API version 21 onwards. - *

          - * If this instance has {@link AudioProcessor}s and tunneling is enabled, care must be taken that - * audio processors do not output buffers with a different duration than their input, and buffer - * processors must produce output corresponding to their last input immediately after that input - * is queued. - * - * @param tunnelingAudioSessionId The audio session id to use. - * @throws IllegalStateException Thrown if enabling tunneling on platform API version < 21. - */ + @Override public void enableTunnelingV21(int tunnelingAudioSessionId) { Assertions.checkState(Util.SDK_INT >= 21); if (!tunneling || audioSessionId != tunnelingAudioSessionId) { @@ -1059,10 +852,7 @@ public final class AudioTrack { } } - /** - * Disables tunneling. If tunneling was previously enabled then the audio track is reset and the - * audio session id is cleared. - */ + @Override public void disableTunneling() { if (tunneling) { tunneling = false; @@ -1071,11 +861,7 @@ public final class AudioTrack { } } - /** - * Sets the playback volume. - * - * @param volume A volume in the range [0.0, 1.0]. - */ + @Override public void setVolume(float volume) { if (this.volume != volume) { this.volume = volume; @@ -1093,9 +879,7 @@ public final class AudioTrack { } } - /** - * Pauses playback. - */ + @Override public void pause() { playing = false; if (isInitialized()) { @@ -1104,13 +888,7 @@ public final class AudioTrack { } } - /** - * Releases the underlying audio track asynchronously. - *

          - * Calling {@link #handleBuffer(ByteBuffer, long)} will block until the audio track has been - * released, so it is safe to use the audio track immediately after a reset. The audio session may - * remain active until {@link #release()} is called. - */ + @Override public void reset() { if (isInitialized()) { submittedPcmBytes = 0; @@ -1146,7 +924,7 @@ public final class AudioTrack { audioTrack.pause(); } // AudioTrack.release can take some time, so we call it on a background thread. - final android.media.AudioTrack toRelease = audioTrack; + final AudioTrack toRelease = audioTrack; audioTrack = null; audioTrackUtil.reconfigure(null, false); releasingConditionVariable.close(); @@ -1164,9 +942,7 @@ public final class AudioTrack { } } - /** - * Releases all resources associated with this instance. - */ + @Override public void release() { reset(); releaseKeepSessionIdAudioTrack(); @@ -1186,7 +962,7 @@ public final class AudioTrack { } // AudioTrack.release can take some time, so we call it on a background thread. - final android.media.AudioTrack toRelease = keepSessionIdAudioTrack; + final AudioTrack toRelease = keepSessionIdAudioTrack; keepSessionIdAudioTrack = null; new Thread() { @Override @@ -1367,19 +1143,19 @@ public final class AudioTrack { && audioTrack.getPlaybackHeadPosition() == 0; } - private android.media.AudioTrack initializeAudioTrack() throws InitializationException { - android.media.AudioTrack audioTrack; + private AudioTrack initializeAudioTrack() throws InitializationException { + AudioTrack audioTrack; if (Util.SDK_INT >= 21) { audioTrack = createAudioTrackV21(); } else { int streamType = Util.getStreamTypeForAudioUsage(audioAttributes.usage); if (audioSessionId == C.AUDIO_SESSION_ID_UNSET) { - audioTrack = new android.media.AudioTrack(streamType, sampleRate, channelConfig, - outputEncoding, bufferSize, MODE_STREAM); + audioTrack = new AudioTrack(streamType, sampleRate, channelConfig, outputEncoding, + bufferSize, MODE_STREAM); } else { // Re-attach to the same audio session. - audioTrack = new android.media.AudioTrack(streamType, sampleRate, channelConfig, - outputEncoding, bufferSize, MODE_STREAM, audioSessionId); + audioTrack = new AudioTrack(streamType, sampleRate, channelConfig, outputEncoding, + bufferSize, MODE_STREAM, audioSessionId); } } @@ -1397,7 +1173,7 @@ public final class AudioTrack { } @TargetApi(21) - private android.media.AudioTrack createAudioTrackV21() { + private AudioTrack createAudioTrackV21() { android.media.AudioAttributes attributes; if (tunneling) { attributes = new android.media.AudioAttributes.Builder() @@ -1415,17 +1191,16 @@ public final class AudioTrack { .build(); int audioSessionId = this.audioSessionId != C.AUDIO_SESSION_ID_UNSET ? this.audioSessionId : AudioManager.AUDIO_SESSION_ID_GENERATE; - return new android.media.AudioTrack(attributes, format, bufferSize, MODE_STREAM, - audioSessionId); + return new AudioTrack(attributes, format, bufferSize, MODE_STREAM, audioSessionId); } - private android.media.AudioTrack initializeKeepSessionIdAudioTrack(int audioSessionId) { - int sampleRate = 4000; // Equal to private android.media.AudioTrack.MIN_SAMPLE_RATE. + private AudioTrack initializeKeepSessionIdAudioTrack(int audioSessionId) { + int sampleRate = 4000; // Equal to private AudioTrack.MIN_SAMPLE_RATE. int channelConfig = AudioFormat.CHANNEL_OUT_MONO; @C.PcmEncoding int encoding = C.ENCODING_PCM_16BIT; int bufferSize = 2; // Use a two byte buffer, as it is not actually used for playback. - return new android.media.AudioTrack(C.STREAM_TYPE_DEFAULT, sampleRate, channelConfig, encoding, - bufferSize, MODE_STATIC, audioSessionId); + return new AudioTrack(C.STREAM_TYPE_DEFAULT, sampleRate, channelConfig, encoding, bufferSize, + MODE_STATIC, audioSessionId); } @C.Encoding @@ -1457,14 +1232,13 @@ public final class AudioTrack { } @TargetApi(21) - private static int writeNonBlockingV21(android.media.AudioTrack audioTrack, ByteBuffer buffer, - int size) { + private static int writeNonBlockingV21(AudioTrack audioTrack, ByteBuffer buffer, int size) { return audioTrack.write(buffer, size, WRITE_NON_BLOCKING); } @TargetApi(21) - private int writeNonBlockingWithAvSyncV21(android.media.AudioTrack audioTrack, - ByteBuffer buffer, int size, long presentationTimeUs) { + private int writeNonBlockingWithAvSyncV21(AudioTrack audioTrack, ByteBuffer buffer, int size, + long presentationTimeUs) { // TODO: Uncomment this when [Internal ref: b/33627517] is clarified or fixed. // if (Util.SDK_INT >= 23) { // // The underlying platform AudioTrack writes AV sync headers directly. @@ -1502,21 +1276,21 @@ public final class AudioTrack { } @TargetApi(21) - private static void setVolumeInternalV21(android.media.AudioTrack audioTrack, float volume) { + private static void setVolumeInternalV21(AudioTrack audioTrack, float volume) { audioTrack.setVolume(volume); } @SuppressWarnings("deprecation") - private static void setVolumeInternalV3(android.media.AudioTrack audioTrack, float volume) { + private static void setVolumeInternalV3(AudioTrack audioTrack, float volume) { audioTrack.setStereoVolume(volume, volume); } /** - * Wraps an {@link android.media.AudioTrack} to expose useful utility methods. + * Wraps an {@link AudioTrack} to expose useful utility methods. */ private static class AudioTrackUtil { - protected android.media.AudioTrack audioTrack; + protected AudioTrack audioTrack; private boolean needsPassthroughWorkaround; private int sampleRate; private long lastRawPlaybackHeadPosition; @@ -1534,8 +1308,7 @@ public final class AudioTrack { * @param needsPassthroughWorkaround Whether to workaround issues with pausing AC-3 passthrough * audio tracks on platform API version 21/22. */ - public void reconfigure(android.media.AudioTrack audioTrack, - boolean needsPassthroughWorkaround) { + public void reconfigure(AudioTrack audioTrack, boolean needsPassthroughWorkaround) { this.audioTrack = audioTrack; this.needsPassthroughWorkaround = needsPassthroughWorkaround; stopTimestampUs = C.TIME_UNSET; @@ -1574,9 +1347,9 @@ public final class AudioTrack { } /** - * {@link android.media.AudioTrack#getPlaybackHeadPosition()} returns a value intended to be - * interpreted as an unsigned 32 bit integer, which also wraps around periodically. This method - * returns the playback head position as a long that will only wrap around if the value exceeds + * {@link AudioTrack#getPlaybackHeadPosition()} returns a value intended to be interpreted as an + * unsigned 32 bit integer, which also wraps around periodically. This method 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 The playback head position, in frames. @@ -1676,8 +1449,7 @@ public final class AudioTrack { } @Override - public void reconfigure(android.media.AudioTrack audioTrack, - boolean needsPassthroughWorkaround) { + public void reconfigure(AudioTrack audioTrack, boolean needsPassthroughWorkaround) { super.reconfigure(audioTrack, needsPassthroughWorkaround); rawTimestampFramePositionWrapCount = 0; lastRawTimestampFramePosition = 0; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index cbb3a4944d..f8206e94cf 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java @@ -40,13 +40,13 @@ import com.google.android.exoplayer2.util.Util; import java.nio.ByteBuffer; /** - * Decodes and renders audio using {@link MediaCodec} and {@link AudioTrack}. + * Decodes and renders audio using {@link MediaCodec} and an {@link AudioSink}. */ @TargetApi(16) public class MediaCodecAudioRenderer extends MediaCodecRenderer implements MediaClock { private final EventDispatcher eventDispatcher; - private final AudioTrack audioTrack; + private final AudioSink audioSink; private boolean passthroughEnabled; private boolean codecNeedsDiscardChannelsWorkaround; @@ -110,7 +110,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media boolean playClearSamplesWithoutKeys, @Nullable Handler eventHandler, @Nullable AudioRendererEventListener eventListener) { this(mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, eventHandler, - eventListener, null); + eventListener, (AudioCapabilities) null); } /** @@ -135,9 +135,32 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media boolean playClearSamplesWithoutKeys, @Nullable Handler eventHandler, @Nullable AudioRendererEventListener eventListener, @Nullable AudioCapabilities audioCapabilities, AudioProcessor... audioProcessors) { + this(mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, + eventHandler, eventListener, new DefaultAudioSink(audioCapabilities, audioProcessors)); + } + + /** + * @param mediaCodecSelector A decoder selector. + * @param drmSessionManager For use with encrypted content. May be null if support for encrypted + * content is not required. + * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions. + * For example a media file may start with a short clear region so as to allow playback to + * begin in parallel with key acquisition. This parameter specifies whether the renderer is + * permitted to play clear regions of encrypted media files before {@code drmSessionManager} + * has obtained the keys necessary to decrypt encrypted regions of the media. + * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be + * null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + * @param audioSink The sink to which audio will be output. + */ + public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector, + @Nullable DrmSessionManager drmSessionManager, + boolean playClearSamplesWithoutKeys, @Nullable Handler eventHandler, + @Nullable AudioRendererEventListener eventListener, AudioSink audioSink) { super(C.TRACK_TYPE_AUDIO, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys); eventDispatcher = new EventDispatcher(eventHandler, eventListener); - audioTrack = new AudioTrack(audioCapabilities, audioProcessors, new AudioTrackListener()); + this.audioSink = audioSink; + audioSink.setListener(new AudioSinkListener()); } @Override @@ -196,14 +219,14 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media /** * Returns whether encoded audio passthrough should be used for playing back the input format. - * This implementation returns true if the {@link AudioTrack}'s audio capabilities indicate that - * passthrough is supported. + * This implementation returns true if the {@link AudioSink} indicates that passthrough is + * supported. * * @param mimeType The type of input media. - * @return Whether passthrough playback should be used. + * @return Whether passthrough playback is supported. */ protected boolean allowPassthrough(String mimeType) { - return audioTrack.isPassthroughSupported(mimeType); + return audioSink.isPassthroughSupported(mimeType); } @Override @@ -266,9 +289,9 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } try { - audioTrack.configure(mimeType, channelCount, sampleRate, pcmEncoding, 0, channelMap, + audioSink.configure(mimeType, channelCount, sampleRate, pcmEncoding, 0, channelMap, encoderDelay, encoderPadding); - } catch (AudioTrack.ConfigurationException e) { + } catch (AudioSink.ConfigurationException e) { throw ExoPlaybackException.createForRenderer(e, getIndex()); } } @@ -279,21 +302,21 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media * order to spatialize the audio channels. For this use case, any {@link Virtualizer} instances * should be released in {@link #onDisabled()} (if not before). * - * @see AudioTrack.Listener#onAudioSessionId(int) + * @see AudioSink.Listener#onAudioSessionId(int) */ protected void onAudioSessionId(int audioSessionId) { // Do nothing. } /** - * @see AudioTrack.Listener#onPositionDiscontinuity() + * @see AudioSink.Listener#onPositionDiscontinuity() */ protected void onAudioTrackPositionDiscontinuity() { // Do nothing. } /** - * @see AudioTrack.Listener#onUnderrun(int, long, long) + * @see AudioSink.Listener#onUnderrun(int, long, long) */ protected void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) { @@ -306,16 +329,16 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media eventDispatcher.enabled(decoderCounters); int tunnelingAudioSessionId = getConfiguration().tunnelingAudioSessionId; if (tunnelingAudioSessionId != C.AUDIO_SESSION_ID_UNSET) { - audioTrack.enableTunnelingV21(tunnelingAudioSessionId); + audioSink.enableTunnelingV21(tunnelingAudioSessionId); } else { - audioTrack.disableTunneling(); + audioSink.disableTunneling(); } } @Override protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException { super.onPositionReset(positionUs, joining); - audioTrack.reset(); + audioSink.reset(); currentPositionUs = positionUs; allowPositionDiscontinuity = true; } @@ -323,19 +346,19 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media @Override protected void onStarted() { super.onStarted(); - audioTrack.play(); + audioSink.play(); } @Override protected void onStopped() { - audioTrack.pause(); + audioSink.pause(); super.onStopped(); } @Override protected void onDisabled() { try { - audioTrack.release(); + audioSink.release(); } finally { try { super.onDisabled(); @@ -348,18 +371,18 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media @Override public boolean isEnded() { - return super.isEnded() && audioTrack.isEnded(); + return super.isEnded() && audioSink.isEnded(); } @Override public boolean isReady() { - return audioTrack.hasPendingData() || super.isReady(); + return audioSink.hasPendingData() || super.isReady(); } @Override public long getPositionUs() { - long newCurrentPositionUs = audioTrack.getCurrentPositionUs(isEnded()); - if (newCurrentPositionUs != AudioTrack.CURRENT_POSITION_NOT_SET) { + long newCurrentPositionUs = audioSink.getCurrentPositionUs(isEnded()); + if (newCurrentPositionUs != AudioSink.CURRENT_POSITION_NOT_SET) { currentPositionUs = allowPositionDiscontinuity ? newCurrentPositionUs : Math.max(currentPositionUs, newCurrentPositionUs); allowPositionDiscontinuity = false; @@ -369,12 +392,12 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media @Override public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) { - return audioTrack.setPlaybackParameters(playbackParameters); + return audioSink.setPlaybackParameters(playbackParameters); } @Override public PlaybackParameters getPlaybackParameters() { - return audioTrack.getPlaybackParameters(); + return audioSink.getPlaybackParameters(); } @Override @@ -390,17 +413,17 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media if (shouldSkip) { codec.releaseOutputBuffer(bufferIndex, false); decoderCounters.skippedOutputBufferCount++; - audioTrack.handleDiscontinuity(); + audioSink.handleDiscontinuity(); return true; } try { - if (audioTrack.handleBuffer(buffer, bufferPresentationTimeUs)) { + if (audioSink.handleBuffer(buffer, bufferPresentationTimeUs)) { codec.releaseOutputBuffer(bufferIndex, false); decoderCounters.renderedOutputBufferCount++; return true; } - } catch (AudioTrack.InitializationException | AudioTrack.WriteException e) { + } catch (AudioSink.InitializationException | AudioSink.WriteException e) { throw ExoPlaybackException.createForRenderer(e, getIndex()); } return false; @@ -409,8 +432,8 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media @Override protected void renderToEndOfStream() throws ExoPlaybackException { try { - audioTrack.playToEndOfStream(); - } catch (AudioTrack.WriteException e) { + audioSink.playToEndOfStream(); + } catch (AudioSink.WriteException e) { throw ExoPlaybackException.createForRenderer(e, getIndex()); } } @@ -419,11 +442,11 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media public void handleMessage(int messageType, Object message) throws ExoPlaybackException { switch (messageType) { case C.MSG_SET_VOLUME: - audioTrack.setVolume((Float) message); + audioSink.setVolume((Float) message); break; case C.MSG_SET_AUDIO_ATTRIBUTES: AudioAttributes audioAttributes = (AudioAttributes) message; - audioTrack.setAudioAttributes(audioAttributes); + audioSink.setAudioAttributes(audioAttributes); break; default: super.handleMessage(messageType, message); @@ -445,7 +468,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media || Util.DEVICE.startsWith("heroqlte")); } - private final class AudioTrackListener implements AudioTrack.Listener { + private final class AudioSinkListener implements AudioSink.Listener { @Override public void onAudioSessionId(int audioSessionId) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java index 557421e4b3..98a84fdff8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java @@ -72,7 +72,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements private final DrmSessionManager drmSessionManager; private final boolean playClearSamplesWithoutKeys; private final EventDispatcher eventDispatcher; - private final AudioTrack audioTrack; + private final AudioSink audioSink; private final FormatHolder formatHolder; private final DecoderInputBuffer flagsOnlyBuffer; @@ -107,8 +107,8 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements * @param eventListener A listener of events. May be null if delivery of events is not required. * @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output. */ - public SimpleDecoderAudioRenderer(Handler eventHandler, - AudioRendererEventListener eventListener, AudioProcessor... audioProcessors) { + public SimpleDecoderAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, + AudioProcessor... audioProcessors) { this(eventHandler, eventListener, null, null, false, audioProcessors); } @@ -119,8 +119,8 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements * @param audioCapabilities The audio capabilities for playback on this device. May be null if the * default capabilities (no encoded audio passthrough support) should be assumed. */ - public SimpleDecoderAudioRenderer(Handler eventHandler, - AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities) { + public SimpleDecoderAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, + AudioCapabilities audioCapabilities) { this(eventHandler, eventListener, audioCapabilities, null, false); } @@ -139,15 +139,35 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements * has obtained the keys necessary to decrypt encrypted regions of the media. * @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output. */ - public SimpleDecoderAudioRenderer(Handler eventHandler, - AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities, + public SimpleDecoderAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, + AudioCapabilities audioCapabilities, DrmSessionManager drmSessionManager, + boolean playClearSamplesWithoutKeys, AudioProcessor... audioProcessors) { + this(eventHandler, eventListener, drmSessionManager, + playClearSamplesWithoutKeys, new DefaultAudioSink(audioCapabilities, audioProcessors)); + } + + /** + * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be + * null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + * @param drmSessionManager For use with encrypted media. May be null if support for encrypted + * media is not required. + * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions. + * For example a media file may start with a short clear region so as to allow playback to + * begin in parallel with key acquisition. This parameter specifies whether the renderer is + * permitted to play clear regions of encrypted media files before {@code drmSessionManager} + * has obtained the keys necessary to decrypt encrypted regions of the media. + * @param audioSink The sink to which audio will be output. + */ + public SimpleDecoderAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, - AudioProcessor... audioProcessors) { + AudioSink audioSink) { super(C.TRACK_TYPE_AUDIO); this.drmSessionManager = drmSessionManager; this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys; eventDispatcher = new EventDispatcher(eventHandler, eventListener); - audioTrack = new AudioTrack(audioCapabilities, audioProcessors, new AudioTrackListener()); + this.audioSink = audioSink; + audioSink.setListener(new AudioSinkListener()); formatHolder = new FormatHolder(); flagsOnlyBuffer = DecoderInputBuffer.newFlagsOnlyInstance(); decoderReinitializationState = REINITIALIZATION_STATE_NONE; @@ -184,8 +204,8 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { if (outputStreamEnded) { try { - audioTrack.playToEndOfStream(); - } catch (AudioTrack.WriteException e) { + audioSink.playToEndOfStream(); + } catch (AudioSink.WriteException e) { throw ExoPlaybackException.createForRenderer(e, getIndex()); } return; @@ -220,8 +240,8 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements while (drainOutputBuffer()) {} while (feedInputBuffer()) {} TraceUtil.endSection(); - } catch (AudioDecoderException | AudioTrack.ConfigurationException - | AudioTrack.InitializationException | AudioTrack.WriteException e) { + } catch (AudioDecoderException | AudioSink.ConfigurationException + | AudioSink.InitializationException | AudioSink.WriteException e) { throw ExoPlaybackException.createForRenderer(e, getIndex()); } decoderCounters.ensureUpdated(); @@ -234,21 +254,21 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements * order to spatialize the audio channels. For this use case, any {@link Virtualizer} instances * should be released in {@link #onDisabled()} (if not before). * - * @see AudioTrack.Listener#onAudioSessionId(int) + * @see AudioSink.Listener#onAudioSessionId(int) */ protected void onAudioSessionId(int audioSessionId) { // Do nothing. } /** - * @see AudioTrack.Listener#onPositionDiscontinuity() + * @see AudioSink.Listener#onPositionDiscontinuity() */ protected void onAudioTrackPositionDiscontinuity() { // Do nothing. } /** - * @see AudioTrack.Listener#onUnderrun(int, long, long) + * @see AudioSink.Listener#onUnderrun(int, long, long) */ protected void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) { @@ -282,8 +302,8 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements } private boolean drainOutputBuffer() throws ExoPlaybackException, AudioDecoderException, - AudioTrack.ConfigurationException, AudioTrack.InitializationException, - AudioTrack.WriteException { + AudioSink.ConfigurationException, AudioSink.InitializationException, + AudioSink.WriteException { if (outputBuffer == null) { outputBuffer = decoder.dequeueOutputBuffer(); if (outputBuffer == null) { @@ -309,12 +329,12 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements if (audioTrackNeedsConfigure) { Format outputFormat = getOutputFormat(); - audioTrack.configure(outputFormat.sampleMimeType, outputFormat.channelCount, + audioSink.configure(outputFormat.sampleMimeType, outputFormat.channelCount, outputFormat.sampleRate, outputFormat.pcmEncoding, 0, null, encoderDelay, encoderPadding); audioTrackNeedsConfigure = false; } - if (audioTrack.handleBuffer(outputBuffer.data, outputBuffer.timeUs)) { + if (audioSink.handleBuffer(outputBuffer.data, outputBuffer.timeUs)) { decoderCounters.renderedOutputBufferCount++; outputBuffer.release(); outputBuffer = null; @@ -394,8 +414,8 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements private void processEndOfStream() throws ExoPlaybackException { outputStreamEnded = true; try { - audioTrack.playToEndOfStream(); - } catch (AudioTrack.WriteException e) { + audioSink.playToEndOfStream(); + } catch (AudioSink.WriteException e) { throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex()); } } @@ -418,19 +438,19 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements @Override public boolean isEnded() { - return outputStreamEnded && audioTrack.isEnded(); + return outputStreamEnded && audioSink.isEnded(); } @Override public boolean isReady() { - return audioTrack.hasPendingData() + return audioSink.hasPendingData() || (inputFormat != null && !waitingForKeys && (isSourceReady() || outputBuffer != null)); } @Override public long getPositionUs() { - long newCurrentPositionUs = audioTrack.getCurrentPositionUs(isEnded()); - if (newCurrentPositionUs != AudioTrack.CURRENT_POSITION_NOT_SET) { + long newCurrentPositionUs = audioSink.getCurrentPositionUs(isEnded()); + if (newCurrentPositionUs != AudioSink.CURRENT_POSITION_NOT_SET) { currentPositionUs = allowPositionDiscontinuity ? newCurrentPositionUs : Math.max(currentPositionUs, newCurrentPositionUs); allowPositionDiscontinuity = false; @@ -440,12 +460,12 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements @Override public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) { - return audioTrack.setPlaybackParameters(playbackParameters); + return audioSink.setPlaybackParameters(playbackParameters); } @Override public PlaybackParameters getPlaybackParameters() { - return audioTrack.getPlaybackParameters(); + return audioSink.getPlaybackParameters(); } @Override @@ -454,15 +474,15 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements eventDispatcher.enabled(decoderCounters); int tunnelingAudioSessionId = getConfiguration().tunnelingAudioSessionId; if (tunnelingAudioSessionId != C.AUDIO_SESSION_ID_UNSET) { - audioTrack.enableTunnelingV21(tunnelingAudioSessionId); + audioSink.enableTunnelingV21(tunnelingAudioSessionId); } else { - audioTrack.disableTunneling(); + audioSink.disableTunneling(); } } @Override protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException { - audioTrack.reset(); + audioSink.reset(); currentPositionUs = positionUs; allowPositionDiscontinuity = true; inputStreamEnded = false; @@ -474,12 +494,12 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements @Override protected void onStarted() { - audioTrack.play(); + audioSink.play(); } @Override protected void onStopped() { - audioTrack.pause(); + audioSink.pause(); } @Override @@ -489,7 +509,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements waitingForKeys = false; try { releaseDecoder(); - audioTrack.release(); + audioSink.release(); } finally { try { if (drmSession != null) { @@ -599,11 +619,11 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements public void handleMessage(int messageType, Object message) throws ExoPlaybackException { switch (messageType) { case C.MSG_SET_VOLUME: - audioTrack.setVolume((Float) message); + audioSink.setVolume((Float) message); break; case C.MSG_SET_AUDIO_ATTRIBUTES: AudioAttributes audioAttributes = (AudioAttributes) message; - audioTrack.setAudioAttributes(audioAttributes); + audioSink.setAudioAttributes(audioAttributes); break; default: super.handleMessage(messageType, message); @@ -611,7 +631,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements } } - private final class AudioTrackListener implements AudioTrack.Listener { + private final class AudioSinkListener implements AudioSink.Listener { @Override public void onAudioSessionId(int audioSessionId) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/TrimmingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/TrimmingAudioProcessor.java index c66cbf4882..9338c24b76 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/TrimmingAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/TrimmingAudioProcessor.java @@ -55,7 +55,7 @@ import java.nio.ByteOrder; * * @param trimStartSamples The number of audio samples to trim from the start of audio. * @param trimEndSamples The number of audio samples to trim from the end of audio. - * @see AudioTrack#configure(String, int, int, int, int, int[], int, int) + * @see AudioSink#configure(String, int, int, int, int, int[], int, int) */ public void setTrimSampleCount(int trimStartSamples, int trimEndSamples) { this.trimStartSamples = trimStartSamples; diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java index 142c5060b5..ee4018ba0e 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java @@ -29,7 +29,7 @@ import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.audio.AudioRendererEventListener; -import com.google.android.exoplayer2.audio.AudioTrack; +import com.google.android.exoplayer2.audio.DefaultAudioSink; import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; @@ -53,10 +53,10 @@ public abstract class ExoHostedTest extends Player.DefaultEventListener implemen AudioRendererEventListener, VideoRendererEventListener { static { - // ExoPlayer's AudioTrack class is able to work around spurious timestamps reported by the - // platform (by ignoring them). Disable this workaround, since we're interested in testing - // that the underlying platform is behaving correctly. - AudioTrack.failOnSpuriousAudioTimestamp = true; + // DefaultAudioSink is able to work around spurious timestamps reported by the platform (by + // ignoring them). Disable this workaround, since we're interested in testing that the + // underlying platform is behaving correctly. + DefaultAudioSink.failOnSpuriousAudioTimestamp = true; } public static final long MAX_PLAYING_TIME_DISCREPANCY_MS = 2000; @@ -253,7 +253,7 @@ public abstract class ExoHostedTest extends Player.DefaultEventListener implemen } @Override - public void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) { + public void onAudioSinkUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) { Log.e(tag, "audioTrackUnderrun [" + bufferSize + ", " + bufferSizeMs + ", " + elapsedSinceLastFeedMs + "]", null); } From 60de1574105e2669734237f79e7880b063e45834 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 28 Sep 2017 02:32:27 -0700 Subject: [PATCH 0489/2472] Add a few initial tests for SimpleDecoderAudioRenderer ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=170318174 --- .../audio/SimpleDecoderAudioRendererTest.java | 138 ++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRendererTest.java diff --git a/library/core/src/test/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRendererTest.java b/library/core/src/test/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRendererTest.java new file mode 100644 index 0000000000..346b94e5f4 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRendererTest.java @@ -0,0 +1,138 @@ +/* + * 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 static com.google.android.exoplayer2.RendererCapabilities.ADAPTIVE_NOT_SEAMLESS; +import static com.google.android.exoplayer2.RendererCapabilities.FORMAT_HANDLED; +import static com.google.android.exoplayer2.RendererCapabilities.TUNNELING_NOT_SUPPORTED; +import static com.google.android.exoplayer2.RendererCapabilities.TUNNELING_SUPPORTED; +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.RendererConfiguration; +import com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import com.google.android.exoplayer2.decoder.SimpleDecoder; +import com.google.android.exoplayer2.decoder.SimpleOutputBuffer; +import com.google.android.exoplayer2.drm.DrmSessionManager; +import com.google.android.exoplayer2.drm.ExoMediaCrypto; +import com.google.android.exoplayer2.testutil.FakeSampleStream; +import com.google.android.exoplayer2.util.MimeTypes; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Unit test for {@link SimpleDecoderAudioRenderer}. + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public class SimpleDecoderAudioRendererTest { + + private static final Format FORMAT = Format.createSampleFormat(null, MimeTypes.AUDIO_RAW, 0); + + @Mock private AudioSink mockAudioSink; + private SimpleDecoderAudioRenderer audioRenderer; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + audioRenderer = new SimpleDecoderAudioRenderer(null, null, null, false, mockAudioSink) { + @Override + protected int supportsFormatInternal(DrmSessionManager drmSessionManager, + Format format) { + return FORMAT_HANDLED; + } + + @Override + protected SimpleDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto) + throws AudioDecoderException { + return new FakeDecoder(); + } + }; + } + + @Config(sdk = 19) + @Test + public void testSupportsFormatAtApi19() { + assertThat(audioRenderer.supportsFormat(FORMAT)) + .isEqualTo(ADAPTIVE_NOT_SEAMLESS | TUNNELING_NOT_SUPPORTED | FORMAT_HANDLED); + } + + @Config(sdk = 21) + @Test + public void testSupportsFormatAtApi21() { + // From API 21, tunneling is supported. + assertThat(audioRenderer.supportsFormat(FORMAT)) + .isEqualTo(ADAPTIVE_NOT_SEAMLESS | TUNNELING_SUPPORTED | FORMAT_HANDLED); + } + + @Test + public void testImmediatelyReadEndOfStreamPlaysAudioSinkToEndOfStream() throws Exception { + audioRenderer.enable(RendererConfiguration.DEFAULT, new Format[] {FORMAT}, + new FakeSampleStream(FORMAT), 0, false, 0); + audioRenderer.setCurrentStreamFinal(); + when(mockAudioSink.isEnded()).thenReturn(true); + while (!audioRenderer.isEnded()) { + audioRenderer.render(0, 0); + } + verify(mockAudioSink, times(1)).playToEndOfStream(); + audioRenderer.disable(); + verify(mockAudioSink, times(1)).release(); + } + + private static final class FakeDecoder + extends SimpleDecoder { + + public FakeDecoder() { + super(new DecoderInputBuffer[1], new SimpleOutputBuffer[1]); + } + + @Override + public String getName() { + return "FakeDecoder"; + } + + @Override + protected DecoderInputBuffer createInputBuffer() { + return new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED); + } + + @Override + protected SimpleOutputBuffer createOutputBuffer() { + return new SimpleOutputBuffer(this); + } + + @Override + protected AudioDecoderException decode(DecoderInputBuffer inputBuffer, + SimpleOutputBuffer outputBuffer, boolean reset) { + if (inputBuffer.isEndOfStream()) { + outputBuffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); + } + return null; + } + + } + +} From 7665571ff92a55fd7059ff3e03a3b96c11fb308e Mon Sep 17 00:00:00 2001 From: hoangtc Date: Tue, 25 Jul 2017 02:37:22 +0100 Subject: [PATCH 0490/2472] Do not copy data to output frame in libvpx unless necessary. In out libvpx extension, currently we always call vpxGetFrame and copy the data from the native decoder to output frame. However, if the inputBuffer has isDecoderOnly set, we can avoid populating the output buffer, but only setting BUFFER_FLAG_DECODE_ONLY. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=170318527 --- .../android/exoplayer2/ext/vp9/VpxDecoder.java | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java index 4bec5bdf4c..ef999d5d2b 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java @@ -120,14 +120,16 @@ import java.nio.ByteBuffer; } } - outputBuffer.init(inputBuffer.timeUs, outputMode); - int getFrameResult = vpxGetFrame(vpxDecContext, outputBuffer); - if (getFrameResult == 1) { - outputBuffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY); - } else if (getFrameResult == -1) { - return new VpxDecoderException("Buffer initialization failed."); + if (!inputBuffer.isDecodeOnly()) { + outputBuffer.init(inputBuffer.timeUs, outputMode); + int getFrameResult = vpxGetFrame(vpxDecContext, outputBuffer); + if (getFrameResult == 1) { + outputBuffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY); + } else if (getFrameResult == -1) { + return new VpxDecoderException("Buffer initialization failed."); + } + outputBuffer.colorInfo = inputBuffer.colorInfo; } - outputBuffer.colorInfo = inputBuffer.colorInfo; return null; } From a3a9c0f3b0eb5d38702aaecfe4308333ebd273ae Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 28 Sep 2017 08:04:01 -0700 Subject: [PATCH 0491/2472] Sanity check current position <= written frame position This avoids spurious position reports following an underrun. Github: #1874 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=170344399 --- .../com/google/android/exoplayer2/audio/DefaultAudioSink.java | 1 + 1 file changed, 1 insertion(+) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java index 34ea173deb..1cafdc5efe 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java @@ -332,6 +332,7 @@ public final class DefaultAudioSink implements AudioSink { } } + positionUs = Math.min(positionUs, framesToDurationUs(getWrittenFrames())); return startMediaTimeUs + applySpeedup(positionUs); } From 28173991d18f5d4fef0ed70af61b538956a35c3e Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 28 Sep 2017 10:17:19 -0700 Subject: [PATCH 0492/2472] Return requested position when selecting tracks in FakeMediaPeriod. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=170360487 --- .../com/google/android/exoplayer2/testutil/FakeMediaPeriod.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java index 3863cf7987..38a5e37fa5 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java @@ -81,7 +81,7 @@ public class FakeMediaPeriod implements MediaPeriod { streamResetFlags[i] = true; } } - return 0; + return positionUs; } @Override From 096d7651d2b2f1636ee128bda05685009212f0fd Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 29 Sep 2017 01:51:00 -0700 Subject: [PATCH 0493/2472] Execute actions in action schedule immediately. Run next action immediately without using the handler when the requested delay is zero. This ensures that no other code can run between these two actions to improve deterministic test behaviour. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=170456852 --- .../google/android/exoplayer2/testutil/ActionSchedule.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java index ba76c58d11..60c13d1947 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.testutil; import android.os.Handler; +import android.os.Looper; import android.view.Surface; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Player; @@ -342,7 +343,11 @@ public final class ActionSchedule { this.trackSelector = trackSelector; this.surface = surface; this.mainHandler = mainHandler; - clock.postDelayed(mainHandler, this, delayMs); + if (delayMs == 0 && Looper.myLooper() == mainHandler.getLooper()) { + run(); + } else { + clock.postDelayed(mainHandler, this, delayMs); + } } @Override From fd576d218960d0ee862c9478ea5586a41646a332 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 29 Sep 2017 18:35:39 -0700 Subject: [PATCH 0494/2472] Smoother PlaybackControlView updates when speed != 1 * If speed <= 0.1, update every second. * If 0.1 < speed < 1, update approximately once per second in real time, aligned so that each second boundary in media time has a corresponding updatae. * If speed == 1, keep existing behavior. * If 1 < speed <= 5, update every second in media time * If speed > 5, update every 200ms. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=170559037 --- .../exoplayer2/ui/PlaybackControlView.java | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java index f47ac8695c..e2ae1f732b 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java @@ -825,9 +825,19 @@ public class PlaybackControlView extends FrameLayout { if (playbackState != Player.STATE_IDLE && playbackState != Player.STATE_ENDED) { long delayMs; if (player.getPlayWhenReady() && playbackState == Player.STATE_READY) { - delayMs = 1000 - (position % 1000); - if (delayMs < 200) { - delayMs += 1000; + float playbackSpeed = player.getPlaybackParameters().speed; + if (playbackSpeed <= 0.1f) { + delayMs = 1000; + } else if (playbackSpeed <= 5f) { + long mediaTimeUpdatePeriodMs = 1000 / Math.max(1, Math.round(1 / playbackSpeed)); + long mediaTimeDelayMs = mediaTimeUpdatePeriodMs - (position % mediaTimeUpdatePeriodMs); + if (mediaTimeDelayMs < (mediaTimeUpdatePeriodMs / 5)) { + mediaTimeDelayMs += mediaTimeUpdatePeriodMs; + } + delayMs = playbackSpeed == 1 ? mediaTimeDelayMs + : (long) (mediaTimeDelayMs / playbackSpeed); + } else { + delayMs = 200; } } else { delayMs = 1000; From 7e7fea4068d9b178b463ab74c818d78831c894ca Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 2 Oct 2017 04:37:38 -0700 Subject: [PATCH 0495/2472] Add WaitForPlaybackStateChanged action to action schedule. This works similar to the existing WaitForXXXX actions. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=170687058 --- .../android/exoplayer2/testutil/Action.java | 43 +++++++++++++++++++ .../exoplayer2/testutil/ActionSchedule.java | 11 +++++ 2 files changed, 54 insertions(+) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java index 76e39525b4..cc18b63016 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java @@ -379,6 +379,49 @@ public abstract class Action { } + /** + * Waits for a specified playback state, returning either immediately or after a call to + * {@link Player.EventListener#onPlayerStateChanged(boolean, int)}. + */ + public static final class WaitForPlaybackState extends Action { + + private final int targetPlaybackState; + + /** + * @param tag A tag to use for logging. + */ + public WaitForPlaybackState(String tag, int targetPlaybackState) { + super(tag, "WaitForPlaybackState"); + this.targetPlaybackState = targetPlaybackState; + } + + @Override + protected void doActionAndScheduleNextImpl(final SimpleExoPlayer player, + final MappingTrackSelector trackSelector, final Surface surface, final Handler handler, + final ActionNode nextAction) { + if (targetPlaybackState == player.getPlaybackState()) { + nextAction.schedule(player, trackSelector, surface, handler); + } else { + player.addListener(new Player.DefaultEventListener() { + @Override + public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + if (targetPlaybackState == playbackState) { + player.removeListener(this); + nextAction.schedule(player, trackSelector, surface, handler); + } + } + }); + } + } + + @Override + protected void doActionImpl(SimpleExoPlayer player, MappingTrackSelector trackSelector, + Surface surface) { + // Not triggered. + } + + } + /** * Calls {@link Runnable#run()}. */ diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java index 60c13d1947..8a6f3360f3 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java @@ -33,6 +33,7 @@ import com.google.android.exoplayer2.testutil.Action.SetRepeatMode; import com.google.android.exoplayer2.testutil.Action.SetShuffleModeEnabled; import com.google.android.exoplayer2.testutil.Action.SetVideoSurface; import com.google.android.exoplayer2.testutil.Action.Stop; +import com.google.android.exoplayer2.testutil.Action.WaitForPlaybackState; import com.google.android.exoplayer2.testutil.Action.WaitForPositionDiscontinuity; import com.google.android.exoplayer2.testutil.Action.WaitForTimelineChanged; import com.google.android.exoplayer2.trackselection.MappingTrackSelector; @@ -257,6 +258,16 @@ public final class ActionSchedule { return apply(new WaitForPositionDiscontinuity(tag)); } + /** + * Schedules a delay until the playback state changed to the specified state. + * + * @param targetPlaybackState The target playback state. + * @return The builder, for convenience. + */ + public Builder waitForPlaybackState(int targetPlaybackState) { + return apply(new WaitForPlaybackState(tag, targetPlaybackState)); + } + /** * Schedules a {@link Runnable} to be executed. * From 1495b9a47370b95043eaf31aaef56a1e1e0da0ba Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 2 Oct 2017 10:16:59 -0700 Subject: [PATCH 0496/2472] Fix stuck playback when media has uneven track end times * Always assume a renderer is ready if it's read to the end of its current stream and there's a subsequent period already prepared. This prevents getting stuck when a non-clock renderer has a short stream. * Switch to the standalone clock if the renderer providing the media clock has read to the end of its current stream, is no longer ready, and there's a subsequent period already prepared. This prevents getting stuck when a clock renderer has a short stream. * Remove unnecessary clock synchronization logic (since it would need to be made more complicated as a result of this change). * Don't update the playing period holder when playWhenReady is false. This avoids the position jumping to the start of the next period when seeking to the very end of the current period whilst paused (we still end up showing the first frame of video from the next period, but fixing that will have to wait). Github: #1874 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=170717481 --- .../exoplayer2/ExoPlayerImplInternal.java | 128 ++++++++++-------- .../exoplayer2/util/StandaloneMediaClock.java | 10 -- 2 files changed, 70 insertions(+), 68 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 61c5b01cf7..838a44b4ee 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -556,11 +556,17 @@ import java.io.IOException; if (periodPositionUs != C.TIME_UNSET) { resetRendererPosition(periodPositionUs); } else { - if (rendererMediaClockSource != null && !rendererMediaClockSource.isEnded()) { + // Use the standalone clock if there's no renderer clock, or if the providing renderer has + // ended or needs the next sample stream to reenter the ready state. The latter case uses the + // standalone clock to avoid getting stuck if tracks in the current period have uneven + // durations. See: https://github.com/google/ExoPlayer/issues/1874. + if (rendererMediaClockSource == null || rendererMediaClockSource.isEnded() + || (!rendererMediaClockSource.isReady() + && rendererWaitingForNextStream(rendererMediaClockSource))) { + rendererPositionUs = standaloneMediaClock.getPositionUs(); + } else { rendererPositionUs = rendererMediaClock.getPositionUs(); standaloneMediaClock.setPositionUs(rendererPositionUs); - } else { - rendererPositionUs = standaloneMediaClock.getPositionUs(); } periodPositionUs = playingPeriodHolder.toPeriodTime(rendererPositionUs); } @@ -597,9 +603,12 @@ import java.io.IOException; // invocation of this method. renderer.render(rendererPositionUs, elapsedRealtimeUs); allRenderersEnded = allRenderersEnded && renderer.isEnded(); - // Determine whether the renderer is ready (or ended). If it's not, throw an error that's - // preventing the renderer from making progress, if such an error exists. - boolean rendererReadyOrEnded = renderer.isReady() || renderer.isEnded(); + // Determine whether the renderer is ready (or ended). We override to assume the renderer is + // ready if it needs the next sample stream. This is necessary to avoid getting stuck if + // tracks in the current period have uneven durations. See: + // https://github.com/google/ExoPlayer/issues/1874 + boolean rendererReadyOrEnded = renderer.isReady() || renderer.isEnded() + || rendererWaitingForNextStream(renderer); if (!rendererReadyOrEnded) { renderer.maybeThrowStreamError(); } @@ -617,7 +626,7 @@ import java.io.IOException; // 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); + standaloneMediaClock.setPlaybackParameters(playbackParameters); eventHandler.obtainMessage(MSG_PLAYBACK_PARAMETERS_CHANGED, playbackParameters) .sendToTarget(); } @@ -813,9 +822,10 @@ import java.io.IOException; } private void setPlaybackParametersInternal(PlaybackParameters playbackParameters) { - playbackParameters = rendererMediaClock != null - ? rendererMediaClock.setPlaybackParameters(playbackParameters) - : standaloneMediaClock.setPlaybackParameters(playbackParameters); + if (rendererMediaClock != null) { + playbackParameters = rendererMediaClock.setPlaybackParameters(playbackParameters); + } + standaloneMediaClock.setPlaybackParameters(playbackParameters); this.playbackParameters = playbackParameters; eventHandler.obtainMessage(MSG_PLAYBACK_PARAMETERS_CHANGED, playbackParameters).sendToTarget(); } @@ -945,12 +955,6 @@ import java.io.IOException; if (sampleStream != renderer.getStream()) { // We need to disable the renderer. if (renderer == rendererMediaClockSource) { - // The renderer is providing the media clock. - if (sampleStream == null) { - // The renderer won't be re-enabled. Sync standaloneMediaClock so that it can take - // over timing responsibilities. - standaloneMediaClock.synchronize(rendererMediaClock); - } rendererMediaClock = null; rendererMediaClockSource = null; } @@ -1300,8 +1304,8 @@ import java.io.IOException; return; } - // Update the playing and reading periods. - while (playingPeriodHolder != readingPeriodHolder + // Advance the playing period if necessary. + while (playWhenReady && playingPeriodHolder != readingPeriodHolder && rendererPositionUs >= playingPeriodHolder.next.rendererPositionOffsetUs) { // All enabled renderers' streams have been read to the end, and the playback position reached // the end of the playing period, so advance playback to the next period. @@ -1327,55 +1331,60 @@ import java.io.IOException; return; } + // Advance the reading period if necessary. + if (readingPeriodHolder.next == null || !readingPeriodHolder.next.prepared) { + // We don't have a successor to advance the reading period to. + return; + } + for (int i = 0; i < renderers.length; i++) { Renderer renderer = renderers[i]; SampleStream sampleStream = readingPeriodHolder.sampleStreams[i]; if (renderer.getStream() != sampleStream || (sampleStream != null && !renderer.hasReadStreamToEnd())) { + // The current reading period is still being read by at least one renderer. return; } } - if (readingPeriodHolder.next != null && readingPeriodHolder.next.prepared) { - TrackSelectorResult oldTrackSelectorResult = readingPeriodHolder.trackSelectorResult; - readingPeriodHolder = readingPeriodHolder.next; - TrackSelectorResult newTrackSelectorResult = readingPeriodHolder.trackSelectorResult; + TrackSelectorResult oldTrackSelectorResult = readingPeriodHolder.trackSelectorResult; + readingPeriodHolder = readingPeriodHolder.next; + TrackSelectorResult newTrackSelectorResult = readingPeriodHolder.trackSelectorResult; - boolean initialDiscontinuity = - readingPeriodHolder.mediaPeriod.readDiscontinuity() != C.TIME_UNSET; - for (int i = 0; i < renderers.length; i++) { - Renderer renderer = renderers[i]; - boolean rendererWasEnabled = oldTrackSelectorResult.renderersEnabled[i]; - if (!rendererWasEnabled) { - // The renderer was disabled and will be enabled when we play the next period. - } else if (initialDiscontinuity) { - // The new period starts with a discontinuity, so the renderer will play out all data then - // be disabled and re-enabled when it starts playing the next period. + boolean initialDiscontinuity = + readingPeriodHolder.mediaPeriod.readDiscontinuity() != C.TIME_UNSET; + for (int i = 0; i < renderers.length; i++) { + Renderer renderer = renderers[i]; + boolean rendererWasEnabled = oldTrackSelectorResult.renderersEnabled[i]; + if (!rendererWasEnabled) { + // The renderer was disabled and will be enabled when we play the next period. + } else if (initialDiscontinuity) { + // The new period starts with a discontinuity, so the renderer will play out all data then + // be disabled and re-enabled when it starts playing the next period. + renderer.setCurrentStreamFinal(); + } else if (!renderer.isCurrentStreamFinal()) { + TrackSelection newSelection = newTrackSelectorResult.selections.get(i); + boolean newRendererEnabled = newTrackSelectorResult.renderersEnabled[i]; + boolean isNoSampleRenderer = rendererCapabilities[i].getTrackType() == C.TRACK_TYPE_NONE; + RendererConfiguration oldConfig = oldTrackSelectorResult.rendererConfigurations[i]; + RendererConfiguration newConfig = newTrackSelectorResult.rendererConfigurations[i]; + if (newRendererEnabled && newConfig.equals(oldConfig) && !isNoSampleRenderer) { + // Replace the renderer's SampleStream so the transition to playing the next period can + // be seamless. + // This should be avoided for no-sample renderer, because skipping ahead for such + // renderer doesn't have any benefit (the renderer does not consume the sample stream), + // and it will change the provided rendererOffsetUs while the renderer is still + // rendering from the playing media period. + Format[] formats = getFormats(newSelection); + renderer.replaceStream(formats, readingPeriodHolder.sampleStreams[i], + readingPeriodHolder.getRendererOffset()); + } else { + // The renderer will be disabled when transitioning to playing the next period, because + // there's no new selection, or because a configuration change is required, or because + // it's a no-sample renderer for which rendererOffsetUs should be updated only when + // starting to play the next period. Mark the SampleStream as final to play out any + // remaining data. renderer.setCurrentStreamFinal(); - } else if (!renderer.isCurrentStreamFinal()) { - TrackSelection newSelection = newTrackSelectorResult.selections.get(i); - boolean newRendererEnabled = newTrackSelectorResult.renderersEnabled[i]; - boolean isNoSampleRenderer = rendererCapabilities[i].getTrackType() == C.TRACK_TYPE_NONE; - RendererConfiguration oldConfig = oldTrackSelectorResult.rendererConfigurations[i]; - RendererConfiguration newConfig = newTrackSelectorResult.rendererConfigurations[i]; - if (newRendererEnabled && newConfig.equals(oldConfig) && !isNoSampleRenderer) { - // Replace the renderer's SampleStream so the transition to playing the next period can - // be seamless. - // This should be avoided for no-sample renderer, because skipping ahead for such - // renderer doesn't have any benefit (the renderer does not consume the sample stream), - // and it will change the provided rendererOffsetUs while the renderer is still - // rendering from the playing media period. - Format[] formats = getFormats(newSelection); - renderer.replaceStream(formats, readingPeriodHolder.sampleStreams[i], - readingPeriodHolder.getRendererOffset()); - } else { - // The renderer will be disabled when transitioning to playing the next period, because - // there's no new selection, or because a configuration change is required, or because - // it's a no-sample renderer for which rendererOffsetUs should be updated only when - // starting to play the next period. Mark the SampleStream as final to play out any - // remaining data. - renderer.setCurrentStreamFinal(); - } } } } @@ -1478,8 +1487,6 @@ import java.io.IOException; // needed to play the next period, or because we need to re-enable it as its current stream // is final and it's not reading ahead. if (renderer == rendererMediaClockSource) { - // Sync standaloneMediaClock so that it can take over timing responsibilities. - standaloneMediaClock.synchronize(rendererMediaClock); rendererMediaClock = null; rendererMediaClockSource = null; } @@ -1539,6 +1546,11 @@ import java.io.IOException; } } + private boolean rendererWaitingForNextStream(Renderer renderer) { + return readingPeriodHolder.next != null && readingPeriodHolder.next.prepared + && renderer.hasReadStreamToEnd(); + } + @NonNull private static Format[] getFormats(TrackSelection newSelection) { // Build an array of formats contained by the selection. 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 index 5b8d117dd0..96203bb99a 100644 --- 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 @@ -69,16 +69,6 @@ public final class StandaloneMediaClock implements MediaClock { } } - /** - * 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; From 2f622b8bd8f4d647256613ba012276161a12f1b9 Mon Sep 17 00:00:00 2001 From: Nate Roy Date: Mon, 2 Oct 2017 15:43:19 -0400 Subject: [PATCH 0497/2472] Allow passing of HlsPlaylistParser to HlsMediaSource. Also create helper method in HlsMasterPlaylist to allow the copying of the playlist to another, but with the variants reordered based on a passed comparator. Also added an implementation of HlsPlaylistParser which will reorder the variants returned. --- .../playlist/HlsMasterPlaylistParserTest.java | 44 ++++++- .../ReorderingHlsPlaylistParserTest.java | 107 ++++++++++++++++++ .../exoplayer2/source/hls/HlsMediaSource.java | 14 ++- .../hls/playlist/HlsMasterPlaylist.java | 20 ++++ .../hls/playlist/HlsPlaylistTracker.java | 6 +- .../playlist/ReorderingHlsPlaylistParser.java | 39 +++++++ 6 files changed, 225 insertions(+), 5 deletions(-) create mode 100644 library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/ReorderingHlsPlaylistParserTest.java create mode 100644 library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/ReorderingHlsPlaylistParser.java diff --git a/library/hls/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 index 35cd7f03d8..4dedc9526f 100644 --- a/library/hls/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 @@ -16,16 +16,20 @@ package com.google.android.exoplayer2.source.hls.playlist; import android.net.Uri; + import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.util.MimeTypes; + +import junit.framework.TestCase; + import java.io.ByteArrayInputStream; import java.io.IOException; import java.nio.charset.Charset; import java.util.Collections; +import java.util.Comparator; import java.util.List; -import junit.framework.TestCase; /** * Test for {@link HlsMasterPlaylistParserTest} @@ -147,6 +151,44 @@ public class HlsMasterPlaylistParserTest extends TestCase { assertEquals(Collections.emptyList(), playlist.muxedCaptionFormats); } + public void testReorderedVariantCopy() throws IOException { + HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, MASTER_PLAYLIST); + HlsMasterPlaylist nonReorderedPlaylist = + playlist.copyWithReorderedVariants(new Comparator() { + @Override + public int compare(HlsMasterPlaylist.HlsUrl url1, HlsMasterPlaylist.HlsUrl url2) { + return 0; + } + }); + assertEquals(playlist.variants, nonReorderedPlaylist.variants); + HlsMasterPlaylist.HlsUrl preferred = null; + for (HlsMasterPlaylist.HlsUrl url : playlist.variants) { + if (preferred == null || url.format.bitrate > preferred.format.bitrate) { + preferred = url; + } + } + + assertNotNull(preferred); + + final Comparator comparator = Collections.reverseOrder(new Comparator() { + @Override + public int compare(HlsMasterPlaylist.HlsUrl url1, HlsMasterPlaylist.HlsUrl url2) { + if (url1.format.bitrate > url2.format.bitrate) { + return 1; + } + + if (url2.format.bitrate > url1.format.bitrate) { + return -1; + } + + return 0; + } + }); + HlsMasterPlaylist reorderedPlaylist = playlist.copyWithReorderedVariants(comparator); + + assertEquals(reorderedPlaylist.variants.get(0), preferred); + } + private static HlsMasterPlaylist parseMasterPlaylist(String uri, String playlistString) throws IOException { Uri playlistUri = Uri.parse(uri); diff --git a/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/ReorderingHlsPlaylistParserTest.java b/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/ReorderingHlsPlaylistParserTest.java new file mode 100644 index 0000000000..2816832704 --- /dev/null +++ b/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/ReorderingHlsPlaylistParserTest.java @@ -0,0 +1,107 @@ +package com.google.android.exoplayer2.source.hls.playlist; + +import android.net.Uri; + +import com.google.android.exoplayer2.C; + +import junit.framework.TestCase; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.util.Collections; +import java.util.Comparator; + +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +public class ReorderingHlsPlaylistParserTest extends TestCase { + private static final String MASTER_PLAYLIST = " #EXTM3U \n" + + "\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n" + + "http://example.com/low.m3u8\n" + + "\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=2560000,FRAME-RATE=25,RESOLUTION=384x160\n" + + "http://example.com/mid.m3u8\n" + + "\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=7680000,FRAME-RATE=29.997\n" + + "http://example.com/hi.m3u8\n" + + "\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"mp4a.40.5\"\n" + + "http://example.com/audio-only.m3u8"; + + public void testReorderingWithNonMasterPlaylist() throws IOException { + Uri playlistUri = Uri.parse("https://example.com/test.m3u8"); + String playlistString = "#EXTM3U\n" + + "#EXT-X-VERSION:3\n" + + "#EXT-X-PLAYLIST-TYPE:VOD\n" + + "#EXT-X-START:TIME-OFFSET=-25" + + "#EXT-X-TARGETDURATION:8\n" + + "#EXT-X-MEDIA-SEQUENCE:2679\n" + + "#EXT-X-DISCONTINUITY-SEQUENCE:4\n" + + "#EXT-X-ALLOW-CACHE:YES\n" + + "\n" + + "#EXTINF:7.975,\n" + + "#EXT-X-BYTERANGE:51370@0\n" + + "https://priv.example.com/fileSequence2679.ts\n" + + "\n" + + "#EXT-X-KEY:METHOD=AES-128,URI=\"https://priv.example.com/key.php?r=2680\",IV=0x1566B\n" + + "#EXTINF:7.975,\n" + + "#EXT-X-BYTERANGE:51501@2147483648\n" + + "https://priv.example.com/fileSequence2680.ts\n" + + "\n" + + "#EXT-X-KEY:METHOD=NONE\n" + + "#EXTINF:7.941,\n" + + "#EXT-X-BYTERANGE:51501\n" // @2147535149 + + "https://priv.example.com/fileSequence2681.ts\n" + + "\n" + + "#EXT-X-DISCONTINUITY\n" + + "#EXT-X-KEY:METHOD=AES-128,URI=\"https://priv.example.com/key.php?r=2682\"\n" + + "#EXTINF:7.975,\n" + + "#EXT-X-BYTERANGE:51740\n" // @2147586650 + + "https://priv.example.com/fileSequence2682.ts\n" + + "\n" + + "#EXTINF:7.975,\n" + + "https://priv.example.com/fileSequence2683.ts\n" + + "#EXT-X-ENDLIST"; + InputStream inputStream = new ByteArrayInputStream(playlistString.getBytes(Charset.forName(C.UTF8_NAME))); + Comparator comparator = mock(Comparator.class); + ReorderingHlsPlaylistParser playlistParser = new ReorderingHlsPlaylistParser(new HlsPlaylistParser(), + comparator); + final HlsMediaPlaylist playlist = (HlsMediaPlaylist) playlistParser.parse(playlistUri, inputStream); + assertNotNull(playlist); + // We should never compare the variants for a media level playlist. + verify(comparator, never()).compare(any(HlsMasterPlaylist.HlsUrl.class), any(HlsMasterPlaylist.HlsUrl.class)); + } + + public void testReorderingForMasterPlaylist() throws IOException { + Uri playlistUri = Uri.parse("https://example.com/test.m3u8"); + ByteArrayInputStream inputStream = new ByteArrayInputStream( + MASTER_PLAYLIST.getBytes(Charset.forName(C.UTF8_NAME))); + final Comparator comparator = Collections.reverseOrder(new Comparator() { + @Override + public int compare(HlsMasterPlaylist.HlsUrl url1, HlsMasterPlaylist.HlsUrl url2) { + if (url1.format.bitrate > url2.format.bitrate) { + return 1; + } + + if (url2.format.bitrate > url1.format.bitrate) { + return -1; + } + + return 0; + } + }); + ReorderingHlsPlaylistParser playlistParser = new ReorderingHlsPlaylistParser(new HlsPlaylistParser(), + comparator); + final HlsMasterPlaylist reorderedPlaylist = (HlsMasterPlaylist) playlistParser.parse(playlistUri, inputStream); + assertNotNull(reorderedPlaylist); + + inputStream.reset(); + final HlsMasterPlaylist playlist = (HlsMasterPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream); + assertEquals(reorderedPlaylist.variants.get(0).format, playlist.variants.get(2).format); + } +} \ No newline at end of file diff --git a/library/hls/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 index fd3d533337..b7f7124e44 100644 --- a/library/hls/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 @@ -26,9 +26,12 @@ import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.SinglePeriodTimeline; import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; +import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylist; +import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser; import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistTracker; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.ParsingLoadable; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; import java.util.List; @@ -52,6 +55,7 @@ public final class HlsMediaSource implements MediaSource, private final HlsDataSourceFactory dataSourceFactory; private final int minLoadableRetryCount; private final EventDispatcher eventDispatcher; + private final ParsingLoadable.Parser playlistParser; private HlsPlaylistTracker playlistTracker; private Listener sourceListener; @@ -72,9 +76,17 @@ public final class HlsMediaSource implements MediaSource, public HlsMediaSource(Uri manifestUri, HlsDataSourceFactory dataSourceFactory, int minLoadableRetryCount, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { + this(manifestUri, dataSourceFactory, minLoadableRetryCount, eventHandler, eventListener, new HlsPlaylistParser()); + } + + public HlsMediaSource(Uri manifestUri, HlsDataSourceFactory dataSourceFactory, + int minLoadableRetryCount, Handler eventHandler, + AdaptiveMediaSourceEventListener eventListener, + ParsingLoadable.Parser playlistParser) { this.manifestUri = manifestUri; this.dataSourceFactory = dataSourceFactory; this.minLoadableRetryCount = minLoadableRetryCount; + this.playlistParser = playlistParser; eventDispatcher = new EventDispatcher(eventHandler, eventListener); } @@ -82,7 +94,7 @@ public final class HlsMediaSource implements MediaSource, public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { Assertions.checkState(playlistTracker == null); playlistTracker = new HlsPlaylistTracker(manifestUri, dataSourceFactory, eventDispatcher, - minLoadableRetryCount, this); + minLoadableRetryCount, this, playlistParser); sourceListener = listener; playlistTracker.start(); } diff --git a/library/hls/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 index 04192def9d..5ded975f88 100644 --- a/library/hls/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 @@ -19,6 +19,7 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.util.MimeTypes; import java.util.ArrayList; import java.util.Collections; +import java.util.Comparator; import java.util.List; /** @@ -123,6 +124,25 @@ public final class HlsMasterPlaylist extends HlsPlaylist { muxedAudioFormat, muxedCaptionFormats); } + /** + * Returns a copy of this playlist which includes the variants sorted using the passed comparator. NOTE: the variants + * will be sorted in ascending order by default. If you wish to use descending order, you can wrap your comparator in + * {@link Collections#reverseOrder(Comparator)}. + * + * @param variantComparator the comparator to use to sort the variant list. + * @return a copy of this playlist which includes the variants sorted using the passed comparator. + */ + public HlsMasterPlaylist copyWithReorderedVariants(Comparator variantComparator) { + return new HlsMasterPlaylist(baseUri, tags, filterVariants(variants, variantComparator), audios, + subtitles, muxedAudioFormat, muxedCaptionFormats); + } + + private List filterVariants(List variants, Comparator variantComparator) { + List reorderedList = new ArrayList<>(variants); + Collections.sort(reorderedList, variantComparator); + return reorderedList; + } + /** * Creates a playlist with a single variant. * diff --git a/library/hls/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 index d3682217d8..52d5a4b5bc 100644 --- a/library/hls/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 @@ -115,7 +115,7 @@ public final class HlsPlaylistTracker implements Loader.Callback playlistParser; private final int minRetryCount; private final IdentityHashMap playlistBundles; private final Handler playlistRefreshHandler; @@ -140,7 +140,7 @@ public final class HlsPlaylistTracker implements Loader.Callback playlistParser) { this.initialPlaylistUri = initialPlaylistUri; this.dataSourceFactory = dataSourceFactory; this.eventDispatcher = eventDispatcher; @@ -148,7 +148,7 @@ public final class HlsPlaylistTracker implements Loader.Callback(); initialPlaylistLoader = new Loader("HlsPlaylistTracker:MasterPlaylist"); - playlistParser = new HlsPlaylistParser(); + this.playlistParser = playlistParser; playlistBundles = new IdentityHashMap<>(); playlistRefreshHandler = new Handler(); } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/ReorderingHlsPlaylistParser.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/ReorderingHlsPlaylistParser.java new file mode 100644 index 0000000000..bdd47e8c28 --- /dev/null +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/ReorderingHlsPlaylistParser.java @@ -0,0 +1,39 @@ +package com.google.android.exoplayer2.source.hls.playlist; + +import android.net.Uri; + +import com.google.android.exoplayer2.upstream.ParsingLoadable; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Comparator; + +/** + * Parser for {@link HlsPlaylist}s that reorders the variants based on the comparator passed. + */ +public class ReorderingHlsPlaylistParser implements ParsingLoadable.Parser { + private final ParsingLoadable.Parser playlistParser; + private final Comparator variantComparator; + + /** + * @param playlistParser the {@link ParsingLoadable.Parser} to wrap. + * @param variantComparator the {@link Comparator} to use to reorder the variants. + * See {@link HlsMasterPlaylist#copyWithReorderedVariants(Comparator)} for more details. + */ + public ReorderingHlsPlaylistParser(ParsingLoadable.Parser playlistParser, + Comparator variantComparator) { + this.playlistParser = playlistParser; + this.variantComparator = variantComparator; + } + + @Override + public HlsPlaylist parse(Uri uri, InputStream inputStream) throws IOException { + final HlsPlaylist playlist = playlistParser.parse(uri, inputStream); + + if (playlist instanceof HlsMasterPlaylist) { + return ((HlsMasterPlaylist) playlist).copyWithReorderedVariants(variantComparator); + } + + return playlist; + } +} From 9783fc0924334beab16d1350548e5cce73b9b070 Mon Sep 17 00:00:00 2001 From: Nate Roy Date: Fri, 6 Oct 2017 14:35:18 -0400 Subject: [PATCH 0498/2472] remove reordering playlist parser --- .../ReorderingHlsPlaylistParserTest.java | 107 ------------------ .../playlist/ReorderingHlsPlaylistParser.java | 39 ------- 2 files changed, 146 deletions(-) delete mode 100644 library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/ReorderingHlsPlaylistParserTest.java delete mode 100644 library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/ReorderingHlsPlaylistParser.java diff --git a/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/ReorderingHlsPlaylistParserTest.java b/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/ReorderingHlsPlaylistParserTest.java deleted file mode 100644 index 2816832704..0000000000 --- a/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/ReorderingHlsPlaylistParserTest.java +++ /dev/null @@ -1,107 +0,0 @@ -package com.google.android.exoplayer2.source.hls.playlist; - -import android.net.Uri; - -import com.google.android.exoplayer2.C; - -import junit.framework.TestCase; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.Charset; -import java.util.Collections; -import java.util.Comparator; - -import static org.mockito.Matchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; - -public class ReorderingHlsPlaylistParserTest extends TestCase { - private static final String MASTER_PLAYLIST = " #EXTM3U \n" - + "\n" - + "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n" - + "http://example.com/low.m3u8\n" - + "\n" - + "#EXT-X-STREAM-INF:BANDWIDTH=2560000,FRAME-RATE=25,RESOLUTION=384x160\n" - + "http://example.com/mid.m3u8\n" - + "\n" - + "#EXT-X-STREAM-INF:BANDWIDTH=7680000,FRAME-RATE=29.997\n" - + "http://example.com/hi.m3u8\n" - + "\n" - + "#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"mp4a.40.5\"\n" - + "http://example.com/audio-only.m3u8"; - - public void testReorderingWithNonMasterPlaylist() throws IOException { - Uri playlistUri = Uri.parse("https://example.com/test.m3u8"); - String playlistString = "#EXTM3U\n" - + "#EXT-X-VERSION:3\n" - + "#EXT-X-PLAYLIST-TYPE:VOD\n" - + "#EXT-X-START:TIME-OFFSET=-25" - + "#EXT-X-TARGETDURATION:8\n" - + "#EXT-X-MEDIA-SEQUENCE:2679\n" - + "#EXT-X-DISCONTINUITY-SEQUENCE:4\n" - + "#EXT-X-ALLOW-CACHE:YES\n" - + "\n" - + "#EXTINF:7.975,\n" - + "#EXT-X-BYTERANGE:51370@0\n" - + "https://priv.example.com/fileSequence2679.ts\n" - + "\n" - + "#EXT-X-KEY:METHOD=AES-128,URI=\"https://priv.example.com/key.php?r=2680\",IV=0x1566B\n" - + "#EXTINF:7.975,\n" - + "#EXT-X-BYTERANGE:51501@2147483648\n" - + "https://priv.example.com/fileSequence2680.ts\n" - + "\n" - + "#EXT-X-KEY:METHOD=NONE\n" - + "#EXTINF:7.941,\n" - + "#EXT-X-BYTERANGE:51501\n" // @2147535149 - + "https://priv.example.com/fileSequence2681.ts\n" - + "\n" - + "#EXT-X-DISCONTINUITY\n" - + "#EXT-X-KEY:METHOD=AES-128,URI=\"https://priv.example.com/key.php?r=2682\"\n" - + "#EXTINF:7.975,\n" - + "#EXT-X-BYTERANGE:51740\n" // @2147586650 - + "https://priv.example.com/fileSequence2682.ts\n" - + "\n" - + "#EXTINF:7.975,\n" - + "https://priv.example.com/fileSequence2683.ts\n" - + "#EXT-X-ENDLIST"; - InputStream inputStream = new ByteArrayInputStream(playlistString.getBytes(Charset.forName(C.UTF8_NAME))); - Comparator comparator = mock(Comparator.class); - ReorderingHlsPlaylistParser playlistParser = new ReorderingHlsPlaylistParser(new HlsPlaylistParser(), - comparator); - final HlsMediaPlaylist playlist = (HlsMediaPlaylist) playlistParser.parse(playlistUri, inputStream); - assertNotNull(playlist); - // We should never compare the variants for a media level playlist. - verify(comparator, never()).compare(any(HlsMasterPlaylist.HlsUrl.class), any(HlsMasterPlaylist.HlsUrl.class)); - } - - public void testReorderingForMasterPlaylist() throws IOException { - Uri playlistUri = Uri.parse("https://example.com/test.m3u8"); - ByteArrayInputStream inputStream = new ByteArrayInputStream( - MASTER_PLAYLIST.getBytes(Charset.forName(C.UTF8_NAME))); - final Comparator comparator = Collections.reverseOrder(new Comparator() { - @Override - public int compare(HlsMasterPlaylist.HlsUrl url1, HlsMasterPlaylist.HlsUrl url2) { - if (url1.format.bitrate > url2.format.bitrate) { - return 1; - } - - if (url2.format.bitrate > url1.format.bitrate) { - return -1; - } - - return 0; - } - }); - ReorderingHlsPlaylistParser playlistParser = new ReorderingHlsPlaylistParser(new HlsPlaylistParser(), - comparator); - final HlsMasterPlaylist reorderedPlaylist = (HlsMasterPlaylist) playlistParser.parse(playlistUri, inputStream); - assertNotNull(reorderedPlaylist); - - inputStream.reset(); - final HlsMasterPlaylist playlist = (HlsMasterPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream); - assertEquals(reorderedPlaylist.variants.get(0).format, playlist.variants.get(2).format); - } -} \ No newline at end of file diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/ReorderingHlsPlaylistParser.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/ReorderingHlsPlaylistParser.java deleted file mode 100644 index bdd47e8c28..0000000000 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/ReorderingHlsPlaylistParser.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.google.android.exoplayer2.source.hls.playlist; - -import android.net.Uri; - -import com.google.android.exoplayer2.upstream.ParsingLoadable; - -import java.io.IOException; -import java.io.InputStream; -import java.util.Comparator; - -/** - * Parser for {@link HlsPlaylist}s that reorders the variants based on the comparator passed. - */ -public class ReorderingHlsPlaylistParser implements ParsingLoadable.Parser { - private final ParsingLoadable.Parser playlistParser; - private final Comparator variantComparator; - - /** - * @param playlistParser the {@link ParsingLoadable.Parser} to wrap. - * @param variantComparator the {@link Comparator} to use to reorder the variants. - * See {@link HlsMasterPlaylist#copyWithReorderedVariants(Comparator)} for more details. - */ - public ReorderingHlsPlaylistParser(ParsingLoadable.Parser playlistParser, - Comparator variantComparator) { - this.playlistParser = playlistParser; - this.variantComparator = variantComparator; - } - - @Override - public HlsPlaylist parse(Uri uri, InputStream inputStream) throws IOException { - final HlsPlaylist playlist = playlistParser.parse(uri, inputStream); - - if (playlist instanceof HlsMasterPlaylist) { - return ((HlsMasterPlaylist) playlist).copyWithReorderedVariants(variantComparator); - } - - return playlist; - } -} From b8e719b101e529922485ff8afc3ad00c1401f7c9 Mon Sep 17 00:00:00 2001 From: Justin Yorke Date: Fri, 6 Oct 2017 14:52:29 -0700 Subject: [PATCH 0499/2472] Allow multiple PSSH boxes for same system. Updates DefaultDrmSessionManager to use the prefered Widevine version (v1 on >= 23 and v0 for < 23). For other DRM schemes, uses the first scheme found. --- .../drm/DefaultDrmSessionManager.java | 35 +++++++++++--- .../android/exoplayer2/drm/DrmInitData.java | 13 +++--- .../extractor/mp4/PsshAtomUtil.java | 46 +++++++++++++++---- .../exoplayer2/drm/DrmInitDataTest.java | 45 ++++++++++-------- 4 files changed, 98 insertions(+), 41 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java index a0d5a932f2..9125efa458 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java @@ -323,7 +323,7 @@ public class DefaultDrmSessionManager implements DrmSe // If there is no scheme information, assume patternless AES-CTR. return true; } else if (C.CENC_TYPE_cbc1.equals(schemeType) || C.CENC_TYPE_cbcs.equals(schemeType) - || C.CENC_TYPE_cens.equals(schemeType)) { + || C.CENC_TYPE_cens.equals(schemeType)) { // AES-CBC and pattern encryption are supported on API 24 onwards. return Util.SDK_INT >= 24; } @@ -331,6 +331,7 @@ public class DefaultDrmSessionManager implements DrmSe return true; } + @Override public DrmSession acquireSession(Looper playbackLooper, DrmInitData drmInitData) { Assertions.checkState(this.playbackLooper == null || this.playbackLooper == playbackLooper); @@ -435,12 +436,34 @@ public class DefaultDrmSessionManager implements DrmSe * @return The extracted {@link SchemeData}, or null if no suitable data is present. */ private static SchemeData getSchemeData(DrmInitData drmInitData, UUID uuid) { - SchemeData schemeData = drmInitData.get(uuid); - if (schemeData == null && C.CLEARKEY_UUID.equals(uuid)) { - // If present, the Common PSSH box should be used for ClearKey. - schemeData = drmInitData.get(C.COMMON_PSSH_UUID); + List schemeDatas = new ArrayList<>(); + // Look for matching PSSH boxes, or the common box in the case of ClearKey + for (int i = 0; i < drmInitData.schemeDataCount; ++i) { + SchemeData schemeData = drmInitData.get(i); + if (schemeData.matches(uuid) + || (C.CLEARKEY_UUID.equals(uuid) && schemeData.matches(C.COMMON_PSSH_UUID))) { + schemeDatas.add(schemeData); + } } - return schemeData; + + if (schemeDatas.isEmpty()) { + return null; + } + + // For Widevine, we prefer v1 init data on M and higher, v0 for lower + if (C.WIDEVINE_UUID.equals(uuid)) { + for (SchemeData schemeData : schemeDatas ) { + int version = PsshAtomUtil.parseVersion(schemeData.data); + if (Util.SDK_INT < 23 && version == 0) { + return schemeData; + } else if (Util.SDK_INT >= 23 && version == 1) { + return schemeData; + } + } + } + + // If we don't have any special handling for this system, we take the first scheme data found + return schemeDatas.get(0); } private static byte[] getSchemeInitData(SchemeData data, UUID uuid) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java index e346ab800f..e9aeb78f2d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java @@ -22,6 +22,8 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; + +import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.List; @@ -78,12 +80,7 @@ public final class DrmInitData implements Comparator, Parcelable { // Sorting ensures that universal scheme data (i.e. data that applies to all schemes) is matched // last. It's also required by the equals and hashcode implementations. Arrays.sort(schemeDatas, this); - // Check for no duplicates. - for (int i = 1; i < schemeDatas.length; i++) { - if (schemeDatas[i - 1].uuid.equals(schemeDatas[i].uuid)) { - throw new IllegalArgumentException("Duplicate data for uuid: " + schemeDatas[i].uuid); - } - } + this.schemeDatas = schemeDatas; schemeDataCount = schemeDatas.length; } @@ -97,9 +94,11 @@ public final class DrmInitData implements Comparator, Parcelable { /** * Retrieves data for a given DRM scheme, specified by its UUID. * + * @deprecated This will only get the first data found for the scheme. * @param uuid The DRM scheme's UUID. * @return The initialization data for the scheme, or null if the scheme is not supported. */ + @Deprecated public SchemeData get(UUID uuid) { for (SchemeData schemeData : schemeDatas) { if (schemeData.matches(uuid)) { @@ -112,7 +111,7 @@ public final class DrmInitData implements Comparator, Parcelable { /** * Retrieves the {@link SchemeData} at a given index. * - * @param index index of the scheme to return. + * @param index index of the scheme to return. Must not exceed {@link #schemeDataCount}. * @return The {@link SchemeData} at the index. */ public SchemeData get(int index) { diff --git a/library/core/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 index cfca015348..9854c57414 100644 --- a/library/core/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 @@ -86,11 +86,28 @@ public final class PsshAtomUtil { * an unsupported version. */ public static UUID parseUuid(byte[] atom) { - Pair parsedAtom = parsePsshAtom(atom); + PsshAtom parsedAtom = parsePsshAtom(atom); if (parsedAtom == null) { return null; } - return parsedAtom.first; + return parsedAtom.uuid; + } + + /** + * Parses the version from a PSSH atom. Version 0 and 1 PSSH atoms are supported. + *

          + * The UUID is only parsed if the data is a valid PSSH atom. + * + * @param atom The atom to parse. + * @return The parsed UUID. -1 if the input is not a valid PSSH atom, or if the PSSH atom has + * an unsupported version. + */ + public static int parseVersion(byte[] atom) { + PsshAtom parsedAtom = parsePsshAtom(atom); + if (parsedAtom == null) { + return -1; + } + return parsedAtom.version; } /** @@ -105,15 +122,15 @@ public final class PsshAtomUtil { * PSSH atom has an unsupported version, or if the PSSH atom does not match the passed UUID. */ public static byte[] parseSchemeSpecificData(byte[] atom, UUID uuid) { - Pair parsedAtom = parsePsshAtom(atom); + PsshAtom parsedAtom = parsePsshAtom(atom); if (parsedAtom == null) { return null; } - if (uuid != null && !uuid.equals(parsedAtom.first)) { - Log.w(TAG, "UUID mismatch. Expected: " + uuid + ", got: " + parsedAtom.first + "."); + if (uuid != null && !uuid.equals(parsedAtom.uuid)) { + Log.w(TAG, "UUID mismatch. Expected: " + uuid + ", got: " + parsedAtom.uuid + "."); return null; } - return parsedAtom.second; + return parsedAtom.data; } /** @@ -125,7 +142,7 @@ public final class PsshAtomUtil { * not a valid PSSH atom, or if the PSSH atom has an unsupported version. */ // TODO: Support parsing of the key ids for version 1 PSSH atoms. - private static Pair parsePsshAtom(byte[] atom) { + private static PsshAtom parsePsshAtom(byte[] atom) { ParsableByteArray atomData = new ParsableByteArray(atom); if (atomData.limit() < Atom.FULL_HEADER_SIZE + 16 /* UUID */ + 4 /* DataSize */) { // Data too short. @@ -159,7 +176,18 @@ public final class PsshAtomUtil { } byte[] data = new byte[dataSize]; atomData.readBytes(data, 0, dataSize); - return Pair.create(uuid, data); + return new PsshAtom(uuid, atomVersion, data); } -} + private static class PsshAtom { + final UUID uuid; + final int version; + final byte[] data; + + PsshAtom(final UUID uuid, final int version, final byte[] data) { + this.uuid = uuid; + this.version = version; + this.data = data; + } + } +} \ No newline at end of file diff --git a/library/core/src/test/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java b/library/core/src/test/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java index b6c068c218..610dd5368e 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java @@ -31,6 +31,9 @@ import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; +import java.util.ArrayList; +import java.util.List; + /** * Unit test for {@link DrmInitData}. */ @@ -97,6 +100,7 @@ public class DrmInitDataTest { } @Test + @Deprecated public void testGet() { // Basic matching. DrmInitData testInitData = new DrmInitData(DATA_1, DATA_2); @@ -124,27 +128,22 @@ public class DrmInitDataTest { } @Test - public void testDuplicateSchemeDataRejected() { - try { - new DrmInitData(DATA_1, DATA_1); - fail(); - } catch (IllegalArgumentException e) { - // Expected. - } + public void testGetByIndex() { + DrmInitData testInitData = new DrmInitData(DATA_1, DATA_2); + assertThat(getAllSchemeData(testInitData)).containsAllOf(DATA_1, DATA_2); + } - try { - new DrmInitData(DATA_1, DATA_1B); - fail(); - } catch (IllegalArgumentException e) { - // Expected. - } + @Test + public void testDuplicateSchemeData() { + DrmInitData testInitData = new DrmInitData(DATA_1, DATA_1); + assertThat(testInitData.schemeDataCount).isEqualTo(2); - try { - new DrmInitData(DATA_1, DATA_2, DATA_1B); - fail(); - } catch (IllegalArgumentException e) { - // Expected. - } + testInitData = new DrmInitData(DATA_1, DATA_2, DATA_1B); + assertThat(testInitData.schemeDataCount).isEqualTo(3); + assertThat(getAllSchemeData(testInitData)).containsAllOf(DATA_1, DATA_1B, DATA_2); + // Deprecated get method should return first entry + assertThat(testInitData.get(WIDEVINE_UUID)).isEqualTo(DATA_1); + assertThat(testInitData.get(PLAYREADY_UUID)).isEqualTo(DATA_2); } @Test @@ -162,4 +161,12 @@ public class DrmInitDataTest { assertThat(DATA_UNIVERSAL.matches(UUID_NIL)).isTrue(); } + private List getAllSchemeData(DrmInitData drmInitData) { + ArrayList schemeDatas = new ArrayList<>(); + for (int i = 0; i < drmInitData.schemeDataCount; i++) { + schemeDatas.add(drmInitData.get(i)); + } + return schemeDatas; + } + } From 5baddfb56a9db55bd87e2dbc296fd9e28d1c9558 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 3 Oct 2017 02:46:04 -0700 Subject: [PATCH 0500/2472] Add onSeekProcessed callback to Player interface. This is useful to determine when a seek request was processed by the player and all playback state changes (mostly to BUFFERING) have been performed. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=170826793 --- .../android/exoplayer2/demo/EventLogger.java | 5 +++ .../exoplayer2/ext/cast/CastPlayer.java | 23 +++++++----- .../android/exoplayer2/ExoPlayerTest.java | 36 +++++++++++++++++++ .../android/exoplayer2/ExoPlayerImpl.java | 12 +++++-- .../exoplayer2/ExoPlayerImplInternal.java | 17 ++++----- .../com/google/android/exoplayer2/Player.java | 12 +++++++ 6 files changed, 85 insertions(+), 20 deletions(-) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java index 5c2b40e630..68f7ddfd21 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java @@ -209,6 +209,11 @@ import java.util.Locale; Log.d(TAG, "]"); } + @Override + public void onSeekProcessed() { + Log.d(TAG, "seekProcessed"); + } + // MetadataOutput @Override diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java index 9d3636f8ac..ffb06ed232 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java @@ -41,7 +41,6 @@ import com.google.android.gms.cast.framework.SessionManager; import com.google.android.gms.cast.framework.SessionManagerListener; import com.google.android.gms.cast.framework.media.RemoteMediaClient; import com.google.android.gms.cast.framework.media.RemoteMediaClient.MediaChannelResult; -import com.google.android.gms.common.api.CommonStatusCodes; import com.google.android.gms.common.api.PendingResult; import com.google.android.gms.common.api.ResultCallback; import java.util.List; @@ -115,6 +114,7 @@ public final class CastPlayer implements Player { private int currentWindowIndex; private boolean playWhenReady; private long lastReportedPositionMs; + private int pendingSeekCount; private int pendingSeekWindowIndex; private long pendingSeekPositionMs; @@ -333,11 +333,16 @@ public final class CastPlayer implements Player { } else { remoteMediaClient.seek(positionMs).setResultCallback(seekResultCallback); } + pendingSeekCount++; pendingSeekWindowIndex = windowIndex; pendingSeekPositionMs = positionMs; for (EventListener listener : listeners) { listener.onPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK); } + } else if (pendingSeekCount == 0) { + for (EventListener listener : listeners) { + listener.onSeekProcessed(); + } } } @@ -536,7 +541,7 @@ public final class CastPlayer implements Player { } } int currentWindowIndex = fetchCurrentWindowIndex(getMediaStatus()); - if (this.currentWindowIndex != currentWindowIndex) { + if (this.currentWindowIndex != currentWindowIndex && pendingSeekCount == 0) { this.currentWindowIndex = currentWindowIndex; for (EventListener listener : listeners) { listener.onPositionDiscontinuity(DISCONTINUITY_REASON_PERIOD_TRANSITION); @@ -831,18 +836,18 @@ public final class CastPlayer implements Player { @Override public void onResult(@NonNull MediaChannelResult result) { int statusCode = result.getStatus().getStatusCode(); - if (statusCode == CastStatusCodes.REPLACED) { - // A seek was executed before this one completed. Do nothing. - } else { + if (statusCode != CastStatusCodes.SUCCESS && statusCode != CastStatusCodes.REPLACED) { + Log.e(TAG, "Seek failed. Error code " + statusCode + ": " + + CastUtils.getLogString(statusCode)); + } + if (--pendingSeekCount == 0) { pendingSeekWindowIndex = C.INDEX_UNSET; pendingSeekPositionMs = C.TIME_UNSET; - if (statusCode != CommonStatusCodes.SUCCESS) { - Log.e(TAG, "Seek failed. Error code " + statusCode + ": " - + CastUtils.getLogString(statusCode)); + for (EventListener listener : listeners) { + listener.onSeekProcessed(); } } } - } } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index 9f172dc802..2971aaf779 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -28,6 +28,8 @@ import com.google.android.exoplayer2.testutil.FakeRenderer; import com.google.android.exoplayer2.testutil.FakeShuffleOrder; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.CountDownLatch; import junit.framework.TestCase; @@ -259,4 +261,38 @@ public final class ExoPlayerTest extends TestCase { assertTrue(renderer.isEnded); } + public void testSeekProcessedCallback() throws Exception { + Timeline timeline = new FakeTimeline( + new TimelineWindowDefinition(true, false, 100000), + new TimelineWindowDefinition(true, false, 100000)); + ActionSchedule actionSchedule = new ActionSchedule.Builder("testSeekProcessedCallback") + // Initial seek before timeline preparation finished. + .pause().seek(10).waitForPlaybackState(Player.STATE_READY) + // Re-seek to same position, start playback and wait until playback reaches second window. + .seek(10).play().waitForPositionDiscontinuity() + // Seek twice in concession, expecting the first seek to be replaced. + .seek(5).seek(60).build(); + final List playbackStatesWhenSeekProcessed = new ArrayList<>(); + Player.EventListener eventListener = new Player.DefaultEventListener() { + private int currentPlaybackState = Player.STATE_IDLE; + + @Override + public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + currentPlaybackState = playbackState; + } + + @Override + public void onSeekProcessed() { + playbackStatesWhenSeekProcessed.add(currentPlaybackState); + } + }; + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline).setEventListener(eventListener).setActionSchedule(actionSchedule) + .build().start().blockUntilEnded(TIMEOUT_MS); + assertEquals(3, playbackStatesWhenSeekProcessed.size()); + assertEquals(Player.STATE_BUFFERING, (int) playbackStatesWhenSeekProcessed.get(0)); + assertEquals(Player.STATE_READY, (int) playbackStatesWhenSeekProcessed.get(1)); + assertEquals(Player.STATE_BUFFERING, (int) playbackStatesWhenSeekProcessed.get(2)); + } + } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index 75e08aadc6..2222660469 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -482,10 +482,11 @@ import java.util.concurrent.CopyOnWriteArraySet; maskingWindowIndex = 0; maskingWindowPositionMs = 0; } - if (msg.arg1 != 0) { - for (Player.EventListener listener : listeners) { - listener.onPositionDiscontinuity(DISCONTINUITY_REASON_SEEK); + for (Player.EventListener listener : listeners) { + if (msg.arg1 != 0) { + listener.onPositionDiscontinuity(DISCONTINUITY_REASON_INTERNAL); } + listener.onSeekProcessed(); } } break; @@ -516,6 +517,11 @@ import java.util.concurrent.CopyOnWriteArraySet; listener.onTimelineChanged(timeline, manifest); } } + if (pendingSeekAcks == 0 && sourceInfo.seekAcks > 0) { + for (Player.EventListener listener : listeners) { + listener.onSeekProcessed(); + } + } break; } case ExoPlayerImplInternal.MSG_PLAYBACK_PARAMETERS_CHANGED: { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 838a44b4ee..765b2a7634 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -701,12 +701,12 @@ import java.io.IOException; timeline.getFirstWindowIndex(shuffleModeEnabled), window).firstPeriodIndex; // The seek position was valid for the timeline that it was performed into, but the // timeline has changed and a suitable seek position could not be resolved in the new one. - playbackInfo = new PlaybackInfo(firstPeriodIndex, 0); - eventHandler.obtainMessage(MSG_SEEK_ACK, 1, 0, playbackInfo).sendToTarget(); // Set the internal position to (firstPeriodIndex,TIME_UNSET) so that a subsequent seek to // (firstPeriodIndex,0) isn't ignored. playbackInfo = new PlaybackInfo(firstPeriodIndex, C.TIME_UNSET); setState(Player.STATE_ENDED); + eventHandler.obtainMessage(MSG_SEEK_ACK, 1, 0, new PlaybackInfo(firstPeriodIndex, 0)) + .sendToTarget(); // Reset, but retain the source so that it can still be used should a seek occur. resetInternal(false); return; @@ -1031,7 +1031,7 @@ import java.io.IOException; MediaPeriodId periodId = mediaPeriodInfoSequence.resolvePeriodPositionForAds(periodIndex, positionUs); playbackInfo = new PlaybackInfo(periodId, periodId.isAd() ? 0 : positionUs, positionUs); - notifySourceInfoRefresh(manifest, processedInitialSeekCount); + notifySourceInfoRefresh(manifest, playbackInfo, processedInitialSeekCount); } } else if (playbackInfo.startPositionUs == C.TIME_UNSET) { if (timeline.isEmpty()) { @@ -1182,22 +1182,23 @@ import java.io.IOException; int processedInitialSeekCount) { int firstPeriodIndex = timeline.isEmpty() ? 0 : timeline.getWindow( timeline.getFirstWindowIndex(shuffleModeEnabled), window).firstPeriodIndex; - // Set the playback position to (firstPeriodIndex,0) for notifying the eventHandler. - playbackInfo = new PlaybackInfo(firstPeriodIndex, 0); - notifySourceInfoRefresh(manifest, processedInitialSeekCount); // Set the internal position to (firstPeriodIndex,TIME_UNSET) so that a subsequent seek to // (firstPeriodIndex,0) isn't ignored. playbackInfo = new PlaybackInfo(firstPeriodIndex, C.TIME_UNSET); setState(Player.STATE_ENDED); + // Set the playback position to (firstPeriodIndex,0) for notifying the eventHandler. + notifySourceInfoRefresh(manifest, new PlaybackInfo(firstPeriodIndex, 0), + processedInitialSeekCount); // Reset, but retain the source so that it can still be used should a seek occur. resetInternal(false); } private void notifySourceInfoRefresh(Object manifest) { - notifySourceInfoRefresh(manifest, 0); + notifySourceInfoRefresh(manifest, playbackInfo, 0); } - private void notifySourceInfoRefresh(Object manifest, int processedInitialSeekCount) { + private void notifySourceInfoRefresh(Object manifest, PlaybackInfo playbackInfo, + int processedInitialSeekCount) { eventHandler.obtainMessage(MSG_SOURCE_INFO_REFRESHED, new SourceInfo(timeline, manifest, playbackInfo, processedInitialSeekCount)).sendToTarget(); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Player.java b/library/core/src/main/java/com/google/android/exoplayer2/Player.java index 3dd702b85f..983ef878f2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Player.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Player.java @@ -134,6 +134,13 @@ public interface Player { */ void onPlaybackParametersChanged(PlaybackParameters playbackParameters); + /** + * Called when all pending seek requests have been processed by the player. This is guaranteed + * to happen after any necessary changes to the player state were reported to + * {@link #onPlayerStateChanged(boolean, int)}. + */ + void onSeekProcessed(); + } /** @@ -186,6 +193,11 @@ public interface Player { // Do nothing. } + @Override + public void onSeekProcessed() { + // Do nothing. + } + } /** From ad500ca87110d4a43eb1ac56298743f6522278a4 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 3 Oct 2017 03:01:33 -0700 Subject: [PATCH 0501/2472] Replace IMA ad state booleans with an @IntDef imaPausedInAd could only be true when imaPlayingAd was true, so there are only three possible states. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=170827974 --- .../exoplayer2/ext/ima/ImaAdsLoader.java | 110 ++++++++++-------- 1 file changed, 61 insertions(+), 49 deletions(-) diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index a411da0133..6b20404a39 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.ext.ima; import android.content.Context; import android.net.Uri; import android.os.SystemClock; +import android.support.annotation.IntDef; import android.util.Log; import android.view.ViewGroup; import android.webkit.WebView; @@ -49,6 +50,8 @@ import com.google.android.exoplayer2.source.ads.AdPlaybackState; import com.google.android.exoplayer2.source.ads.AdsLoader; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -88,6 +91,25 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A private static final String FOCUS_SKIP_BUTTON_WORKAROUND_JS = "javascript:" + "try{ document.getElementsByClassName(\"videoAdUiSkipButton\")[0].focus(); } catch (e) {}"; + /** + * The state of ad playback based on IMA's calls to {@link #playAd()} and {@link #pauseAd()}. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({IMA_AD_STATE_NONE, IMA_AD_STATE_PLAYING, IMA_AD_STATE_PAUSED}) + private @interface ImaAdState {} + /** + * The ad playback state when IMA is not playing an ad. + */ + private static final int IMA_AD_STATE_NONE = 0; + /** + * The ad playback state when IMA has called {@link #playAd()} and not {@link #pauseAd()}. + */ + private static final int IMA_AD_STATE_PLAYING = 1; + /** + * The ad playback state when IMA has called {@link #pauseAd()} while playing an ad. + */ + private static final int IMA_AD_STATE_PAUSED = 2; + private final Uri adTagUri; private final Timeline.Period period; private final List adCallbacks; @@ -117,15 +139,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A */ private boolean imaPausedContent; /** - * If {@link #playingAd} is set, stores whether IMA has called {@link #playAd()} and not - * {@link #stopAd()}. + * The current ad playback state based on IMA's calls to {@link #playAd()} and {@link #stopAd()}. */ - private boolean imaPlayingAd; - /** - * If {@link #playingAd} is set, stores whether IMA has called {@link #pauseAd()} since a - * preceding call to {@link #playAd()} for the current ad. - */ - private boolean imaPausedInAd; + private @ImaAdState int imaAdState; /** * Whether {@link com.google.ads.interactivemedia.v3.api.AdsLoader#contentComplete()} has been * called since starting ad playback. @@ -439,21 +455,23 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A playWhenReadyOverriddenForAds = true; player.setPlayWhenReady(true); } - if (imaPlayingAd && !imaPausedInAd) { - // Work around an issue where IMA does not always call stopAd before resuming content. - // See [Internal: b/38354028, b/63320878]. - Log.w(TAG, "Unexpected playAd without stopAd"); - } - if (!imaPlayingAd) { - imaPlayingAd = true; - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onPlay(); - } - } else if (imaPausedInAd) { - imaPausedInAd = false; - for (int i = 0; i < adCallbacks.size(); i++) { - adCallbacks.get(i).onResume(); - } + switch (imaAdState) { + case IMA_AD_STATE_PLAYING: + // IMA does not always call stopAd before resuming content. + // See [Internal: b/38354028, b/63320878]. + Log.w(TAG, "Unexpected playAd without stopAd"); + break; + case IMA_AD_STATE_NONE: + imaAdState = IMA_AD_STATE_PLAYING; + for (int i = 0; i < adCallbacks.size(); i++) { + adCallbacks.get(i).onPlay(); + } + break; + case IMA_AD_STATE_PAUSED: + imaAdState = IMA_AD_STATE_PLAYING; + for (int i = 0; i < adCallbacks.size(); i++) { + adCallbacks.get(i).onResume(); + } } } @@ -466,7 +484,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A // Sometimes messages from IMA arrive after detaching the player. See [Internal: b/63801642]. Log.w(TAG, "Unexpected stopAd while detached"); } - if (!imaPlayingAd) { + if (imaAdState == IMA_AD_STATE_NONE) { Log.w(TAG, "Unexpected stopAd"); return; } @@ -478,11 +496,11 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A if (DEBUG) { Log.d(TAG, "pauseAd"); } - if (!imaPlayingAd) { + if (imaAdState == IMA_AD_STATE_NONE) { // This method is called after content is resumed. return; } - imaPausedInAd = true; + imaAdState = IMA_AD_STATE_PAUSED; for (int i = 0; i < adCallbacks.size(); i++) { adCallbacks.get(i).onPause(); } @@ -514,9 +532,10 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A return; } - if (!imaPlayingAd && playbackState == Player.STATE_BUFFERING && playWhenReady) { + if (imaAdState == IMA_AD_STATE_NONE && playbackState == Player.STATE_BUFFERING + && playWhenReady) { checkForContentComplete(); - } else if (imaPlayingAd && playbackState == Player.STATE_ENDED) { + } else if (imaAdState != IMA_AD_STATE_NONE && playbackState == Player.STATE_ENDED) { // IMA is waiting for the ad playback to finish so invoke the callback now. // Either CONTENT_RESUME_REQUESTED will be passed next, or playAd will be called again. for (int i = 0; i < adCallbacks.size(); i++) { @@ -603,7 +622,8 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A } private void resumeContentInternal() { - if (imaPlayingAd) { + if (imaAdState != IMA_AD_STATE_NONE) { + imaAdState = IMA_AD_STATE_NONE; if (DEBUG) { Log.d(TAG, "Unexpected CONTENT_RESUME_REQUESTED without stopAd"); } @@ -613,10 +633,10 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A adGroupIndex = C.INDEX_UNSET; updateAdPlaybackState(); } - clearFlags(); } private void pauseContentInternal() { + imaAdState = IMA_AD_STATE_NONE; if (sentPendingContentPositionMs) { pendingContentPositionMs = C.TIME_UNSET; sentPendingContentPositionMs = false; @@ -624,24 +644,16 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A // IMA is requesting to pause content, so stop faking the content position. fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET; fakeContentProgressOffsetMs = C.TIME_UNSET; - clearFlags(); } private void stopAdInternal() { - Assertions.checkState(imaPlayingAd); + Assertions.checkState(imaAdState != IMA_AD_STATE_NONE); + imaAdState = IMA_AD_STATE_NONE; adPlaybackState.playedAd(adGroupIndex); updateAdPlaybackState(); if (!playingAd) { adGroupIndex = C.INDEX_UNSET; } - clearFlags(); - } - - private void clearFlags() { - // If an ad is displayed, these flags will be updated in response to playAd/pauseAd/stopAd until - // the content is resumed. - imaPlayingAd = false; - imaPausedInAd = false; } private void checkForContentComplete() { @@ -663,6 +675,15 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A } } + private void focusSkipButton() { + if (playingAd && adUiViewGroup != null && adUiViewGroup.getChildCount() > 0 + && adUiViewGroup.getChildAt(0) instanceof WebView) { + WebView webView = (WebView) (adUiViewGroup.getChildAt(0)); + webView.requestFocus(); + webView.loadUrl(FOCUS_SKIP_BUTTON_WORKAROUND_JS); + } + } + private static long[] getAdGroupTimesUs(List cuePoints) { if (cuePoints.isEmpty()) { // If no cue points are specified, there is a preroll ad. @@ -679,13 +700,4 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A return adGroupTimesUs; } - private void focusSkipButton() { - if (playingAd && adUiViewGroup != null && adUiViewGroup.getChildCount() > 0 - && adUiViewGroup.getChildAt(0) instanceof WebView) { - WebView webView = (WebView) (adUiViewGroup.getChildAt(0)); - webView.requestFocus(); - webView.loadUrl(FOCUS_SKIP_BUTTON_WORKAROUND_JS); - } - } - } From 45c1f0838394a386d7e1af4b9ea10d2cbe5dbbee Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 3 Oct 2017 03:37:26 -0700 Subject: [PATCH 0502/2472] Annotate the content type local ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=170831088 --- .../com/google/android/exoplayer2/demo/PlayerActivity.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index c0838390ed..716af02c01 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -34,6 +34,7 @@ import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.C.ContentType; import com.google.android.exoplayer2.DefaultRenderersFactory; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayerFactory; @@ -357,7 +358,7 @@ public class PlayerActivity extends Activity implements OnClickListener, } private MediaSource buildMediaSource(Uri uri, String overrideExtension) { - int type = TextUtils.isEmpty(overrideExtension) ? Util.inferContentType(uri) + @ContentType int type = TextUtils.isEmpty(overrideExtension) ? Util.inferContentType(uri) : Util.inferContentType("." + overrideExtension); switch (type) { case C.TYPE_SS: From b3d462df398578836139f14cd708230fdf8a9b04 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 4 Oct 2017 00:59:14 -0700 Subject: [PATCH 0503/2472] Catch up video rendering by dropping to keyframes If the current output buffer is very late and the playback position is in a later group of pictures, drop all buffers to the keyframe preceding the playback position. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=170975259 --- .../exoplayer2/decoder/DecoderCounters.java | 9 ++ .../video/MediaCodecVideoRenderer.java | 107 ++++++++++++++++-- .../exoplayer2/ui/DebugTextViewHelper.java | 3 +- .../testutil/DebugRenderersFactory.java | 2 +- 4 files changed, 111 insertions(+), 10 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/decoder/DecoderCounters.java b/library/core/src/main/java/com/google/android/exoplayer2/decoder/DecoderCounters.java index 7a532110d3..e1dff12e52 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/decoder/DecoderCounters.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/decoder/DecoderCounters.java @@ -65,6 +65,14 @@ public final class DecoderCounters { * Skipped output buffers are ignored for the purposes of calculating this value. */ public int maxConsecutiveDroppedOutputBufferCount; + /** + * The number of times all buffers to a keyframe were dropped. + *

          + * Each time buffers to a keyframe are dropped, this counter is increased by one, and the dropped + * output buffer counters are increased by one (for the current output buffer) plus the number of + * buffers dropped from the source to advance to the keyframe. + */ + public int droppedToKeyframeCount; /** * Should be called to ensure counter values are made visible across threads. The playback thread @@ -91,6 +99,7 @@ public final class DecoderCounters { droppedOutputBufferCount += other.droppedOutputBufferCount; maxConsecutiveDroppedOutputBufferCount = Math.max(maxConsecutiveDroppedOutputBufferCount, other.maxConsecutiveDroppedOutputBufferCount); + droppedToKeyframeCount += other.droppedToKeyframeCount; } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index 4c1f4c0eb2..84073f9338 100644 --- a/library/core/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.CallSuper; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Log; @@ -85,10 +86,12 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { @C.VideoScalingMode private int scalingMode; private boolean renderedFirstFrame; + private boolean forceRenderFrame; private long joiningDeadlineMs; private long droppedFrameAccumulationStartTimeMs; private int droppedFrames; private int consecutiveDroppedFrameCount; + private int buffersInCodecCount; private int pendingRotationDegrees; private float pendingPixelWidthHeightRatio; @@ -414,11 +417,14 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } } + @CallSuper @Override protected void releaseCodec() { try { super.releaseCodec(); } finally { + buffersInCodecCount = 0; + forceRenderFrame = false; if (dummySurface != null) { if (surface == dummySurface) { surface = null; @@ -429,6 +435,14 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } } + @CallSuper + @Override + protected void flushCodec() throws ExoPlaybackException { + super.flushCodec(); + buffersInCodecCount = 0; + forceRenderFrame = false; + } + @Override protected void onCodecInitialized(String name, long initializedTimestampMs, long initializationDurationMs) { @@ -444,8 +458,15 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { pendingRotationDegrees = getRotationDegrees(newFormat); } + /** + * Called immediately before an input buffer is queued into the codec. + * + * @param buffer The buffer to be queued. + */ + @CallSuper @Override protected void onQueueInputBuffer(DecoderInputBuffer buffer) { + buffersInCodecCount++; if (Util.SDK_INT < 23 && tunneling) { maybeNotifyRenderedFirstFrame(); } @@ -492,7 +513,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { @Override protected boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs, MediaCodec codec, ByteBuffer buffer, int bufferIndex, int bufferFlags, long bufferPresentationTimeUs, - boolean shouldSkip) { + boolean shouldSkip) throws ExoPlaybackException { while (pendingOutputStreamOffsetCount != 0 && bufferPresentationTimeUs >= pendingOutputStreamOffsetsUs[0]) { outputStreamOffsetUs = pendingOutputStreamOffsetsUs[0]; @@ -517,7 +538,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { return false; } - if (!renderedFirstFrame) { + if (!renderedFirstFrame || forceRenderFrame) { + forceRenderFrame = false; if (Util.SDK_INT >= 21) { renderOutputBufferV21(codec, bufferIndex, presentationTimeUs, System.nanoTime()); } else { @@ -544,7 +566,11 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { bufferPresentationTimeUs, unadjustedFrameReleaseTimeNs); earlyUs = (adjustedReleaseTimeNs - systemTimeNs) / 1000; - if (shouldDropOutputBuffer(earlyUs, elapsedRealtimeUs)) { + if (shouldDropBuffersToKeyframe(earlyUs, elapsedRealtimeUs) + && maybeDropBuffersToKeyframe(codec, bufferIndex, presentationTimeUs, positionUs)) { + forceRenderFrame = true; + return true; + } else if (shouldDropOutputBuffer(earlyUs, elapsedRealtimeUs)) { dropOutputBuffer(codec, bufferIndex, presentationTimeUs); return true; } @@ -577,6 +603,17 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { return false; } + /** + * Called when an output buffer is successfully processed. + * + * @param presentationTimeUs The timestamp associated with the output buffer. + */ + @CallSuper + @Override + protected void onProcessedOutputBuffer(long presentationTimeUs) { + buffersInCodecCount--; + } + /** * Returns whether the buffer being processed should be dropped. * @@ -589,6 +626,19 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { return isBufferLate(earlyUs); } + /** + * Returns whether to drop all buffers from the buffer being processed to the keyframe at or after + * the current playback position, if possible. + * + * @param earlyUs The time until the current buffer should be presented in microseconds. A + * negative value indicates that the buffer is late. + * @param elapsedRealtimeUs {@link android.os.SystemClock#elapsedRealtime()} in microseconds, + * measured at the start of the current iteration of the rendering loop. + */ + protected boolean shouldDropBuffersToKeyframe(long earlyUs, long elapsedRealtimeUs) { + return isBufferVeryLate(earlyUs); + } + /** * Skips the output buffer with the specified index. * @@ -614,12 +664,48 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { TraceUtil.beginSection("dropVideoBuffer"); codec.releaseOutputBuffer(index, false); TraceUtil.endSection(); - decoderCounters.droppedOutputBufferCount++; - droppedFrames++; - consecutiveDroppedFrameCount++; + updateDroppedBufferCounters(1); + } + + /** + * Drops frames from the current output buffer to the next keyframe at or before the playback + * position. If no such keyframe exists, as the playback position is inside the same group of + * pictures as the buffer being processed, returns {@code false}. Returns {@code true} otherwise. + * + * @param codec The codec that owns the output buffer. + * @param index The index of the output buffer to drop. + * @param presentationTimeUs The presentation time of the output buffer, in microseconds. + * @param positionUs The current playback position, in microseconds. + * @return Whether any buffers were dropped. + * @throws ExoPlaybackException If an error occurs flushing the codec. + */ + protected boolean maybeDropBuffersToKeyframe(MediaCodec codec, int index, long presentationTimeUs, + long positionUs) throws ExoPlaybackException { + int droppedSourceBufferCount = skipSource(positionUs); + if (droppedSourceBufferCount == 0) { + return false; + } + decoderCounters.droppedToKeyframeCount++; + // We dropped some buffers to catch up, so update the decoder counters and flush the codec, + // which releases all pending buffers buffers including the current output buffer. + updateDroppedBufferCounters(buffersInCodecCount + droppedSourceBufferCount); + flushCodec(); + return true; + } + + /** + * Updates decoder counters to reflect that {@code droppedBufferCount} additional buffers were + * dropped. + * + * @param droppedBufferCount The number of additional dropped buffers. + */ + protected void updateDroppedBufferCounters(int droppedBufferCount) { + decoderCounters.droppedOutputBufferCount += droppedBufferCount; + droppedFrames += droppedBufferCount; + consecutiveDroppedFrameCount += droppedBufferCount; decoderCounters.maxConsecutiveDroppedOutputBufferCount = Math.max(consecutiveDroppedFrameCount, decoderCounters.maxConsecutiveDroppedOutputBufferCount); - if (droppedFrames == maxDroppedFramesToNotify) { + if (droppedFrames >= maxDroppedFramesToNotify) { maybeNotifyDroppedFrames(); } } @@ -740,10 +826,15 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } private static boolean isBufferLate(long earlyUs) { - // Class a buffer as late if it should have been presented more than 30ms ago. + // Class a buffer as late if it should have been presented more than 30 ms ago. return earlyUs < -30000; } + private static boolean isBufferVeryLate(long earlyUs) { + // Class a buffer as very late if it should have been presented more than 500 ms ago. + return earlyUs < -500000; + } + @TargetApi(23) private static void setOutputSurfaceV23(MediaCodec codec, Surface surface) { codec.setOutputSurface(surface); diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java index f443c2a06f..9d9272f10e 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java @@ -155,7 +155,8 @@ public final class DebugTextViewHelper extends Player.DefaultEventListener imple + " sb:" + counters.skippedOutputBufferCount + " rb:" + counters.renderedOutputBufferCount + " db:" + counters.droppedOutputBufferCount - + " mcdb:" + counters.maxConsecutiveDroppedOutputBufferCount; + + " mcdb:" + counters.maxConsecutiveDroppedOutputBufferCount + + " dk:" + counters.droppedToKeyframeCount; } private static String getPixelAspectRatioString(float pixelAspectRatio) { diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java index b63afd3984..392a4907d4 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java @@ -124,7 +124,7 @@ public class DebugRenderersFactory extends DefaultRenderersFactory { @Override protected boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs, MediaCodec codec, ByteBuffer buffer, int bufferIndex, int bufferFlags, long bufferPresentationTimeUs, - boolean shouldSkip) { + boolean shouldSkip) throws ExoPlaybackException { if (skipToPositionBeforeRenderingFirstFrame && bufferPresentationTimeUs < positionUs) { // After the codec has been initialized, don't render the first frame until we've caught up // to the playback position. Else test runs on devices that do not support dummy surface From 498ff144395b511dbeb1865cc60027cfd456fb26 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 4 Oct 2017 02:04:03 -0700 Subject: [PATCH 0504/2472] Add @IntDef annotations to DefaultEventListener ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=170980737 --- .../src/main/java/com/google/android/exoplayer2/Player.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Player.java b/library/core/src/main/java/com/google/android/exoplayer2/Player.java index 983ef878f2..af653ec2bd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Player.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Player.java @@ -169,7 +169,7 @@ public interface Player { } @Override - public void onRepeatModeChanged(int repeatMode) { + public void onRepeatModeChanged(@RepeatMode int repeatMode) { // Do nothing. } @@ -184,7 +184,7 @@ public interface Player { } @Override - public void onPositionDiscontinuity(int reason) { + public void onPositionDiscontinuity(@DiscontinuityReason int reason) { // Do nothing. } From d7b4f8a645e0751be6c4500ddd185762683be2a1 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 4 Oct 2017 07:14:06 -0700 Subject: [PATCH 0505/2472] Amend seek action in ActionSchedule to optionally wait until playback resumes. This allows more deterministic action schedules, especially for real media which may take an arbitrary amount of time to rebuffer after seeking. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=171005231 --- .../gts/CommonEncryptionDrmTest.java | 4 +-- .../playbacktests/gts/DashStreamingTest.java | 6 ++-- .../android/exoplayer2/testutil/Action.java | 33 +++++++++++++++++++ .../exoplayer2/testutil/ActionSchedule.java | 13 ++++++++ 4 files changed, 51 insertions(+), 5 deletions(-) diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/CommonEncryptionDrmTest.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/CommonEncryptionDrmTest.java index 15acae96fd..8c6285cef3 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/CommonEncryptionDrmTest.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/CommonEncryptionDrmTest.java @@ -39,8 +39,8 @@ public final class CommonEncryptionDrmTest extends ActivityInstrumentationTestCa // Seeks help reproduce playback issues in certain devices. private static final ActionSchedule ACTION_SCHEDULE_WITH_SEEKS = new ActionSchedule.Builder(TAG) - .delay(30000).seek(300000).delay(10000).seek(270000).delay(10000).seek(200000).delay(10000) - .seek(732000).build(); + .delay(30000).seekAndWait(300000).delay(10000).seekAndWait(270000).delay(10000) + .seekAndWait(200000).delay(10000).seekAndWait(732000).build(); private DashTestRunner testRunner; diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashStreamingTest.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashStreamingTest.java index 529f57582e..251cf7b56b 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashStreamingTest.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashStreamingTest.java @@ -33,10 +33,10 @@ public final class DashStreamingTest extends ActivityInstrumentationTestCase2 Date: Wed, 4 Oct 2017 07:59:09 -0700 Subject: [PATCH 0506/2472] Wait until playback started in certain action schedules. Some action schedules (especially those for real media with potentially long initial buffering times) don't wait until the playback started before executing the rest of the schedule. Added waitForPlaybackStateChanged(STATE_READY) to all applicable action schedules. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=171009434 --- .../playbacktests/gts/CommonEncryptionDrmTest.java | 6 ++++-- .../exoplayer2/playbacktests/gts/DashStreamingTest.java | 3 +++ .../playbacktests/gts/DashWidevineOfflineTest.java | 2 ++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/CommonEncryptionDrmTest.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/CommonEncryptionDrmTest.java index 8c6285cef3..a590e45f5f 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/CommonEncryptionDrmTest.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/CommonEncryptionDrmTest.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.playbacktests.gts; import android.test.ActivityInstrumentationTestCase2; +import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.testutil.ActionSchedule; import com.google.android.exoplayer2.testutil.HostActivity; import com.google.android.exoplayer2.util.MimeTypes; @@ -39,8 +40,9 @@ public final class CommonEncryptionDrmTest extends ActivityInstrumentationTestCa // Seeks help reproduce playback issues in certain devices. private static final ActionSchedule ACTION_SCHEDULE_WITH_SEEKS = new ActionSchedule.Builder(TAG) - .delay(30000).seekAndWait(300000).delay(10000).seekAndWait(270000).delay(10000) - .seekAndWait(200000).delay(10000).seekAndWait(732000).build(); + .waitForPlaybackState(Player.STATE_READY).delay(30000).seekAndWait(300000).delay(10000) + .seekAndWait(270000).delay(10000).seekAndWait(200000).delay(10000).seekAndWait(732000) + .build(); private DashTestRunner testRunner; diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashStreamingTest.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashStreamingTest.java index 251cf7b56b..39cdc7ee43 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashStreamingTest.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashStreamingTest.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.playbacktests.gts; import android.test.ActivityInstrumentationTestCase2; import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.mediacodec.MediaCodecInfo; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; @@ -33,12 +34,14 @@ public final class DashStreamingTest extends ActivityInstrumentationTestCase2 Date: Sat, 15 Jul 2017 00:49:09 +0100 Subject: [PATCH 0507/2472] Rename droppedOutputBufferCount Now this counter includes input buffers too, which are dropped as part of skipping to keyframes for catch up. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=171119930 --- .../ext/vp9/LibvpxVideoRenderer.java | 6 ++--- .../exoplayer2/decoder/DecoderCounters.java | 20 ++++++++-------- .../video/MediaCodecVideoRenderer.java | 6 ++--- .../exoplayer2/ui/DebugTextViewHelper.java | 4 ++-- .../playbacktests/gts/DashTestRunner.java | 14 +++++------ .../testutil/DecoderCountersUtil.java | 23 +++++++++---------- 6 files changed, 36 insertions(+), 37 deletions(-) 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 100ca6f00f..ec18db2470 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 @@ -342,11 +342,11 @@ public final class LibvpxVideoRenderer extends BaseRenderer { } private void dropBuffer() { - decoderCounters.droppedOutputBufferCount++; + decoderCounters.droppedBufferCount++; droppedFrames++; consecutiveDroppedFrameCount++; - decoderCounters.maxConsecutiveDroppedOutputBufferCount = Math.max( - consecutiveDroppedFrameCount, decoderCounters.maxConsecutiveDroppedOutputBufferCount); + decoderCounters.maxConsecutiveDroppedBufferCount = Math.max( + consecutiveDroppedFrameCount, decoderCounters.maxConsecutiveDroppedBufferCount); if (droppedFrames == maxDroppedFramesToNotify) { maybeNotifyDroppedFrames(); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/decoder/DecoderCounters.java b/library/core/src/main/java/com/google/android/exoplayer2/decoder/DecoderCounters.java index e1dff12e52..8409bab558 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/decoder/DecoderCounters.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/decoder/DecoderCounters.java @@ -53,24 +53,24 @@ public final class DecoderCounters { */ public int skippedOutputBufferCount; /** - * The number of dropped output buffers. + * The number of dropped buffers. *

          - * A dropped output buffer is an output buffer that was supposed to be rendered, but was instead + * A dropped buffer is an buffer that was supposed to be decoded/rendered, but was instead * dropped because it could not be rendered in time. */ - public int droppedOutputBufferCount; + public int droppedBufferCount; /** - * The maximum number of dropped output buffers without an interleaving rendered output buffer. + * The maximum number of dropped buffers without an interleaving rendered output buffer. *

          * Skipped output buffers are ignored for the purposes of calculating this value. */ - public int maxConsecutiveDroppedOutputBufferCount; + public int maxConsecutiveDroppedBufferCount; /** * The number of times all buffers to a keyframe were dropped. *

          * Each time buffers to a keyframe are dropped, this counter is increased by one, and the dropped - * output buffer counters are increased by one (for the current output buffer) plus the number of - * buffers dropped from the source to advance to the keyframe. + * buffer counters are increased by one (for the current output buffer) plus the number of buffers + * dropped from the source to advance to the keyframe. */ public int droppedToKeyframeCount; @@ -96,9 +96,9 @@ public final class DecoderCounters { skippedInputBufferCount += other.skippedInputBufferCount; renderedOutputBufferCount += other.renderedOutputBufferCount; skippedOutputBufferCount += other.skippedOutputBufferCount; - droppedOutputBufferCount += other.droppedOutputBufferCount; - maxConsecutiveDroppedOutputBufferCount = Math.max(maxConsecutiveDroppedOutputBufferCount, - other.maxConsecutiveDroppedOutputBufferCount); + droppedBufferCount += other.droppedBufferCount; + maxConsecutiveDroppedBufferCount = Math.max(maxConsecutiveDroppedBufferCount, + other.maxConsecutiveDroppedBufferCount); droppedToKeyframeCount += other.droppedToKeyframeCount; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index 84073f9338..cb443e38ee 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -700,11 +700,11 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { * @param droppedBufferCount The number of additional dropped buffers. */ protected void updateDroppedBufferCounters(int droppedBufferCount) { - decoderCounters.droppedOutputBufferCount += droppedBufferCount; + decoderCounters.droppedBufferCount += droppedBufferCount; droppedFrames += droppedBufferCount; consecutiveDroppedFrameCount += droppedBufferCount; - decoderCounters.maxConsecutiveDroppedOutputBufferCount = Math.max(consecutiveDroppedFrameCount, - decoderCounters.maxConsecutiveDroppedOutputBufferCount); + decoderCounters.maxConsecutiveDroppedBufferCount = Math.max(consecutiveDroppedFrameCount, + decoderCounters.maxConsecutiveDroppedBufferCount); if (droppedFrames >= maxDroppedFramesToNotify) { maybeNotifyDroppedFrames(); } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java index 9d9272f10e..fda74db28d 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java @@ -154,8 +154,8 @@ public final class DebugTextViewHelper extends Player.DefaultEventListener imple return " sib:" + counters.skippedInputBufferCount + " sb:" + counters.skippedOutputBufferCount + " rb:" + counters.renderedOutputBufferCount - + " db:" + counters.droppedOutputBufferCount - + " mcdb:" + counters.maxConsecutiveDroppedOutputBufferCount + + " db:" + counters.droppedBufferCount + + " mcdb:" + counters.maxConsecutiveDroppedBufferCount + " dk:" + counters.droppedToKeyframeCount; } diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java index 9b8d6483b9..06dab1164b 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java @@ -323,9 +323,9 @@ public final class DashTestRunner { metricsLogger.logMetric(MetricsLogger.KEY_TEST_NAME, streamName); metricsLogger.logMetric(MetricsLogger.KEY_IS_CDD_LIMITED_RETRY, isCddLimitedRetry); metricsLogger.logMetric(MetricsLogger.KEY_FRAMES_DROPPED_COUNT, - videoCounters.droppedOutputBufferCount); + videoCounters.droppedBufferCount); metricsLogger.logMetric(MetricsLogger.KEY_MAX_CONSECUTIVE_FRAMES_DROPPED_COUNT, - videoCounters.maxConsecutiveDroppedOutputBufferCount); + videoCounters.maxConsecutiveDroppedBufferCount); metricsLogger.logMetric(MetricsLogger.KEY_FRAMES_SKIPPED_COUNT, videoCounters.skippedOutputBufferCount); metricsLogger.logMetric(MetricsLogger.KEY_FRAMES_RENDERED_COUNT, @@ -343,20 +343,20 @@ public final class DashTestRunner { .assertSkippedOutputBufferCount(tag + VIDEO_TAG_SUFFIX, videoCounters, 0); // We allow one fewer output buffer due to the way that MediaCodecRenderer and the // underlying decoders handle the end of stream. This should be tightened up in the future. - DecoderCountersUtil.assertTotalOutputBufferCount(tag + AUDIO_TAG_SUFFIX, audioCounters, + DecoderCountersUtil.assertTotalBufferCount(tag + AUDIO_TAG_SUFFIX, audioCounters, audioCounters.inputBufferCount - 1, audioCounters.inputBufferCount); - DecoderCountersUtil.assertTotalOutputBufferCount(tag + VIDEO_TAG_SUFFIX, videoCounters, + DecoderCountersUtil.assertTotalBufferCount(tag + VIDEO_TAG_SUFFIX, videoCounters, videoCounters.inputBufferCount - 1, videoCounters.inputBufferCount); } try { int droppedFrameLimit = (int) Math.ceil(MAX_DROPPED_VIDEO_FRAME_FRACTION - * DecoderCountersUtil.getTotalOutputBuffers(videoCounters)); + * DecoderCountersUtil.getTotalBufferCount(videoCounters)); // Assert that performance is acceptable. // Assert that total dropped frames were within limit. - DecoderCountersUtil.assertDroppedOutputBufferLimit(tag + VIDEO_TAG_SUFFIX, videoCounters, + DecoderCountersUtil.assertDroppedBufferLimit(tag + VIDEO_TAG_SUFFIX, videoCounters, droppedFrameLimit); // Assert that consecutive dropped frames were within limit. - DecoderCountersUtil.assertConsecutiveDroppedOutputBufferLimit(tag + VIDEO_TAG_SUFFIX, + DecoderCountersUtil.assertConsecutiveDroppedBufferLimit(tag + VIDEO_TAG_SUFFIX, videoCounters, MAX_CONSECUTIVE_DROPPED_VIDEO_FRAMES); } catch (AssertionFailedError e) { if (trackSelector.includedAdditionalVideoFormats) { diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/DecoderCountersUtil.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/DecoderCountersUtil.java index 448ec79c2d..16af394cdf 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/DecoderCountersUtil.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/DecoderCountersUtil.java @@ -31,8 +31,9 @@ public final class DecoderCountersUtil { * @param counters The counters for which the total should be calculated. * @return The sum of the skipped, dropped and rendered buffers. */ - public static int getTotalOutputBuffers(DecoderCounters counters) { - return counters.skippedOutputBufferCount + counters.droppedOutputBufferCount + public static int getTotalBufferCount(DecoderCounters counters) { + counters.ensureUpdated(); + return counters.skippedOutputBufferCount + counters.droppedBufferCount + counters.renderedOutputBufferCount; } @@ -44,26 +45,24 @@ public final class DecoderCountersUtil { + expected + ".", expected, actual); } - public static void assertTotalOutputBufferCount(String name, DecoderCounters counters, - int minCount, int maxCount) { - counters.ensureUpdated(); - int actual = getTotalOutputBuffers(counters); + public static void assertTotalBufferCount(String name, DecoderCounters counters, int minCount, + int maxCount) { + int actual = getTotalBufferCount(counters); TestCase.assertTrue("Codec(" + name + ") output " + actual + " buffers. Expected in range [" + minCount + ", " + maxCount + "].", minCount <= actual && actual <= maxCount); } - public static void assertDroppedOutputBufferLimit(String name, DecoderCounters counters, - int limit) { + public static void assertDroppedBufferLimit(String name, DecoderCounters counters, int limit) { counters.ensureUpdated(); - int actual = counters.droppedOutputBufferCount; + int actual = counters.droppedBufferCount; TestCase.assertTrue("Codec(" + name + ") was late decoding: " + actual + " buffers. " + "Limit: " + limit + ".", actual <= limit); } - public static void assertConsecutiveDroppedOutputBufferLimit(String name, - DecoderCounters counters, int limit) { + public static void assertConsecutiveDroppedBufferLimit(String name, DecoderCounters counters, + int limit) { counters.ensureUpdated(); - int actual = counters.maxConsecutiveDroppedOutputBufferCount; + int actual = counters.maxConsecutiveDroppedBufferCount; TestCase.assertTrue("Codec(" + name + ") was late decoding: " + actual + " buffers consecutively. " + "Limit: " + limit + ".", actual <= limit); } From 20e43ac4f8539229f407700da011d008c253fabd Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 5 Oct 2017 01:36:23 -0700 Subject: [PATCH 0508/2472] Allow ads to be paused/resumed Controls are still hidden while playing ads, but if the app pauses the player, controls will be shown. During ads, the player is not seekable. When the player enters the background then returns to the foreground, the content period may not be prepared, so also cache the content window duration. This means that if the app reenters the foreground while an ad is paused the time bar can be populated. Issue: #3303 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=171123428 --- .../exoplayer2/ext/ima/ImaAdsLoader.java | 42 +++++++++++-------- .../source/ads/AdPlaybackState.java | 11 ++++- .../exoplayer2/source/ads/AdsMediaSource.java | 3 +- .../source/ads/SinglePeriodAdTimeline.java | 18 +++++++- .../exoplayer2/ui/PlaybackControlView.java | 6 +-- .../exoplayer2/ui/SimpleExoPlayerView.java | 22 +++++++++- .../res/layout/exo_simple_player_view.xml | 8 ++-- 7 files changed, 78 insertions(+), 32 deletions(-) diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index 6b20404a39..cbed5d166e 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -150,10 +150,6 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A // Fields tracking the player/loader state. - /** - * Whether the player's play when ready flag has temporarily been set to true for playing ads. - */ - private boolean playWhenReadyOverriddenForAds; /** * Whether the player is playing an ad. */ @@ -243,7 +239,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A player.addListener(this); if (adPlaybackState != null) { eventListener.onAdPlaybackState(adPlaybackState.copy()); - if (imaPausedContent) { + if (imaPausedContent && player.getPlayWhenReady()) { adsManager.resume(); } } else { @@ -448,13 +444,6 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A if (DEBUG) { Log.d(TAG, "playAd"); } - if (player == null) { - // Sometimes messages from IMA arrive after detaching the player. See [Internal: b/63801642]. - Log.w(TAG, "Unexpected playAd while detached"); - } else if (!player.getPlayWhenReady()) { - playWhenReadyOverriddenForAds = true; - player.setPlayWhenReady(true); - } switch (imaAdState) { case IMA_AD_STATE_PLAYING: // IMA does not always call stopAd before resuming content. @@ -472,6 +461,15 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A for (int i = 0; i < adCallbacks.size(); i++) { adCallbacks.get(i).onResume(); } + break; + default: + throw new IllegalStateException(); + } + if (player == null) { + // Sometimes messages from IMA arrive after detaching the player. See [Internal: b/63801642]. + Log.w(TAG, "Unexpected playAd while detached"); + } else if (!player.getPlayWhenReady()) { + adsManager.pause(); } } @@ -522,7 +520,11 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A } Assertions.checkArgument(timeline.getPeriodCount() == 1); this.timeline = timeline; - contentDurationMs = C.usToMs(timeline.getPeriod(0, period).durationUs); + long contentDurationUs = timeline.getPeriod(0, period).durationUs; + contentDurationMs = C.usToMs(contentDurationUs); + if (contentDurationUs != C.TIME_UNSET) { + adPlaybackState.contentDurationUs = contentDurationUs; + } updateImaStateForPlayerState(); } @@ -532,6 +534,16 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A return; } + if (imaAdState == IMA_AD_STATE_PLAYING && !playWhenReady) { + adsManager.pause(); + return; + } + + if (imaAdState == IMA_AD_STATE_PAUSED && playWhenReady) { + adsManager.resume(); + return; + } + if (imaAdState == IMA_AD_STATE_NONE && playbackState == Player.STATE_BUFFERING && playWhenReady) { checkForContentComplete(); @@ -593,10 +605,6 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A private void updateImaStateForPlayerState() { boolean wasPlayingAd = playingAd; playingAd = player.isPlayingAd(); - if (!playingAd && playWhenReadyOverriddenForAds) { - playWhenReadyOverriddenForAds = false; - player.setPlayWhenReady(false); - } if (!sentContentComplete) { boolean adFinished = (wasPlayingAd && !playingAd) || playingAdIndexInAdGroup != player.getCurrentAdIndexInAdGroup(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java index 97c97dec8f..58fa149b59 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdPlaybackState.java @@ -51,6 +51,10 @@ public final class AdPlaybackState { */ public final Uri[][] adUris; + /** + * The content duration in microseconds, if known. {@link C#TIME_UNSET} otherwise. + */ + public long contentDurationUs; /** * The position offset in the first unplayed ad at which to begin playback, in microseconds. */ @@ -71,15 +75,17 @@ public final class AdPlaybackState { adUris = new Uri[adGroupCount][]; Arrays.fill(adUris, new Uri[0]); adsLoadedCounts = new int[adGroupTimesUs.length]; + contentDurationUs = C.TIME_UNSET; } private AdPlaybackState(long[] adGroupTimesUs, int[] adCounts, int[] adsLoadedCounts, - int[] adsPlayedCounts, Uri[][] adUris, long adResumePositionUs) { + int[] adsPlayedCounts, Uri[][] adUris, long contentDurationUs, long adResumePositionUs) { this.adGroupTimesUs = adGroupTimesUs; this.adCounts = adCounts; this.adsLoadedCounts = adsLoadedCounts; this.adsPlayedCounts = adsPlayedCounts; this.adUris = adUris; + this.contentDurationUs = contentDurationUs; this.adResumePositionUs = adResumePositionUs; adGroupCount = adGroupTimesUs.length; } @@ -94,7 +100,8 @@ public final class AdPlaybackState { } return new AdPlaybackState(Arrays.copyOf(adGroupTimesUs, adGroupCount), Arrays.copyOf(adCounts, adGroupCount), Arrays.copyOf(adsLoadedCounts, adGroupCount), - Arrays.copyOf(adsPlayedCounts, adGroupCount), adUris, adResumePositionUs); + Arrays.copyOf(adsPlayedCounts, adGroupCount), adUris, contentDurationUs, + adResumePositionUs); } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java index 41a856f83f..9c75b5ee5e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java @@ -273,7 +273,8 @@ public final class AdsMediaSource implements MediaSource { Timeline timeline = adPlaybackState.adGroupCount == 0 ? contentTimeline : new SinglePeriodAdTimeline(contentTimeline, adPlaybackState.adGroupTimesUs, adPlaybackState.adCounts, adPlaybackState.adsLoadedCounts, - adPlaybackState.adsPlayedCounts, adDurationsUs, adPlaybackState.adResumePositionUs); + adPlaybackState.adsPlayedCounts, adDurationsUs, adPlaybackState.adResumePositionUs, + adPlaybackState.contentDurationUs); listener.onSourceInfoRefreshed(timeline, contentManifest); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/SinglePeriodAdTimeline.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/SinglePeriodAdTimeline.java index c2974681db..0a04c9ab4b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/SinglePeriodAdTimeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/SinglePeriodAdTimeline.java @@ -31,6 +31,7 @@ import com.google.android.exoplayer2.util.Assertions; private final int[] adsPlayedCounts; private final long[][] adDurationsUs; private final long adResumePositionUs; + private final long contentDurationUs; /** * Creates a new timeline with a single period containing the specified ads. @@ -48,10 +49,12 @@ import com.google.android.exoplayer2.util.Assertions; * may be {@link C#TIME_UNSET} if the duration is not yet known. * @param adResumePositionUs The position offset in the earliest unplayed ad at which to begin * playback, in microseconds. + * @param contentDurationUs The content duration in microseconds, if known. {@link C#TIME_UNSET} + * otherwise. */ public SinglePeriodAdTimeline(Timeline contentTimeline, long[] adGroupTimesUs, int[] adCounts, - int[] adsLoadedCounts, int[] adsPlayedCounts, long[][] adDurationsUs, - long adResumePositionUs) { + int[] adsLoadedCounts, int[] adsPlayedCounts, long[][] adDurationsUs, long adResumePositionUs, + long contentDurationUs) { super(contentTimeline); Assertions.checkState(contentTimeline.getPeriodCount() == 1); Assertions.checkState(contentTimeline.getWindowCount() == 1); @@ -61,6 +64,7 @@ import com.google.android.exoplayer2.util.Assertions; this.adsPlayedCounts = adsPlayedCounts; this.adDurationsUs = adDurationsUs; this.adResumePositionUs = adResumePositionUs; + this.contentDurationUs = contentDurationUs; } @Override @@ -72,4 +76,14 @@ import com.google.android.exoplayer2.util.Assertions; return period; } + @Override + public Window getWindow(int windowIndex, Window window, boolean setIds, + long defaultPositionProjectionUs) { + window = super.getWindow(windowIndex, window, setIds, defaultPositionProjectionUs); + if (window.durationUs == C.TIME_UNSET) { + window.durationUs = contentDurationUs; + } + return window; + } + } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java index e2ae1f732b..964eda7de0 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java @@ -656,17 +656,13 @@ public class PlaybackControlView extends FrameLayout { boolean isSeekable = false; boolean enablePrevious = false; boolean enableNext = false; - if (haveNonEmptyTimeline) { + if (haveNonEmptyTimeline && !player.isPlayingAd()) { int windowIndex = player.getCurrentWindowIndex(); timeline.getWindow(windowIndex, window); isSeekable = window.isSeekable; enablePrevious = isSeekable || !window.isDynamic || player.getPreviousWindowIndex() != C.INDEX_UNSET; enableNext = window.isDynamic || player.getNextWindowIndex() != C.INDEX_UNSET; - if (player.isPlayingAd()) { - // Always hide player controls during ads. - hide(); - } } setButtonEnabled(enablePrevious, previousButton); setButtonEnabled(enableNext, nextButton); diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index 053bc26a6e..f34ede3e6d 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -37,6 +37,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ControlDispatcher; import com.google.android.exoplayer2.DefaultControlDispatcher; import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Player.DiscontinuityReason; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.id3.ApicFrame; @@ -751,6 +752,10 @@ public final class SimpleExoPlayerView extends FrameLayout { * Shows the playback controls, but only if forced or shown indefinitely. */ private void maybeShowController(boolean isForced) { + if (isPlayingAd()) { + // Never show the controller if an ad is currently playing. + return; + } if (useController) { boolean wasShowingIndefinitely = controller.isVisible() && controller.getShowTimeoutMs() <= 0; boolean shouldShowIndefinitely = shouldShowControllerIndefinitely(); @@ -777,6 +782,10 @@ public final class SimpleExoPlayerView extends FrameLayout { controller.show(); } + private boolean isPlayingAd() { + return player != null && player.isPlayingAd() && player.getPlayWhenReady(); + } + private void updateForCurrentTrackSelections() { if (player == null) { return; @@ -907,7 +916,18 @@ public final class SimpleExoPlayerView extends FrameLayout { @Override public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { - maybeShowController(false); + if (isPlayingAd()) { + hideController(); + } else { + maybeShowController(false); + } + } + + @Override + public void onPositionDiscontinuity(@DiscontinuityReason int reason) { + if (isPlayingAd()) { + hideController(); + } } } diff --git a/library/ui/src/main/res/layout/exo_simple_player_view.xml b/library/ui/src/main/res/layout/exo_simple_player_view.xml index 1f59b7796d..340113da6c 100644 --- a/library/ui/src/main/res/layout/exo_simple_player_view.xml +++ b/library/ui/src/main/res/layout/exo_simple_player_view.xml @@ -38,12 +38,12 @@ - - + + From f73a5bf692a227a4729f4b94f1109e258aaa4399 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 5 Oct 2017 03:02:10 -0700 Subject: [PATCH 0509/2472] Update gradle version to 3.0.0-beta5. Android Studio refuses to build with the current beta4 version. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=171129696 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index e95dd83d90..47538ddb52 100644 --- a/build.gradle +++ b/build.gradle @@ -17,7 +17,7 @@ buildscript { google() } dependencies { - classpath 'com.android.tools.build:gradle:3.0.0-beta4' + classpath 'com.android.tools.build:gradle:3.0.0-beta5' classpath 'com.novoda:bintray-release:0.5.0' } // Workaround for the following test coverage issue. Remove when fixed: From d5101d8d461f3d6ff2a87ee638400abf953948d9 Mon Sep 17 00:00:00 2001 From: hoangtc Date: Fri, 6 Oct 2017 02:56:52 -0700 Subject: [PATCH 0510/2472] Allow DefaultDRMSession to retry provisioning/key request For initial DRM provisioning and key request, we allow the requests to be retried (with increasing delay for each successive retry) before failing. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=171271384 --- .../exoplayer2/drm/DefaultDrmSession.java | 60 ++++++++++++++----- .../drm/DefaultDrmSessionManager.java | 32 +++++++++- 2 files changed, 75 insertions(+), 17 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java index e82c7e46b2..4e60e466a8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java @@ -82,6 +82,7 @@ import java.util.UUID; private final HashMap optionalKeyRequestParameters; private final Handler eventHandler; private final DefaultDrmSessionManager.EventListener eventListener; + private final int initialDrmRequestRetryCount; /* package */ final MediaDrmCallback callback; /* package */ final UUID uuid; @@ -89,7 +90,7 @@ import java.util.UUID; private @DrmSession.State int state; private int openCount; private HandlerThread requestHandlerThread; - private Handler postRequestHandler; + private PostRequestHandler postRequestHandler; private T mediaCrypto; private DrmSessionException lastException; private byte[] sessionId; @@ -111,13 +112,16 @@ import java.util.UUID; * @param playbackLooper The playback looper. * @param eventHandler The handler to post listener events. * @param eventListener The DRM session manager event listener. + * @param initialDrmRequestRetryCount The number of times to retry for initial provisioning and + * key request before reporting error. */ public DefaultDrmSession(UUID uuid, ExoMediaDrm mediaDrm, ProvisioningManager provisioningManager, byte[] initData, String mimeType, @DefaultDrmSessionManager.Mode int mode, byte[] offlineLicenseKeySetId, HashMap optionalKeyRequestParameters, MediaDrmCallback callback, Looper playbackLooper, Handler eventHandler, - DefaultDrmSessionManager.EventListener eventListener) { + DefaultDrmSessionManager.EventListener eventListener, + int initialDrmRequestRetryCount) { this.uuid = uuid; this.provisioningManager = provisioningManager; this.mediaDrm = mediaDrm; @@ -125,6 +129,7 @@ import java.util.UUID; this.offlineLicenseKeySetId = offlineLicenseKeySetId; this.optionalKeyRequestParameters = optionalKeyRequestParameters; this.callback = callback; + this.initialDrmRequestRetryCount = initialDrmRequestRetryCount; this.eventHandler = eventHandler; this.eventListener = eventListener; @@ -152,7 +157,7 @@ import java.util.UUID; return; } if (openInternal(true)) { - doLicense(); + doLicense(true); } } } @@ -191,12 +196,12 @@ import java.util.UUID; public void provision() { ProvisionRequest request = mediaDrm.getProvisionRequest(); - postRequestHandler.obtainMessage(MSG_PROVISION, request).sendToTarget(); + postRequestHandler.obtainMessage(MSG_PROVISION, request, true).sendToTarget(); } public void onProvisionCompleted() { if (openInternal(false)) { - doLicense(); + doLicense(true); } } @@ -285,12 +290,12 @@ import java.util.UUID; provisioningManager.onProvisionCompleted(); } - private void doLicense() { + private void doLicense(boolean allowRetry) { switch (mode) { case DefaultDrmSessionManager.MODE_PLAYBACK: case DefaultDrmSessionManager.MODE_QUERY: if (offlineLicenseKeySetId == null) { - postKeyRequest(ExoMediaDrm.KEY_TYPE_STREAMING); + postKeyRequest(ExoMediaDrm.KEY_TYPE_STREAMING, allowRetry); } else { if (restoreKeys()) { long licenseDurationRemainingSec = getLicenseDurationRemainingSec(); @@ -298,7 +303,7 @@ import java.util.UUID; && licenseDurationRemainingSec <= MAX_LICENSE_DURATION_TO_RENEW) { Log.d(TAG, "Offline license has expired or will expire soon. " + "Remaining seconds: " + licenseDurationRemainingSec); - postKeyRequest(ExoMediaDrm.KEY_TYPE_OFFLINE); + postKeyRequest(ExoMediaDrm.KEY_TYPE_OFFLINE, allowRetry); } else if (licenseDurationRemainingSec <= 0) { onError(new KeysExpiredException()); } else { @@ -317,11 +322,11 @@ import java.util.UUID; break; case DefaultDrmSessionManager.MODE_DOWNLOAD: if (offlineLicenseKeySetId == null) { - postKeyRequest(ExoMediaDrm.KEY_TYPE_OFFLINE); + postKeyRequest(ExoMediaDrm.KEY_TYPE_OFFLINE, allowRetry); } else { // Renew if (restoreKeys()) { - postKeyRequest(ExoMediaDrm.KEY_TYPE_OFFLINE); + postKeyRequest(ExoMediaDrm.KEY_TYPE_OFFLINE, allowRetry); } } break; @@ -329,7 +334,7 @@ import java.util.UUID; // It's not necessary to restore the key (and open a session to do that) before releasing it // but this serves as a good sanity/fast-failure check. if (restoreKeys()) { - postKeyRequest(ExoMediaDrm.KEY_TYPE_RELEASE); + postKeyRequest(ExoMediaDrm.KEY_TYPE_RELEASE, allowRetry); } break; default: @@ -356,12 +361,12 @@ import java.util.UUID; return Math.min(pair.first, pair.second); } - private void postKeyRequest(int type) { + private void postKeyRequest(int type, boolean allowRetry) { byte[] scope = type == ExoMediaDrm.KEY_TYPE_RELEASE ? offlineLicenseKeySetId : sessionId; try { KeyRequest request = mediaDrm.getKeyRequest(scope, initData, mimeType, type, optionalKeyRequestParameters); - postRequestHandler.obtainMessage(MSG_KEYS, request).sendToTarget(); + postRequestHandler.obtainMessage(MSG_KEYS, request, allowRetry).sendToTarget(); } catch (Exception e) { onKeysError(e); } @@ -452,7 +457,7 @@ import java.util.UUID; } switch (what) { case ExoMediaDrm.EVENT_KEY_REQUIRED: - doLicense(); + doLicense(false); break; case ExoMediaDrm.EVENT_KEY_EXPIRED: // When an already expired key is loaded MediaDrm sends this event immediately. Ignore @@ -501,6 +506,11 @@ import java.util.UUID; super(backgroundLooper); } + Message obtainMessage(int what, Object object, boolean allowRetry) { + return obtainMessage(what, allowRetry ? 1 : 0 /* allow retry*/, 0 /* error count */, + object); + } + @Override public void handleMessage(Message msg) { Object response; @@ -516,11 +526,33 @@ import java.util.UUID; throw new RuntimeException(); } } catch (Exception e) { + if (maybeRetryRequest(msg)) { + return; + } response = e; } postResponseHandler.obtainMessage(msg.what, response).sendToTarget(); } + private boolean maybeRetryRequest(Message originalMsg) { + boolean allowRetry = originalMsg.arg1 == 1; + if (!allowRetry) { + return false; + } + int errorCount = originalMsg.arg2 + 1; + if (errorCount > initialDrmRequestRetryCount) { + return false; + } + Message retryMsg = Message.obtain(originalMsg); + retryMsg.arg2 = errorCount; + sendMessageDelayed(retryMsg, getRetryDelayMillis(errorCount)); + return true; + } + + private long getRetryDelayMillis(int errorCount) { + return Math.min((errorCount - 1) * 1000, 5000); + } + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java index a0d5a932f2..10cfe318a9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java @@ -97,6 +97,8 @@ public class DefaultDrmSessionManager implements DrmSe public static final int MODE_DOWNLOAD = 2; /** Releases an existing offline license. */ public static final int MODE_RELEASE = 3; + /** Number of times to retry for initial provisioning and key request for reporting error. */ + public static final int INITIAL_DRM_REQUEST_RETRY_COUNT = 3; private final UUID uuid; private final ExoMediaDrm mediaDrm; @@ -105,6 +107,7 @@ public class DefaultDrmSessionManager implements DrmSe private final Handler eventHandler; private final EventListener eventListener; private final boolean multiSession; + private final int initialDrmRequestRetryCount; private final List> sessions; private final List> provisioningSessions; @@ -176,7 +179,8 @@ public class DefaultDrmSessionManager implements DrmSe UUID uuid, MediaDrmCallback callback, HashMap optionalKeyRequestParameters, Handler eventHandler, EventListener eventListener) throws UnsupportedDrmException { return new DefaultDrmSessionManager<>(uuid, FrameworkMediaDrm.newInstance(uuid), callback, - optionalKeyRequestParameters, eventHandler, eventListener, false); + optionalKeyRequestParameters, eventHandler, eventListener, false, + INITIAL_DRM_REQUEST_RETRY_COUNT); } /** @@ -193,7 +197,7 @@ public class DefaultDrmSessionManager implements DrmSe HashMap optionalKeyRequestParameters, Handler eventHandler, EventListener eventListener) { this(uuid, mediaDrm, callback, optionalKeyRequestParameters, eventHandler, eventListener, - false); + false, INITIAL_DRM_REQUEST_RETRY_COUNT); } /** @@ -211,6 +215,27 @@ public class DefaultDrmSessionManager implements DrmSe public DefaultDrmSessionManager(UUID uuid, ExoMediaDrm mediaDrm, MediaDrmCallback callback, HashMap optionalKeyRequestParameters, Handler eventHandler, EventListener eventListener, boolean multiSession) { + this(uuid, mediaDrm, callback, optionalKeyRequestParameters, eventHandler, eventListener, + multiSession, INITIAL_DRM_REQUEST_RETRY_COUNT); + } + + /** + * @param uuid The UUID of the drm scheme. + * @param mediaDrm An underlying {@link ExoMediaDrm} for use by the manager. + * @param callback Performs key and provisioning requests. + * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument + * to {@link ExoMediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null. + * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be + * null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + * @param multiSession A boolean that specify whether multiple key session support is enabled. + * Default is false. + * @param initialDrmRequestRetryCount The number of times to retry for initial provisioning and + * key request before reporting error. + */ + public DefaultDrmSessionManager(UUID uuid, ExoMediaDrm mediaDrm, MediaDrmCallback callback, + HashMap optionalKeyRequestParameters, Handler eventHandler, + EventListener eventListener, boolean multiSession, int initialDrmRequestRetryCount) { Assertions.checkNotNull(uuid); Assertions.checkNotNull(mediaDrm); Assertions.checkArgument(!C.COMMON_PSSH_UUID.equals(uuid), "Use C.CLEARKEY_UUID instead"); @@ -221,6 +246,7 @@ public class DefaultDrmSessionManager implements DrmSe this.eventHandler = eventHandler; this.eventListener = eventListener; this.multiSession = multiSession; + this.initialDrmRequestRetryCount = initialDrmRequestRetryCount; mode = MODE_PLAYBACK; sessions = new ArrayList<>(); provisioningSessions = new ArrayList<>(); @@ -377,7 +403,7 @@ public class DefaultDrmSessionManager implements DrmSe // Create a new session. session = new DefaultDrmSession<>(uuid, mediaDrm, this, initData, mimeType, mode, offlineLicenseKeySetId, optionalKeyRequestParameters, callback, playbackLooper, - eventHandler, eventListener); + eventHandler, eventListener, initialDrmRequestRetryCount); sessions.add(session); } session.acquire(); From 10f8192c48dcacfd8aea90c39bb0a3712307a991 Mon Sep 17 00:00:00 2001 From: eguven Date: Fri, 6 Oct 2017 03:25:20 -0700 Subject: [PATCH 0511/2472] Add ActionFile which stores and loads DownloadActions to/from a file. This change also replaces individual DownloadAction versions with a single master version. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=171273880 --- .../exoplayer2/offline/ActionFile.java | 95 +++++++ .../google/android/exoplayer2/util/Util.java | 7 +- .../exoplayer2/offline/ActionFileTest.java | 253 ++++++++++++++++++ .../ProgressiveDownloadActionTest.java | 2 +- 4 files changed, 355 insertions(+), 2 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFile.java create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/offline/ActionFileTest.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFile.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFile.java new file mode 100644 index 0000000000..38f599cc6e --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFile.java @@ -0,0 +1,95 @@ +/* + * 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.offline; + +import com.google.android.exoplayer2.offline.DownloadAction.Deserializer; +import com.google.android.exoplayer2.util.AtomicFile; +import com.google.android.exoplayer2.util.ClosedSource; +import com.google.android.exoplayer2.util.Util; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * Stores and loads {@link DownloadAction}s to/from a file. + */ +@ClosedSource(reason = "Not ready yet") +public final class ActionFile { + + private final AtomicFile atomicFile; + + /** + * @param actionFile File to be used to store and load {@link DownloadAction}s. + */ + public ActionFile(File actionFile) { + atomicFile = new AtomicFile(actionFile); + } + + /** + * Loads {@link DownloadAction}s from file. + * + * @param deserializers {@link Deserializer}s to deserialize DownloadActions. + * @return Loaded DownloadActions. + * @throws IOException If there is an error during loading. + */ + public DownloadAction[] load(Deserializer... deserializers) throws IOException { + InputStream inputStream = null; + try { + inputStream = atomicFile.openRead(); + DataInputStream dataInputStream = new DataInputStream(inputStream); + int version = dataInputStream.readInt(); + if (version > DownloadAction.MASTER_VERSION) { + throw new IOException("Not supported action file version: " + version); + } + int actionCount = dataInputStream.readInt(); + DownloadAction[] actions = new DownloadAction[actionCount]; + for (int i = 0; i < actionCount; i++) { + actions[i] = DownloadAction.deserializeFromStream(deserializers, dataInputStream, version); + } + return actions; + } finally { + Util.closeQuietly(inputStream); + } + } + + /** + * Stores {@link DownloadAction}s to file. + * + * @param downloadActions DownloadActions to store to file. + * @throws IOException If there is an error during storing. + */ + public void store(DownloadAction... downloadActions) throws IOException { + OutputStream outputStream = null; + try { + outputStream = atomicFile.startWrite(); + DataOutputStream dataOutputStream = new DataOutputStream(outputStream); + dataOutputStream.writeInt(DownloadAction.MASTER_VERSION); + dataOutputStream.writeInt(downloadActions.length); + for (DownloadAction action : downloadActions) { + DownloadAction.serializeToStream(action, dataOutputStream); + } + atomicFile.endWrite(outputStream); + // Avoid calling close twice. + outputStream = null; + } finally { + Util.closeQuietly(outputStream); + } + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java index 5d93f413d5..24132e400c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -1076,12 +1076,17 @@ public final class Util { /** Creates an empty directory in the directory returned by {@link Context#getCacheDir()}. */ public static File createTempDirectory(Context context, String prefix) throws IOException { - File tempFile = File.createTempFile(prefix, null, context.getCacheDir()); + File tempFile = createTempFile(context, prefix); tempFile.delete(); // Delete the temp file. tempFile.mkdir(); // Create a directory with the same name. return tempFile; } + /** Creates a new empty file in the directory returned by {@link Context#getCacheDir()}. */ + public static File createTempFile(Context context, String prefix) throws IOException { + return File.createTempFile(prefix, null, context.getCacheDir()); + } + /** * Returns the result of updating a CRC with the specified bytes in a "most significant bit first" * order. diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/ActionFileTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/ActionFileTest.java new file mode 100644 index 0000000000..cc6a3e84ad --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/ActionFileTest.java @@ -0,0 +1,253 @@ +/* + * 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.offline; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.android.exoplayer2.offline.DownloadAction.Deserializer; +import com.google.android.exoplayer2.util.ClosedSource; +import com.google.android.exoplayer2.util.Util; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +/** + * Unit tests for {@link ProgressiveDownloadAction}. + */ +@ClosedSource(reason = "Not ready yet") +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public class ActionFileTest { + + private File tempFile; + + @Before + public void setUp() throws Exception { + tempFile = Util.createTempFile(RuntimeEnvironment.application, "ExoPlayerTest"); + } + + @After + public void tearDown() throws Exception { + tempFile.delete(); + } + + @Test + public void testLoadNoDataThrowsIOException() throws Exception { + try { + loadActions(new Object[] {}); + Assert.fail(); + } catch (IOException e) { + // Expected exception. + } + } + + @Test + public void testLoadIncompleteHeaderThrowsIOException() throws Exception { + try { + loadActions(new Object[] {DownloadAction.MASTER_VERSION}); + Assert.fail(); + } catch (IOException e) { + // Expected exception. + } + } + + @Test + public void testLoadCompleteHeaderZeroAction() throws Exception { + DownloadAction[] actions = + loadActions(new Object[] {DownloadAction.MASTER_VERSION, /*action count*/0}); + assertThat(actions).isNotNull(); + assertThat(actions).hasLength(0); + } + + @Test + public void testLoadAction() throws Exception { + DownloadAction[] actions = loadActions( + new Object[] {DownloadAction.MASTER_VERSION, /*action count*/1, /*action 1*/"type2", 321}, + new FakeDeserializer("type2")); + assertThat(actions).isNotNull(); + assertThat(actions).hasLength(1); + assertAction(actions[0], "type2", DownloadAction.MASTER_VERSION, 321); + } + + @Test + public void testLoadActions() throws Exception { + DownloadAction[] actions = loadActions( + new Object[] {DownloadAction.MASTER_VERSION, /*action count*/2, /*action 1*/"type1", 123, + /*action 2*/"type2", 321}, // Action 2 + new FakeDeserializer("type1"), new FakeDeserializer("type2")); + assertThat(actions).isNotNull(); + assertThat(actions).hasLength(2); + assertAction(actions[0], "type1", DownloadAction.MASTER_VERSION, 123); + assertAction(actions[1], "type2", DownloadAction.MASTER_VERSION, 321); + } + + @Test + public void testLoadNotSupportedVersion() throws Exception { + try { + loadActions(new Object[] {DownloadAction.MASTER_VERSION + 1, /*action count*/1, + /*action 1*/"type2", 321}, new FakeDeserializer("type2")); + Assert.fail(); + } catch (IOException e) { + // Expected exception. + } + } + + @Test + public void testLoadNotSupportedType() throws Exception { + try { + loadActions(new Object[] {DownloadAction.MASTER_VERSION, /*action count*/1, + /*action 1*/"type2", 321}, new FakeDeserializer("type1")); + Assert.fail(); + } catch (DownloadException e) { + // Expected exception. + } + } + + @Test + public void testStoreAndLoadNoActions() throws Exception { + doTestSerializationRoundTrip(new DownloadAction[0]); + } + + @Test + public void testStoreAndLoadActions() throws Exception { + doTestSerializationRoundTrip(new DownloadAction[] { + new FakeDownloadAction("type1", DownloadAction.MASTER_VERSION, 123), + new FakeDownloadAction("type2", DownloadAction.MASTER_VERSION, 321), + }, new FakeDeserializer("type1"), new FakeDeserializer("type2")); + } + + private void doTestSerializationRoundTrip(DownloadAction[] actions, + Deserializer... deserializers) throws IOException { + ActionFile actionFile = new ActionFile(tempFile); + actionFile.store(actions); + assertThat(actionFile.load(deserializers)).isEqualTo(actions); + } + + private DownloadAction[] loadActions(Object[] values, Deserializer... deserializers) + throws IOException { + FileOutputStream fileOutputStream = new FileOutputStream(tempFile); + DataOutputStream dataOutputStream = new DataOutputStream(fileOutputStream); + try { + for (Object value : values) { + if (value instanceof Integer) { + dataOutputStream.writeInt((Integer) value); // Action count + } else if (value instanceof String) { + dataOutputStream.writeUTF((String) value); // Action count + } else { + throw new IllegalArgumentException(); + } + } + } finally { + dataOutputStream.close(); + } + return new ActionFile(tempFile).load(deserializers); + } + + private static void assertAction(DownloadAction action, String type, int version, int data) { + assertThat(action).isInstanceOf(FakeDownloadAction.class); + assertThat(action.getType()).isEqualTo(type); + assertThat(((FakeDownloadAction) action).version).isEqualTo(version); + assertThat(((FakeDownloadAction) action).data).isEqualTo(data); + } + + private static class FakeDeserializer implements Deserializer { + final String type; + + FakeDeserializer(String type) { + this.type = type; + } + + @Override + public String getType() { + return type; + } + + @Override + public DownloadAction readFromStream(int version, DataInputStream input) throws IOException { + return new FakeDownloadAction(type, version, input.readInt()); + } + } + + private static class FakeDownloadAction extends DownloadAction { + final String type; + final int version; + final int data; + + private FakeDownloadAction(String type, int version, int data) { + this.type = type; + this.version = version; + this.data = data; + } + + @Override + protected String getType() { + return type; + } + + @Override + protected void writeToStream(DataOutputStream output) throws IOException { + output.writeInt(data); + } + + @Override + protected boolean isRemoveAction() { + return false; + } + + @Override + protected boolean isSameMedia(DownloadAction other) { + return false; + } + + @Override + protected Downloader createDownloader(DownloaderConstructorHelper downloaderConstructorHelper) { + return null; + } + + // auto generated code + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + FakeDownloadAction that = (FakeDownloadAction) o; + return version == that.version && data == that.data && type.equals(that.type); + } + + @Override + public int hashCode() { + int result = type.hashCode(); + result = 31 * result + version; + result = 31 * result + data; + return result; + } + } + +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/ProgressiveDownloadActionTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/ProgressiveDownloadActionTest.java index 44fe6c069b..85a2b02799 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/offline/ProgressiveDownloadActionTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/ProgressiveDownloadActionTest.java @@ -136,7 +136,7 @@ public class ProgressiveDownloadActionTest { ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); DataInputStream input = new DataInputStream(in); DownloadAction action2 = - ProgressiveDownloadAction.DESERIALIZER.readFromStream(action1.getVersion(), input); + ProgressiveDownloadAction.DESERIALIZER.readFromStream(DownloadAction.MASTER_VERSION, input); assertThat(action2).isEqualTo(action1); } From e88716595539ea5034b8bea460ac5aa1ba87a0d1 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 9 Oct 2017 01:49:56 -0700 Subject: [PATCH 0512/2472] Provide a getter for IMA's AdsLoader Issue: #3322 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=171508635 --- .../android/exoplayer2/ext/ima/ImaAdsLoader.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index cbed5d166e..0b11a97f84 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -228,6 +228,16 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A contentDurationMs = C.TIME_UNSET; } + /** + * Returns the underlying {@code com.google.ads.interactivemedia.v3.api.AdsLoader} wrapped by + * this instance. + */ + public com.google.ads.interactivemedia.v3.api.AdsLoader getAdsLoader() { + return adsLoader; + } + + // AdsLoader implementation. + @Override public void attachPlayer(ExoPlayer player, EventListener eventListener, ViewGroup adUiViewGroup) { this.player = player; @@ -270,7 +280,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A } } - // AdsLoader.AdsLoadedListener implementation. + // com.google.ads.interactivemedia.v3.api.AdsLoader.AdsLoadedListener implementation. @Override public void onAdsManagerLoaded(AdsManagerLoadedEvent adsManagerLoadedEvent) { From 54d3df4b63a6291d8b98bcee67748e78012d0269 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 9 Oct 2017 03:35:45 -0700 Subject: [PATCH 0513/2472] Drop to keyframe in LibvpxVideoRenderer ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=171517156 --- .../ext/vp9/LibvpxVideoRenderer.java | 88 +++++++++++++++---- .../video/MediaCodecVideoRenderer.java | 3 +- 2 files changed, 74 insertions(+), 17 deletions(-) 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 ec18db2470..dd303af0d8 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 @@ -109,12 +109,12 @@ public final class LibvpxVideoRenderer extends BaseRenderer { private DrmSession drmSession; private DrmSession pendingDrmSession; - @ReinitializationState - private int decoderReinitializationState; + private @ReinitializationState int decoderReinitializationState; private boolean decoderReceivedBuffers; private Bitmap bitmap; private boolean renderedFirstFrame; + private boolean forceRenderFrame; private long joiningDeadlineMs; private Surface surface; private VpxOutputBufferRenderer outputBufferRenderer; @@ -129,6 +129,7 @@ public final class LibvpxVideoRenderer extends BaseRenderer { private long droppedFrameAccumulationStartTimeMs; private int droppedFrames; private int consecutiveDroppedFrameCount; + private int buffersInCodecCount; /** * @param scaleToFit Whether video frames should be scaled to fit when rendering. @@ -257,6 +258,7 @@ public final class LibvpxVideoRenderer extends BaseRenderer { return false; } decoderCounters.skippedOutputBufferCount += outputBuffer.skippedOutputBufferCount; + buffersInCodecCount -= outputBuffer.skippedOutputBufferCount; } if (nextOutputBuffer == null) { @@ -279,26 +281,42 @@ public final class LibvpxVideoRenderer extends BaseRenderer { 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 (isBufferLate(outputBuffer.timeUs - positionUs)) { + forceRenderFrame = false; skipBuffer(); + buffersInCodecCount--; return true; } return false; } + if (forceRenderFrame) { + forceRenderFrame = false; + renderBuffer(); + buffersInCodecCount--; + return true; + } + final long nextOutputBufferTimeUs = nextOutputBuffer != null && !nextOutputBuffer.isEndOfStream() ? nextOutputBuffer.timeUs : C.TIME_UNSET; - if (shouldDropOutputBuffer( + + long earlyUs = outputBuffer.timeUs - positionUs; + if (shouldDropBuffersToKeyframe(earlyUs) && maybeDropBuffersToKeyframe(positionUs)) { + forceRenderFrame = true; + return false; + } else if (shouldDropOutputBuffer( outputBuffer.timeUs, nextOutputBufferTimeUs, positionUs, joiningDeadlineMs)) { dropBuffer(); + buffersInCodecCount--; return true; } // 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)) { + || (getState() == STATE_STARTED && earlyUs <= 30000)) { renderBuffer(); + buffersInCodecCount--; } return false; } @@ -307,18 +325,29 @@ public final class LibvpxVideoRenderer extends BaseRenderer { * 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 C#TIME_UNSET} if the next output buffer is unavailable. + * @param nextOutputBufferTimeUs The timestamp of the next output buffer or {@link C#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, + private boolean shouldDropOutputBuffer(long outputBufferTimeUs, long nextOutputBufferTimeUs, long positionUs, long joiningDeadlineMs) { return isBufferLate(outputBufferTimeUs - positionUs) && (joiningDeadlineMs != C.TIME_UNSET || nextOutputBufferTimeUs != C.TIME_UNSET); } + /** + * Returns whether to drop all buffers from the buffer being processed to the keyframe at or after + * the current playback position, if possible. + * + * @param earlyUs The time until the current buffer should be presented in microseconds. A + * negative value indicates that the buffer is late. + */ + private boolean shouldDropBuffersToKeyframe(long earlyUs) { + return isBufferVeryLate(earlyUs); + } + private void renderBuffer() { int bufferMode = outputBuffer.mode; boolean renderRgb = bufferMode == VpxDecoder.OUTPUT_MODE_RGB && surface != null; @@ -342,18 +371,35 @@ public final class LibvpxVideoRenderer extends BaseRenderer { } private void dropBuffer() { - decoderCounters.droppedBufferCount++; - droppedFrames++; - consecutiveDroppedFrameCount++; - decoderCounters.maxConsecutiveDroppedBufferCount = Math.max( - consecutiveDroppedFrameCount, decoderCounters.maxConsecutiveDroppedBufferCount); - if (droppedFrames == maxDroppedFramesToNotify) { - maybeNotifyDroppedFrames(); - } + updateDroppedBufferCounters(1); outputBuffer.release(); outputBuffer = null; } + private boolean maybeDropBuffersToKeyframe(long positionUs) throws ExoPlaybackException { + int droppedSourceBufferCount = skipSource(positionUs); + if (droppedSourceBufferCount == 0) { + return false; + } + decoderCounters.droppedToKeyframeCount++; + // We dropped some buffers to catch up, so update the decoder counters and flush the codec, + // which releases all pending buffers buffers including the current output buffer. + updateDroppedBufferCounters(buffersInCodecCount + droppedSourceBufferCount); + flushDecoder(); + return true; + } + + private void updateDroppedBufferCounters(int droppedBufferCount) { + decoderCounters.droppedBufferCount += droppedBufferCount; + droppedFrames += droppedBufferCount; + consecutiveDroppedFrameCount += droppedBufferCount; + decoderCounters.maxConsecutiveDroppedBufferCount = Math.max(consecutiveDroppedFrameCount, + decoderCounters.maxConsecutiveDroppedBufferCount); + if (droppedFrames >= maxDroppedFramesToNotify) { + maybeNotifyDroppedFrames(); + } + } + private void skipBuffer() { decoderCounters.skippedOutputBufferCount++; outputBuffer.release(); @@ -426,6 +472,7 @@ public final class LibvpxVideoRenderer extends BaseRenderer { inputBuffer.flip(); inputBuffer.colorInfo = formatHolder.format.colorInfo; decoder.queueInputBuffer(inputBuffer); + buffersInCodecCount++; decoderReceivedBuffers = true; decoderCounters.inputBufferCount++; inputBuffer = null; @@ -445,6 +492,8 @@ public final class LibvpxVideoRenderer extends BaseRenderer { private void flushDecoder() throws ExoPlaybackException { waitingForKeys = false; + forceRenderFrame = false; + buffersInCodecCount = 0; if (decoderReinitializationState != REINITIALIZATION_STATE_NONE) { releaseDecoder(); maybeInitDecoder(); @@ -601,6 +650,8 @@ public final class LibvpxVideoRenderer extends BaseRenderer { decoderCounters.decoderReleaseCount++; decoderReinitializationState = REINITIALIZATION_STATE_NONE; decoderReceivedBuffers = false; + forceRenderFrame = false; + buffersInCodecCount = 0; } private void onInputFormatChanged(Format newFormat) throws ExoPlaybackException { @@ -735,8 +786,13 @@ public final class LibvpxVideoRenderer extends BaseRenderer { } private static boolean isBufferLate(long earlyUs) { - // Class a buffer as late if it should have been presented more than 30ms ago. + // Class a buffer as late if it should have been presented more than 30 ms ago. return earlyUs < -30000; } + private static boolean isBufferVeryLate(long earlyUs) { + // Class a buffer as very late if it should have been presented more than 500 ms ago. + return earlyUs < -500000; + } + } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index cb443e38ee..40f2a7b081 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -532,6 +532,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { if (surface == dummySurface) { // Skip frames in sync with playback, so we'll be at the right frame if the mode changes. if (isBufferLate(earlyUs)) { + forceRenderFrame = false; skipOutputBuffer(codec, bufferIndex, presentationTimeUs); return true; } @@ -569,7 +570,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { if (shouldDropBuffersToKeyframe(earlyUs, elapsedRealtimeUs) && maybeDropBuffersToKeyframe(codec, bufferIndex, presentationTimeUs, positionUs)) { forceRenderFrame = true; - return true; + return false; } else if (shouldDropOutputBuffer(earlyUs, elapsedRealtimeUs)) { dropOutputBuffer(codec, bufferIndex, presentationTimeUs); return true; From 030f52b41bb5ae2da5d8e1659af50c58435da57b Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 9 Oct 2017 05:51:37 -0700 Subject: [PATCH 0514/2472] Wait for HandlerThread to terminate after calling quit. Calling HandlerThread.quit() or .quitSafely() doesn't immediately terminate the thread. It just instructs the Looper not to accept any new messages and to terminate at the next opportunity. Added a HandlerThread.join() everywhere where the intention is to close and release all resources and to stop all threads. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=171525241 --- .../google/android/exoplayer2/ExoPlayerImplInternal.java | 2 +- .../android/exoplayer2/testutil/FakeSimpleExoPlayer.java | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 765b2a7634..a0ca448de7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -304,7 +304,6 @@ import java.io.IOException; // Restore the interrupted status. Thread.currentThread().interrupt(); } - internalPlaybackThread.quit(); } public Looper getPlaybackLooper() { @@ -840,6 +839,7 @@ import java.io.IOException; resetInternal(true); loadControl.onReleased(); setState(Player.STATE_IDLE); + internalPlaybackThread.quit(); synchronized (this) { released = true; notifyAll(); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java index b56c299e78..70c0f84051 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java @@ -212,9 +212,17 @@ public class FakeSimpleExoPlayer extends SimpleExoPlayer { } @Override + @SuppressWarnings("ThreadJoinLoop") public void release() { stop(); playbackThread.quitSafely(); + while (playbackThread.isAlive()) { + try { + playbackThread.join(); + } catch (InterruptedException e) { + // Ignore interrupt. + } + } } @Override From fc985fe4dabe287d54653aaa3f743607965ac1f8 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 9 Oct 2017 07:39:40 -0700 Subject: [PATCH 0515/2472] Update IMA documentation for AdsLoader ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=171533782 --- extensions/ima/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/extensions/ima/README.md b/extensions/ima/README.md index 4f63214f04..a796ca8694 100644 --- a/extensions/ima/README.md +++ b/extensions/ima/README.md @@ -1,11 +1,11 @@ # ExoPlayer IMA extension # -The IMA extension is a [MediaSource][] implementation wrapping the +The IMA extension is an [AdsLoader][] implementation wrapping the [Interactive Media Ads SDK for Android][IMA]. You can use it to insert ads alongside content. [IMA]: https://developers.google.com/interactive-media-ads/docs/sdks/android/ -[MediaSource]: https://google.github.io/ExoPlayer/doc/reference/index.html?com/google/android/exoplayer2/source/MediaSource.html +[AdsLoader]: https://google.github.io/ExoPlayer/doc/reference/index.html?com/google/android/exoplayer2/source/ads/AdsLoader.html ## Getting the extension ## @@ -27,7 +27,7 @@ locally. Instructions for doing this can be found in ExoPlayer's ## Using the extension ## To play ads alongside a single-window content `MediaSource`, prepare the player -with an `ImaAdsMediaSource` constructed using an `ImaAdsLoader`, the content +with an `AdsMediaSource` constructed using an `ImaAdsLoader`, the content `MediaSource` and an overlay `ViewGroup` on top of the player. Pass an ad tag URI from your ad campaign when creating the `ImaAdsLoader`. The IMA documentation includes some [sample ad tags][] for testing. @@ -38,7 +38,7 @@ background, and are recreated when the player returns to the foreground. When playing ads it is necessary to persist ad playback state while in the background by keeping a reference to the `ImaAdsLoader`. Reuse it when resuming playback of the same content/ads by passing it in when constructing the new -`ImaAdsMediaSource`. It is also important to persist the player position when +`AdsMediaSource`. It is also important to persist the player position when entering the background by storing the value of `player.getContentPosition()`. On returning to the foreground, seek to that position before preparing the new player instance. Finally, it is important to call `ImaAdsLoader.release()` when From 9a6d717a7a0389a57fd0c9c62dc3ae2c072fd2f2 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 10 Oct 2017 12:39:34 -0700 Subject: [PATCH 0516/2472] Allow overriding of DefaultDashChunkSource.getNextChunk ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=171718775 --- .../source/dash/DefaultDashChunkSource.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/library/dash/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 index 732934515b..bb798b0af9 100644 --- a/library/dash/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 @@ -175,7 +175,7 @@ public class DefaultDashChunkSource implements DashChunkSource { } @Override - public final void getNextChunk(MediaChunk previous, long playbackPositionUs, ChunkHolder out) { + public void getNextChunk(MediaChunk previous, long playbackPositionUs, ChunkHolder out) { if (fatalError != null) { return; } @@ -304,7 +304,7 @@ public class DefaultDashChunkSource implements DashChunkSource { trackSelection.indexOf(chunk.trackFormat), e); } - // Private methods. + // Internal methods. private ArrayList getRepresentations() { List manifestAdapationSets = manifest.getPeriod(periodIndex).adaptationSets; @@ -329,7 +329,12 @@ public class DefaultDashChunkSource implements DashChunkSource { } } - private static Chunk newInitializationChunk(RepresentationHolder representationHolder, + private long resolveTimeToLiveEdgeUs(long playbackPositionUs) { + boolean resolveTimeToLiveEdgePossible = manifest.dynamic && liveEdgeTimeUs != C.TIME_UNSET; + return resolveTimeToLiveEdgePossible ? liveEdgeTimeUs - playbackPositionUs : C.TIME_UNSET; + } + + protected static Chunk newInitializationChunk(RepresentationHolder representationHolder, DataSource dataSource, Format trackFormat, int trackSelectionReason, Object trackSelectionData, RangedUri initializationUri, RangedUri indexUri) { RangedUri requestUri; @@ -350,7 +355,7 @@ public class DefaultDashChunkSource implements DashChunkSource { trackSelectionReason, trackSelectionData, representationHolder.extractorWrapper); } - private static Chunk newMediaChunk(RepresentationHolder representationHolder, + protected static Chunk newMediaChunk(RepresentationHolder representationHolder, DataSource dataSource, int trackType, Format trackFormat, int trackSelectionReason, Object trackSelectionData, int firstSegmentNum, int maxSegmentCount) { Representation representation = representationHolder.representation; @@ -385,11 +390,6 @@ public class DefaultDashChunkSource implements DashChunkSource { } } - private long resolveTimeToLiveEdgeUs(long playbackPositionUs) { - boolean resolveTimeToLiveEdgePossible = manifest.dynamic && liveEdgeTimeUs != C.TIME_UNSET; - return resolveTimeToLiveEdgePossible ? liveEdgeTimeUs - playbackPositionUs : C.TIME_UNSET; - } - // Protected classes. /** From 5d4fa335f98d11c41b5d1e46a4945225545836ce Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 10 Oct 2017 12:40:02 -0700 Subject: [PATCH 0517/2472] Expose public constructors for FrameworkMediaCrypto MediaCodecRenderer implementations require DrmSessionManager, but it's currently not possible for an app to provide a custom implementation due to FrameworkMediaCrypto having a package private constructor. This change exposes public FrameworkMediaCrypto constructors, hence removing this restriction. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=171718853 --- .../exoplayer2/drm/FrameworkMediaCrypto.java | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaCrypto.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaCrypto.java index 5bee85f449..4e58ed6a31 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaCrypto.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaCrypto.java @@ -28,12 +28,29 @@ public final class FrameworkMediaCrypto implements ExoMediaCrypto { private final MediaCrypto mediaCrypto; private final boolean forceAllowInsecureDecoderComponents; - /* package */ FrameworkMediaCrypto(MediaCrypto mediaCrypto, + /** + * @param mediaCrypto The {@link MediaCrypto} to wrap. + */ + public FrameworkMediaCrypto(MediaCrypto mediaCrypto) { + this(mediaCrypto, false); + } + + /** + * @param mediaCrypto The {@link MediaCrypto} to wrap. + * @param forceAllowInsecureDecoderComponents Whether to force + * {@link #requiresSecureDecoderComponent(String)} to return {@code false}, rather than + * {@link MediaCrypto#requiresSecureDecoderComponent(String)} of the wrapped + * {@link MediaCrypto}. + */ + public FrameworkMediaCrypto(MediaCrypto mediaCrypto, boolean forceAllowInsecureDecoderComponents) { this.mediaCrypto = Assertions.checkNotNull(mediaCrypto); this.forceAllowInsecureDecoderComponents = forceAllowInsecureDecoderComponents; } + /** + * Returns the wrapped {@link MediaCrypto}. + */ public MediaCrypto getWrappedMediaCrypto() { return mediaCrypto; } From 763f663d019823ea00ae659cda839889648d921e Mon Sep 17 00:00:00 2001 From: hoangtc Date: Wed, 11 Oct 2017 03:54:15 -0700 Subject: [PATCH 0518/2472] Update DefaultTrackSelector to use more conditions when selecting audio track Update the audio track selection logic in DefaultTrackSelector: - When forcing lowest bitrate, use bitrate as tie-breaker when track scores are the same, prefer the lower bitrate. - Otherwise, use one of the following values as tie-breaker in order: - ChannelCount - SampleRate - BitRate If the format being checked is within renderer's capabilities, select it if it has higher tie-break value, else, select it if it has lower tie-break value. If all tie-break values are the same, prefer the already selected track. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=171803092 --- .../trackselection/DefaultTrackSelector.java | 132 +++- .../DefaultTrackSelectorTest.java | 660 ++++++++++++++++++ 2 files changed, 762 insertions(+), 30 deletions(-) create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index 0ab4f62866..42eeebde11 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -762,10 +762,9 @@ public class DefaultTrackSelector extends MappingTrackSelector { protected TrackSelection selectAudioTrack(TrackGroupArray groups, int[][] formatSupport, Parameters params, TrackSelection.Factory adaptiveTrackSelectionFactory) throws ExoPlaybackException { - int selectedGroupIndex = C.INDEX_UNSET; int selectedTrackIndex = C.INDEX_UNSET; - int selectedTrackScore = 0; - int selectedBitrate = Format.NO_VALUE; + int selectedGroupIndex = C.INDEX_UNSET; + AudioTrackScore selectedTrackScore = null; for (int groupIndex = 0; groupIndex < groups.length; groupIndex++) { TrackGroup trackGroup = groups.get(groupIndex); int[] trackFormatSupport = formatSupport[groupIndex]; @@ -773,15 +772,12 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (isSupported(trackFormatSupport[trackIndex], params.exceedRendererCapabilitiesIfNecessary)) { Format format = trackGroup.getFormat(trackIndex); - int trackScore = getAudioTrackScore(trackFormatSupport[trackIndex], - params.preferredAudioLanguage, format); - if (trackScore > selectedTrackScore - || (trackScore == selectedTrackScore && params.forceLowestBitrate - && compareFormatValues(format.bitrate, selectedBitrate) < 0)) { + AudioTrackScore trackScore = + new AudioTrackScore(format, params, trackFormatSupport[trackIndex]); + if (selectedTrackScore == null || trackScore.compareTo(selectedTrackScore) > 0) { selectedGroupIndex = groupIndex; selectedTrackIndex = trackIndex; selectedTrackScore = trackScore; - selectedBitrate = format.bitrate; } } } @@ -804,27 +800,6 @@ public class DefaultTrackSelector extends MappingTrackSelector { 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; @@ -1090,6 +1065,103 @@ public class DefaultTrackSelector extends MappingTrackSelector { } } + /** + * A representation of how well a track fits with our track selection {@link Parameters}. + * + *

          This is used to rank different audio tracks relatively with each other. + */ + private static final class AudioTrackScore implements Comparable { + private final Parameters parameters; + private final int withinRendererCapabilitiesScore; + private final int matchLanguageScore; + private final int defaultSelectionFlagScore; + private final int channelCount; + private final int sampleRate; + private final int bitrate; + + public AudioTrackScore(Format format, Parameters parameters, int formatSupport) { + this.parameters = parameters; + withinRendererCapabilitiesScore = isSupported(formatSupport, false) ? 1 : 0; + matchLanguageScore = formatHasLanguage(format, parameters.preferredAudioLanguage) ? 1 : 0; + defaultSelectionFlagScore = (format.selectionFlags & C.SELECTION_FLAG_DEFAULT) != 0 ? 1 : 0; + channelCount = format.channelCount; + sampleRate = format.sampleRate; + bitrate = format.bitrate; + } + + /** + * Compares the score of the current track format with another {@link AudioTrackScore}. + * + * @param other The other score to compare to. + * @return A positive integer if this score is better than the other. Zero if they are + * equal. A negative integer if this score is worse than the other. + */ + @Override + public int compareTo(AudioTrackScore other) { + if (this.withinRendererCapabilitiesScore != other.withinRendererCapabilitiesScore) { + return compareInts(this.withinRendererCapabilitiesScore, + other.withinRendererCapabilitiesScore); + } else if (this.matchLanguageScore != other.matchLanguageScore) { + return compareInts(this.matchLanguageScore, other.matchLanguageScore); + } else if (this.defaultSelectionFlagScore != other.defaultSelectionFlagScore) { + return compareInts(this.defaultSelectionFlagScore, other.defaultSelectionFlagScore); + } else if (parameters.forceLowestBitrate) { + return compareInts(other.bitrate, this.bitrate); + } else { + // If the format are within renderer capabilities, prefer higher values of channel count, + // sample rate and bit rate in that order. Otherwise, prefer lower values. + int resultSign = withinRendererCapabilitiesScore == 1 ? 1 : -1; + if (this.channelCount != other.channelCount) { + return resultSign * compareInts(this.channelCount, other.channelCount); + } else if (this.sampleRate != other.sampleRate) { + return resultSign * compareInts(this.sampleRate, other.sampleRate); + } + return resultSign * compareInts(this.bitrate, other.bitrate); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + AudioTrackScore that = (AudioTrackScore) o; + + return withinRendererCapabilitiesScore == that.withinRendererCapabilitiesScore + && matchLanguageScore == that.matchLanguageScore + && defaultSelectionFlagScore == that.defaultSelectionFlagScore + && channelCount == that.channelCount && sampleRate == that.sampleRate + && bitrate == that.bitrate; + } + + @Override + public int hashCode() { + int result = withinRendererCapabilitiesScore; + result = 31 * result + matchLanguageScore; + result = 31 * result + defaultSelectionFlagScore; + result = 31 * result + channelCount; + result = 31 * result + sampleRate; + result = 31 * result + bitrate; + return result; + } + } + + /** + * Compares two integers in a safe way and avoiding potential overflow. + * + * @param first The first value. + * @param second The second value. + * @return A negative integer if the first value is less than the second. Zero if they are equal. + * A positive integer if the first value is greater than the second. + */ + private static int compareInts(int first, int second) { + return first > second ? 1 : (second > first ? -1 : 0); + } + private static final class AudioConfigurationTuple { public final int channelCount; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java new file mode 100644 index 0000000000..a0e499139c --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java @@ -0,0 +1,660 @@ +package com.google.android.exoplayer2.trackselection; + +import static com.google.android.exoplayer2.RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES; +import static com.google.android.exoplayer2.RendererCapabilities.FORMAT_HANDLED; +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.MockitoAnnotations.initMocks; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlaybackException; +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; +import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.Parameters; +import com.google.android.exoplayer2.trackselection.TrackSelector.InvalidationListener; +import com.google.android.exoplayer2.util.MimeTypes; +import java.util.HashMap; +import java.util.Map; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Unit tests for {@link DefaultTrackSelector}. + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class DefaultTrackSelectorTest { + + private static final Parameters DEFAULT_PARAMETERS = new Parameters(); + private static final RendererCapabilities ALL_AUDIO_FORMAT_SUPPORTED_RENDERER_CAPABILITIES = + new FakeRendererCapabilities(C.TRACK_TYPE_AUDIO); + private static final RendererCapabilities ALL_AUDIO_FORMAT_EXCEEDED_RENDERER_CAPABILITIES = + new FakeRendererCapabilities(C.TRACK_TYPE_AUDIO, FORMAT_EXCEEDS_CAPABILITIES); + + @Mock + private InvalidationListener invalidationListener; + + private DefaultTrackSelector trackSelector; + + @Before + public void setUp() { + initMocks(this); + trackSelector = new DefaultTrackSelector(); + } + + /** + * Tests that track selector will not call + * {@link InvalidationListener#onTrackSelectionsInvalidated()} when it's set with default + * values of {@link Parameters}. + */ + @Test + public void testSetParameterWithDefaultParametersDoesNotNotifyInvalidationListener() + throws Exception { + trackSelector.init(invalidationListener); + trackSelector.setParameters(DEFAULT_PARAMETERS); + + verify(invalidationListener, never()).onTrackSelectionsInvalidated(); + } + + /** + * Tests that track selector will call {@link InvalidationListener#onTrackSelectionsInvalidated()} + * when it's set with non-default values of {@link Parameters}. + */ + @Test + public void testSetParameterWithNonDefaultParameterNotifyInvalidationListener() + throws Exception { + Parameters parameters = DEFAULT_PARAMETERS.withPreferredAudioLanguage("en"); + trackSelector.init(invalidationListener); + trackSelector.setParameters(parameters); + + verify(invalidationListener).onTrackSelectionsInvalidated(); + } + + /** + * Tests that track selector will not call + * {@link InvalidationListener#onTrackSelectionsInvalidated()} again when it's set with + * the same values of {@link Parameters}. + */ + @Test + public void testSetParameterWithSameParametersDoesNotNotifyInvalidationListenerAgain() + throws Exception { + Parameters parameters = DEFAULT_PARAMETERS.withPreferredAudioLanguage("en"); + trackSelector.init(invalidationListener); + trackSelector.setParameters(parameters); + trackSelector.setParameters(parameters); + + verify(invalidationListener, times(1)).onTrackSelectionsInvalidated(); + } + + /** + * Tests that track selector will select audio track with {@link C#SELECTION_FLAG_DEFAULT} + * given default values of {@link Parameters}. + */ + @Test + public void testSelectTracksSelectTrackWithSelectionFlag() throws Exception { + Format audioFormat = + Format.createAudioSampleFormat("audio", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, + Format.NO_VALUE, 2, 44100, null, null, 0, null); + Format formatWithSelectionFlag = + Format.createAudioSampleFormat("audio", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, + Format.NO_VALUE, 2, 44100, null, null, C.SELECTION_FLAG_DEFAULT, null); + + TrackSelectorResult result = trackSelector.selectTracks( + new RendererCapabilities[] {ALL_AUDIO_FORMAT_SUPPORTED_RENDERER_CAPABILITIES}, + singleTrackGroup(formatWithSelectionFlag, audioFormat)); + assertThat(result.selections.get(0).getSelectedFormat()).isEqualTo(formatWithSelectionFlag); + } + + /** + * Tests that track selector will select audio track with language that match preferred language + * given by {@link Parameters}. + */ + @Test + public void testSelectTracksSelectPreferredAudioLanguage() + throws Exception { + Parameters parameters = DEFAULT_PARAMETERS.withPreferredAudioLanguage("en"); + trackSelector.setParameters(parameters); + + Format frAudioFormat = + Format.createAudioSampleFormat("audio", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, + Format.NO_VALUE, 2, 44100, null, null, 0, "fr"); + Format enAudioFormat = + Format.createAudioSampleFormat("audio", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, + Format.NO_VALUE, 2, 44100, null, null, 0, "en"); + + TrackSelectorResult result = trackSelector.selectTracks( + new RendererCapabilities[] {ALL_AUDIO_FORMAT_SUPPORTED_RENDERER_CAPABILITIES}, + singleTrackGroup(frAudioFormat, enAudioFormat)); + + assertThat(result.selections.get(0).getSelectedFormat()).isEqualTo(enAudioFormat); + } + + /** + * Tests that track selector will prefer selecting audio track with language that match preferred + * language given by {@link Parameters} over track with {@link C#SELECTION_FLAG_DEFAULT}. + */ + @Test + public void testSelectTracksSelectPreferredAudioLanguageOverSelectionFlag() + throws Exception { + Parameters parameters = DEFAULT_PARAMETERS.withPreferredAudioLanguage("en"); + trackSelector.setParameters(parameters); + + Format frAudioFormat = + Format.createAudioSampleFormat("audio", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, + Format.NO_VALUE, 2, 44100, null, null, C.SELECTION_FLAG_DEFAULT, "fr"); + Format enAudioFormat = + Format.createAudioSampleFormat("audio", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, + Format.NO_VALUE, 2, 44100, null, null, 0, "en"); + + TrackSelectorResult result = trackSelector.selectTracks( + new RendererCapabilities[] {ALL_AUDIO_FORMAT_SUPPORTED_RENDERER_CAPABILITIES}, + singleTrackGroup(frAudioFormat, enAudioFormat)); + + assertThat(result.selections.get(0).getSelectedFormat()).isEqualTo(enAudioFormat); + } + + /** + * Tests that track selector will prefer tracks that are within renderer's capabilities over + * track that exceed renderer's capabilities. + */ + @Test + public void testSelectTracksPreferTrackWithinCapabilities() + throws Exception { + Format supportedFormat = + Format.createAudioSampleFormat("supportedFormat", MimeTypes.AUDIO_AAC, null, + Format.NO_VALUE, Format.NO_VALUE, 2, 44100, null, null, 0, null); + Format exceededFormat = + Format.createAudioSampleFormat("exceededFormat", MimeTypes.AUDIO_AAC, null, + Format.NO_VALUE, Format.NO_VALUE, 2, 44100, null, null, 0, null); + + Map mappedCapabilities = new HashMap<>(); + mappedCapabilities.put(supportedFormat.id, FORMAT_HANDLED); + mappedCapabilities.put(exceededFormat.id, FORMAT_EXCEEDS_CAPABILITIES); + RendererCapabilities mappedAudioRendererCapabilities = + new FakeMappedRendererCapabilities(C.TRACK_TYPE_AUDIO, mappedCapabilities); + + TrackSelectorResult result = trackSelector.selectTracks( + new RendererCapabilities[] {mappedAudioRendererCapabilities}, + singleTrackGroup(exceededFormat, supportedFormat)); + + assertThat(result.selections.get(0).getSelectedFormat()).isEqualTo(supportedFormat); + } + + /** + * Tests that track selector will select a track that exceeds the renderer's capabilities when + * there are no other choice, given the default {@link Parameters}. + */ + @Test + public void testSelectTracksWithNoTrackWithinCapabilitiesSelectExceededCapabilityTrack() + throws Exception { + + Format audioFormat = + Format.createAudioSampleFormat("audio", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, + Format.NO_VALUE, 2, 44100, null, null, 0, null); + TrackSelectorResult result = trackSelector.selectTracks( + new RendererCapabilities[] {ALL_AUDIO_FORMAT_EXCEEDED_RENDERER_CAPABILITIES}, + singleTrackGroup(audioFormat)); + + assertThat(result.selections.get(0).getSelectedFormat()).isEqualTo(audioFormat); + } + + /** + * Tests that track selector will return a null track selection for a renderer when + * all tracks exceed that renderer's capabilities when {@link Parameters} does not allow + * exceeding-capabilities tracks. + */ + @Test + public void testSelectTracksWithNoTrackWithinCapabilitiesAndSetByParamsReturnNoSelection() + throws Exception { + Parameters parameters = DEFAULT_PARAMETERS.withExceedRendererCapabilitiesIfNecessary(false); + trackSelector.setParameters(parameters); + + Format audioFormat = + Format.createAudioSampleFormat("audio", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, + Format.NO_VALUE, 2, 44100, null, null, 0, null); + TrackSelectorResult result = trackSelector.selectTracks( + new RendererCapabilities[] {ALL_AUDIO_FORMAT_EXCEEDED_RENDERER_CAPABILITIES}, + singleTrackGroup(audioFormat)); + + assertThat(result.selections.get(0)).isNull(); + } + + /** + * Tests that track selector will prefer tracks that are within renderer's capabilities over + * tracks that have {@link C#SELECTION_FLAG_DEFAULT} but exceed renderer's capabilities. + */ + @Test + public void testSelectTracksPreferTrackWithinCapabilitiesOverSelectionFlag() + throws Exception { + Format supportedFormat = + Format.createAudioSampleFormat("supportedFormat", MimeTypes.AUDIO_AAC, null, + Format.NO_VALUE, Format.NO_VALUE, 2, 44100, null, null, 0, null); + Format exceededWithSelectionFlagFormat = + Format.createAudioSampleFormat("exceededFormat", MimeTypes.AUDIO_AAC, null, + Format.NO_VALUE, Format.NO_VALUE, 2, 44100, null, null, C.SELECTION_FLAG_DEFAULT, null); + + Map mappedCapabilities = new HashMap<>(); + mappedCapabilities.put(supportedFormat.id, FORMAT_HANDLED); + mappedCapabilities.put(exceededWithSelectionFlagFormat.id, FORMAT_EXCEEDS_CAPABILITIES); + RendererCapabilities mappedAudioRendererCapabilities = + new FakeMappedRendererCapabilities(C.TRACK_TYPE_AUDIO, mappedCapabilities); + + TrackSelectorResult result = trackSelector.selectTracks( + new RendererCapabilities[] {mappedAudioRendererCapabilities}, + singleTrackGroup(exceededWithSelectionFlagFormat, supportedFormat)); + + assertThat(result.selections.get(0).getSelectedFormat()).isEqualTo(supportedFormat); + } + + /** + * Tests that track selector will prefer tracks that are within renderer's capabilities over + * track that have language matching preferred audio given by {@link Parameters} but exceed + * renderer's capabilities. + */ + @Test + public void testSelectTracksPreferTrackWithinCapabilitiesOverPreferredLanguage() + throws Exception { + Parameters parameters = DEFAULT_PARAMETERS.withPreferredAudioLanguage("en"); + trackSelector.setParameters(parameters); + + Format supportedFrFormat = + Format.createAudioSampleFormat("supportedFormat", MimeTypes.AUDIO_AAC, null, + Format.NO_VALUE, Format.NO_VALUE, 2, 44100, null, null, 0, "fr"); + Format exceededEnFormat = + Format.createAudioSampleFormat("exceededFormat", MimeTypes.AUDIO_AAC, null, + Format.NO_VALUE, Format.NO_VALUE, 2, 44100, null, null, 0, "en"); + + Map mappedCapabilities = new HashMap<>(); + mappedCapabilities.put(exceededEnFormat.id, FORMAT_EXCEEDS_CAPABILITIES); + mappedCapabilities.put(supportedFrFormat.id, FORMAT_HANDLED); + RendererCapabilities mappedAudioRendererCapabilities = + new FakeMappedRendererCapabilities(C.TRACK_TYPE_AUDIO, mappedCapabilities); + + TrackSelectorResult result = trackSelector.selectTracks( + new RendererCapabilities[] {mappedAudioRendererCapabilities}, + singleTrackGroup(exceededEnFormat, supportedFrFormat)); + + assertThat(result.selections.get(0).getSelectedFormat()).isEqualTo(supportedFrFormat); + } + + /** + * Tests that track selector will prefer tracks that are within renderer's capabilities over + * track that have both language matching preferred audio given by {@link Parameters} and + * {@link C#SELECTION_FLAG_DEFAULT}, but exceed renderer's capabilities. + */ + @Test + public void testSelectTracksPreferTrackWithinCapabilitiesOverSelectionFlagAndPreferredLanguage() + throws Exception { + Parameters parameters = DEFAULT_PARAMETERS.withPreferredAudioLanguage("en"); + trackSelector.setParameters(parameters); + + Format supportedFrFormat = + Format.createAudioSampleFormat("supportedFormat", MimeTypes.AUDIO_AAC, null, + Format.NO_VALUE, Format.NO_VALUE, 2, 44100, null, null, 0, "fr"); + Format exceededDefaultSelectionEnFormat = + Format.createAudioSampleFormat("exceededFormat", MimeTypes.AUDIO_AAC, null, + Format.NO_VALUE, Format.NO_VALUE, 2, 44100, null, null, C.SELECTION_FLAG_DEFAULT, "en"); + + Map mappedCapabilities = new HashMap<>(); + mappedCapabilities.put(exceededDefaultSelectionEnFormat.id, FORMAT_EXCEEDS_CAPABILITIES); + mappedCapabilities.put(supportedFrFormat.id, FORMAT_HANDLED); + RendererCapabilities mappedAudioRendererCapabilities = + new FakeMappedRendererCapabilities(C.TRACK_TYPE_AUDIO, mappedCapabilities); + + TrackSelectorResult result = trackSelector.selectTracks( + new RendererCapabilities[] {mappedAudioRendererCapabilities}, + singleTrackGroup(exceededDefaultSelectionEnFormat, supportedFrFormat)); + + assertThat(result.selections.get(0).getSelectedFormat()).isEqualTo(supportedFrFormat); + } + + /** + * Tests that track selector will select audio tracks with higher num channel when other factors + * are the same, and tracks are within renderer's capabilities. + */ + @Test + public void testSelectTracksWithinCapabilitiesSelectHigherNumChannel() + throws Exception { + Format lowerChannelFormat = + Format.createAudioSampleFormat("audioFormat", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, + Format.NO_VALUE, 2, 44100, null, null, 0, null); + Format higherChannelFormat = + Format.createAudioSampleFormat("audioFormat", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, + Format.NO_VALUE, 6, 44100, null, null, 0, null); + + TrackSelectorResult result = trackSelector.selectTracks( + new RendererCapabilities[] {ALL_AUDIO_FORMAT_SUPPORTED_RENDERER_CAPABILITIES}, + singleTrackGroup(higherChannelFormat, lowerChannelFormat)); + + assertThat(result.selections.get(0).getSelectedFormat()).isEqualTo(higherChannelFormat); + } + + /** + * Tests that track selector will select audio tracks with higher sample rate when other factors + * are the same, and tracks are within renderer's capabilities. + */ + @Test + public void testSelectTracksWithinCapabilitiesSelectHigherSampleRate() + throws Exception { + Format higherSampleRateFormat = + Format.createAudioSampleFormat("audioFormat", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, + Format.NO_VALUE, 2, 44100, null, null, 0, null); + Format lowerSampleRateFormat = + Format.createAudioSampleFormat("audioFormat", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, + Format.NO_VALUE, 2, 22050, null, null, 0, null); + + TrackSelectorResult result = trackSelector.selectTracks( + new RendererCapabilities[] {ALL_AUDIO_FORMAT_SUPPORTED_RENDERER_CAPABILITIES}, + singleTrackGroup(higherSampleRateFormat, lowerSampleRateFormat)); + + assertThat(result.selections.get(0).getSelectedFormat()).isEqualTo(higherSampleRateFormat); + } + + /** + * Tests that track selector will select audio tracks with higher bit-rate when other factors + * are the same, and tracks are within renderer's capabilities. + */ + @Test + public void testSelectTracksWithinCapabilitiesSelectHigherBitrate() + throws Exception { + Format lowerBitrateFormat = + Format.createAudioSampleFormat("audioFormat", MimeTypes.AUDIO_AAC, null, 15000, + Format.NO_VALUE, 2, 44100, null, null, 0, null); + Format higherBitrateFormat = + Format.createAudioSampleFormat("audioFormat", MimeTypes.AUDIO_AAC, null, 30000, + Format.NO_VALUE, 2, 44100, null, null, 0, null); + + TrackSelectorResult result = trackSelector.selectTracks( + new RendererCapabilities[] {ALL_AUDIO_FORMAT_SUPPORTED_RENDERER_CAPABILITIES}, + singleTrackGroup(lowerBitrateFormat, higherBitrateFormat)); + + assertThat(result.selections.get(0).getSelectedFormat()).isEqualTo(higherBitrateFormat); + } + + /** + * Tests that track selector will prefer audio tracks with higher channel count over tracks with + * higher sample rate when other factors are the same, and tracks are within renderer's + * capabilities. + */ + @Test + public void testSelectTracksPreferHigherNumChannelBeforeSampleRate() + throws Exception { + Format lowerChannelHigherSampleRateFormat = + Format.createAudioSampleFormat("audioFormat", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, + Format.NO_VALUE, 2, 44100, null, null, 0, null); + Format higherChannelLowerSampleRateFormat = + Format.createAudioSampleFormat("audioFormat", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, + Format.NO_VALUE, 6, 22050, null, null, 0, null); + + TrackSelectorResult result = trackSelector.selectTracks( + new RendererCapabilities[] {ALL_AUDIO_FORMAT_SUPPORTED_RENDERER_CAPABILITIES}, + singleTrackGroup(higherChannelLowerSampleRateFormat, lowerChannelHigherSampleRateFormat)); + + assertThat(result.selections.get(0).getSelectedFormat()) + .isEqualTo(higherChannelLowerSampleRateFormat); + } + + /** + * Tests that track selector will prefer audio tracks with higher sample rate over tracks with + * higher bitrate when other factors are the same, and tracks are within renderer's + * capabilities. + */ + @Test + public void testSelectTracksPreferHigherSampleRateBeforeBitrate() + throws Exception { + Format higherSampleRateLowerBitrateFormat = + Format.createAudioSampleFormat("audioFormat", MimeTypes.AUDIO_AAC, null, 15000, + Format.NO_VALUE, 2, 44100, null, null, 0, null); + Format lowerSampleRateHigherBitrateFormat = + Format.createAudioSampleFormat("audioFormat", MimeTypes.AUDIO_AAC, null, 30000, + Format.NO_VALUE, 2, 22050, null, null, 0, null); + + TrackSelectorResult result = trackSelector.selectTracks( + new RendererCapabilities[] {ALL_AUDIO_FORMAT_SUPPORTED_RENDERER_CAPABILITIES}, + singleTrackGroup(higherSampleRateLowerBitrateFormat, lowerSampleRateHigherBitrateFormat)); + + assertThat(result.selections.get(0).getSelectedFormat()) + .isEqualTo(higherSampleRateLowerBitrateFormat); + } + + /** + * Tests that track selector will select audio tracks with lower num channel when other factors + * are the same, and tracks exceed renderer's capabilities. + */ + @Test + public void testSelectTracksExceedingCapabilitiesSelectLowerNumChannel() + throws Exception { + Format lowerChannelFormat = + Format.createAudioSampleFormat("audioFormat", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, + Format.NO_VALUE, 2, 44100, null, null, 0, null); + Format higherChannelFormat = + Format.createAudioSampleFormat("audioFormat", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, + Format.NO_VALUE, 6, 44100, null, null, 0, null); + + TrackSelectorResult result = trackSelector.selectTracks( + new RendererCapabilities[] {ALL_AUDIO_FORMAT_EXCEEDED_RENDERER_CAPABILITIES}, + singleTrackGroup(higherChannelFormat, lowerChannelFormat)); + + assertThat(result.selections.get(0).getSelectedFormat()).isEqualTo(lowerChannelFormat); + } + + /** + * Tests that track selector will select audio tracks with lower sample rate when other factors + * are the same, and tracks exceed renderer's capabilities. + */ + @Test + public void testSelectTracksExceedingCapabilitiesSelectLowerSampleRate() + throws Exception { + Format lowerSampleRateFormat = + Format.createAudioSampleFormat("audioFormat", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, + Format.NO_VALUE, 2, 22050, null, null, 0, null); + Format higherSampleRateFormat = + Format.createAudioSampleFormat("audioFormat", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, + Format.NO_VALUE, 2, 44100, null, null, 0, null); + + TrackSelectorResult result = trackSelector.selectTracks( + new RendererCapabilities[] {ALL_AUDIO_FORMAT_EXCEEDED_RENDERER_CAPABILITIES}, + singleTrackGroup(higherSampleRateFormat, lowerSampleRateFormat)); + + assertThat(result.selections.get(0).getSelectedFormat()).isEqualTo(lowerSampleRateFormat); + } + + /** + * Tests that track selector will select audio tracks with lower bit-rate when other factors + * are the same, and tracks exceed renderer's capabilities. + */ + @Test + public void testSelectTracksExceedingCapabilitiesSelectLowerBitrate() + throws Exception { + Format lowerBitrateFormat = + Format.createAudioSampleFormat("audioFormat", MimeTypes.AUDIO_AAC, null, 15000, + Format.NO_VALUE, 2, 44100, null, null, 0, null); + Format higherBitrateFormat = + Format.createAudioSampleFormat("audioFormat", MimeTypes.AUDIO_AAC, null, 30000, + Format.NO_VALUE, 2, 44100, null, null, 0, null); + + TrackSelectorResult result = trackSelector.selectTracks( + new RendererCapabilities[] {ALL_AUDIO_FORMAT_EXCEEDED_RENDERER_CAPABILITIES}, + singleTrackGroup(lowerBitrateFormat, higherBitrateFormat)); + + assertThat(result.selections.get(0).getSelectedFormat()).isEqualTo(lowerBitrateFormat); + } + + /** + * Tests that track selector will prefer audio tracks with lower channel count over tracks with + * lower sample rate when other factors are the same, and tracks are within renderer's + * capabilities. + */ + @Test + public void testSelectTracksExceedingCapabilitiesPreferLowerNumChannelBeforeSampleRate() + throws Exception { + Format lowerChannelHigherSampleRateFormat = + Format.createAudioSampleFormat("audioFormat", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, + Format.NO_VALUE, 2, 44100, null, null, 0, null); + Format higherChannelLowerSampleRateFormat = + Format.createAudioSampleFormat("audioFormat", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, + Format.NO_VALUE, 6, 22050, null, null, 0, null); + + TrackSelectorResult result = trackSelector.selectTracks( + new RendererCapabilities[] {ALL_AUDIO_FORMAT_EXCEEDED_RENDERER_CAPABILITIES}, + singleTrackGroup(higherChannelLowerSampleRateFormat, lowerChannelHigherSampleRateFormat)); + + assertThat(result.selections.get(0).getSelectedFormat()) + .isEqualTo(lowerChannelHigherSampleRateFormat); + } + + /** + * Tests that track selector will prefer audio tracks with lower sample rate over tracks with + * lower bitrate when other factors are the same, and tracks are within renderer's + * capabilities. + */ + @Test + public void testSelectTracksExceedingCapabilitiesPreferLowerSampleRateBeforeBitrate() + throws Exception { + Format higherSampleRateLowerBitrateFormat = + Format.createAudioSampleFormat("audioFormat", MimeTypes.AUDIO_AAC, null, 15000, + Format.NO_VALUE, 2, 44100, null, null, 0, null); + Format lowerSampleRateHigherBitrateFormat = + Format.createAudioSampleFormat("audioFormat", MimeTypes.AUDIO_AAC, null, 30000, + Format.NO_VALUE, 2, 22050, null, null, 0, null); + + TrackSelectorResult result = trackSelector.selectTracks( + new RendererCapabilities[] {ALL_AUDIO_FORMAT_EXCEEDED_RENDERER_CAPABILITIES}, + singleTrackGroup(higherSampleRateLowerBitrateFormat, lowerSampleRateHigherBitrateFormat)); + + assertThat(result.selections.get(0).getSelectedFormat()) + .isEqualTo(lowerSampleRateHigherBitrateFormat); + } + + /** + * Tests that track selector will select audio tracks with lower bitrate when {@link Parameters} + * indicate lowest bitrate preference, even when tracks are within capabilities. + */ + @Test + public void testSelectTracksWithinCapabilitiesAndForceLowestBitrateSelectLowerBitrate() + throws Exception { + Parameters parameters = DEFAULT_PARAMETERS.withForceLowestBitrate(true); + trackSelector.setParameters(parameters); + + Format lowerBitrateFormat = + Format.createAudioSampleFormat("audioFormat", MimeTypes.AUDIO_AAC, null, 15000, + Format.NO_VALUE, 2, 44100, null, null, 0, null); + Format higherBitrateFormat = + Format.createAudioSampleFormat("audioFormat", MimeTypes.AUDIO_AAC, null, 30000, + Format.NO_VALUE, 2, 44100, null, null, 0, null); + + TrackSelectorResult result = trackSelector.selectTracks( + new RendererCapabilities[] {ALL_AUDIO_FORMAT_SUPPORTED_RENDERER_CAPABILITIES}, + singleTrackGroup(lowerBitrateFormat, higherBitrateFormat)); + + assertThat(result.selections.get(0).getSelectedFormat()).isEqualTo(lowerBitrateFormat); + } + + private static TrackGroupArray singleTrackGroup(Format... formats) { + return new TrackGroupArray(new TrackGroup(formats)); + } + + /** + * A {@link RendererCapabilities} that advertises support for all formats of a given type using + * a provided support value. For any format that does not have the given track type, + * {@link #supportsFormat(Format)} will return {@link #FORMAT_UNSUPPORTED_TYPE}. + */ + private static final class FakeRendererCapabilities implements RendererCapabilities { + + private final int trackType; + private final int supportValue; + + /** + * Returns {@link FakeRendererCapabilities} that advertises adaptive support for all + * tracks of the given type. + * + * @param trackType the track type of all formats that this renderer capabilities advertises + * support for. + */ + FakeRendererCapabilities(int trackType) { + this(trackType, FORMAT_HANDLED | ADAPTIVE_SEAMLESS); + } + + /** + * Returns {@link FakeRendererCapabilities} that advertises support level using given value + * for all tracks of the given type. + * + * @param trackType the track type of all formats that this renderer capabilities advertises + * support for. + * @param supportValue the support level value that will be returned for formats with + * the given type. + */ + FakeRendererCapabilities(int trackType, int supportValue) { + this.trackType = trackType; + this.supportValue = supportValue; + } + + @Override + public int getTrackType() { + return trackType; + } + + @Override + public int supportsFormat(Format format) throws ExoPlaybackException { + return MimeTypes.getTrackType(format.sampleMimeType) == trackType + ? (supportValue) : FORMAT_UNSUPPORTED_TYPE; + } + + @Override + public int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException { + return ADAPTIVE_SEAMLESS; + } + + } + + /** + * A {@link RendererCapabilities} that advertises support for different formats using a mapping + * between format ID and format-support value. + */ + private static final class FakeMappedRendererCapabilities implements RendererCapabilities { + + private final int trackType; + private final Map formatToCapability; + + /** + * Returns {@link FakeRendererCapabilities} that advertises support level using the given + * mapping between format ID and format-support value. + * + * @param trackType the track type to be returned for {@link #getTrackType()} + * @param formatToCapability a map of (format id, support level) that will be used to return + * support level for any given format. For any format that's not in the map, + * {@link #supportsFormat(Format)} will return {@link #FORMAT_UNSUPPORTED_TYPE}. + */ + FakeMappedRendererCapabilities(int trackType, Map formatToCapability) { + this.trackType = trackType; + this.formatToCapability = new HashMap<>(formatToCapability); + } + + @Override + public int getTrackType() { + return trackType; + } + + @Override + public int supportsFormat(Format format) throws ExoPlaybackException { + return format.id != null && formatToCapability.containsKey(format.id) + ? formatToCapability.get(format.id) + : FORMAT_UNSUPPORTED_TYPE; + } + + @Override + public int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException { + return ADAPTIVE_SEAMLESS; + } + + } + +} From ac3501dd84394bafe9dc14ec2e0a6c04fbfa04a6 Mon Sep 17 00:00:00 2001 From: Drew Hill Date: Thu, 12 Oct 2017 08:39:06 -0400 Subject: [PATCH 0519/2472] make videoframereleasetimehelper get refresh rate when it's enabled, so we can reuse video renderer for multiple clips with different frame rates --- .../video/MediaCodecVideoRenderer.java | 2 +- .../video/VideoFrameReleaseTimeHelper.java | 36 +++++++++++-------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index 4c1f4c0eb2..f8c3eace24 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -236,7 +236,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { tunnelingAudioSessionId = getConfiguration().tunnelingAudioSessionId; tunneling = tunnelingAudioSessionId != C.AUDIO_SESSION_ID_UNSET; eventDispatcher.enabled(decoderCounters); - frameReleaseTimeHelper.enable(); + frameReleaseTimeHelper.enable(context); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java index ad489c2312..96693a1bd2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java @@ -38,10 +38,10 @@ public final class VideoFrameReleaseTimeHelper { private static final long VSYNC_OFFSET_PERCENTAGE = 80; private static final int MIN_FRAMES_FOR_ADJUSTMENT = 6; - private final VSyncSampler vsyncSampler; - private final boolean useDefaultDisplayVsync; - private final long vsyncDurationNs; - private final long vsyncOffsetNs; + private VSyncSampler vsyncSampler; + private boolean useDefaultDisplayVsync; + private long vsyncDurationNs; + private long vsyncOffsetNs; private long lastFramePresentationTimeUs; private long adjustedLastFrameTimeNs; @@ -71,24 +71,16 @@ public final class VideoFrameReleaseTimeHelper { } private VideoFrameReleaseTimeHelper(double defaultDisplayRefreshRate) { - useDefaultDisplayVsync = defaultDisplayRefreshRate != DISPLAY_REFRESH_RATE_UNKNOWN; - if (useDefaultDisplayVsync) { - vsyncSampler = VSyncSampler.getInstance(); - vsyncDurationNs = (long) (C.NANOS_PER_SECOND / defaultDisplayRefreshRate); - vsyncOffsetNs = (vsyncDurationNs * VSYNC_OFFSET_PERCENTAGE) / 100; - } else { - vsyncSampler = null; - vsyncDurationNs = -1; // Value unused. - vsyncOffsetNs = -1; // Value unused. - } + setSync(defaultDisplayRefreshRate); } /** * Enables the helper. */ - public void enable() { + public void enable(Context context) { haveSync = false; if (useDefaultDisplayVsync) { + setSync(getDefaultDisplayRefreshRate(context)); vsyncSampler.addObserver(); } } @@ -102,6 +94,20 @@ public final class VideoFrameReleaseTimeHelper { } } + private void setSync(double defaultDisplayRefreshRate) { + + useDefaultDisplayVsync = defaultDisplayRefreshRate != DISPLAY_REFRESH_RATE_UNKNOWN; + if (useDefaultDisplayVsync) { + vsyncSampler = VSyncSampler.getInstance(); + vsyncDurationNs = (long) (C.NANOS_PER_SECOND / defaultDisplayRefreshRate); + vsyncOffsetNs = (vsyncDurationNs * VSYNC_OFFSET_PERCENTAGE) / 100; + } else { + vsyncSampler = null; + vsyncDurationNs = -1; // Value unused. + vsyncOffsetNs = -1; // Value unused. + } + } + /** * Adjusts a frame release timestamp. * From 3a1a032fa81e184c5467ec105838d425a5a7a82e Mon Sep 17 00:00:00 2001 From: Drew Hill Date: Thu, 12 Oct 2017 14:52:14 -0400 Subject: [PATCH 0520/2472] change to use displaymanager listener for refresh rate updating --- .../video/MediaCodecVideoRenderer.java | 2 +- .../video/VideoFrameReleaseTimeHelper.java | 71 +++++++++++++++++-- 2 files changed, 68 insertions(+), 5 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index f8c3eace24..4c1f4c0eb2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -236,7 +236,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { tunnelingAudioSessionId = getConfiguration().tunnelingAudioSessionId; tunneling = tunnelingAudioSessionId != C.AUDIO_SESSION_ID_UNSET; eventDispatcher.enabled(decoderCounters); - frameReleaseTimeHelper.enable(context); + frameReleaseTimeHelper.enable(); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java index 96693a1bd2..af06432261 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java @@ -17,6 +17,8 @@ package com.google.android.exoplayer2.video; import android.annotation.TargetApi; import android.content.Context; +import android.hardware.display.DisplayManager; +import android.os.Build; import android.os.Handler; import android.os.HandlerThread; import android.os.Message; @@ -31,6 +33,7 @@ import com.google.android.exoplayer2.C; @TargetApi(16) public final class VideoFrameReleaseTimeHelper { + private static final int DISPLAY_ID_UNKNOWN = -1; private static final double DISPLAY_REFRESH_RATE_UNKNOWN = -1; private static final long CHOREOGRAPHER_SAMPLE_DELAY_MILLIS = 500; private static final long MAX_ALLOWED_DRIFT_NS = 20000000; @@ -38,6 +41,9 @@ public final class VideoFrameReleaseTimeHelper { private static final long VSYNC_OFFSET_PERCENTAGE = 80; private static final int MIN_FRAMES_FOR_ADJUSTMENT = 6; + private DisplayManager.DisplayListener displayListener = null; + private Context context = null; + private VSyncSampler vsyncSampler; private boolean useDefaultDisplayVsync; private long vsyncDurationNs; @@ -68,6 +74,8 @@ public final class VideoFrameReleaseTimeHelper { */ public VideoFrameReleaseTimeHelper(Context context) { this(getDefaultDisplayRefreshRate(context)); + this.context = context; + registerDisplayListener(); } private VideoFrameReleaseTimeHelper(double defaultDisplayRefreshRate) { @@ -77,12 +85,12 @@ public final class VideoFrameReleaseTimeHelper { /** * Enables the helper. */ - public void enable(Context context) { + public void enable() { haveSync = false; if (useDefaultDisplayVsync) { - setSync(getDefaultDisplayRefreshRate(context)); vsyncSampler.addObserver(); } + registerDisplayListener(); } /** @@ -92,6 +100,29 @@ public final class VideoFrameReleaseTimeHelper { if (useDefaultDisplayVsync) { vsyncSampler.removeObserver(); } + unregisterDisplayListener(); + } + + private void registerDisplayListener() { + if (displayListener == null && context != null && + Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + DisplayManager displayManager = context.getSystemService(DisplayManager.class); + if (displayManager != null) { + displayListener = new DefaultDisplayListener(context); + displayManager.registerDisplayListener(displayListener, null); + } + } + } + + private void unregisterDisplayListener() { + if (context != null && displayListener != null && + Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + DisplayManager displayManager = context.getSystemService(DisplayManager.class); + if (displayManager != null) { + displayManager.unregisterDisplayListener(displayListener); + displayListener = null; + } + } } private void setSync(double defaultDisplayRefreshRate) { @@ -206,10 +237,16 @@ public final class VideoFrameReleaseTimeHelper { return snappedAfterDiff < snappedBeforeDiff ? snappedAfterNs : snappedBeforeNs; } + private static int getDefaultDisplayId(Context context) { + WindowManager manager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + return manager != null && manager.getDefaultDisplay() != null ? + manager.getDefaultDisplay().getDisplayId() : DISPLAY_ID_UNKNOWN; + } + private static double getDefaultDisplayRefreshRate(Context context) { WindowManager manager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); - return manager.getDefaultDisplay() != null ? manager.getDefaultDisplay().getRefreshRate() - : DISPLAY_REFRESH_RATE_UNKNOWN; + return manager != null && manager.getDefaultDisplay() != null ? + manager.getDefaultDisplay().getRefreshRate() : DISPLAY_REFRESH_RATE_UNKNOWN; } /** @@ -307,4 +344,30 @@ public final class VideoFrameReleaseTimeHelper { } + @TargetApi(17) + private class DefaultDisplayListener implements DisplayManager.DisplayListener { + + private final Context context; + DefaultDisplayListener(Context context) { + this.context = context; + } + + @Override + public void onDisplayAdded(int displayId) { + } + + @Override + public void onDisplayRemoved(int displayId) { + } + + @Override + public void onDisplayChanged(int displayId) { + final int defaultDisplayId = getDefaultDisplayId(context); + if (displayId == defaultDisplayId || defaultDisplayId == DISPLAY_ID_UNKNOWN) { + setSync(getDefaultDisplayRefreshRate(context)); + } + } + + } + } From a11c704d27c09ffeb0b50ad51b3911acdd2225fd Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 11 Oct 2017 07:34:11 -0700 Subject: [PATCH 0521/2472] Upgrade dependency versions ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=171819854 --- build.gradle | 2 +- constants.gradle | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index 47538ddb52..d260f491da 100644 --- a/build.gradle +++ b/build.gradle @@ -17,7 +17,7 @@ buildscript { google() } dependencies { - classpath 'com.android.tools.build:gradle:3.0.0-beta5' + classpath 'com.android.tools.build:gradle:3.0.0-beta7' classpath 'com.novoda:bintray-release:0.5.0' } // Workaround for the following test coverage issue. Remove when fixed: diff --git a/constants.gradle b/constants.gradle index 73d6aa5c56..4778a0c66f 100644 --- a/constants.gradle +++ b/constants.gradle @@ -19,10 +19,10 @@ project.ext { minSdkVersion = 14 compileSdkVersion = 26 targetSdkVersion = 26 - buildToolsVersion = '26' + buildToolsVersion = '26.0.2' testSupportLibraryVersion = '0.5' - supportLibraryVersion = '26.0.2' - playServicesLibraryVersion = '11.2.0' + supportLibraryVersion = '26.1.0' + playServicesLibraryVersion = '11.4.2' dexmakerVersion = '1.2' mockitoVersion = '1.9.5' junitVersion = '4.12' From 333e745e7bf9d03aa3a4b4fbf83f29dede662c58 Mon Sep 17 00:00:00 2001 From: bachinger Date: Thu, 12 Oct 2017 09:10:00 -0700 Subject: [PATCH 0522/2472] show PlaybackControlView when on key event for DPAD center or any direction and consume event without moving the focus. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=171967057 --- .../android/exoplayer2/demo/PlayerActivity.java | 4 ++-- .../exoplayer2/ui/SimpleExoPlayerView.java | 15 +++++++++++++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index 716af02c01..1f92473bc9 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -218,8 +218,8 @@ public class PlayerActivity extends Activity implements OnClickListener, @Override public boolean dispatchKeyEvent(KeyEvent event) { - // If the event was not handled then see if the player view can handle it. - return super.dispatchKeyEvent(event) || simpleExoPlayerView.dispatchKeyEvent(event); + // See whether the player view wants to handle media or DPAD keys events. + return simpleExoPlayerView.dispatchKeyEvent(event) || super.dispatchKeyEvent(event); } // OnClickListener methods diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index f34ede3e6d..fc41031756 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.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.Resources; @@ -521,8 +522,10 @@ public final class SimpleExoPlayerView extends FrameLayout { overlayFrameLayout.requestFocus(); return super.dispatchKeyEvent(event); } + boolean isDpadWhenControlHidden = isDpadKey(event.getKeyCode()) && useController + && !controller.isVisible(); maybeShowController(true); - return dispatchMediaKeyEvent(event) || super.dispatchKeyEvent(event); + return isDpadWhenControlHidden || dispatchMediaKeyEvent(event) || super.dispatchKeyEvent(event); } /** @@ -871,12 +874,20 @@ public final class SimpleExoPlayerView extends FrameLayout { logo.setBackgroundColor(resources.getColor(R.color.exo_edit_mode_background_color)); } - @SuppressWarnings("ResourceType") private static void setResizeModeRaw(AspectRatioFrameLayout aspectRatioFrame, int resizeMode) { aspectRatioFrame.setResizeMode(resizeMode); } + @SuppressLint("InlinedApi") + private boolean isDpadKey(int keyCode) { + return keyCode == KeyEvent.KEYCODE_DPAD_UP || keyCode == KeyEvent.KEYCODE_DPAD_UP_RIGHT + || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT || keyCode == KeyEvent.KEYCODE_DPAD_DOWN_RIGHT + || keyCode == KeyEvent.KEYCODE_DPAD_DOWN || keyCode == KeyEvent.KEYCODE_DPAD_DOWN_LEFT + || keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_UP_LEFT + || keyCode == KeyEvent.KEYCODE_DPAD_CENTER; + } + private final class ComponentListener extends Player.DefaultEventListener implements TextOutput, SimpleExoPlayer.VideoListener { From b71effb7b0b2da1f97e62ba8f4e5b659f13f7f85 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 12 Oct 2017 10:12:04 -0700 Subject: [PATCH 0523/2472] Fix MobileHarness playback tests. This change fixes various issues: - MobileHarness sometimes allocated devices with SDK < 16. As we have no tests running on these SDKs, a new dimension filter for the mobile_test target ensures that only devices with SDK >= 16 are selected. A similar filter for SDK version is also added to the ABR playback tests to ensure no old devices are selected. - DRM specific tests are skipped for Api < 18, but were not able to run because the DashTestRunner class tried to link to the MediaDrm constructor. Moved the constructor to a seperate Builder class to allow execution on Api levels 16 and 17. - DashWidevineOfflineTest also tried to access code for Api >= 18 without checking the current level. - Action implementations which are waiting for events did not ensure that they have a nextAction to wait for. This caused NullPointerExceptions when this next action was scheduled. - DefaultDrmSession always restored the offline keys when a new license was requested, even if the keys were already restored. These repeated slow calls to restoreKeys resulted in high numbers of dropped buffers. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=171974859 --- .../exoplayer2/drm/DefaultDrmSession.java | 38 ++++++++--------- .../playbacktests/gts/DashTestRunner.java | 41 ++++++++++++++----- .../gts/DashWidevineOfflineTest.java | 6 ++- .../android/exoplayer2/testutil/Action.java | 12 ++++++ 4 files changed, 64 insertions(+), 33 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java index 4e60e466a8..4e5696ef1f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java @@ -296,26 +296,24 @@ import java.util.UUID; case DefaultDrmSessionManager.MODE_QUERY: if (offlineLicenseKeySetId == null) { postKeyRequest(ExoMediaDrm.KEY_TYPE_STREAMING, allowRetry); - } else { - if (restoreKeys()) { - long licenseDurationRemainingSec = getLicenseDurationRemainingSec(); - if (mode == DefaultDrmSessionManager.MODE_PLAYBACK - && licenseDurationRemainingSec <= MAX_LICENSE_DURATION_TO_RENEW) { - Log.d(TAG, "Offline license has expired or will expire soon. " - + "Remaining seconds: " + licenseDurationRemainingSec); - postKeyRequest(ExoMediaDrm.KEY_TYPE_OFFLINE, allowRetry); - } else if (licenseDurationRemainingSec <= 0) { - onError(new KeysExpiredException()); - } else { - state = STATE_OPENED_WITH_KEYS; - if (eventHandler != null && eventListener != null) { - eventHandler.post(new Runnable() { - @Override - public void run() { - eventListener.onDrmKeysRestored(); - } - }); - } + } else if (state == STATE_OPENED_WITH_KEYS || restoreKeys()) { + long licenseDurationRemainingSec = getLicenseDurationRemainingSec(); + if (mode == DefaultDrmSessionManager.MODE_PLAYBACK + && licenseDurationRemainingSec <= MAX_LICENSE_DURATION_TO_RENEW) { + Log.d(TAG, "Offline license has expired or will expire soon. " + + "Remaining seconds: " + licenseDurationRemainingSec); + postKeyRequest(ExoMediaDrm.KEY_TYPE_OFFLINE, allowRetry); + } else if (licenseDurationRemainingSec <= 0) { + onError(new KeysExpiredException()); + } else { + state = STATE_OPENED_WITH_KEYS; + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onDrmKeysRestored(); + } + }); } } } diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java index 06dab1164b..85cefbc2f6 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java @@ -108,21 +108,23 @@ public final class DashTestRunner { private String widevineLicenseUrl; private DataSource.Factory dataSourceFactory; - @TargetApi(18) @SuppressWarnings("ResourceType") public static boolean isL1WidevineAvailable(String mimeType) { - try { - // Force L3 if secure decoder is not available. - if (MediaCodecUtil.getDecoderInfo(mimeType, true) == null) { - return false; + if (Util.SDK_INT >= 18) { + try { + // Force L3 if secure decoder is not available. + if (MediaCodecUtil.getDecoderInfo(mimeType, true) == null) { + return false; + } + MediaDrm mediaDrm = MediaDrmBuilder.build(); + String securityProperty = mediaDrm.getPropertyString(SECURITY_LEVEL_PROPERTY); + mediaDrm.release(); + return WIDEVINE_SECURITY_LEVEL_1.equals(securityProperty); + } catch (MediaCodecUtil.DecoderQueryException e) { + throw new IllegalStateException(e); } - MediaDrm mediaDrm = new MediaDrm(WIDEVINE_UUID); - String securityProperty = mediaDrm.getPropertyString(SECURITY_LEVEL_PROPERTY); - mediaDrm.release(); - return WIDEVINE_SECURITY_LEVEL_1.equals(securityProperty); - } catch (MediaCodecUtil.DecoderQueryException | UnsupportedSchemeException e) { - throw new IllegalStateException(e); } + return false; } public DashTestRunner(String tag, HostActivity activity, Instrumentation instrumentation) { @@ -457,4 +459,21 @@ public final class DashTestRunner { } + /** + * Creates a new {@code MediaDrm} object. The encapsulation ensures that the tests can be + * executed for API level < 18. + */ + @TargetApi(18) + private static final class MediaDrmBuilder { + + public static MediaDrm build () { + try { + return new MediaDrm(WIDEVINE_UUID); + } catch (UnsupportedSchemeException e) { + throw new IllegalStateException(e); + } + } + + } + } 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 1ddceb551c..0d79a803d3 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 @@ -67,8 +67,10 @@ public final class DashWidevineOfflineTest extends ActivityInstrumentationTestCa boolean useL1Widevine = DashTestRunner.isL1WidevineAvailable(MimeTypes.VIDEO_H264); String widevineLicenseUrl = DashTestData.getWidevineLicenseUrl(true, useL1Widevine); httpDataSourceFactory = new DefaultHttpDataSourceFactory(USER_AGENT); - offlineLicenseHelper = OfflineLicenseHelper.newWidevineInstance(widevineLicenseUrl, - httpDataSourceFactory); + if (Util.SDK_INT >= 18) { + offlineLicenseHelper = OfflineLicenseHelper.newWidevineInstance(widevineLicenseUrl, + httpDataSourceFactory); + } } @Override diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java index 66e034f21c..2abe521883 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java @@ -322,6 +322,9 @@ public abstract class Action { protected void doActionAndScheduleNextImpl(final SimpleExoPlayer player, final MappingTrackSelector trackSelector, final Surface surface, final Handler handler, final ActionNode nextAction) { + if (nextAction == null) { + return; + } Player.EventListener listener = new Player.DefaultEventListener() { @Override public void onTimelineChanged(Timeline timeline, Object manifest) { @@ -362,6 +365,9 @@ public abstract class Action { protected void doActionAndScheduleNextImpl(final SimpleExoPlayer player, final MappingTrackSelector trackSelector, final Surface surface, final Handler handler, final ActionNode nextAction) { + if (nextAction == null) { + return; + } player.addListener(new Player.DefaultEventListener() { @Override public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { @@ -399,6 +405,9 @@ public abstract class Action { protected void doActionAndScheduleNextImpl(final SimpleExoPlayer player, final MappingTrackSelector trackSelector, final Surface surface, final Handler handler, final ActionNode nextAction) { + if (nextAction == null) { + return; + } if (targetPlaybackState == player.getPlaybackState()) { nextAction.schedule(player, trackSelector, surface, handler); } else { @@ -438,6 +447,9 @@ public abstract class Action { protected void doActionAndScheduleNextImpl(final SimpleExoPlayer player, final MappingTrackSelector trackSelector, final Surface surface, final Handler handler, final ActionNode nextAction) { + if (nextAction == null) { + return; + } player.addListener(new Player.DefaultEventListener() { @Override public void onSeekProcessed() { From 3ae4143be30318c90e1856d6387fccff44bcecbf Mon Sep 17 00:00:00 2001 From: byungh Date: Thu, 12 Oct 2017 13:03:05 -0700 Subject: [PATCH 0524/2472] Cookie-based validation in CronetDataSource Using cookie validation from streamer, streamer can enforce that only clients who have the cookie are able to stream the video. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=171999924 --- .../ext/cronet/CronetDataSourceTest.java | 70 ++++++++++- .../ext/cronet/CronetDataSource.java | 109 ++++++++++++++++-- 2 files changed, 168 insertions(+), 11 deletions(-) diff --git a/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java b/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java index c7050dbd0c..dadc75b5d2 100644 --- a/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java +++ b/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.ext.cronet; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.Matchers.any; @@ -45,6 +46,7 @@ import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.Predicate; import java.io.IOException; +import java.net.CookieManager; import java.net.SocketTimeoutException; import java.net.UnknownHostException; import java.nio.ByteBuffer; @@ -102,12 +104,14 @@ public final class CronetDataSourceTest { @Mock private CronetEngine mockCronetEngine; private CronetDataSource dataSourceUnderTest; + private boolean redirectCalled; @Before public void setUp() throws Exception { System.setProperty("dexmaker.dexcache", InstrumentationRegistry.getTargetContext().getCacheDir().getPath()); initMocks(this); + CookieManager cookieManager = new CookieManager(); dataSourceUnderTest = spy( new CronetDataSource( mockCronetEngine, @@ -118,7 +122,8 @@ public final class CronetDataSourceTest { TEST_READ_TIMEOUT_MS, true, // resetTimeoutOnRedirects mockClock, - null)); + null, + cookieManager)); when(mockContentTypePredicate.evaluate(anyString())).thenReturn(true); when(mockCronetEngine.newUrlRequestBuilder( anyString(), any(UrlRequest.Callback.class), any(Executor.class))) @@ -138,10 +143,14 @@ public final class CronetDataSourceTest { } private UrlResponseInfo createUrlResponseInfo(int statusCode) { + return createUrlResponseInfoWithUrl(TEST_URL, statusCode); + } + + private UrlResponseInfo createUrlResponseInfoWithUrl(String url, int statusCode) { ArrayList> responseHeaderList = new ArrayList<>(); responseHeaderList.addAll(testResponseHeader.entrySet()); return new UrlResponseInfoImpl( - Collections.singletonList(TEST_URL), + Collections.singletonList(url), statusCode, null, // httpStatusText responseHeaderList, @@ -150,11 +159,11 @@ public final class CronetDataSourceTest { null); // proxyServer } - @Test(expected = IllegalStateException.class) + @Test public void testOpeningTwiceThrows() throws HttpDataSourceException { mockResponseStartSuccess(); dataSourceUnderTest.open(testDataSpec); - dataSourceUnderTest.open(testDataSpec); + assertThrows(IllegalStateException.class, () -> dataSourceUnderTest.open(testDataSpec)); } @Test @@ -649,6 +658,27 @@ public final class CronetDataSourceTest { assertEquals(1, openExceptions.get()); } + @Test + public void testRedirectParseAndAttachCookie() throws HttpDataSourceException { + mockSingleRedirectSuccess(); + + testResponseHeader.put("Set-Cookie", "testcookie=testcookie; Path=/video"); + dataSourceUnderTest.open(testDataSpec); + verify(mockUrlRequestBuilder).addHeader(eq("Cookie"), any(String.class)); + verify(mockUrlRequest, never()).followRedirect(); + verify(mockUrlRequest, times(2)).start(); + } + + @Test + public void testRedirectNoSetCookieFollowsRedirect() throws HttpDataSourceException { + mockSingleRedirectSuccess(); + mockFollowRedirectSuccess(); + + dataSourceUnderTest.open(testDataSpec); + verify(mockUrlRequestBuilder, never()).addHeader(eq("Cookie"), any(String.class)); + verify(mockUrlRequest).followRedirect(); + } + @Test public void testExceptionFromTransferListener() throws HttpDataSourceException { mockResponseStartSuccess(); @@ -731,6 +761,38 @@ public final class CronetDataSourceTest { }).when(mockUrlRequest).start(); } + private void mockSingleRedirectSuccess() { + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + if (!redirectCalled) { + redirectCalled = true; + dataSourceUnderTest.onRedirectReceived( + mockUrlRequest, + createUrlResponseInfoWithUrl("http://example.com/video", 300), + "http://example.com/video/redirect"); + } else { + dataSourceUnderTest.onResponseStarted( + mockUrlRequest, + testUrlResponseInfo); + } + return null; + } + }).when(mockUrlRequest).start(); + } + + private void mockFollowRedirectSuccess() { + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + dataSourceUnderTest.onResponseStarted( + mockUrlRequest, + testUrlResponseInfo); + return null; + } + }).when(mockUrlRequest).followRedirect(); + } + private void mockResponseStartFailure() { doAnswer(new Answer() { @Override diff --git a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java index cdc8eb7b35..52d76d61f4 100644 --- a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java +++ b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java @@ -29,9 +29,13 @@ import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.Predicate; import java.io.IOException; +import java.net.CookieManager; import java.net.SocketTimeoutException; +import java.net.URI; +import java.net.URISyntaxException; import java.net.UnknownHostException; import java.nio.ByteBuffer; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -93,6 +97,7 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou Pattern.compile("^bytes (\\d+)-(\\d+)/(\\d+)$"); // The size of read buffer passed to cronet UrlRequest.read(). private static final int READ_BUFFER_SIZE_BYTES = 32 * 1024; + private static final String SET_COOKIE = "Set-Cookie"; private final CronetEngine cronetEngine; private final Executor executor; @@ -105,6 +110,7 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou private final RequestProperties requestProperties; private final ConditionVariable operation; private final Clock clock; + private final CookieManager cookieManager; // Accessed by the calling thread only. private boolean opened; @@ -144,7 +150,7 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou public CronetDataSource(CronetEngine cronetEngine, Executor executor, Predicate contentTypePredicate, TransferListener listener) { this(cronetEngine, executor, contentTypePredicate, listener, DEFAULT_CONNECT_TIMEOUT_MILLIS, - DEFAULT_READ_TIMEOUT_MILLIS, false, null); + DEFAULT_READ_TIMEOUT_MILLIS, false, null, /* cookieManager= */ null); } /** @@ -168,13 +174,42 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects, RequestProperties defaultRequestProperties) { this(cronetEngine, executor, contentTypePredicate, listener, connectTimeoutMs, - readTimeoutMs, resetTimeoutOnRedirects, Clock.DEFAULT, defaultRequestProperties); + readTimeoutMs, resetTimeoutOnRedirects, Clock.DEFAULT, defaultRequestProperties, + /* cookieManager= */ null); + } + + + /** + * @param cronetEngine A CronetEngine. + * @param executor The {@link java.util.concurrent.Executor} that will handle responses. + * This may be a direct executor (i.e. executes tasks on the calling thread) in order + * to avoid a thread hop from Cronet's internal network thread to the response handling + * thread. However, to avoid slowing down overall network performance, care must be taken + * to make sure response handling is a fast operation when using a direct executor. + * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the + * predicate then an {@link InvalidContentTypeException} is thrown from + * {@link #open(DataSpec)}. + * @param listener An optional listener. + * @param connectTimeoutMs The connection timeout, in milliseconds. + * @param readTimeoutMs The read timeout, in milliseconds. + * @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs. + * @param defaultRequestProperties The default request properties to be used. + * @param cookieManager An optional {@link CookieManager} to be used to handle "Set-Cookie" + * requests. If this is null, then "Set-Cookie" requests will be ignored. + */ + public CronetDataSource(CronetEngine cronetEngine, Executor executor, + Predicate contentTypePredicate, TransferListener listener, + int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects, + RequestProperties defaultRequestProperties, CookieManager cookieManager) { + this(cronetEngine, executor, contentTypePredicate, listener, connectTimeoutMs, + readTimeoutMs, resetTimeoutOnRedirects, Clock.DEFAULT, defaultRequestProperties, + cookieManager); } /* package */ CronetDataSource(CronetEngine cronetEngine, Executor executor, Predicate contentTypePredicate, TransferListener listener, int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects, Clock clock, - RequestProperties defaultRequestProperties) { + RequestProperties defaultRequestProperties, CookieManager cookieManager) { this.cronetEngine = Assertions.checkNotNull(cronetEngine); this.executor = Assertions.checkNotNull(executor); this.contentTypePredicate = contentTypePredicate; @@ -184,6 +219,7 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou this.resetTimeoutOnRedirects = resetTimeoutOnRedirects; this.clock = Assertions.checkNotNull(clock); this.defaultRequestProperties = defaultRequestProperties; + this.cookieManager = cookieManager; requestProperties = new RequestProperties(); operation = new ConditionVariable(); } @@ -223,7 +259,12 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou operation.close(); resetConnectTimeout(); currentDataSpec = dataSpec; - currentUrlRequest = buildRequest(dataSpec); + + try { + currentUrlRequest = buildRequest(dataSpec); + } catch (IOException e) { + throw new OpenException(e, dataSpec, Status.IDLE); + } currentUrlRequest.start(); boolean requestStarted = blockUntilConnectTimeout(); @@ -379,7 +420,24 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou if (resetTimeoutOnRedirects) { resetConnectTimeout(); } - request.followRedirect(); + + Map> headers = info.getAllHeaders(); + if (cookieManager == null || isEmpty(headers.get(SET_COOKIE))) { + request.followRedirect(); + } else { + currentUrlRequest.cancel(); + UrlRequest.Builder requestBuilder = + cronetEngine.newUrlRequestBuilder(newLocationUrl, this, executor).allowDirectExecutor(); + try { + parseCookies(info); + attachCookies(newLocationUrl, requestBuilder); + currentUrlRequest = requestBuilder.build(); + currentUrlRequest.start(); + } catch (IOException e) { + exception = e; + operation.open(); + } + } } @Override @@ -387,7 +445,12 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou if (request != currentUrlRequest) { return; } - responseInfo = info; + try { + parseCookies(info); + responseInfo = info; + } catch (IOException e) { + exception = e; + } operation.open(); } @@ -427,7 +490,7 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou // Internal methods. - private UrlRequest buildRequest(DataSpec dataSpec) throws OpenException { + private UrlRequest buildRequest(DataSpec dataSpec) throws IOException { UrlRequest.Builder requestBuilder = cronetEngine.newUrlRequestBuilder( dataSpec.uri.toString(), this, executor).allowDirectExecutor(); // Set the headers. @@ -474,6 +537,7 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou executor); } } + attachCookies(dataSpec.uri.toString(), requestBuilder); return requestBuilder.build(); } @@ -491,6 +555,37 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou currentConnectTimeoutMs = clock.elapsedRealtime() + connectTimeoutMs; } + private void parseCookies(UrlResponseInfo info) throws IOException { + if (cookieManager == null) { + return; + } + try { + cookieManager.put(new URI(info.getUrl()), info.getAllHeaders()); + } catch (URISyntaxException e) { + throw new IOException("Failed to parse cookies", e); + } + } + + private void attachCookies(String url, UrlRequest.Builder requestBuilder) throws IOException { + if (cookieManager == null) { + return; + } + try { + for (Entry> headers : + cookieManager.get(new URI(url), Collections.emptyMap()).entrySet()) { + StringBuilder cookies = new StringBuilder(); + for (String cookie : headers.getValue()) { + cookies.append(cookie).append("; "); + } + if (cookies.length() > 0) { + requestBuilder.addHeader(headers.getKey(), cookies.substring(0, cookies.length() - 1)); + } + } + } catch (URISyntaxException e) { + throw new IOException("Failed to attach cookies", e); + } + } + private static boolean getIsCompressed(UrlResponseInfo info) { for (Map.Entry entry : info.getAllHeadersAsList()) { if (entry.getKey().equalsIgnoreCase("Content-Encoding")) { From 7038c8fb7052715aa8252f3a9e2cd6a6d63c1002 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 13 Oct 2017 01:15:20 -0700 Subject: [PATCH 0525/2472] Add chunk size variation to fake adaptive data set. The chunk size of real video data varies around the given average bitrate. To account for this fact in the fake adaptive data set, the chunk size varies randomly with a given standard deviation. The standard deviation used for the BandwidthProfileSimulator is based on the chunk size variation measured on the 1 hour playlist of real media. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=172068110 --- .../testutil/FakeAdaptiveDataSet.java | 64 +++++++++++++++---- 1 file changed, 50 insertions(+), 14 deletions(-) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveDataSet.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveDataSet.java index add0c5d22f..82c14a5b32 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveDataSet.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveDataSet.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.testutil; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.source.TrackGroup; +import java.util.Random; /** * Fake data set emulating the data of an adaptive media source. @@ -30,46 +31,81 @@ public final class FakeAdaptiveDataSet extends FakeDataSet { */ public static final class Factory { - private final long chunkDurationUs; + private static final Random random = new Random(); - public Factory(long chunkDurationUs) { + private final long chunkDurationUs; + private final double bitratePercentStdDev; + + /** + * Set up factory for {@link FakeAdaptiveDataSet}s with a chunk duration and the standard + * deviation of the chunk size. + * + * @param chunkDurationUs The chunk duration to use in microseconds. + * @param bitratePercentStdDev The standard deviation used to generate the chunk sizes centered + * around the average bitrate of the {@link Format}s. The standard deviation is given in + * percent (of the average size). + */ + public Factory(long chunkDurationUs, double bitratePercentStdDev) { this.chunkDurationUs = chunkDurationUs; + this.bitratePercentStdDev = bitratePercentStdDev; } + /** + * Returns a new {@link FakeAdaptiveDataSet} for the given {@link TrackGroup}. + * + * @param trackGroup The {@link TrackGroup} for which the data set is to be created. + * @param mediaDurationUs The total duration of the fake data set in microseconds. + */ public FakeAdaptiveDataSet createDataSet(TrackGroup trackGroup, long mediaDurationUs) { - return new FakeAdaptiveDataSet(trackGroup, mediaDurationUs, chunkDurationUs); + return new FakeAdaptiveDataSet(trackGroup, mediaDurationUs, chunkDurationUs, + bitratePercentStdDev, random); } } - private final long chunkCount; + private final int chunkCount; private final long chunkDurationUs; private final long lastChunkDurationUs; - public FakeAdaptiveDataSet(TrackGroup trackGroup, long mediaDurationUs, long chunkDurationUs) { + /** + * Create {@link FakeAdaptiveDataSet} using a {@link TrackGroup} and meta data about the media. + * + * @param trackGroup The {@link TrackGroup} for which the data set is to be created. + * @param mediaDurationUs The total duration of the fake data set in microseconds. + * @param chunkDurationUs The chunk duration to use in microseconds. + * @param bitratePercentStdDev The standard deviation used to generate the chunk sizes centered + * around the average bitrate of the {@link Format}s in the {@link TrackGroup}. The standard + * deviation is given in percent (of the average size). + * @param random A {@link Random} instance used to generate random chunk sizes. + */ + /* package */ FakeAdaptiveDataSet(TrackGroup trackGroup, long mediaDurationUs, + long chunkDurationUs, double bitratePercentStdDev, Random random) { this.chunkDurationUs = chunkDurationUs; - int trackCount = trackGroup.length; long lastChunkDurationUs = mediaDurationUs % chunkDurationUs; int fullChunks = (int) (mediaDurationUs / chunkDurationUs); - for (int i = 0; i < trackCount; i++) { + this.lastChunkDurationUs = lastChunkDurationUs == 0 ? chunkDurationUs : lastChunkDurationUs; + this.chunkCount = lastChunkDurationUs == 0 ? fullChunks : fullChunks + 1; + double[] bitrateFactors = new double[chunkCount]; + for (int i = 0; i < chunkCount; i++) { + bitrateFactors[i] = 1.0 + random.nextGaussian() * bitratePercentStdDev / 100.0; + } + for (int i = 0; i < trackGroup.length; i++) { String uri = getUri(i); Format format = trackGroup.getFormat(i); - int chunkLength = (int) (format.bitrate * chunkDurationUs / (8 * C.MICROS_PER_SECOND)); + double avgChunkLength = format.bitrate * chunkDurationUs / (8 * C.MICROS_PER_SECOND); FakeData newData = this.newData(uri); for (int j = 0; j < fullChunks; j++) { - newData.appendReadData(chunkLength); + newData.appendReadData((int) (avgChunkLength * bitrateFactors[j])); } if (lastChunkDurationUs > 0) { - int lastChunkLength = (int) (format.bitrate * (mediaDurationUs % chunkDurationUs) - / (8 * C.MICROS_PER_SECOND)); + int lastChunkLength = (int) (format.bitrate * bitrateFactors[bitrateFactors.length - 1] + * (mediaDurationUs % chunkDurationUs) / (8 * C.MICROS_PER_SECOND)); newData.appendReadData(lastChunkLength); } } - this.lastChunkDurationUs = lastChunkDurationUs == 0 ? chunkDurationUs : lastChunkDurationUs; - this.chunkCount = lastChunkDurationUs == 0 ? fullChunks : fullChunks + 1; } - public long getChunkCount() { + public int getChunkCount() { return chunkCount; } From c9ed9366ceebea9f566d406c873338ef596baaba Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 13 Oct 2017 03:30:29 -0700 Subject: [PATCH 0526/2472] Fix Cronet extension build and test. Recently added Java 8 features in the cronet extension and the linked native libs require to enable Java 8 desugaring in gradle. Moreover, junit.assertThrows is not available in our version and its usage has been replaced by the manual check. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=172077967 --- extensions/cronet/build.gradle | 5 +++++ .../exoplayer2/ext/cronet/CronetDataSourceTest.java | 8 ++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/extensions/cronet/build.gradle b/extensions/cronet/build.gradle index 930a53c7c5..197dec80a5 100644 --- a/extensions/cronet/build.gradle +++ b/extensions/cronet/build.gradle @@ -27,6 +27,11 @@ android { sourceSets.main { jniLibs.srcDirs = ['jniLibs'] } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } } dependencies { diff --git a/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java b/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java index dadc75b5d2..8107e87440 100644 --- a/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java +++ b/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java @@ -18,7 +18,6 @@ package com.google.android.exoplayer2.ext.cronet; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.Matchers.any; @@ -163,7 +162,12 @@ public final class CronetDataSourceTest { public void testOpeningTwiceThrows() throws HttpDataSourceException { mockResponseStartSuccess(); dataSourceUnderTest.open(testDataSpec); - assertThrows(IllegalStateException.class, () -> dataSourceUnderTest.open(testDataSpec)); + try { + dataSourceUnderTest.open(testDataSpec); + fail("Expected IllegalStateException."); + } catch (IllegalStateException e) { + // Expected. + } } @Test From 2fee010938f9a81ab241b1c7200117d018e46357 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 13 Oct 2017 09:31:00 -0700 Subject: [PATCH 0527/2472] Workaround/Fix #3351 1. Ignore edit list where the sequence doesn't contain a sync sample, rather than failing. 2. Make Mp4Extractor.readAtomPayload so it doesn't try and read the same payload twice if a failure occurs parsing it. 3. Make processAtomEnded so that it doesn't pop the moov if parsing it fails. Issue: #3351 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=172106244 --- .../android/exoplayer2/extractor/mp4/AtomParsers.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/library/core/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 index c754c4b566..42504519f2 100644 --- a/library/core/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 @@ -395,7 +395,11 @@ import java.util.List; hasSyncSample |= (editedFlags[i] & C.BUFFER_FLAG_KEY_FRAME) != 0; } if (!hasSyncSample) { - throw new ParserException("The edited sample sequence does not contain a sync sample."); + // We don't support edit lists where the edited sample sequence doesn't contain a sync sample. + // Such edit lists are often (although not always) broken, so we ignore it and continue. + Log.w(TAG, "Ignoring edit list: Edited sample sequence does not contain a sync sample."); + Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale); + return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags); } return new TrackSampleTable(editedOffsets, editedSizes, editedMaximumSize, editedTimestamps, From cad88512f5c1aae39d90a8f75cf6e2bbb8faf983 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 13 Oct 2017 11:38:47 -0700 Subject: [PATCH 0528/2472] Only parse common-encryption sinf boxes ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=172124807 --- .../exoplayer2/extractor/mp4/AtomParsers.java | 13 +++++++------ .../extractor/mp4/AtomParsersTest.java | 18 ++++++++++++++---- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/library/core/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 index 42504519f2..86bdc2b41c 100644 --- a/library/core/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 @@ -1064,8 +1064,8 @@ import java.util.List; Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive"); int childAtomType = parent.readInt(); if (childAtomType == Atom.TYPE_sinf) { - Pair result = parseSinfFromParent(parent, childPosition, - childAtomSize); + Pair result = parseCommonEncryptionSinfFromParent(parent, + childPosition, childAtomSize); if (result != null) { return result; } @@ -1075,8 +1075,8 @@ import java.util.List; return null; } - private static Pair parseSinfFromParent(ParsableByteArray parent, - int position, int size) { + /* package */ static Pair parseCommonEncryptionSinfFromParent( + ParsableByteArray parent, int position, int size) { int childPosition = position + Atom.HEADER_SIZE; int schemeInformationBoxPosition = C.POSITION_UNSET; int schemeInformationBoxSize = 0; @@ -1090,7 +1090,7 @@ import java.util.List; dataFormat = parent.readInt(); } else if (childAtomType == Atom.TYPE_schm) { parent.skipBytes(4); - // scheme_type field. Defined in ISO/IEC 23001-7:2016, section 4.1. + // Common encryption scheme_type values are defined in ISO/IEC 23001-7:2016, section 4.1. schemeType = parent.readString(4); } else if (childAtomType == Atom.TYPE_schi) { schemeInformationBoxPosition = childPosition; @@ -1099,7 +1099,8 @@ import java.util.List; childPosition += childAtomSize; } - if (schemeType != null) { + if (C.CENC_TYPE_cenc.equals(schemeType) || C.CENC_TYPE_cbc1.equals(schemeType) + || C.CENC_TYPE_cens.equals(schemeType) || C.CENC_TYPE_cbcs.equals(schemeType)) { Assertions.checkArgument(dataFormat != null, "frma atom is mandatory"); Assertions.checkArgument(schemeInformationBoxPosition != C.POSITION_UNSET, "schi atom is mandatory"); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/AtomParsersTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/AtomParsersTest.java index 0c69e0b176..b0c37ee452 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/AtomParsersTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/AtomParsersTest.java @@ -40,22 +40,32 @@ public final class AtomParsersTest { private static final byte[] SIXTEEN_BIT_STZ2 = Util.getBytesFromHexString(ATOM_HEADER + "00000010" + SAMPLE_COUNT + "0001000200030004"); + @Test + public void testParseCommonEncryptionSinfFromParentIgnoresUnknownSchemeType() { + byte[] cencSinf = new byte[] { + 0, 0, 0, 24, 115, 105, 110, 102, // size (4), 'sinf' (4) + 0, 0, 0, 16, 115, 99, 104, 109, // size (4), 'schm' (4) + 0, 0, 0, 0, 88, 88, 88, 88}; // version (1), flags (3), 'xxxx' (4) + assertThat(AtomParsers.parseCommonEncryptionSinfFromParent( + new ParsableByteArray(cencSinf), 0, cencSinf.length)).isNull(); + } + @Test public void testStz2Parsing4BitFieldSize() { - verifyParsing(new Atom.LeafAtom(Atom.TYPE_stsz, new ParsableByteArray(FOUR_BIT_STZ2))); + verifyStz2Parsing(new Atom.LeafAtom(Atom.TYPE_stsz, new ParsableByteArray(FOUR_BIT_STZ2))); } @Test public void testStz2Parsing8BitFieldSize() { - verifyParsing(new Atom.LeafAtom(Atom.TYPE_stsz, new ParsableByteArray(EIGHT_BIT_STZ2))); + verifyStz2Parsing(new Atom.LeafAtom(Atom.TYPE_stsz, new ParsableByteArray(EIGHT_BIT_STZ2))); } @Test public void testStz2Parsing16BitFieldSize() { - verifyParsing(new Atom.LeafAtom(Atom.TYPE_stsz, new ParsableByteArray(SIXTEEN_BIT_STZ2))); + verifyStz2Parsing(new Atom.LeafAtom(Atom.TYPE_stsz, new ParsableByteArray(SIXTEEN_BIT_STZ2))); } - private void verifyParsing(Atom.LeafAtom stz2Atom) { + private static void verifyStz2Parsing(Atom.LeafAtom stz2Atom) { AtomParsers.Stz2SampleSizeBox box = new AtomParsers.Stz2SampleSizeBox(stz2Atom); assertThat(box.getSampleCount()).isEqualTo(4); assertThat(box.isFixedSampleSize()).isFalse(); From 12513e9898d99324e45c0a96b0e58df1ece47c7f Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 13 Oct 2017 20:40:08 +0100 Subject: [PATCH 0529/2472] Tweak recently merged pull requests --- .../drm/DefaultDrmSessionManager.java | 30 ++++++------ .../android/exoplayer2/drm/DrmInitData.java | 9 ++-- .../extractor/mp4/PsshAtomUtil.java | 30 ++++++------ .../exoplayer2/drm/DrmInitDataTest.java | 22 ++++----- .../playlist/HlsMasterPlaylistParserTest.java | 46 +------------------ .../playlist/HlsMediaPlaylistParserTest.java | 2 +- .../exoplayer2/source/hls/HlsMediaSource.java | 9 ++-- .../hls/playlist/HlsMasterPlaylist.java | 20 -------- .../hls/playlist/HlsPlaylistTracker.java | 5 +- 9 files changed, 54 insertions(+), 119 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java index 239882e399..52bb084281 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java @@ -349,7 +349,7 @@ public class DefaultDrmSessionManager implements DrmSe // If there is no scheme information, assume patternless AES-CTR. return true; } else if (C.CENC_TYPE_cbc1.equals(schemeType) || C.CENC_TYPE_cbcs.equals(schemeType) - || C.CENC_TYPE_cens.equals(schemeType)) { + || C.CENC_TYPE_cens.equals(schemeType)) { // AES-CBC and pattern encryption are supported on API 24 onwards. return Util.SDK_INT >= 24; } @@ -357,7 +357,6 @@ public class DefaultDrmSessionManager implements DrmSe return true; } - @Override public DrmSession acquireSession(Looper playbackLooper, DrmInitData drmInitData) { Assertions.checkState(this.playbackLooper == null || this.playbackLooper == playbackLooper); @@ -462,34 +461,35 @@ public class DefaultDrmSessionManager implements DrmSe * @return The extracted {@link SchemeData}, or null if no suitable data is present. */ private static SchemeData getSchemeData(DrmInitData drmInitData, UUID uuid) { - List schemeDatas = new ArrayList<>(); - // Look for matching PSSH boxes, or the common box in the case of ClearKey - for (int i = 0; i < drmInitData.schemeDataCount; ++i) { + // Look for matching scheme data (matching the Common PSSH box for ClearKey). + List matchingSchemeDatas = new ArrayList<>(drmInitData.schemeDataCount); + for (int i = 0; i < drmInitData.schemeDataCount; i++) { SchemeData schemeData = drmInitData.get(i); if (schemeData.matches(uuid) - || (C.CLEARKEY_UUID.equals(uuid) && schemeData.matches(C.COMMON_PSSH_UUID))) { - schemeDatas.add(schemeData); + || (C.CLEARKEY_UUID.equals(uuid) && schemeData.matches(C.COMMON_PSSH_UUID))) { + matchingSchemeDatas.add(schemeData); } } - if (schemeDatas.isEmpty()) { + if (matchingSchemeDatas.isEmpty()) { return null; } - // For Widevine, we prefer v1 init data on M and higher, v0 for lower + // For Widevine PSSH boxes, prefer V1 boxes from API 23 and V0 before. if (C.WIDEVINE_UUID.equals(uuid)) { - for (SchemeData schemeData : schemeDatas ) { - int version = PsshAtomUtil.parseVersion(schemeData.data); + for (int i = 0; i < matchingSchemeDatas.size(); i++) { + SchemeData matchingSchemeData = matchingSchemeDatas.get(i); + int version = PsshAtomUtil.parseVersion(matchingSchemeData.data); if (Util.SDK_INT < 23 && version == 0) { - return schemeData; + return matchingSchemeData; } else if (Util.SDK_INT >= 23 && version == 1) { - return schemeData; + return matchingSchemeData; } } } - // If we don't have any special handling for this system, we take the first scheme data found - return schemeDatas.get(0); + // If we don't have any special handling, prefer the first matching scheme data. + return matchingSchemeDatas.get(0); } private static byte[] getSchemeInitData(SchemeData data, UUID uuid) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java index e9aeb78f2d..703efcb452 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmInitData.java @@ -22,8 +22,6 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; - -import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.List; @@ -80,7 +78,6 @@ public final class DrmInitData implements Comparator, Parcelable { // Sorting ensures that universal scheme data (i.e. data that applies to all schemes) is matched // last. It's also required by the equals and hashcode implementations. Arrays.sort(schemeDatas, this); - this.schemeDatas = schemeDatas; schemeDataCount = schemeDatas.length; } @@ -94,7 +91,7 @@ public final class DrmInitData implements Comparator, Parcelable { /** * Retrieves data for a given DRM scheme, specified by its UUID. * - * @deprecated This will only get the first data found for the scheme. + * @deprecated Use {@link #get(int)} and {@link SchemeData#matches(UUID)} instead. * @param uuid The DRM scheme's UUID. * @return The initialization data for the scheme, or null if the scheme is not supported. */ @@ -111,8 +108,8 @@ public final class DrmInitData implements Comparator, Parcelable { /** * Retrieves the {@link SchemeData} at a given index. * - * @param index index of the scheme to return. Must not exceed {@link #schemeDataCount}. - * @return The {@link SchemeData} at the index. + * @param index The index of the scheme to return. Must not exceed {@link #schemeDataCount}. + * @return The {@link SchemeData} at the specified index. */ public SchemeData get(int index) { return schemeDatas[index]; diff --git a/library/core/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 index 9854c57414..55ce41e4b1 100644 --- a/library/core/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 @@ -16,7 +16,6 @@ package com.google.android.exoplayer2.extractor.mp4; import android.util.Log; -import android.util.Pair; import com.google.android.exoplayer2.util.ParsableByteArray; import java.nio.ByteBuffer; import java.util.UUID; @@ -96,10 +95,10 @@ public final class PsshAtomUtil { /** * Parses the version from a PSSH atom. Version 0 and 1 PSSH atoms are supported. *

          - * The UUID is only parsed if the data is a valid PSSH atom. + * The version is only parsed if the data is a valid PSSH atom. * * @param atom The atom to parse. - * @return The parsed UUID. -1 if the input is not a valid PSSH atom, or if the PSSH atom has + * @return The parsed version. -1 if the input is not a valid PSSH atom, or if the PSSH atom has * an unsupported version. */ public static int parseVersion(byte[] atom) { @@ -130,16 +129,15 @@ public final class PsshAtomUtil { Log.w(TAG, "UUID mismatch. Expected: " + uuid + ", got: " + parsedAtom.uuid + "."); return null; } - return parsedAtom.data; + return parsedAtom.schemeData; } /** - * Parses the UUID and scheme specific data from a PSSH atom. Version 0 and 1 PSSH atoms are - * supported. + * Parses a PSSH atom. Version 0 and 1 PSSH atoms are supported. * * @param atom The atom to parse. - * @return A pair consisting of the parsed UUID and scheme specific data. Null if the input is - * not a valid PSSH atom, or if the PSSH atom has an unsupported version. + * @return The parsed PSSH atom. Null if the input is not a valid PSSH atom, or if the PSSH atom + * has an unsupported version. */ // TODO: Support parsing of the key ids for version 1 PSSH atoms. private static PsshAtom parsePsshAtom(byte[] atom) { @@ -179,15 +177,19 @@ public final class PsshAtomUtil { return new PsshAtom(uuid, atomVersion, data); } + // TODO: Consider exposing this and making parsePsshAtom public. private static class PsshAtom { - final UUID uuid; - final int version; - final byte[] data; - PsshAtom(final UUID uuid, final int version, final byte[] data) { + private final UUID uuid; + private final int version; + private final byte[] schemeData; + + public PsshAtom(UUID uuid, int version, byte[] schemeData) { this.uuid = uuid; this.version = version; - this.data = data; + this.schemeData = schemeData; } + } -} \ No newline at end of file + +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java b/library/core/src/test/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java index 610dd5368e..9fc6e801d3 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/drm/DrmInitDataTest.java @@ -20,20 +20,18 @@ import static com.google.android.exoplayer2.C.UUID_NIL; import static com.google.android.exoplayer2.C.WIDEVINE_UUID; import static com.google.android.exoplayer2.util.MimeTypes.VIDEO_MP4; import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assert.fail; import android.os.Parcel; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; import com.google.android.exoplayer2.testutil.TestUtil; +import java.util.ArrayList; +import java.util.List; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; -import java.util.ArrayList; -import java.util.List; - /** * Unit test for {@link DrmInitData}. */ @@ -101,7 +99,7 @@ public class DrmInitDataTest { @Test @Deprecated - public void testGet() { + public void testGetByUuid() { // Basic matching. DrmInitData testInitData = new DrmInitData(DATA_1, DATA_2); assertThat(testInitData.get(WIDEVINE_UUID)).isEqualTo(DATA_1); @@ -134,16 +132,14 @@ public class DrmInitDataTest { } @Test - public void testDuplicateSchemeData() { - DrmInitData testInitData = new DrmInitData(DATA_1, DATA_1); + public void testSchemeDatasWithSameUuid() { + DrmInitData testInitData = new DrmInitData(DATA_1, DATA_1B); assertThat(testInitData.schemeDataCount).isEqualTo(2); - - testInitData = new DrmInitData(DATA_1, DATA_2, DATA_1B); - assertThat(testInitData.schemeDataCount).isEqualTo(3); - assertThat(getAllSchemeData(testInitData)).containsAllOf(DATA_1, DATA_1B, DATA_2); - // Deprecated get method should return first entry + // Deprecated get method should return first entry. assertThat(testInitData.get(WIDEVINE_UUID)).isEqualTo(DATA_1); - assertThat(testInitData.get(PLAYREADY_UUID)).isEqualTo(DATA_2); + // Test retrieval of first and second entry. + assertThat(testInitData.get(0)).isEqualTo(DATA_1); + assertThat(testInitData.get(1)).isEqualTo(DATA_1B); } @Test diff --git a/library/hls/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 index 4dedc9526f..8b0d76d2e5 100644 --- a/library/hls/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 @@ -16,23 +16,19 @@ package com.google.android.exoplayer2.source.hls.playlist; import android.net.Uri; - import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.util.MimeTypes; - -import junit.framework.TestCase; - import java.io.ByteArrayInputStream; import java.io.IOException; import java.nio.charset.Charset; import java.util.Collections; -import java.util.Comparator; import java.util.List; +import junit.framework.TestCase; /** - * Test for {@link HlsMasterPlaylistParserTest} + * Test for {@link HlsMasterPlaylistParserTest}. */ public class HlsMasterPlaylistParserTest extends TestCase { @@ -151,44 +147,6 @@ public class HlsMasterPlaylistParserTest extends TestCase { assertEquals(Collections.emptyList(), playlist.muxedCaptionFormats); } - public void testReorderedVariantCopy() throws IOException { - HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, MASTER_PLAYLIST); - HlsMasterPlaylist nonReorderedPlaylist = - playlist.copyWithReorderedVariants(new Comparator() { - @Override - public int compare(HlsMasterPlaylist.HlsUrl url1, HlsMasterPlaylist.HlsUrl url2) { - return 0; - } - }); - assertEquals(playlist.variants, nonReorderedPlaylist.variants); - HlsMasterPlaylist.HlsUrl preferred = null; - for (HlsMasterPlaylist.HlsUrl url : playlist.variants) { - if (preferred == null || url.format.bitrate > preferred.format.bitrate) { - preferred = url; - } - } - - assertNotNull(preferred); - - final Comparator comparator = Collections.reverseOrder(new Comparator() { - @Override - public int compare(HlsMasterPlaylist.HlsUrl url1, HlsMasterPlaylist.HlsUrl url2) { - if (url1.format.bitrate > url2.format.bitrate) { - return 1; - } - - if (url2.format.bitrate > url1.format.bitrate) { - return -1; - } - - return 0; - } - }); - HlsMasterPlaylist reorderedPlaylist = playlist.copyWithReorderedVariants(comparator); - - assertEquals(reorderedPlaylist.variants.get(0), preferred); - } - private static HlsMasterPlaylist parseMasterPlaylist(String uri, String playlistString) throws IOException { Uri playlistUri = Uri.parse(uri); diff --git a/library/hls/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 index b036f13a0f..e4edb07926 100644 --- a/library/hls/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 @@ -27,7 +27,7 @@ import java.util.Locale; import junit.framework.TestCase; /** - * Test for {@link HlsMediaPlaylistParserTest} + * Test for {@link HlsMediaPlaylistParserTest}. */ public class HlsMediaPlaylistParserTest extends TestCase { diff --git a/library/hls/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 index b7f7124e44..10a0536612 100644 --- a/library/hls/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 @@ -76,13 +76,14 @@ public final class HlsMediaSource implements MediaSource, public HlsMediaSource(Uri manifestUri, HlsDataSourceFactory dataSourceFactory, int minLoadableRetryCount, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { - this(manifestUri, dataSourceFactory, minLoadableRetryCount, eventHandler, eventListener, new HlsPlaylistParser()); + this(manifestUri, dataSourceFactory, minLoadableRetryCount, eventHandler, eventListener, + new HlsPlaylistParser()); } public HlsMediaSource(Uri manifestUri, HlsDataSourceFactory dataSourceFactory, - int minLoadableRetryCount, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener, - ParsingLoadable.Parser playlistParser) { + int minLoadableRetryCount, Handler eventHandler, + AdaptiveMediaSourceEventListener eventListener, + ParsingLoadable.Parser playlistParser) { this.manifestUri = manifestUri; this.dataSourceFactory = dataSourceFactory; this.minLoadableRetryCount = minLoadableRetryCount; diff --git a/library/hls/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 index 5ded975f88..04192def9d 100644 --- a/library/hls/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 @@ -19,7 +19,6 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.util.MimeTypes; import java.util.ArrayList; import java.util.Collections; -import java.util.Comparator; import java.util.List; /** @@ -124,25 +123,6 @@ public final class HlsMasterPlaylist extends HlsPlaylist { muxedAudioFormat, muxedCaptionFormats); } - /** - * Returns a copy of this playlist which includes the variants sorted using the passed comparator. NOTE: the variants - * will be sorted in ascending order by default. If you wish to use descending order, you can wrap your comparator in - * {@link Collections#reverseOrder(Comparator)}. - * - * @param variantComparator the comparator to use to sort the variant list. - * @return a copy of this playlist which includes the variants sorted using the passed comparator. - */ - public HlsMasterPlaylist copyWithReorderedVariants(Comparator variantComparator) { - return new HlsMasterPlaylist(baseUri, tags, filterVariants(variants, variantComparator), audios, - subtitles, muxedAudioFormat, muxedCaptionFormats); - } - - private List filterVariants(List variants, Comparator variantComparator) { - List reorderedList = new ArrayList<>(variants); - Collections.sort(reorderedList, variantComparator); - return reorderedList; - } - /** * Creates a playlist with a single variant. * diff --git a/library/hls/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 index 52d5a4b5bc..da73aa3996 100644 --- a/library/hls/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 @@ -140,15 +140,16 @@ public final class HlsPlaylistTracker implements Loader.Callback playlistParser) { + PrimaryPlaylistListener primaryPlaylistListener, + ParsingLoadable.Parser playlistParser) { this.initialPlaylistUri = initialPlaylistUri; this.dataSourceFactory = dataSourceFactory; this.eventDispatcher = eventDispatcher; this.minRetryCount = minRetryCount; this.primaryPlaylistListener = primaryPlaylistListener; + this.playlistParser = playlistParser; listeners = new ArrayList<>(); initialPlaylistLoader = new Loader("HlsPlaylistTracker:MasterPlaylist"); - this.playlistParser = playlistParser; playlistBundles = new IdentityHashMap<>(); playlistRefreshHandler = new Handler(); } From 29ba640351dc7d9dd0c31b3a9403c137c23ffc9a Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 13 Oct 2017 23:14:49 +0100 Subject: [PATCH 0530/2472] Delete accidentally added files --- .../exoplayer2/offline/ActionFile.java | 95 ------- .../exoplayer2/offline/ActionFileTest.java | 253 ------------------ 2 files changed, 348 deletions(-) delete mode 100644 library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFile.java delete mode 100644 library/core/src/test/java/com/google/android/exoplayer2/offline/ActionFileTest.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFile.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFile.java deleted file mode 100644 index 38f599cc6e..0000000000 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/ActionFile.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * 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.offline; - -import com.google.android.exoplayer2.offline.DownloadAction.Deserializer; -import com.google.android.exoplayer2.util.AtomicFile; -import com.google.android.exoplayer2.util.ClosedSource; -import com.google.android.exoplayer2.util.Util; -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -/** - * Stores and loads {@link DownloadAction}s to/from a file. - */ -@ClosedSource(reason = "Not ready yet") -public final class ActionFile { - - private final AtomicFile atomicFile; - - /** - * @param actionFile File to be used to store and load {@link DownloadAction}s. - */ - public ActionFile(File actionFile) { - atomicFile = new AtomicFile(actionFile); - } - - /** - * Loads {@link DownloadAction}s from file. - * - * @param deserializers {@link Deserializer}s to deserialize DownloadActions. - * @return Loaded DownloadActions. - * @throws IOException If there is an error during loading. - */ - public DownloadAction[] load(Deserializer... deserializers) throws IOException { - InputStream inputStream = null; - try { - inputStream = atomicFile.openRead(); - DataInputStream dataInputStream = new DataInputStream(inputStream); - int version = dataInputStream.readInt(); - if (version > DownloadAction.MASTER_VERSION) { - throw new IOException("Not supported action file version: " + version); - } - int actionCount = dataInputStream.readInt(); - DownloadAction[] actions = new DownloadAction[actionCount]; - for (int i = 0; i < actionCount; i++) { - actions[i] = DownloadAction.deserializeFromStream(deserializers, dataInputStream, version); - } - return actions; - } finally { - Util.closeQuietly(inputStream); - } - } - - /** - * Stores {@link DownloadAction}s to file. - * - * @param downloadActions DownloadActions to store to file. - * @throws IOException If there is an error during storing. - */ - public void store(DownloadAction... downloadActions) throws IOException { - OutputStream outputStream = null; - try { - outputStream = atomicFile.startWrite(); - DataOutputStream dataOutputStream = new DataOutputStream(outputStream); - dataOutputStream.writeInt(DownloadAction.MASTER_VERSION); - dataOutputStream.writeInt(downloadActions.length); - for (DownloadAction action : downloadActions) { - DownloadAction.serializeToStream(action, dataOutputStream); - } - atomicFile.endWrite(outputStream); - // Avoid calling close twice. - outputStream = null; - } finally { - Util.closeQuietly(outputStream); - } - } - -} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/ActionFileTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/ActionFileTest.java deleted file mode 100644 index cc6a3e84ad..0000000000 --- a/library/core/src/test/java/com/google/android/exoplayer2/offline/ActionFileTest.java +++ /dev/null @@ -1,253 +0,0 @@ -/* - * 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.offline; - -import static com.google.common.truth.Truth.assertThat; - -import com.google.android.exoplayer2.offline.DownloadAction.Deserializer; -import com.google.android.exoplayer2.util.ClosedSource; -import com.google.android.exoplayer2.util.Util; -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import org.junit.After; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.RuntimeEnvironment; -import org.robolectric.annotation.Config; - -/** - * Unit tests for {@link ProgressiveDownloadAction}. - */ -@ClosedSource(reason = "Not ready yet") -@RunWith(RobolectricTestRunner.class) -@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) -public class ActionFileTest { - - private File tempFile; - - @Before - public void setUp() throws Exception { - tempFile = Util.createTempFile(RuntimeEnvironment.application, "ExoPlayerTest"); - } - - @After - public void tearDown() throws Exception { - tempFile.delete(); - } - - @Test - public void testLoadNoDataThrowsIOException() throws Exception { - try { - loadActions(new Object[] {}); - Assert.fail(); - } catch (IOException e) { - // Expected exception. - } - } - - @Test - public void testLoadIncompleteHeaderThrowsIOException() throws Exception { - try { - loadActions(new Object[] {DownloadAction.MASTER_VERSION}); - Assert.fail(); - } catch (IOException e) { - // Expected exception. - } - } - - @Test - public void testLoadCompleteHeaderZeroAction() throws Exception { - DownloadAction[] actions = - loadActions(new Object[] {DownloadAction.MASTER_VERSION, /*action count*/0}); - assertThat(actions).isNotNull(); - assertThat(actions).hasLength(0); - } - - @Test - public void testLoadAction() throws Exception { - DownloadAction[] actions = loadActions( - new Object[] {DownloadAction.MASTER_VERSION, /*action count*/1, /*action 1*/"type2", 321}, - new FakeDeserializer("type2")); - assertThat(actions).isNotNull(); - assertThat(actions).hasLength(1); - assertAction(actions[0], "type2", DownloadAction.MASTER_VERSION, 321); - } - - @Test - public void testLoadActions() throws Exception { - DownloadAction[] actions = loadActions( - new Object[] {DownloadAction.MASTER_VERSION, /*action count*/2, /*action 1*/"type1", 123, - /*action 2*/"type2", 321}, // Action 2 - new FakeDeserializer("type1"), new FakeDeserializer("type2")); - assertThat(actions).isNotNull(); - assertThat(actions).hasLength(2); - assertAction(actions[0], "type1", DownloadAction.MASTER_VERSION, 123); - assertAction(actions[1], "type2", DownloadAction.MASTER_VERSION, 321); - } - - @Test - public void testLoadNotSupportedVersion() throws Exception { - try { - loadActions(new Object[] {DownloadAction.MASTER_VERSION + 1, /*action count*/1, - /*action 1*/"type2", 321}, new FakeDeserializer("type2")); - Assert.fail(); - } catch (IOException e) { - // Expected exception. - } - } - - @Test - public void testLoadNotSupportedType() throws Exception { - try { - loadActions(new Object[] {DownloadAction.MASTER_VERSION, /*action count*/1, - /*action 1*/"type2", 321}, new FakeDeserializer("type1")); - Assert.fail(); - } catch (DownloadException e) { - // Expected exception. - } - } - - @Test - public void testStoreAndLoadNoActions() throws Exception { - doTestSerializationRoundTrip(new DownloadAction[0]); - } - - @Test - public void testStoreAndLoadActions() throws Exception { - doTestSerializationRoundTrip(new DownloadAction[] { - new FakeDownloadAction("type1", DownloadAction.MASTER_VERSION, 123), - new FakeDownloadAction("type2", DownloadAction.MASTER_VERSION, 321), - }, new FakeDeserializer("type1"), new FakeDeserializer("type2")); - } - - private void doTestSerializationRoundTrip(DownloadAction[] actions, - Deserializer... deserializers) throws IOException { - ActionFile actionFile = new ActionFile(tempFile); - actionFile.store(actions); - assertThat(actionFile.load(deserializers)).isEqualTo(actions); - } - - private DownloadAction[] loadActions(Object[] values, Deserializer... deserializers) - throws IOException { - FileOutputStream fileOutputStream = new FileOutputStream(tempFile); - DataOutputStream dataOutputStream = new DataOutputStream(fileOutputStream); - try { - for (Object value : values) { - if (value instanceof Integer) { - dataOutputStream.writeInt((Integer) value); // Action count - } else if (value instanceof String) { - dataOutputStream.writeUTF((String) value); // Action count - } else { - throw new IllegalArgumentException(); - } - } - } finally { - dataOutputStream.close(); - } - return new ActionFile(tempFile).load(deserializers); - } - - private static void assertAction(DownloadAction action, String type, int version, int data) { - assertThat(action).isInstanceOf(FakeDownloadAction.class); - assertThat(action.getType()).isEqualTo(type); - assertThat(((FakeDownloadAction) action).version).isEqualTo(version); - assertThat(((FakeDownloadAction) action).data).isEqualTo(data); - } - - private static class FakeDeserializer implements Deserializer { - final String type; - - FakeDeserializer(String type) { - this.type = type; - } - - @Override - public String getType() { - return type; - } - - @Override - public DownloadAction readFromStream(int version, DataInputStream input) throws IOException { - return new FakeDownloadAction(type, version, input.readInt()); - } - } - - private static class FakeDownloadAction extends DownloadAction { - final String type; - final int version; - final int data; - - private FakeDownloadAction(String type, int version, int data) { - this.type = type; - this.version = version; - this.data = data; - } - - @Override - protected String getType() { - return type; - } - - @Override - protected void writeToStream(DataOutputStream output) throws IOException { - output.writeInt(data); - } - - @Override - protected boolean isRemoveAction() { - return false; - } - - @Override - protected boolean isSameMedia(DownloadAction other) { - return false; - } - - @Override - protected Downloader createDownloader(DownloaderConstructorHelper downloaderConstructorHelper) { - return null; - } - - // auto generated code - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - FakeDownloadAction that = (FakeDownloadAction) o; - return version == that.version && data == that.data && type.equals(that.type); - } - - @Override - public int hashCode() { - int result = type.hashCode(); - result = 31 * result + version; - result = 31 * result + data; - return result; - } - } - -} From ebf19c40287ec638ca2140c6ed25b73fa002fe62 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 13 Oct 2017 12:51:32 -0700 Subject: [PATCH 0531/2472] Update moe equivalence ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=172133622 --- .../ProgressiveDownloadActionTest.java | 144 ------------------ 1 file changed, 144 deletions(-) delete mode 100644 library/core/src/test/java/com/google/android/exoplayer2/offline/ProgressiveDownloadActionTest.java diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/ProgressiveDownloadActionTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/ProgressiveDownloadActionTest.java deleted file mode 100644 index 85a2b02799..0000000000 --- a/library/core/src/test/java/com/google/android/exoplayer2/offline/ProgressiveDownloadActionTest.java +++ /dev/null @@ -1,144 +0,0 @@ -/* - * 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.offline; - -import static com.google.common.truth.Truth.assertThat; - -import com.google.android.exoplayer2.upstream.DummyDataSource; -import com.google.android.exoplayer2.upstream.cache.Cache; -import com.google.android.exoplayer2.util.ClosedSource; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; - -/** - * Unit tests for {@link ProgressiveDownloadAction}. - */ -@ClosedSource(reason = "Not ready yet") -@RunWith(RobolectricTestRunner.class) -@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) -public class ProgressiveDownloadActionTest { - - @Test - public void testDownloadActionIsNotRemoveAction() throws Exception { - ProgressiveDownloadAction action = new ProgressiveDownloadAction("uri", null, false); - assertThat(action.isRemoveAction()).isFalse(); - } - - @Test - public void testRemoveActionIsRemoveAction() throws Exception { - ProgressiveDownloadAction action2 = new ProgressiveDownloadAction("uri", null, true); - assertThat(action2.isRemoveAction()).isTrue(); - } - - @Test - public void testCreateDownloader() throws Exception { - MockitoAnnotations.initMocks(this); - ProgressiveDownloadAction action = new ProgressiveDownloadAction("uri", null, false); - DownloaderConstructorHelper constructorHelper = new DownloaderConstructorHelper( - Mockito.mock(Cache.class), DummyDataSource.FACTORY); - assertThat(action.createDownloader(constructorHelper)).isNotNull(); - } - - @Test - public void testSameUriCacheKeyDifferentAction_IsSameMedia() throws Exception { - ProgressiveDownloadAction action1 = new ProgressiveDownloadAction("uri", null, true); - ProgressiveDownloadAction action2 = new ProgressiveDownloadAction("uri", null, false); - assertThat(action1.isSameMedia(action2)).isTrue(); - } - - @Test - public void testNullCacheKeyDifferentUriAction_IsNotSameMedia() throws Exception { - ProgressiveDownloadAction action3 = new ProgressiveDownloadAction("uri2", null, true); - ProgressiveDownloadAction action4 = new ProgressiveDownloadAction("uri", null, false); - assertThat(action3.isSameMedia(action4)).isFalse(); - } - - @Test - public void testSameCacheKeyDifferentUriAction_IsSameMedia() throws Exception { - ProgressiveDownloadAction action5 = new ProgressiveDownloadAction("uri2", "key", true); - ProgressiveDownloadAction action6 = new ProgressiveDownloadAction("uri", "key", false); - assertThat(action5.isSameMedia(action6)).isTrue(); - } - - @Test - public void testSameUriDifferentCacheKeyAction_IsNotSameMedia() throws Exception { - ProgressiveDownloadAction action7 = new ProgressiveDownloadAction("uri", "key", true); - ProgressiveDownloadAction action8 = new ProgressiveDownloadAction("uri", "key2", false); - assertThat(action7.isSameMedia(action8)).isFalse(); - } - - @Test - public void testEquals() throws Exception { - ProgressiveDownloadAction action1 = new ProgressiveDownloadAction("uri", null, true); - assertThat(action1.equals(action1)).isTrue(); - - ProgressiveDownloadAction action2 = new ProgressiveDownloadAction("uri", null, true); - ProgressiveDownloadAction action3 = new ProgressiveDownloadAction("uri", null, true); - assertThat(action2.equals(action3)).isTrue(); - - ProgressiveDownloadAction action4 = new ProgressiveDownloadAction("uri", null, true); - ProgressiveDownloadAction action5 = new ProgressiveDownloadAction("uri", null, false); - assertThat(action4.equals(action5)).isFalse(); - - ProgressiveDownloadAction action6 = new ProgressiveDownloadAction("uri", null, true); - ProgressiveDownloadAction action7 = new ProgressiveDownloadAction("uri", "key", true); - assertThat(action6.equals(action7)).isFalse(); - - ProgressiveDownloadAction action8 = new ProgressiveDownloadAction("uri", "key2", true); - ProgressiveDownloadAction action9 = new ProgressiveDownloadAction("uri", "key", true); - assertThat(action8.equals(action9)).isFalse(); - - ProgressiveDownloadAction action10 = new ProgressiveDownloadAction("uri", null, true); - ProgressiveDownloadAction action11 = new ProgressiveDownloadAction("uri2", null, true); - assertThat(action10.equals(action11)).isFalse(); - } - - @Test - public void testSerializerGetType() throws Exception { - ProgressiveDownloadAction action = new ProgressiveDownloadAction("uri", null, false); - assertThat(action.getType()).isNotNull(); - } - - @Test - public void testSerializerWriteRead() throws Exception { - doTestSerializationRoundTrip(new ProgressiveDownloadAction("uri1", null, false)); - doTestSerializationRoundTrip(new ProgressiveDownloadAction("uri2", "key", true)); - } - - private static void doTestSerializationRoundTrip(ProgressiveDownloadAction action1) - throws IOException { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - DataOutputStream output = new DataOutputStream(out); - action1.writeToStream(output); - - ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); - DataInputStream input = new DataInputStream(in); - DownloadAction action2 = - ProgressiveDownloadAction.DESERIALIZER.readFromStream(DownloadAction.MASTER_VERSION, input); - - assertThat(action2).isEqualTo(action1); - } - -} From 0a2f485079066e83c9c4b3508da1897eba34f0e9 Mon Sep 17 00:00:00 2001 From: eguven Date: Wed, 26 Jul 2017 08:58:02 -0700 Subject: [PATCH 0532/2472] Add HlsMasterPlaylist.copy method Creates a copy of this playlist which includes only the variants identified by the given variantUrls. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=163212562 --- .../hls/playlist/HlsMasterPlaylist.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/library/hls/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 index b38763f7e8..0b237e75e7 100644 --- a/library/hls/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,6 +17,7 @@ package com.google.android.exoplayer2.source.hls.playlist; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.util.MimeTypes; +import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -108,6 +109,20 @@ public final class HlsMasterPlaylist extends HlsPlaylist { ? Collections.unmodifiableList(muxedCaptionFormats) : null; } + /** + * Returns a copy of this playlist which includes only the renditions identified by the given + * urls. + * + * @param renditionUrls List of rendition urls. + * @return A copy of this playlist which includes only the renditions identified by the given + * urls. + */ + public HlsMasterPlaylist copy(List renditionUrls) { + return new HlsMasterPlaylist(baseUri, tags, copyRenditionList(variants, renditionUrls), + copyRenditionList(audios, renditionUrls), copyRenditionList(subtitles, renditionUrls), + muxedAudioFormat, muxedCaptionFormats); + } + /** * Creates a playlist with a single variant. * @@ -121,4 +136,15 @@ public final class HlsMasterPlaylist extends HlsPlaylist { emptyList, null, null); } + private static List copyRenditionList(List variants, List variantUrls) { + List copyVariants = new ArrayList<>(); + for (int i = 0; i < variants.size(); i++) { + HlsUrl variant = variants.get(i); + if (variantUrls.contains(variant.url)) { + copyVariants.add(variant); + } + } + return copyVariants; + } + } From a7032ede38a1428eb89fdbb34158b7ef77945c77 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 27 Jul 2017 06:20:24 -0700 Subject: [PATCH 0533/2472] Change copyRenditionsList parameters names Also instantiate the resulting list with a predicted size to minimize list resizing. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=163332285 --- .../source/hls/playlist/HlsMasterPlaylist.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/library/hls/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 index 0b237e75e7..04192def9d 100644 --- a/library/hls/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 @@ -118,8 +118,8 @@ public final class HlsMasterPlaylist extends HlsPlaylist { * urls. */ public HlsMasterPlaylist copy(List renditionUrls) { - return new HlsMasterPlaylist(baseUri, tags, copyRenditionList(variants, renditionUrls), - copyRenditionList(audios, renditionUrls), copyRenditionList(subtitles, renditionUrls), + return new HlsMasterPlaylist(baseUri, tags, copyRenditionsList(variants, renditionUrls), + copyRenditionsList(audios, renditionUrls), copyRenditionsList(subtitles, renditionUrls), muxedAudioFormat, muxedCaptionFormats); } @@ -136,15 +136,15 @@ public final class HlsMasterPlaylist extends HlsPlaylist { emptyList, null, null); } - private static List copyRenditionList(List variants, List variantUrls) { - List copyVariants = new ArrayList<>(); - for (int i = 0; i < variants.size(); i++) { - HlsUrl variant = variants.get(i); - if (variantUrls.contains(variant.url)) { - copyVariants.add(variant); + private static List copyRenditionsList(List renditions, List urls) { + List copiedRenditions = new ArrayList<>(urls.size()); + for (int i = 0; i < renditions.size(); i++) { + HlsUrl rendition = renditions.get(i); + if (urls.contains(rendition.url)) { + copiedRenditions.add(rendition); } } - return copyVariants; + return copiedRenditions; } } From 39cb3f932e984f76494c13ce940a6f8c5b31a57e Mon Sep 17 00:00:00 2001 From: Nate Roy Date: Mon, 2 Oct 2017 15:43:19 -0400 Subject: [PATCH 0534/2472] Allow passing of HlsPlaylistParser to HlsMediaSource. Also create helper method in HlsMasterPlaylist to allow the copying of the playlist to another, but with the variants reordered based on a passed comparator. Also added an implementation of HlsPlaylistParser which will reorder the variants returned. --- .../playlist/HlsMasterPlaylistParserTest.java | 44 ++++++- .../ReorderingHlsPlaylistParserTest.java | 107 ++++++++++++++++++ .../exoplayer2/source/hls/HlsMediaSource.java | 14 ++- .../hls/playlist/HlsMasterPlaylist.java | 20 ++++ .../hls/playlist/HlsPlaylistTracker.java | 6 +- .../playlist/ReorderingHlsPlaylistParser.java | 39 +++++++ 6 files changed, 225 insertions(+), 5 deletions(-) create mode 100644 library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/ReorderingHlsPlaylistParserTest.java create mode 100644 library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/ReorderingHlsPlaylistParser.java diff --git a/library/hls/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 index f835c87466..40663fa236 100644 --- a/library/hls/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 @@ -16,16 +16,20 @@ package com.google.android.exoplayer2.source.hls.playlist; import android.net.Uri; + import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.util.MimeTypes; + +import junit.framework.TestCase; + import java.io.ByteArrayInputStream; import java.io.IOException; import java.nio.charset.Charset; import java.util.Collections; +import java.util.Comparator; import java.util.List; -import junit.framework.TestCase; /** * Test for {@link HlsMasterPlaylistParserTest} @@ -144,6 +148,44 @@ public class HlsMasterPlaylistParserTest extends TestCase { assertEquals(Collections.emptyList(), playlist.muxedCaptionFormats); } + public void testReorderedVariantCopy() throws IOException { + HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, MASTER_PLAYLIST); + HlsMasterPlaylist nonReorderedPlaylist = + playlist.copyWithReorderedVariants(new Comparator() { + @Override + public int compare(HlsMasterPlaylist.HlsUrl url1, HlsMasterPlaylist.HlsUrl url2) { + return 0; + } + }); + assertEquals(playlist.variants, nonReorderedPlaylist.variants); + HlsMasterPlaylist.HlsUrl preferred = null; + for (HlsMasterPlaylist.HlsUrl url : playlist.variants) { + if (preferred == null || url.format.bitrate > preferred.format.bitrate) { + preferred = url; + } + } + + assertNotNull(preferred); + + final Comparator comparator = Collections.reverseOrder(new Comparator() { + @Override + public int compare(HlsMasterPlaylist.HlsUrl url1, HlsMasterPlaylist.HlsUrl url2) { + if (url1.format.bitrate > url2.format.bitrate) { + return 1; + } + + if (url2.format.bitrate > url1.format.bitrate) { + return -1; + } + + return 0; + } + }); + HlsMasterPlaylist reorderedPlaylist = playlist.copyWithReorderedVariants(comparator); + + assertEquals(reorderedPlaylist.variants.get(0), preferred); + } + private static HlsMasterPlaylist parseMasterPlaylist(String uri, String playlistString) throws IOException { Uri playlistUri = Uri.parse(uri); diff --git a/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/ReorderingHlsPlaylistParserTest.java b/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/ReorderingHlsPlaylistParserTest.java new file mode 100644 index 0000000000..2816832704 --- /dev/null +++ b/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/ReorderingHlsPlaylistParserTest.java @@ -0,0 +1,107 @@ +package com.google.android.exoplayer2.source.hls.playlist; + +import android.net.Uri; + +import com.google.android.exoplayer2.C; + +import junit.framework.TestCase; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.util.Collections; +import java.util.Comparator; + +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +public class ReorderingHlsPlaylistParserTest extends TestCase { + private static final String MASTER_PLAYLIST = " #EXTM3U \n" + + "\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n" + + "http://example.com/low.m3u8\n" + + "\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=2560000,FRAME-RATE=25,RESOLUTION=384x160\n" + + "http://example.com/mid.m3u8\n" + + "\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=7680000,FRAME-RATE=29.997\n" + + "http://example.com/hi.m3u8\n" + + "\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"mp4a.40.5\"\n" + + "http://example.com/audio-only.m3u8"; + + public void testReorderingWithNonMasterPlaylist() throws IOException { + Uri playlistUri = Uri.parse("https://example.com/test.m3u8"); + String playlistString = "#EXTM3U\n" + + "#EXT-X-VERSION:3\n" + + "#EXT-X-PLAYLIST-TYPE:VOD\n" + + "#EXT-X-START:TIME-OFFSET=-25" + + "#EXT-X-TARGETDURATION:8\n" + + "#EXT-X-MEDIA-SEQUENCE:2679\n" + + "#EXT-X-DISCONTINUITY-SEQUENCE:4\n" + + "#EXT-X-ALLOW-CACHE:YES\n" + + "\n" + + "#EXTINF:7.975,\n" + + "#EXT-X-BYTERANGE:51370@0\n" + + "https://priv.example.com/fileSequence2679.ts\n" + + "\n" + + "#EXT-X-KEY:METHOD=AES-128,URI=\"https://priv.example.com/key.php?r=2680\",IV=0x1566B\n" + + "#EXTINF:7.975,\n" + + "#EXT-X-BYTERANGE:51501@2147483648\n" + + "https://priv.example.com/fileSequence2680.ts\n" + + "\n" + + "#EXT-X-KEY:METHOD=NONE\n" + + "#EXTINF:7.941,\n" + + "#EXT-X-BYTERANGE:51501\n" // @2147535149 + + "https://priv.example.com/fileSequence2681.ts\n" + + "\n" + + "#EXT-X-DISCONTINUITY\n" + + "#EXT-X-KEY:METHOD=AES-128,URI=\"https://priv.example.com/key.php?r=2682\"\n" + + "#EXTINF:7.975,\n" + + "#EXT-X-BYTERANGE:51740\n" // @2147586650 + + "https://priv.example.com/fileSequence2682.ts\n" + + "\n" + + "#EXTINF:7.975,\n" + + "https://priv.example.com/fileSequence2683.ts\n" + + "#EXT-X-ENDLIST"; + InputStream inputStream = new ByteArrayInputStream(playlistString.getBytes(Charset.forName(C.UTF8_NAME))); + Comparator comparator = mock(Comparator.class); + ReorderingHlsPlaylistParser playlistParser = new ReorderingHlsPlaylistParser(new HlsPlaylistParser(), + comparator); + final HlsMediaPlaylist playlist = (HlsMediaPlaylist) playlistParser.parse(playlistUri, inputStream); + assertNotNull(playlist); + // We should never compare the variants for a media level playlist. + verify(comparator, never()).compare(any(HlsMasterPlaylist.HlsUrl.class), any(HlsMasterPlaylist.HlsUrl.class)); + } + + public void testReorderingForMasterPlaylist() throws IOException { + Uri playlistUri = Uri.parse("https://example.com/test.m3u8"); + ByteArrayInputStream inputStream = new ByteArrayInputStream( + MASTER_PLAYLIST.getBytes(Charset.forName(C.UTF8_NAME))); + final Comparator comparator = Collections.reverseOrder(new Comparator() { + @Override + public int compare(HlsMasterPlaylist.HlsUrl url1, HlsMasterPlaylist.HlsUrl url2) { + if (url1.format.bitrate > url2.format.bitrate) { + return 1; + } + + if (url2.format.bitrate > url1.format.bitrate) { + return -1; + } + + return 0; + } + }); + ReorderingHlsPlaylistParser playlistParser = new ReorderingHlsPlaylistParser(new HlsPlaylistParser(), + comparator); + final HlsMasterPlaylist reorderedPlaylist = (HlsMasterPlaylist) playlistParser.parse(playlistUri, inputStream); + assertNotNull(reorderedPlaylist); + + inputStream.reset(); + final HlsMasterPlaylist playlist = (HlsMasterPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream); + assertEquals(reorderedPlaylist.variants.get(0).format, playlist.variants.get(2).format); + } +} \ No newline at end of file diff --git a/library/hls/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 index fd3d533337..b7f7124e44 100644 --- a/library/hls/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 @@ -26,9 +26,12 @@ import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.SinglePeriodTimeline; import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; +import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylist; +import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser; import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistTracker; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.ParsingLoadable; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; import java.util.List; @@ -52,6 +55,7 @@ public final class HlsMediaSource implements MediaSource, private final HlsDataSourceFactory dataSourceFactory; private final int minLoadableRetryCount; private final EventDispatcher eventDispatcher; + private final ParsingLoadable.Parser playlistParser; private HlsPlaylistTracker playlistTracker; private Listener sourceListener; @@ -72,9 +76,17 @@ public final class HlsMediaSource implements MediaSource, public HlsMediaSource(Uri manifestUri, HlsDataSourceFactory dataSourceFactory, int minLoadableRetryCount, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { + this(manifestUri, dataSourceFactory, minLoadableRetryCount, eventHandler, eventListener, new HlsPlaylistParser()); + } + + public HlsMediaSource(Uri manifestUri, HlsDataSourceFactory dataSourceFactory, + int minLoadableRetryCount, Handler eventHandler, + AdaptiveMediaSourceEventListener eventListener, + ParsingLoadable.Parser playlistParser) { this.manifestUri = manifestUri; this.dataSourceFactory = dataSourceFactory; this.minLoadableRetryCount = minLoadableRetryCount; + this.playlistParser = playlistParser; eventDispatcher = new EventDispatcher(eventHandler, eventListener); } @@ -82,7 +94,7 @@ public final class HlsMediaSource implements MediaSource, public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { Assertions.checkState(playlistTracker == null); playlistTracker = new HlsPlaylistTracker(manifestUri, dataSourceFactory, eventDispatcher, - minLoadableRetryCount, this); + minLoadableRetryCount, this, playlistParser); sourceListener = listener; playlistTracker.start(); } diff --git a/library/hls/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 index 04192def9d..5ded975f88 100644 --- a/library/hls/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 @@ -19,6 +19,7 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.util.MimeTypes; import java.util.ArrayList; import java.util.Collections; +import java.util.Comparator; import java.util.List; /** @@ -123,6 +124,25 @@ public final class HlsMasterPlaylist extends HlsPlaylist { muxedAudioFormat, muxedCaptionFormats); } + /** + * Returns a copy of this playlist which includes the variants sorted using the passed comparator. NOTE: the variants + * will be sorted in ascending order by default. If you wish to use descending order, you can wrap your comparator in + * {@link Collections#reverseOrder(Comparator)}. + * + * @param variantComparator the comparator to use to sort the variant list. + * @return a copy of this playlist which includes the variants sorted using the passed comparator. + */ + public HlsMasterPlaylist copyWithReorderedVariants(Comparator variantComparator) { + return new HlsMasterPlaylist(baseUri, tags, filterVariants(variants, variantComparator), audios, + subtitles, muxedAudioFormat, muxedCaptionFormats); + } + + private List filterVariants(List variants, Comparator variantComparator) { + List reorderedList = new ArrayList<>(variants); + Collections.sort(reorderedList, variantComparator); + return reorderedList; + } + /** * Creates a playlist with a single variant. * diff --git a/library/hls/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 index 567dbd4af6..a0e299632d 100644 --- a/library/hls/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 @@ -120,7 +120,7 @@ public final class HlsPlaylistTracker implements Loader.Callback playlistParser; private final int minRetryCount; private final IdentityHashMap playlistBundles; private final Handler playlistRefreshHandler; @@ -145,7 +145,7 @@ public final class HlsPlaylistTracker implements Loader.Callback playlistParser) { this.initialPlaylistUri = initialPlaylistUri; this.dataSourceFactory = dataSourceFactory; this.eventDispatcher = eventDispatcher; @@ -153,7 +153,7 @@ public final class HlsPlaylistTracker implements Loader.Callback(); initialPlaylistLoader = new Loader("HlsPlaylistTracker:MasterPlaylist"); - playlistParser = new HlsPlaylistParser(); + this.playlistParser = playlistParser; playlistBundles = new IdentityHashMap<>(); playlistRefreshHandler = new Handler(); } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/ReorderingHlsPlaylistParser.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/ReorderingHlsPlaylistParser.java new file mode 100644 index 0000000000..bdd47e8c28 --- /dev/null +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/ReorderingHlsPlaylistParser.java @@ -0,0 +1,39 @@ +package com.google.android.exoplayer2.source.hls.playlist; + +import android.net.Uri; + +import com.google.android.exoplayer2.upstream.ParsingLoadable; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Comparator; + +/** + * Parser for {@link HlsPlaylist}s that reorders the variants based on the comparator passed. + */ +public class ReorderingHlsPlaylistParser implements ParsingLoadable.Parser { + private final ParsingLoadable.Parser playlistParser; + private final Comparator variantComparator; + + /** + * @param playlistParser the {@link ParsingLoadable.Parser} to wrap. + * @param variantComparator the {@link Comparator} to use to reorder the variants. + * See {@link HlsMasterPlaylist#copyWithReorderedVariants(Comparator)} for more details. + */ + public ReorderingHlsPlaylistParser(ParsingLoadable.Parser playlistParser, + Comparator variantComparator) { + this.playlistParser = playlistParser; + this.variantComparator = variantComparator; + } + + @Override + public HlsPlaylist parse(Uri uri, InputStream inputStream) throws IOException { + final HlsPlaylist playlist = playlistParser.parse(uri, inputStream); + + if (playlist instanceof HlsMasterPlaylist) { + return ((HlsMasterPlaylist) playlist).copyWithReorderedVariants(variantComparator); + } + + return playlist; + } +} From 1c594b4cdb6c4fd1362c00a9b523b38ed640531c Mon Sep 17 00:00:00 2001 From: Nate Roy Date: Fri, 6 Oct 2017 14:35:18 -0400 Subject: [PATCH 0535/2472] remove reordering playlist parser --- .../ReorderingHlsPlaylistParserTest.java | 107 ------------------ .../playlist/ReorderingHlsPlaylistParser.java | 39 ------- 2 files changed, 146 deletions(-) delete mode 100644 library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/ReorderingHlsPlaylistParserTest.java delete mode 100644 library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/ReorderingHlsPlaylistParser.java diff --git a/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/ReorderingHlsPlaylistParserTest.java b/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/ReorderingHlsPlaylistParserTest.java deleted file mode 100644 index 2816832704..0000000000 --- a/library/hls/src/androidTest/java/com/google/android/exoplayer2/source/hls/playlist/ReorderingHlsPlaylistParserTest.java +++ /dev/null @@ -1,107 +0,0 @@ -package com.google.android.exoplayer2.source.hls.playlist; - -import android.net.Uri; - -import com.google.android.exoplayer2.C; - -import junit.framework.TestCase; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.Charset; -import java.util.Collections; -import java.util.Comparator; - -import static org.mockito.Matchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; - -public class ReorderingHlsPlaylistParserTest extends TestCase { - private static final String MASTER_PLAYLIST = " #EXTM3U \n" - + "\n" - + "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n" - + "http://example.com/low.m3u8\n" - + "\n" - + "#EXT-X-STREAM-INF:BANDWIDTH=2560000,FRAME-RATE=25,RESOLUTION=384x160\n" - + "http://example.com/mid.m3u8\n" - + "\n" - + "#EXT-X-STREAM-INF:BANDWIDTH=7680000,FRAME-RATE=29.997\n" - + "http://example.com/hi.m3u8\n" - + "\n" - + "#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"mp4a.40.5\"\n" - + "http://example.com/audio-only.m3u8"; - - public void testReorderingWithNonMasterPlaylist() throws IOException { - Uri playlistUri = Uri.parse("https://example.com/test.m3u8"); - String playlistString = "#EXTM3U\n" - + "#EXT-X-VERSION:3\n" - + "#EXT-X-PLAYLIST-TYPE:VOD\n" - + "#EXT-X-START:TIME-OFFSET=-25" - + "#EXT-X-TARGETDURATION:8\n" - + "#EXT-X-MEDIA-SEQUENCE:2679\n" - + "#EXT-X-DISCONTINUITY-SEQUENCE:4\n" - + "#EXT-X-ALLOW-CACHE:YES\n" - + "\n" - + "#EXTINF:7.975,\n" - + "#EXT-X-BYTERANGE:51370@0\n" - + "https://priv.example.com/fileSequence2679.ts\n" - + "\n" - + "#EXT-X-KEY:METHOD=AES-128,URI=\"https://priv.example.com/key.php?r=2680\",IV=0x1566B\n" - + "#EXTINF:7.975,\n" - + "#EXT-X-BYTERANGE:51501@2147483648\n" - + "https://priv.example.com/fileSequence2680.ts\n" - + "\n" - + "#EXT-X-KEY:METHOD=NONE\n" - + "#EXTINF:7.941,\n" - + "#EXT-X-BYTERANGE:51501\n" // @2147535149 - + "https://priv.example.com/fileSequence2681.ts\n" - + "\n" - + "#EXT-X-DISCONTINUITY\n" - + "#EXT-X-KEY:METHOD=AES-128,URI=\"https://priv.example.com/key.php?r=2682\"\n" - + "#EXTINF:7.975,\n" - + "#EXT-X-BYTERANGE:51740\n" // @2147586650 - + "https://priv.example.com/fileSequence2682.ts\n" - + "\n" - + "#EXTINF:7.975,\n" - + "https://priv.example.com/fileSequence2683.ts\n" - + "#EXT-X-ENDLIST"; - InputStream inputStream = new ByteArrayInputStream(playlistString.getBytes(Charset.forName(C.UTF8_NAME))); - Comparator comparator = mock(Comparator.class); - ReorderingHlsPlaylistParser playlistParser = new ReorderingHlsPlaylistParser(new HlsPlaylistParser(), - comparator); - final HlsMediaPlaylist playlist = (HlsMediaPlaylist) playlistParser.parse(playlistUri, inputStream); - assertNotNull(playlist); - // We should never compare the variants for a media level playlist. - verify(comparator, never()).compare(any(HlsMasterPlaylist.HlsUrl.class), any(HlsMasterPlaylist.HlsUrl.class)); - } - - public void testReorderingForMasterPlaylist() throws IOException { - Uri playlistUri = Uri.parse("https://example.com/test.m3u8"); - ByteArrayInputStream inputStream = new ByteArrayInputStream( - MASTER_PLAYLIST.getBytes(Charset.forName(C.UTF8_NAME))); - final Comparator comparator = Collections.reverseOrder(new Comparator() { - @Override - public int compare(HlsMasterPlaylist.HlsUrl url1, HlsMasterPlaylist.HlsUrl url2) { - if (url1.format.bitrate > url2.format.bitrate) { - return 1; - } - - if (url2.format.bitrate > url1.format.bitrate) { - return -1; - } - - return 0; - } - }); - ReorderingHlsPlaylistParser playlistParser = new ReorderingHlsPlaylistParser(new HlsPlaylistParser(), - comparator); - final HlsMasterPlaylist reorderedPlaylist = (HlsMasterPlaylist) playlistParser.parse(playlistUri, inputStream); - assertNotNull(reorderedPlaylist); - - inputStream.reset(); - final HlsMasterPlaylist playlist = (HlsMasterPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream); - assertEquals(reorderedPlaylist.variants.get(0).format, playlist.variants.get(2).format); - } -} \ No newline at end of file diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/ReorderingHlsPlaylistParser.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/ReorderingHlsPlaylistParser.java deleted file mode 100644 index bdd47e8c28..0000000000 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/ReorderingHlsPlaylistParser.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.google.android.exoplayer2.source.hls.playlist; - -import android.net.Uri; - -import com.google.android.exoplayer2.upstream.ParsingLoadable; - -import java.io.IOException; -import java.io.InputStream; -import java.util.Comparator; - -/** - * Parser for {@link HlsPlaylist}s that reorders the variants based on the comparator passed. - */ -public class ReorderingHlsPlaylistParser implements ParsingLoadable.Parser { - private final ParsingLoadable.Parser playlistParser; - private final Comparator variantComparator; - - /** - * @param playlistParser the {@link ParsingLoadable.Parser} to wrap. - * @param variantComparator the {@link Comparator} to use to reorder the variants. - * See {@link HlsMasterPlaylist#copyWithReorderedVariants(Comparator)} for more details. - */ - public ReorderingHlsPlaylistParser(ParsingLoadable.Parser playlistParser, - Comparator variantComparator) { - this.playlistParser = playlistParser; - this.variantComparator = variantComparator; - } - - @Override - public HlsPlaylist parse(Uri uri, InputStream inputStream) throws IOException { - final HlsPlaylist playlist = playlistParser.parse(uri, inputStream); - - if (playlist instanceof HlsMasterPlaylist) { - return ((HlsMasterPlaylist) playlist).copyWithReorderedVariants(variantComparator); - } - - return playlist; - } -} From feceabadebb7275cc69f85c2854876f3a2045170 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 13 Oct 2017 20:40:08 +0100 Subject: [PATCH 0536/2472] Tweak recently merged pull requests --- .../playlist/HlsMasterPlaylistParserTest.java | 46 +------------------ .../playlist/HlsMediaPlaylistParserTest.java | 2 +- .../exoplayer2/source/hls/HlsMediaSource.java | 9 ++-- .../hls/playlist/HlsMasterPlaylist.java | 20 -------- .../hls/playlist/HlsPlaylistTracker.java | 5 +- 5 files changed, 11 insertions(+), 71 deletions(-) diff --git a/library/hls/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 index 40663fa236..6bbcaecd1f 100644 --- a/library/hls/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 @@ -16,23 +16,19 @@ package com.google.android.exoplayer2.source.hls.playlist; import android.net.Uri; - import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.util.MimeTypes; - -import junit.framework.TestCase; - import java.io.ByteArrayInputStream; import java.io.IOException; import java.nio.charset.Charset; import java.util.Collections; -import java.util.Comparator; import java.util.List; +import junit.framework.TestCase; /** - * Test for {@link HlsMasterPlaylistParserTest} + * Test for {@link HlsMasterPlaylistParserTest}. */ public class HlsMasterPlaylistParserTest extends TestCase { @@ -148,44 +144,6 @@ public class HlsMasterPlaylistParserTest extends TestCase { assertEquals(Collections.emptyList(), playlist.muxedCaptionFormats); } - public void testReorderedVariantCopy() throws IOException { - HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, MASTER_PLAYLIST); - HlsMasterPlaylist nonReorderedPlaylist = - playlist.copyWithReorderedVariants(new Comparator() { - @Override - public int compare(HlsMasterPlaylist.HlsUrl url1, HlsMasterPlaylist.HlsUrl url2) { - return 0; - } - }); - assertEquals(playlist.variants, nonReorderedPlaylist.variants); - HlsMasterPlaylist.HlsUrl preferred = null; - for (HlsMasterPlaylist.HlsUrl url : playlist.variants) { - if (preferred == null || url.format.bitrate > preferred.format.bitrate) { - preferred = url; - } - } - - assertNotNull(preferred); - - final Comparator comparator = Collections.reverseOrder(new Comparator() { - @Override - public int compare(HlsMasterPlaylist.HlsUrl url1, HlsMasterPlaylist.HlsUrl url2) { - if (url1.format.bitrate > url2.format.bitrate) { - return 1; - } - - if (url2.format.bitrate > url1.format.bitrate) { - return -1; - } - - return 0; - } - }); - HlsMasterPlaylist reorderedPlaylist = playlist.copyWithReorderedVariants(comparator); - - assertEquals(reorderedPlaylist.variants.get(0), preferred); - } - private static HlsMasterPlaylist parseMasterPlaylist(String uri, String playlistString) throws IOException { Uri playlistUri = Uri.parse(uri); diff --git a/library/hls/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 index e2eb173df8..6855f786ec 100644 --- a/library/hls/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 @@ -27,7 +27,7 @@ import java.util.Locale; import junit.framework.TestCase; /** - * Test for {@link HlsMediaPlaylistParserTest} + * Test for {@link HlsMediaPlaylistParserTest}. */ public class HlsMediaPlaylistParserTest extends TestCase { diff --git a/library/hls/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 index b7f7124e44..10a0536612 100644 --- a/library/hls/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 @@ -76,13 +76,14 @@ public final class HlsMediaSource implements MediaSource, public HlsMediaSource(Uri manifestUri, HlsDataSourceFactory dataSourceFactory, int minLoadableRetryCount, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { - this(manifestUri, dataSourceFactory, minLoadableRetryCount, eventHandler, eventListener, new HlsPlaylistParser()); + this(manifestUri, dataSourceFactory, minLoadableRetryCount, eventHandler, eventListener, + new HlsPlaylistParser()); } public HlsMediaSource(Uri manifestUri, HlsDataSourceFactory dataSourceFactory, - int minLoadableRetryCount, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener, - ParsingLoadable.Parser playlistParser) { + int minLoadableRetryCount, Handler eventHandler, + AdaptiveMediaSourceEventListener eventListener, + ParsingLoadable.Parser playlistParser) { this.manifestUri = manifestUri; this.dataSourceFactory = dataSourceFactory; this.minLoadableRetryCount = minLoadableRetryCount; diff --git a/library/hls/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 index 5ded975f88..04192def9d 100644 --- a/library/hls/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 @@ -19,7 +19,6 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.util.MimeTypes; import java.util.ArrayList; import java.util.Collections; -import java.util.Comparator; import java.util.List; /** @@ -124,25 +123,6 @@ public final class HlsMasterPlaylist extends HlsPlaylist { muxedAudioFormat, muxedCaptionFormats); } - /** - * Returns a copy of this playlist which includes the variants sorted using the passed comparator. NOTE: the variants - * will be sorted in ascending order by default. If you wish to use descending order, you can wrap your comparator in - * {@link Collections#reverseOrder(Comparator)}. - * - * @param variantComparator the comparator to use to sort the variant list. - * @return a copy of this playlist which includes the variants sorted using the passed comparator. - */ - public HlsMasterPlaylist copyWithReorderedVariants(Comparator variantComparator) { - return new HlsMasterPlaylist(baseUri, tags, filterVariants(variants, variantComparator), audios, - subtitles, muxedAudioFormat, muxedCaptionFormats); - } - - private List filterVariants(List variants, Comparator variantComparator) { - List reorderedList = new ArrayList<>(variants); - Collections.sort(reorderedList, variantComparator); - return reorderedList; - } - /** * Creates a playlist with a single variant. * diff --git a/library/hls/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 index a0e299632d..3d8d4eb3af 100644 --- a/library/hls/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 @@ -145,15 +145,16 @@ public final class HlsPlaylistTracker implements Loader.Callback playlistParser) { + PrimaryPlaylistListener primaryPlaylistListener, + ParsingLoadable.Parser playlistParser) { this.initialPlaylistUri = initialPlaylistUri; this.dataSourceFactory = dataSourceFactory; this.eventDispatcher = eventDispatcher; this.minRetryCount = minRetryCount; this.primaryPlaylistListener = primaryPlaylistListener; + this.playlistParser = playlistParser; listeners = new ArrayList<>(); initialPlaylistLoader = new Loader("HlsPlaylistTracker:MasterPlaylist"); - this.playlistParser = playlistParser; playlistBundles = new IdentityHashMap<>(); playlistRefreshHandler = new Handler(); } From dbdae4ca25fd023e2d13874ca52236b521c9a8a5 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 13 Oct 2017 11:38:47 -0700 Subject: [PATCH 0537/2472] Only parse common-encryption sinf boxes ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=172124807 --- .../exoplayer2/extractor/mp4/AtomParsersTest.java | 8 ++++---- .../exoplayer2/extractor/mp4/AtomParsers.java | 13 +++++++------ 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/library/core/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 index d0213337b8..d644af2677 100644 --- a/library/core/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 @@ -34,18 +34,18 @@ public final class AtomParsersTest extends TestCase { + SAMPLE_COUNT + "0001000200030004"); public void testStz2Parsing4BitFieldSize() { - verifyParsing(new Atom.LeafAtom(Atom.TYPE_stsz, new ParsableByteArray(FOUR_BIT_STZ2))); + verifyStz2Parsing(new Atom.LeafAtom(Atom.TYPE_stsz, new ParsableByteArray(FOUR_BIT_STZ2))); } public void testStz2Parsing8BitFieldSize() { - verifyParsing(new Atom.LeafAtom(Atom.TYPE_stsz, new ParsableByteArray(EIGHT_BIT_STZ2))); + verifyStz2Parsing(new Atom.LeafAtom(Atom.TYPE_stsz, new ParsableByteArray(EIGHT_BIT_STZ2))); } public void testStz2Parsing16BitFieldSize() { - verifyParsing(new Atom.LeafAtom(Atom.TYPE_stsz, new ParsableByteArray(SIXTEEN_BIT_STZ2))); + verifyStz2Parsing(new Atom.LeafAtom(Atom.TYPE_stsz, new ParsableByteArray(SIXTEEN_BIT_STZ2))); } - private void verifyParsing(Atom.LeafAtom stz2Atom) { + private static void verifyStz2Parsing(Atom.LeafAtom stz2Atom) { AtomParsers.Stz2SampleSizeBox box = new AtomParsers.Stz2SampleSizeBox(stz2Atom); assertEquals(4, box.getSampleCount()); assertFalse(box.isFixedSampleSize()); diff --git a/library/core/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 index 9a03311ccf..84538b8122 100644 --- a/library/core/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 @@ -1060,8 +1060,8 @@ import java.util.List; Assertions.checkArgument(childAtomSize > 0, "childAtomSize should be positive"); int childAtomType = parent.readInt(); if (childAtomType == Atom.TYPE_sinf) { - Pair result = parseSinfFromParent(parent, childPosition, - childAtomSize); + Pair result = parseCommonEncryptionSinfFromParent(parent, + childPosition, childAtomSize); if (result != null) { return result; } @@ -1071,8 +1071,8 @@ import java.util.List; return null; } - private static Pair parseSinfFromParent(ParsableByteArray parent, - int position, int size) { + /* package */ static Pair parseCommonEncryptionSinfFromParent( + ParsableByteArray parent, int position, int size) { int childPosition = position + Atom.HEADER_SIZE; int schemeInformationBoxPosition = C.POSITION_UNSET; int schemeInformationBoxSize = 0; @@ -1086,7 +1086,7 @@ import java.util.List; dataFormat = parent.readInt(); } else if (childAtomType == Atom.TYPE_schm) { parent.skipBytes(4); - // scheme_type field. Defined in ISO/IEC 23001-7:2016, section 4.1. + // Common encryption scheme_type values are defined in ISO/IEC 23001-7:2016, section 4.1. schemeType = parent.readString(4); } else if (childAtomType == Atom.TYPE_schi) { schemeInformationBoxPosition = childPosition; @@ -1095,7 +1095,8 @@ import java.util.List; childPosition += childAtomSize; } - if (schemeType != null) { + if (C.CENC_TYPE_cenc.equals(schemeType) || C.CENC_TYPE_cbc1.equals(schemeType) + || C.CENC_TYPE_cens.equals(schemeType) || C.CENC_TYPE_cbcs.equals(schemeType)) { Assertions.checkArgument(dataFormat != null, "frma atom is mandatory"); Assertions.checkArgument(schemeInformationBoxPosition != C.POSITION_UNSET, "schi atom is mandatory"); From 04d10ea67db38035d3b7eef9a2111b32c1a88468 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 10 Oct 2017 12:40:02 -0700 Subject: [PATCH 0538/2472] Expose public constructors for FrameworkMediaCrypto MediaCodecRenderer implementations require DrmSessionManager, but it's currently not possible for an app to provide a custom implementation due to FrameworkMediaCrypto having a package private constructor. This change exposes public FrameworkMediaCrypto constructors, hence removing this restriction. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=171718853 --- .../exoplayer2/drm/FrameworkMediaCrypto.java | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaCrypto.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaCrypto.java index 5bee85f449..4e58ed6a31 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaCrypto.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaCrypto.java @@ -28,12 +28,29 @@ public final class FrameworkMediaCrypto implements ExoMediaCrypto { private final MediaCrypto mediaCrypto; private final boolean forceAllowInsecureDecoderComponents; - /* package */ FrameworkMediaCrypto(MediaCrypto mediaCrypto, + /** + * @param mediaCrypto The {@link MediaCrypto} to wrap. + */ + public FrameworkMediaCrypto(MediaCrypto mediaCrypto) { + this(mediaCrypto, false); + } + + /** + * @param mediaCrypto The {@link MediaCrypto} to wrap. + * @param forceAllowInsecureDecoderComponents Whether to force + * {@link #requiresSecureDecoderComponent(String)} to return {@code false}, rather than + * {@link MediaCrypto#requiresSecureDecoderComponent(String)} of the wrapped + * {@link MediaCrypto}. + */ + public FrameworkMediaCrypto(MediaCrypto mediaCrypto, boolean forceAllowInsecureDecoderComponents) { this.mediaCrypto = Assertions.checkNotNull(mediaCrypto); this.forceAllowInsecureDecoderComponents = forceAllowInsecureDecoderComponents; } + /** + * Returns the wrapped {@link MediaCrypto}. + */ public MediaCrypto getWrappedMediaCrypto() { return mediaCrypto; } From e6bf3736122a4a989e85d90b97b70b0451e6fc8f Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 10 Oct 2017 12:39:34 -0700 Subject: [PATCH 0539/2472] Allow overriding of DefaultDashChunkSource.getNextChunk ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=171718775 --- .../source/dash/DefaultDashChunkSource.java | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/library/dash/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 index dd62d47621..ec1c90bf75 100644 --- a/library/dash/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 @@ -173,7 +173,7 @@ public class DefaultDashChunkSource implements DashChunkSource { } @Override - public final void getNextChunk(MediaChunk previous, long playbackPositionUs, ChunkHolder out) { + public void getNextChunk(MediaChunk previous, long playbackPositionUs, ChunkHolder out) { if (fatalError != null) { return; } @@ -300,7 +300,7 @@ public class DefaultDashChunkSource implements DashChunkSource { trackSelection.indexOf(chunk.trackFormat), e); } - // Private methods. + // Internal methods. private ArrayList getRepresentations() { List manifestAdapationSets = manifest.getPeriod(periodIndex).adaptationSets; @@ -319,7 +319,12 @@ public class DefaultDashChunkSource implements DashChunkSource { } } - private static Chunk newInitializationChunk(RepresentationHolder representationHolder, + private long resolveTimeToLiveEdgeUs(long playbackPositionUs) { + boolean resolveTimeToLiveEdgePossible = manifest.dynamic && liveEdgeTimeUs != C.TIME_UNSET; + return resolveTimeToLiveEdgePossible ? liveEdgeTimeUs - playbackPositionUs : C.TIME_UNSET; + } + + protected static Chunk newInitializationChunk(RepresentationHolder representationHolder, DataSource dataSource, Format trackFormat, int trackSelectionReason, Object trackSelectionData, RangedUri initializationUri, RangedUri indexUri) { RangedUri requestUri; @@ -340,7 +345,7 @@ public class DefaultDashChunkSource implements DashChunkSource { trackSelectionReason, trackSelectionData, representationHolder.extractorWrapper); } - private static Chunk newMediaChunk(RepresentationHolder representationHolder, + protected static Chunk newMediaChunk(RepresentationHolder representationHolder, DataSource dataSource, int trackType, Format trackFormat, int trackSelectionReason, Object trackSelectionData, int firstSegmentNum, int maxSegmentCount) { Representation representation = representationHolder.representation; From 758de818db009bf0e21372976cf2ffc172523397 Mon Sep 17 00:00:00 2001 From: yqritc Date: Thu, 21 Sep 2017 12:01:25 +0900 Subject: [PATCH 0540/2472] fix primarySnapshotAccessAgeMs --- .../exoplayer2/source/hls/playlist/HlsPlaylistTracker.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/hls/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 index 3d8d4eb3af..63498989c7 100644 --- a/library/hls/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 @@ -352,7 +352,7 @@ public final class HlsPlaylistTracker implements Loader.Callback PRIMARY_URL_KEEPALIVE_MS) { primaryHlsUrl = url; playlistBundles.get(primaryHlsUrl).loadPlaylist(); From 3b8fd4985cd879e9734b775739779efec1b73746 Mon Sep 17 00:00:00 2001 From: yqritc Date: Thu, 21 Sep 2017 14:18:59 +0900 Subject: [PATCH 0541/2472] remove keep alive check for updating primary url to avoid redundant playlist loading --- .../source/hls/playlist/HlsPlaylistTracker.java | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/library/hls/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 index 63498989c7..c3e903334a 100644 --- a/library/hls/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 @@ -112,11 +112,6 @@ public final class HlsPlaylistTracker implements Loader.Callback PRIMARY_URL_KEEPALIVE_MS) { - primaryHlsUrl = url; - playlistBundles.get(primaryHlsUrl).loadPlaylist(); - } + + primaryHlsUrl = url; + playlistBundles.get(primaryHlsUrl).loadPlaylist(); } private void createBundles(List urls) { From 98f6e89efd368e17f65a19c02491674b5d7d0092 Mon Sep 17 00:00:00 2001 From: yqritc Date: Thu, 21 Sep 2017 16:01:32 +0900 Subject: [PATCH 0542/2472] remove space --- .../exoplayer2/source/hls/playlist/HlsPlaylistTracker.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/hls/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 index c3e903334a..81d3de7307 100644 --- a/library/hls/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 @@ -201,7 +201,7 @@ public final class HlsPlaylistTracker implements Loader.Callback Date: Tue, 26 Sep 2017 17:06:36 +0100 Subject: [PATCH 0543/2472] Clean up HlsPlaylistTracker --- .../hls/playlist/HlsPlaylistTracker.java | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/library/hls/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 index 81d3de7307..fdb9fb1e4e 100644 --- a/library/hls/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 @@ -201,7 +201,7 @@ public final class HlsPlaylistTracker implements Loader.Callback urls) { int listSize = urls.size(); - long currentTimeMs = SystemClock.elapsedRealtime(); for (int i = 0; i < listSize; i++) { HlsUrl url = urls.get(i); - MediaPlaylistBundle bundle = new MediaPlaylistBundle(url, currentTimeMs); + MediaPlaylistBundle bundle = new MediaPlaylistBundle(url); playlistBundles.put(url, bundle); } } @@ -471,14 +470,12 @@ public final class HlsPlaylistTracker implements Loader.Callback( dataSourceFactory.createDataSource(C.DATA_TYPE_MANIFEST), @@ -487,7 +484,6 @@ public final class HlsPlaylistTracker implements Loader.Callback Date: Tue, 26 Sep 2017 11:09:45 -0700 Subject: [PATCH 0544/2472] Prevent unnecessary consecutive playlist loads ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=170078933 --- .../hls/playlist/HlsPlaylistTracker.java | 69 ++++++++++++------- 1 file changed, 43 insertions(+), 26 deletions(-) diff --git a/library/hls/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 index fdb9fb1e4e..da73aa3996 100644 --- a/library/hls/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 @@ -364,9 +364,8 @@ public final class HlsPlaylistTracker implements Loader.Callback C.usToMs(playlistSnapshot.targetDurationUs) * PLAYLIST_STUCK_TARGET_DURATION_COEFFICIENT) { - // The playlist seems to be stuck, we blacklist it. + // The playlist seems to be stuck. Blacklist it. playlistError = new PlaylistStuckException(playlistUrl.url); blacklistPlaylist(); - } else if (loadedPlaylist.mediaSequence + loadedPlaylist.segments.size() - < playlistSnapshot.mediaSequence) { - // The media sequence has jumped backwards. The server has likely reset. - playlistError = new PlaylistResetException(playlistUrl.url); } - refreshDelayUs = playlistSnapshot.targetDurationUs / 2; } - if (refreshDelayUs != C.TIME_UNSET) { - // See HLS spec v20, section 6.3.4 for more information on media playlist refreshing. - pendingRefresh = playlistRefreshHandler.postDelayed(this, C.usToMs(refreshDelayUs)); + // Do not allow the playlist to load again within the target duration if we obtained a new + // snapshot, or half the target duration otherwise. + earliestNextLoadTimeMs = currentTimeMs + C.usToMs(playlistSnapshot != oldPlaylist + ? playlistSnapshot.targetDurationUs : (playlistSnapshot.targetDurationUs / 2)); + // Schedule a load if this is the primary playlist and it doesn't have an end tag. Else the + // next load will be scheduled when refreshPlaylist is called, or when this playlist becomes + // the primary. + if (playlistUrl == primaryHlsUrl && !playlistSnapshot.hasEndTag) { + loadPlaylist(); } } - private void blacklistPlaylist() { + /** + * Blacklists the playlist. + * + * @return Whether the playlist is the primary, despite being blacklisted. + */ + private boolean blacklistPlaylist() { blacklistUntilMs = SystemClock.elapsedRealtime() + ChunkedTrackBlacklistUtil.DEFAULT_TRACK_BLACKLIST_MS; notifyPlaylistBlacklisting(playlistUrl, ChunkedTrackBlacklistUtil.DEFAULT_TRACK_BLACKLIST_MS); + return primaryHlsUrl == playlistUrl && !maybeSelectNewPrimaryUrl(); } } From 51fd3365bb325d9580e7034ede488c80528a56fe Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 17 Oct 2017 17:57:27 +0100 Subject: [PATCH 0545/2472] Remove spurious method --- .../exoplayer2/source/dash/DefaultDashChunkSource.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/library/dash/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 index ec1c90bf75..b715b487b9 100644 --- a/library/dash/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 @@ -319,11 +319,6 @@ public class DefaultDashChunkSource implements DashChunkSource { } } - private long resolveTimeToLiveEdgeUs(long playbackPositionUs) { - boolean resolveTimeToLiveEdgePossible = manifest.dynamic && liveEdgeTimeUs != C.TIME_UNSET; - return resolveTimeToLiveEdgePossible ? liveEdgeTimeUs - playbackPositionUs : C.TIME_UNSET; - } - protected static Chunk newInitializationChunk(RepresentationHolder representationHolder, DataSource dataSource, Format trackFormat, int trackSelectionReason, Object trackSelectionData, RangedUri initializationUri, RangedUri indexUri) { From d0758c93938c6b85fac3ef98c634cb6dec90eb48 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Mon, 16 Oct 2017 08:30:55 -0700 Subject: [PATCH 0546/2472] Fix typo ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=172328148 --- .../com/google/android/exoplayer2/text/cea/Cea608Decoder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/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 index fe9a5fbc5c..e2c592be6b 100644 --- a/library/core/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 @@ -350,7 +350,7 @@ public final class Cea608Decoder extends CeaDecoder { repeatableControlSet = false; return true; } else { - // This is a repeatable command, but we haven't see it yet, so set the repeabable control + // This is a repeatable command, but we haven't see it yet, so set the repeatable control // flag (to ensure we ignore the next one should it be a duplicate) and continue processing // the command. repeatableControlSet = true; From 93563631175d5c2b1856f932b457d94e2ce82c6e Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 16 Oct 2017 08:46:54 -0700 Subject: [PATCH 0547/2472] Document load() exceptions ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=172329677 --- .../java/com/google/android/exoplayer2/upstream/Loader.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/Loader.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/Loader.java index 02e9a32116..bd70150573 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/Loader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/Loader.java @@ -61,8 +61,8 @@ public final class Loader implements LoaderErrorThrower { /** * Performs the load, returning on completion or cancellation. * - * @throws IOException - * @throws InterruptedException + * @throws IOException If the input could not be loaded. + * @throws InterruptedException If the thread was interrupted. */ void load() throws IOException, InterruptedException; From e46a7600cf593280ca091423fbdfed4488f7ee12 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 16 Oct 2017 10:04:52 -0700 Subject: [PATCH 0548/2472] Document DefaultTimeBar attributes ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=172339124 --- .../android/exoplayer2/ui/DefaultTimeBar.java | 135 +++++++++++++++--- 1 file changed, 119 insertions(+), 16 deletions(-) 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 index 8fe8dbfa5d..d8617888ce 100644 --- 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 @@ -42,13 +42,124 @@ import java.util.Locale; /** * A time bar that shows a current position, buffered position, duration and ad markers. + *

          + * A DefaultTimeBar can be customized by setting attributes, as outlined below. + * + *

          Attributes

          + * The following attributes can be set on a DefaultTimeBar when used in a layout XML file: + *

          + *

            + *
          • {@code bar_height} - Dimension for the height of the time bar. + *
              + *
            • Default: {@link #DEFAULT_BAR_HEIGHT_DP}
            • + *
            + *
          • + *
          • {@code touch_target_height} - Dimension for the height of the area in which touch + * interactions with the time bar are handled. If no height is specified, this also determines + * the height of the view. + *
              + *
            • Default: {@link #DEFAULT_TOUCH_TARGET_HEIGHT_DP}
            • + *
            + *
          • + *
          • {@code ad_marker_width} - Dimension for the width of any ad markers shown on the + * bar. Ad markers are superimposed on the time bar to show the times at which ads will play. + *
              + *
            • Default: {@link #DEFAULT_AD_MARKER_WIDTH_DP}
            • + *
            + *
          • + *
          • {@code scrubber_enabled_size} - Dimension for the diameter of the circular scrubber + * handle when scrubbing is enabled but not in progress. Set to zero if no scrubber handle + * should be shown. + *
              + *
            • Default: {@link #DEFAULT_SCRUBBER_ENABLED_SIZE_DP}
            • + *
            + *
          • + *
          • {@code scrubber_disabled_size} - Dimension for the diameter of the circular scrubber + * handle when scrubbing isn't enabled. Set to zero if no scrubber handle should be shown. + *
              + *
            • Default: {@link #DEFAULT_SCRUBBER_DISABLED_SIZE_DP}
            • + *
            + *
          • + *
          • {@code scrubber_dragged_size} - Dimension for the diameter of the circular scrubber + * handle when scrubbing is in progress. Set to zero if no scrubber handle should be shown. + *
              + *
            • Default: {@link #DEFAULT_SCRUBBER_DRAGGED_SIZE_DP}
            • + *
            + *
          • + *
          • {@code played_color} - Color for the portion of the time bar representing media + * before the current playback position. + *
              + *
            • Default: {@link #DEFAULT_PLAYED_COLOR}
            • + *
            + *
          • + *
          • {@code scrubber_color} - Color for the scrubber handle. + *
              + *
            • Default: see {@link #getDefaultScrubberColor(int)}
            • + *
            + *
          • + *
          • {@code buffered_color} - Color for the portion of the time bar after the current + * played position up to the current buffered position. + *
              + *
            • Default: see {@link #getDefaultBufferedColor(int)}
            • + *
            + *
          • + *
          • {@code unplayed_color} - Color for the portion of the time bar after the current + * buffered position. + *
              + *
            • Default: see {@link #getDefaultUnplayedColor(int)}
            • + *
            + *
          • + *
          • {@code ad_marker_color} - Color for unplayed ad markers. + *
              + *
            • Default: {@link #DEFAULT_AD_MARKER_COLOR}
            • + *
            + *
          • + *
          • {@code played_ad_marker_color} - Color for played ad markers. + *
              + *
            • Default: see {@link #getDefaultPlayedAdMarkerColor(int)}
            • + *
            + *
          • + *
          */ public class DefaultTimeBar extends View implements TimeBar { + /** + * Default height for the time bar, in dp. + */ + public static final int DEFAULT_BAR_HEIGHT_DP = 4; + /** + * Default height for the touch target, in dp. + */ + public static final int DEFAULT_TOUCH_TARGET_HEIGHT_DP = 26; + /** + * Default width for ad markers, in dp. + */ + public static final int DEFAULT_AD_MARKER_WIDTH_DP = 4; + /** + * Default diameter for the scrubber when enabled, in dp. + */ + public static final int DEFAULT_SCRUBBER_ENABLED_SIZE_DP = 12; + /** + * Default diameter for the scrubber when disabled, in dp. + */ + public static final int DEFAULT_SCRUBBER_DISABLED_SIZE_DP = 0; + /** + * Default diameter for the scrubber when dragged, in dp. + */ + public static final int DEFAULT_SCRUBBER_DRAGGED_SIZE_DP = 16; + /** + * Default color for the played portion of the time bar. + */ + public static final int DEFAULT_PLAYED_COLOR = 0xFFFFFFFF; + /** + * Default color for ad markers. + */ + public static final int DEFAULT_AD_MARKER_COLOR = 0xB2FFFF00; + /** * The threshold in dps above the bar at which touch events trigger fine scrub mode. */ - private static final int FINE_SCRUB_Y_THRESHOLD = -50; + private static final int FINE_SCRUB_Y_THRESHOLD_DP = -50; /** * The ratio by which times are reduced in fine scrub mode. */ @@ -59,14 +170,6 @@ public class DefaultTimeBar extends View implements TimeBar { */ 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 = 0xFFFFFFFF; - 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 final Rect seekBounds; private final Rect progressBar; @@ -126,13 +229,13 @@ public class DefaultTimeBar extends View implements TimeBar { // 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); + fineScrubYThreshold = dpToPx(displayMetrics, FINE_SCRUB_Y_THRESHOLD_DP); + int defaultBarHeight = dpToPx(displayMetrics, DEFAULT_BAR_HEIGHT_DP); + int defaultTouchTargetHeight = dpToPx(displayMetrics, DEFAULT_TOUCH_TARGET_HEIGHT_DP); + int defaultAdMarkerWidth = dpToPx(displayMetrics, DEFAULT_AD_MARKER_WIDTH_DP); + int defaultScrubberEnabledSize = dpToPx(displayMetrics, DEFAULT_SCRUBBER_ENABLED_SIZE_DP); + int defaultScrubberDisabledSize = dpToPx(displayMetrics, DEFAULT_SCRUBBER_DISABLED_SIZE_DP); + int defaultScrubberDraggedSize = dpToPx(displayMetrics, DEFAULT_SCRUBBER_DRAGGED_SIZE_DP); if (attrs != null) { TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.DefaultTimeBar, 0, 0); From ac31dc7c7a83a0cac7314e50369865006ae91359 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 17 Oct 2017 01:00:13 -0700 Subject: [PATCH 0549/2472] Allow setting output sample rate in SonicAudioProcessor This is not really useful with the DefaultAudioSink, but could be used in a custom AudioSink when mixing audio from sources that have different sample rates. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=172434482 --- .../exoplayer2/ext/gvr/GvrAudioProcessor.java | 5 + .../exoplayer2/audio/AudioProcessor.java | 32 +++- .../android/exoplayer2/audio/AudioSink.java | 18 +-- .../audio/ChannelMappingAudioProcessor.java | 5 + .../exoplayer2/audio/DefaultAudioSink.java | 43 +++--- .../audio/ResamplingAudioProcessor.java | 5 + .../android/exoplayer2/audio/Sonic.java | 64 +++----- .../exoplayer2/audio/SonicAudioProcessor.java | 65 +++++++-- .../audio/TrimmingAudioProcessor.java | 8 + .../audio/SonicAudioProcessorTest.java | 137 ++++++++++++++++++ 10 files changed, 292 insertions(+), 90 deletions(-) create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/audio/SonicAudioProcessorTest.java 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 5750f5f04d..8d71f551cd 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 @@ -138,6 +138,11 @@ public final class GvrAudioProcessor implements AudioProcessor { return C.ENCODING_PCM_16BIT; } + @Override + public int getOutputSampleRateHz() { + return sampleRateHz; + } + @Override public void queueInput(ByteBuffer input) { int position = input.position(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioProcessor.java index eced040812..8a3d624222 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioProcessor.java @@ -20,7 +20,15 @@ import java.nio.ByteBuffer; import java.nio.ByteOrder; /** - * Interface for audio processors. + * Interface for audio processors, which take audio data as input and transform it, potentially + * modifying its channel count, encoding and/or sample rate. + *

          + * Call {@link #configure(int, int, int)} to configure the processor to receive input audio, then + * call {@link #isActive()} to determine whether the processor is active. + * {@link #queueInput(ByteBuffer)}, {@link #queueEndOfStream()}, {@link #getOutput()}, + * {@link #isEnded()}, {@link #getOutputChannelCount()}, {@link #getOutputEncoding()} and + * {@link #getOutputSampleRateHz()} may only be called if the processor is active. Call + * {@link #reset()} to reset the processor to its unconfigured state. */ public interface AudioProcessor { @@ -46,8 +54,9 @@ public interface AudioProcessor { * method, {@link #isActive()} returns whether the processor needs to handle buffers; if not, the * processor will not accept any buffers until it is reconfigured. Returns {@code true} if the * processor must be flushed, or if the value returned by {@link #isActive()} has changed as a - * result of the call. If it's active, {@link #getOutputChannelCount()} and - * {@link #getOutputEncoding()} return the processor's output format. + * result of the call. If it's active, {@link #getOutputSampleRateHz()}, + * {@link #getOutputChannelCount()} and {@link #getOutputEncoding()} return the processor's output + * format. * * @param sampleRateHz The sample rate of input audio in Hz. * @param channelCount The number of interleaved channels in input audio. @@ -65,16 +74,27 @@ public interface AudioProcessor { boolean isActive(); /** - * Returns the number of audio channels in the data output by the processor. + * Returns the number of audio channels in the data output by the processor. The value may change + * as a result of calling {@link #configure(int, int, int)} and is undefined if the instance is + * not active. */ int getOutputChannelCount(); /** - * Returns the audio encoding used in the data output by the processor. + * Returns the audio encoding used in the data output by the processor. The value may change as a + * result of calling {@link #configure(int, int, int)} and is undefined if the instance is not + * active. */ @C.Encoding int getOutputEncoding(); + /** + * Returns the sample rate of audio output by the processor, in hertz. The value may change as a + * result of calling {@link #configure(int, int, int)} and is undefined if the instance is not + * active. + */ + int getOutputSampleRateHz(); + /** * Queues audio data between the position and limit of the input {@code buffer} for processing. * {@code buffer} must be a direct byte buffer with native byte order. Its contents are treated as @@ -116,7 +136,7 @@ public interface AudioProcessor { void flush(); /** - * Resets the processor to its initial state. + * Resets the processor to its unconfigured state. */ void reset(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java index 879769b0e2..8b39a28182 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java @@ -186,12 +186,12 @@ public interface AudioSink { /** * Configures (or reconfigures) the sink. * - * @param mimeType The MIME type of audio data provided in the input buffers. - * @param channelCount The number of channels. - * @param sampleRate The sample rate in Hz. - * @param pcmEncoding For PCM formats, the encoding used. One of {@link C#ENCODING_PCM_16BIT}, - * {@link C#ENCODING_PCM_16BIT}, {@link C#ENCODING_PCM_24BIT} and - * {@link C#ENCODING_PCM_32BIT}. + * @param inputMimeType The MIME type of audio data provided in the input buffers. + * @param inputChannelCount The number of channels. + * @param inputSampleRate The sample rate in Hz. + * @param inputPcmEncoding For PCM formats, the encoding used. One of + * {@link C#ENCODING_PCM_16BIT}, {@link C#ENCODING_PCM_16BIT}, {@link C#ENCODING_PCM_24BIT} + * and {@link C#ENCODING_PCM_32BIT}. * @param specifiedBufferSize A specific size for the playback buffer in bytes, or 0 to infer a * suitable buffer size. * @param outputChannels A mapping from input to output channels that is applied to this sink's @@ -206,9 +206,9 @@ public interface AudioSink { * {@link #configure(String, int, int, int, int, int[], int, int)}. * @throws ConfigurationException If an error occurs configuring the sink. */ - void configure(String mimeType, int channelCount, int sampleRate, @C.PcmEncoding int pcmEncoding, - int specifiedBufferSize, @Nullable int[] outputChannels, int trimStartSamples, - int trimEndSamples) throws ConfigurationException; + void configure(String inputMimeType, int inputChannelCount, int inputSampleRate, + @C.PcmEncoding int inputPcmEncoding, int specifiedBufferSize, @Nullable int[] outputChannels, + int trimStartSamples, int trimEndSamples) throws ConfigurationException; /** * Starts or resumes consuming audio if initialized. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java index 03bbd5817b..c3f3e32526 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java @@ -103,6 +103,11 @@ import java.util.Arrays; return C.ENCODING_PCM_16BIT; } + @Override + public int getOutputSampleRateHz() { + return sampleRateHz; + } + @Override public void queueInput(ByteBuffer inputBuffer) { int position = inputBuffer.position(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java index 1cafdc5efe..a5173db284 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java @@ -148,13 +148,6 @@ public final class DefaultAudioSink implements AudioSink { 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 @@ -189,6 +182,7 @@ public final class DefaultAudioSink implements AudioSink { */ private AudioTrack keepSessionIdAudioTrack; private AudioTrack audioTrack; + private int inputSampleRate; private int sampleRate; private int channelConfig; private @C.Encoding int encoding; @@ -337,14 +331,18 @@ public final class DefaultAudioSink implements AudioSink { } @Override - public void configure(String mimeType, int channelCount, int sampleRate, - @C.PcmEncoding int pcmEncoding, int specifiedBufferSize, @Nullable int[] outputChannels, + public void configure(String inputMimeType, int inputChannelCount, int inputSampleRate, + @C.PcmEncoding int inputPcmEncoding, int specifiedBufferSize, @Nullable int[] outputChannels, int trimStartSamples, int trimEndSamples) throws ConfigurationException { - boolean passthrough = !MimeTypes.AUDIO_RAW.equals(mimeType); - @C.Encoding int encoding = passthrough ? getEncodingForMimeType(mimeType) : pcmEncoding; + this.inputSampleRate = inputSampleRate; + int channelCount = inputChannelCount; + int sampleRate = inputSampleRate; + @C.Encoding int encoding; + boolean passthrough = !MimeTypes.AUDIO_RAW.equals(inputMimeType); boolean flush = false; if (!passthrough) { - pcmFrameSize = Util.getPcmFrameSize(pcmEncoding, channelCount); + encoding = inputPcmEncoding; + pcmFrameSize = Util.getPcmFrameSize(inputPcmEncoding, channelCount); trimmingAudioProcessor.setTrimSampleCount(trimStartSamples, trimEndSamples); channelMappingAudioProcessor.setChannelMap(outputChannels); for (AudioProcessor audioProcessor : availableAudioProcessors) { @@ -355,12 +353,15 @@ public final class DefaultAudioSink implements AudioSink { } if (audioProcessor.isActive()) { channelCount = audioProcessor.getOutputChannelCount(); + sampleRate = audioProcessor.getOutputSampleRateHz(); encoding = audioProcessor.getOutputEncoding(); } } if (flush) { resetAudioProcessors(); } + } else { + encoding = getEncodingForMimeType(inputMimeType); } int channelConfig; @@ -598,8 +599,8 @@ public final class DefaultAudioSink implements AudioSink { startMediaTimeState = START_IN_SYNC; } else { // Sanity check that presentationTimeUs is consistent with the expected value. - long expectedPresentationTimeUs = startMediaTimeUs - + framesToDurationUs(getSubmittedFrames()); + long expectedPresentationTimeUs = + startMediaTimeUs + inputFramesToDurationUs(getSubmittedFrames()); if (startMediaTimeState == START_IN_SYNC && Math.abs(expectedPresentationTimeUs - presentationTimeUs) > 200000) { Log.e(TAG, "Discontinuity detected [expected " + expectedPresentationTimeUs + ", got " @@ -997,15 +998,11 @@ public final class DefaultAudioSink implements AudioSink { return positionUs + playbackParametersOffsetUs - playbackParametersPositionUs; } - if (playbackParametersCheckpoints.isEmpty() - && sonicAudioProcessor.getOutputByteCount() >= SONIC_MIN_BYTES_FOR_SPEEDUP) { + if (playbackParametersCheckpoints.isEmpty()) { return playbackParametersOffsetUs - + Util.scaleLargeTimestamp(positionUs - playbackParametersPositionUs, - sonicAudioProcessor.getInputByteCount(), sonicAudioProcessor.getOutputByteCount()); + + sonicAudioProcessor.scaleDurationForSpeedup(positionUs - playbackParametersPositionUs); } - - // 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. + // We are playing data at a previous playback speed, so fall back to multiplying by the speed. return playbackParametersOffsetUs + (long) ((double) playbackParameters.speed * (positionUs - playbackParametersPositionUs)); } @@ -1098,6 +1095,10 @@ public final class DefaultAudioSink implements AudioSink { return audioTrack != null; } + private long inputFramesToDurationUs(long frameCount) { + return (frameCount * C.MICROS_PER_SECOND) / inputSampleRate; + } + private long framesToDurationUs(long frameCount) { return (frameCount * C.MICROS_PER_SECOND) / sampleRate; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java index 0dd062150d..a78adbcee3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java @@ -79,6 +79,11 @@ import java.nio.ByteOrder; return C.ENCODING_PCM_16BIT; } + @Override + public int getOutputSampleRateHz() { + return sampleRateHz; + } + @Override public void queueInput(ByteBuffer inputBuffer) { // Prepare the output buffer. 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 index 5c5ac06da3..dfd122c397 100644 --- 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 @@ -32,8 +32,11 @@ import java.util.Arrays; private static final int MAXIMUM_PITCH = 400; private static final int AMDF_FREQUENCY = 4000; - private final int sampleRate; + private final int inputSampleRateHz; private final int numChannels; + private final float speed; + private final float pitch; + private final float rate; private final int minPeriod; private final int maxPeriod; private final int maxRequired; @@ -47,8 +50,6 @@ import java.util.Arrays; private short[] pitchBuffer; private int oldRatePosition; private int newRatePosition; - private float speed; - private float pitch; private int numInputSamples; private int numOutputSamples; private int numPitchSamples; @@ -61,14 +62,18 @@ import java.util.Arrays; /** * Creates a new Sonic audio stream processor. * - * @param sampleRate The sample rate of input audio. + * @param inputSampleRateHz The sample rate of input audio, in hertz. * @param numChannels The number of channels in the input audio. + * @param speed The speedup factor for output audio. + * @param pitch The pitch factor for output audio. + * @param outputSampleRateHz The sample rate for output audio, in hertz. */ - public Sonic(int sampleRate, int numChannels) { - this.sampleRate = sampleRate; + public Sonic(int inputSampleRateHz, int numChannels, float speed, float pitch, + int outputSampleRateHz) { + this.inputSampleRateHz = inputSampleRateHz; this.numChannels = numChannels; - minPeriod = sampleRate / MAXIMUM_PITCH; - maxPeriod = sampleRate / MINIMUM_PITCH; + minPeriod = inputSampleRateHz / MAXIMUM_PITCH; + maxPeriod = inputSampleRateHz / MINIMUM_PITCH; maxRequired = 2 * maxPeriod; downSampleBuffer = new short[maxRequired]; inputBufferSize = maxRequired; @@ -80,36 +85,9 @@ import java.util.Arrays; 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; + this.rate = (float) inputSampleRateHz / outputSampleRateHz; } /** @@ -148,8 +126,9 @@ import java.util.Arrays; public void queueEndOfStream() { int remainingSamples = numInputSamples; float s = speed / pitch; + float r = rate * pitch; int expectedOutputSamples = - numOutputSamples + (int) ((remainingSamples / s + numPitchSamples) / pitch + 0.5f); + numOutputSamples + (int) ((remainingSamples / s + numPitchSamples) / r + 0.5f); // Add enough silence to flush both input and pitch buffers. enlargeInputBufferIfNeeded(remainingSamples + 2 * maxRequired); @@ -292,7 +271,7 @@ import java.util.Arrays; // sampling. int period; int retPeriod; - int skip = sampleRate > AMDF_FREQUENCY ? sampleRate / AMDF_FREQUENCY : 1; + int skip = inputSampleRateHz > AMDF_FREQUENCY ? inputSampleRateHz / AMDF_FREQUENCY : 1; if (numChannels == 1 && skip == 1) { period = findPitchPeriodInRange(samples, position, minPeriod, maxPeriod); } else { @@ -388,8 +367,8 @@ import java.util.Arrays; if (numOutputSamples == originalNumOutputSamples) { return; } - int newSampleRate = (int) (sampleRate / rate); - int oldSampleRate = sampleRate; + int newSampleRate = (int) (inputSampleRateHz / rate); + int oldSampleRate = inputSampleRateHz; // Set these values to help with the integer math. while (newSampleRate > (1 << 14) || oldSampleRate > (1 << 14)) { newSampleRate /= 2; @@ -476,6 +455,7 @@ import java.util.Arrays; // Resample as many pitch periods as we have buffered on the input. int originalNumOutputSamples = numOutputSamples; float s = speed / pitch; + float r = rate * pitch; if (s > 1.00001 || s < 0.99999) { changeSpeed(s); } else { @@ -486,8 +466,8 @@ import java.util.Arrays; if (pitch != 1.0f) { adjustPitch(originalNumOutputSamples); } - } else if (!USE_CHORD_PITCH && pitch != 1.0f) { - adjustRate(pitch, originalNumOutputSamples); + } else if (r != 1.0f) { + adjustRate(r, originalNumOutputSamples); } } 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 index df20139255..370ddb2809 100644 --- 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 @@ -24,7 +24,7 @@ import java.nio.ByteOrder; import java.nio.ShortBuffer; /** - * An {@link AudioProcessor} that uses the Sonic library to modify the speed/pitch of audio. + * An {@link AudioProcessor} that uses the Sonic library to modify audio speed/pitch/sample rate. */ public final class SonicAudioProcessor implements AudioProcessor { @@ -44,18 +44,30 @@ public final class SonicAudioProcessor implements AudioProcessor { * The minimum allowed pitch in {@link #setPitch(float)}. */ public static final float MINIMUM_PITCH = 0.1f; + /** + * Indicates that the output sample rate should be the same as the input. + */ + public static final int SAMPLE_RATE_NO_CHANGE = -1; /** * The threshold below which the difference between two pitch/speed factors is negligible. */ private static final float CLOSE_THRESHOLD = 0.01f; + /** + * The minimum number of output bytes at which the speedup is calculated using the input/output + * byte counts, rather than using the current playback parameters speed. + */ + private static final int MIN_BYTES_FOR_SPEEDUP_CALCULATION = 1024; + + private int pendingOutputSampleRateHz; private int channelCount; private int sampleRateHz; private Sonic sonic; private float speed; private float pitch; + private int outputSampleRateHz; private ByteBuffer buffer; private ShortBuffer shortBuffer; @@ -72,9 +84,11 @@ public final class SonicAudioProcessor implements AudioProcessor { pitch = 1f; channelCount = Format.NO_VALUE; sampleRateHz = Format.NO_VALUE; + outputSampleRateHz = Format.NO_VALUE; buffer = EMPTY_BUFFER; shortBuffer = buffer.asShortBuffer(); outputBuffer = EMPTY_BUFFER; + pendingOutputSampleRateHz = SAMPLE_RATE_NO_CHANGE; } /** @@ -100,17 +114,34 @@ public final class SonicAudioProcessor implements AudioProcessor { } /** - * Returns the number of bytes of input queued since the last call to {@link #flush()}. + * Sets the sample rate for output audio, in hertz. Pass {@link #SAMPLE_RATE_NO_CHANGE} to output + * audio at the same sample rate as the input. After calling this method, call + * {@link #configure(int, int, int)} to start using the new sample rate. + * + * @param sampleRateHz The sample rate for output audio, in hertz. + * @see #configure(int, int, int) */ - public long getInputByteCount() { - return inputBytes; + public void setOutputSampleRateHz(int sampleRateHz) { + pendingOutputSampleRateHz = sampleRateHz; } /** - * Returns the number of bytes of output dequeued since the last call to {@link #flush()}. + * Returns the specified duration scaled to take into account the speedup factor of this instance, + * in the same units as {@code duration}. + * + * @param duration The duration to scale taking into account speedup. + * @return The specified duration scaled to take into account speedup, in the same units as + * {@code duration}. */ - public long getOutputByteCount() { - return outputBytes; + public long scaleDurationForSpeedup(long duration) { + if (outputBytes >= MIN_BYTES_FOR_SPEEDUP_CALCULATION) { + return outputSampleRateHz == sampleRateHz + ? Util.scaleLargeTimestamp(duration, inputBytes, outputBytes) + : Util.scaleLargeTimestamp(duration, inputBytes * outputSampleRateHz, + outputBytes * sampleRateHz); + } else { + return (long) ((double) speed * duration); + } } @Override @@ -119,17 +150,22 @@ public final class SonicAudioProcessor implements AudioProcessor { if (encoding != C.ENCODING_PCM_16BIT) { throw new UnhandledFormatException(sampleRateHz, channelCount, encoding); } - if (this.sampleRateHz == sampleRateHz && this.channelCount == channelCount) { + int outputSampleRateHz = pendingOutputSampleRateHz == SAMPLE_RATE_NO_CHANGE + ? sampleRateHz : pendingOutputSampleRateHz; + if (this.sampleRateHz == sampleRateHz && this.channelCount == channelCount + && this.outputSampleRateHz == outputSampleRateHz) { return false; } this.sampleRateHz = sampleRateHz; this.channelCount = channelCount; + this.outputSampleRateHz = outputSampleRateHz; return true; } @Override public boolean isActive() { - return Math.abs(speed - 1f) >= CLOSE_THRESHOLD || Math.abs(pitch - 1f) >= CLOSE_THRESHOLD; + return Math.abs(speed - 1f) >= CLOSE_THRESHOLD || Math.abs(pitch - 1f) >= CLOSE_THRESHOLD + || outputSampleRateHz != sampleRateHz; } @Override @@ -142,6 +178,11 @@ public final class SonicAudioProcessor implements AudioProcessor { return C.ENCODING_PCM_16BIT; } + @Override + public int getOutputSampleRateHz() { + return outputSampleRateHz; + } + @Override public void queueInput(ByteBuffer inputBuffer) { if (inputBuffer.hasRemaining()) { @@ -187,9 +228,7 @@ public final class SonicAudioProcessor implements AudioProcessor { @Override public void flush() { - sonic = new Sonic(sampleRateHz, channelCount); - sonic.setSpeed(speed); - sonic.setPitch(pitch); + sonic = new Sonic(sampleRateHz, channelCount, speed, pitch, outputSampleRateHz); outputBuffer = EMPTY_BUFFER; inputBytes = 0; outputBytes = 0; @@ -204,9 +243,11 @@ public final class SonicAudioProcessor implements AudioProcessor { outputBuffer = EMPTY_BUFFER; channelCount = Format.NO_VALUE; sampleRateHz = Format.NO_VALUE; + outputSampleRateHz = Format.NO_VALUE; inputBytes = 0; outputBytes = 0; inputEnded = false; + pendingOutputSampleRateHz = SAMPLE_RATE_NO_CHANGE; } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/TrimmingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/TrimmingAudioProcessor.java index 9338c24b76..068a867f06 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/TrimmingAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/TrimmingAudioProcessor.java @@ -31,6 +31,7 @@ import java.nio.ByteOrder; private int trimStartSamples; private int trimEndSamples; private int channelCount; + private int sampleRateHz; private int pendingTrimStartBytes; private ByteBuffer buffer; @@ -69,6 +70,7 @@ import java.nio.ByteOrder; throw new UnhandledFormatException(sampleRateHz, channelCount, encoding); } this.channelCount = channelCount; + this.sampleRateHz = sampleRateHz; endBuffer = new byte[trimEndSamples * channelCount * 2]; endBufferSize = 0; pendingTrimStartBytes = trimStartSamples * channelCount * 2; @@ -92,6 +94,11 @@ import java.nio.ByteOrder; return C.ENCODING_PCM_16BIT; } + @Override + public int getOutputSampleRateHz() { + return sampleRateHz; + } + @Override public void queueInput(ByteBuffer inputBuffer) { int position = inputBuffer.position(); @@ -174,6 +181,7 @@ import java.nio.ByteOrder; flush(); buffer = EMPTY_BUFFER; channelCount = Format.NO_VALUE; + sampleRateHz = Format.NO_VALUE; endBuffer = null; } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/audio/SonicAudioProcessorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/audio/SonicAudioProcessorTest.java new file mode 100644 index 0000000000..a4f02f8257 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/audio/SonicAudioProcessorTest.java @@ -0,0 +1,137 @@ +/* + * 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 static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + +import com.google.android.exoplayer2.C; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Unit test for {@link SonicAudioProcessor}. + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class SonicAudioProcessorTest { + + private SonicAudioProcessor sonicAudioProcessor; + + @Before + public void setUp() { + sonicAudioProcessor = new SonicAudioProcessor(); + } + + @Test + public void testReconfigureWithSameSampleRate() throws Exception { + // When configured for resampling from 44.1 kHz to 48 kHz, the output sample rate is correct. + sonicAudioProcessor.setOutputSampleRateHz(48000); + sonicAudioProcessor.configure(44100, 2, C.ENCODING_PCM_16BIT); + assertThat(sonicAudioProcessor.getOutputSampleRateHz()).isEqualTo(48000); + assertThat(sonicAudioProcessor.isActive()).isTrue(); + // When reconfigured with 48 kHz input, there is no resampling. + sonicAudioProcessor.configure(48000, 2, C.ENCODING_PCM_16BIT); + assertThat(sonicAudioProcessor.getOutputSampleRateHz()).isEqualTo(48000); + assertThat(sonicAudioProcessor.isActive()).isFalse(); + // When reconfigure with 44.1 kHz input, resampling is enabled again. + sonicAudioProcessor.configure(44100, 2, C.ENCODING_PCM_16BIT); + assertThat(sonicAudioProcessor.getOutputSampleRateHz()).isEqualTo(48000); + assertThat(sonicAudioProcessor.isActive()).isTrue(); + } + + @Test + public void testNoSampleRateChange() throws Exception { + // Configure for resampling 44.1 kHz to 48 kHz. + sonicAudioProcessor.setOutputSampleRateHz(48000); + sonicAudioProcessor.configure(44100, 2, C.ENCODING_PCM_16BIT); + // Reconfigure to not modify the sample rate. + sonicAudioProcessor.setOutputSampleRateHz(SonicAudioProcessor.SAMPLE_RATE_NO_CHANGE); + sonicAudioProcessor.configure(22050, 2, C.ENCODING_PCM_16BIT); + // The sample rate is unmodified, and the audio processor is not active. + assertThat(sonicAudioProcessor.getOutputSampleRateHz()).isEqualTo(22050); + assertThat(sonicAudioProcessor.isActive()).isFalse(); + } + + @Test + public void testBecomesActiveAfterConfigure() throws Exception { + sonicAudioProcessor.configure(44100, 2, C.ENCODING_PCM_16BIT); + // Set a new sample rate. + sonicAudioProcessor.setOutputSampleRateHz(22050); + // The new sample rate is not active yet. + assertThat(sonicAudioProcessor.isActive()).isFalse(); + assertThat(sonicAudioProcessor.getOutputSampleRateHz()).isEqualTo(44100); + } + + @Test + public void testSampleRateChangeBecomesActiveAfterConfigure() throws Exception { + // Configure for resampling 44.1 kHz to 48 kHz. + sonicAudioProcessor.setOutputSampleRateHz(48000); + sonicAudioProcessor.configure(44100, 2, C.ENCODING_PCM_16BIT); + // Set a new sample rate, which isn't active yet. + sonicAudioProcessor.setOutputSampleRateHz(22050); + assertThat(sonicAudioProcessor.getOutputSampleRateHz()).isEqualTo(48000); + // The new sample rate takes effect on reconfiguration. + sonicAudioProcessor.configure(44100, 2, C.ENCODING_PCM_16BIT); + assertThat(sonicAudioProcessor.getOutputSampleRateHz()).isEqualTo(22050); + } + + @Test + public void testIsActiveWithSpeedChange() throws Exception { + sonicAudioProcessor.setSpeed(1.5f); + sonicAudioProcessor.configure(44100, 2, C.ENCODING_PCM_16BIT); + assertThat(sonicAudioProcessor.isActive()).isTrue(); + } + + @Test + public void testIsActiveWithPitchChange() throws Exception { + sonicAudioProcessor.setPitch(1.5f); + sonicAudioProcessor.configure(44100, 2, C.ENCODING_PCM_16BIT); + assertThat(sonicAudioProcessor.isActive()).isTrue(); + } + + @Test + public void testIsNotActiveWithNoChange() throws Exception { + sonicAudioProcessor.configure(44100, 2, C.ENCODING_PCM_16BIT); + assertThat(sonicAudioProcessor.isActive()).isFalse(); + } + + @Test + public void testDoesNotSupportNon16BitInput() throws Exception { + try { + sonicAudioProcessor.configure(44100, 2, C.ENCODING_PCM_8BIT); + fail(); + } catch (AudioProcessor.UnhandledFormatException e) { + // Expected. + } + try { + sonicAudioProcessor.configure(44100, 2, C.ENCODING_PCM_24BIT); + fail(); + } catch (AudioProcessor.UnhandledFormatException e) { + // Expected. + } + try { + sonicAudioProcessor.configure(44100, 2, C.ENCODING_PCM_32BIT); + fail(); + } catch (AudioProcessor.UnhandledFormatException e) { + // Expected. + } + } + +} From f6821c6d9597389e71a61b3722f2b3d6a1d43b3e Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 17 Oct 2017 03:48:15 -0700 Subject: [PATCH 0550/2472] Split playbacktests mobile_test into multiple version-dependent test targets. MobileHarness allocates random devices for each test and repeats tests up to 5 times to account for spurious test failures. Some of our tests automatically pass for SDK versions below a certain threshold. Thus, even if such a version-guarded test would always fail, the MobileHarness test is likely to succeed as it only needs one random allocation to a device with a lower SDK version. To prevent this behaviour and to make sure all tests are actually run, the mobile_test target is split into multiple targets one for each minimum SDK version. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=172447046 --- .../gts/CommonEncryptionDrmTest.java | 8 +- .../playbacktests/gts/DashStreamingTest.java | 111 +++++++++++------- .../gts/DashWidevineOfflineTest.java | 8 +- 3 files changed, 75 insertions(+), 52 deletions(-) diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/CommonEncryptionDrmTest.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/CommonEncryptionDrmTest.java index a590e45f5f..a4cd35911b 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/CommonEncryptionDrmTest.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/CommonEncryptionDrmTest.java @@ -67,7 +67,7 @@ public final class CommonEncryptionDrmTest extends ActivityInstrumentationTestCa super.tearDown(); } - public void testCencSchemeType() { + public void testCencSchemeTypeV18() { if (Util.SDK_INT < 18) { // Pass. return; @@ -75,7 +75,7 @@ public final class CommonEncryptionDrmTest extends ActivityInstrumentationTestCa testRunner.setStreamName("test_widevine_h264_scheme_cenc").setManifestUrl(URL_cenc).run(); } - public void testCbc1SchemeType() { + public void testCbc1SchemeTypeV25() { if (Util.SDK_INT < 25) { // cbc1 support was added in API 24, but it is stable from API 25 onwards. // See [internal: b/65634809]. @@ -85,7 +85,7 @@ public final class CommonEncryptionDrmTest extends ActivityInstrumentationTestCa testRunner.setStreamName("test_widevine_h264_scheme_cbc1").setManifestUrl(URL_cbc1).run(); } - public void testCbcsSchemeType() { + public void testCbcsSchemeTypeV25() { if (Util.SDK_INT < 25) { // cbcs support was added in API 24, but it is stable from API 25 onwards. // See [internal: b/65634809]. @@ -95,7 +95,7 @@ public final class CommonEncryptionDrmTest extends ActivityInstrumentationTestCa testRunner.setStreamName("test_widevine_h264_scheme_cbcs").setManifestUrl(URL_cbcs).run(); } - public void testCensSchemeType() { + public void testCensSchemeTypeV25() { // TODO: Implement once content is available. Track [internal: b/31219813]. } diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashStreamingTest.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashStreamingTest.java index 39cdc7ee43..3748779b9d 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashStreamingTest.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashStreamingTest.java @@ -162,7 +162,7 @@ public final class DashStreamingTest extends ActivityInstrumentationTestCase2 Date: Tue, 17 Oct 2017 03:49:49 -0700 Subject: [PATCH 0551/2472] We're not playing an ad if the timeline is empty. Issue: #3334 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=172447125 --- .../java/com/google/android/exoplayer2/ExoPlayerImpl.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index 2222660469..6bd6cd4795 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -386,17 +386,17 @@ import java.util.concurrent.CopyOnWriteArraySet; @Override public boolean isPlayingAd() { - return pendingSeekAcks == 0 && playbackInfo.periodId.isAd(); + return !timeline.isEmpty() && pendingSeekAcks == 0 && playbackInfo.periodId.isAd(); } @Override public int getCurrentAdGroupIndex() { - return pendingSeekAcks == 0 ? playbackInfo.periodId.adGroupIndex : C.INDEX_UNSET; + return isPlayingAd() ? playbackInfo.periodId.adGroupIndex : C.INDEX_UNSET; } @Override public int getCurrentAdIndexInAdGroup() { - return pendingSeekAcks == 0 ? playbackInfo.periodId.adIndexInAdGroup : C.INDEX_UNSET; + return isPlayingAd() ? playbackInfo.periodId.adIndexInAdGroup : C.INDEX_UNSET; } @Override From f9249d23eac4d9143f99f2fe5066b93fc96ceb88 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 17 Oct 2017 07:33:19 -0700 Subject: [PATCH 0552/2472] Add an extractor flag for ignoring edit lists Issue: #3358 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=172464053 --- .../extractor/DefaultExtractorsFactory.java | 15 +++++++++- .../exoplayer2/extractor/mp4/AtomParsers.java | 16 +++++++--- .../extractor/mp4/FragmentedMp4Extractor.java | 8 +++-- .../extractor/mp4/Mp4Extractor.java | 30 ++++++++++++++++++- .../exoplayer2/extractor/mp4/Track.java | 6 ++-- 5 files changed, 64 insertions(+), 11 deletions(-) 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 index ccc5c0eb3e..87165e7a9b 100644 --- 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 @@ -67,6 +67,7 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory { } private @MatroskaExtractor.Flags int matroskaFlags; + private @Mp4Extractor.Flags int mp4Flags; private @FragmentedMp4Extractor.Flags int fragmentedMp4Flags; private @Mp3Extractor.Flags int mp3Flags; private @TsExtractor.Mode int tsMode; @@ -89,6 +90,18 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory { return this; } + /** + * Sets flags for {@link Mp4Extractor} instances created by the factory. + * + * @see Mp4Extractor#Mp4Extractor(int) + * @param flags The flags to use. + * @return The factory, for convenience. + */ + public synchronized DefaultExtractorsFactory setMp4ExtractorFlags(@Mp4Extractor.Flags int flags) { + this.mp4Flags = flags; + return this; + } + /** * Sets flags for {@link FragmentedMp4Extractor} instances created by the factory. * @@ -145,7 +158,7 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory { 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[2] = new Mp4Extractor(mp4Flags); extractors[3] = new Mp3Extractor(mp3Flags); extractors[4] = new AdtsExtractor(); extractors[5] = new Ac3Extractor(); diff --git a/library/core/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 index 86bdc2b41c..1c4ca995f6 100644 --- a/library/core/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 @@ -60,11 +60,13 @@ import java.util.List; * @param duration The duration in units of the timescale declared in the mvhd atom, or * {@link C#TIME_UNSET} if the duration should be parsed from the tkhd atom. * @param drmInitData {@link DrmInitData} to be included in the format. + * @param ignoreEditLists Whether to ignore any edit lists in the trak box. * @param isQuickTime True for QuickTime media. False otherwise. * @return A {@link Track} instance, or {@code null} if the track's type isn't supported. */ public static Track parseTrak(Atom.ContainerAtom trak, Atom.LeafAtom mvhd, long duration, - DrmInitData drmInitData, boolean isQuickTime) throws ParserException { + DrmInitData drmInitData, boolean ignoreEditLists, boolean isQuickTime) + throws ParserException { Atom.ContainerAtom mdia = trak.getContainerAtomOfType(Atom.TYPE_mdia); int trackType = parseHdlr(mdia.getLeafAtomOfType(Atom.TYPE_hdlr).data); if (trackType == C.TRACK_TYPE_UNKNOWN) { @@ -88,11 +90,17 @@ import java.util.List; Pair mdhdData = parseMdhd(mdia.getLeafAtomOfType(Atom.TYPE_mdhd).data); StsdData stsdData = parseStsd(stbl.getLeafAtomOfType(Atom.TYPE_stsd).data, tkhdData.id, tkhdData.rotationDegrees, mdhdData.second, drmInitData, isQuickTime); - Pair edtsData = parseEdts(trak.getContainerAtomOfType(Atom.TYPE_edts)); + long[] editListDurations = null; + long[] editListMediaTimes = null; + if (!ignoreEditLists) { + Pair edtsData = parseEdts(trak.getContainerAtomOfType(Atom.TYPE_edts)); + editListDurations = edtsData.first; + editListMediaTimes = edtsData.second; + } return stsdData.format == null ? null : new Track(tkhdData.id, trackType, mdhdData.first, movieTimescale, durationUs, stsdData.format, stsdData.requiredSampleTransformation, stsdData.trackEncryptionBoxes, - stsdData.nalUnitLengthFieldLength, edtsData.first, edtsData.second); + stsdData.nalUnitLengthFieldLength, editListDurations, editListMediaTimes); } /** @@ -783,7 +791,7 @@ import java.util.List; * * @param edtsAtom edts (edit box) atom to decode. * @return Pair of edit list durations and edit list media times, or a pair of nulls if they are - * not present. + * not present. */ private static Pair parseEdts(Atom.ContainerAtom edtsAtom) { Atom.LeafAtom elst; diff --git a/library/core/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 index 4807e05277..867e4501fa 100644 --- a/library/core/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 @@ -74,7 +74,7 @@ public final class FragmentedMp4Extractor implements Extractor { @Retention(RetentionPolicy.SOURCE) @IntDef(flag = true, value = {FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME, FLAG_WORKAROUND_IGNORE_TFDT_BOX, FLAG_ENABLE_EMSG_TRACK, FLAG_ENABLE_CEA608_TRACK, - FLAG_SIDELOADED}) + FLAG_SIDELOADED, FLAG_WORKAROUND_IGNORE_EDIT_LISTS}) public @interface Flags {} /** * Flag to work around an issue in some video streams where every frame is marked as a sync frame. @@ -103,6 +103,10 @@ public final class FragmentedMp4Extractor implements Extractor { * container. */ private static final int FLAG_SIDELOADED = 16; + /** + * Flag to ignore any edit lists in the stream. + */ + public static final int FLAG_WORKAROUND_IGNORE_EDIT_LISTS = 32; private static final String TAG = "FragmentedMp4Extractor"; private static final int SAMPLE_GROUP_TYPE_seig = Util.getIntegerCodeForString("seig"); @@ -432,7 +436,7 @@ public final class FragmentedMp4Extractor implements Extractor { Atom.ContainerAtom atom = moov.containerChildren.get(i); if (atom.type == Atom.TYPE_trak) { Track track = AtomParsers.parseTrak(atom, moov.getLeafAtomOfType(Atom.TYPE_mvhd), duration, - drmInitData, false); + drmInitData, (flags & FLAG_WORKAROUND_IGNORE_EDIT_LISTS) != 0, false); if (track != null) { tracks.put(track.id, track); } diff --git a/library/core/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 index d3fe9e0d05..473e17e3e0 100644 --- a/library/core/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 @@ -28,6 +28,7 @@ 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.mp4.Atom.ContainerAtom; +import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor.Flags; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.NalUnitUtil; @@ -57,6 +58,17 @@ public final class Mp4Extractor implements Extractor, SeekMap { }; + /** + * Flags controlling the behavior of the extractor. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(flag = true, value = {FLAG_WORKAROUND_IGNORE_EDIT_LISTS}) + public @interface Flags {} + /** + * Flag to ignore any edit lists in the stream. + */ + public static final int FLAG_WORKAROUND_IGNORE_EDIT_LISTS = 1; + /** * Parser states. */ @@ -76,6 +88,8 @@ public final class Mp4Extractor implements Extractor, SeekMap { */ private static final long RELOAD_MINIMUM_SEEK_DISTANCE = 256 * 1024; + private final @Flags int flags; + // Temporary arrays. private final ParsableByteArray nalStartCode; private final ParsableByteArray nalLength; @@ -98,7 +112,21 @@ public final class Mp4Extractor implements Extractor, SeekMap { private long durationUs; private boolean isQuickTime; + /** + * Creates a new extractor for unfragmented MP4 streams. + */ public Mp4Extractor() { + this(0); + } + + /** + * Creates a new extractor for unfragmented MP4 streams, using the specified flags to control the + * extractor's behavior. + * + * @param flags Flags that control the extractor's behavior. + */ + public Mp4Extractor(@Flags int flags) { + this.flags = flags; atomHeader = new ParsableByteArray(Atom.LONG_HEADER_SIZE); containerAtoms = new Stack<>(); nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE); @@ -345,7 +373,7 @@ public final class Mp4Extractor implements Extractor, SeekMap { } Track track = AtomParsers.parseTrak(atom, moov.getLeafAtomOfType(Atom.TYPE_mvhd), - C.TIME_UNSET, null, isQuickTime); + C.TIME_UNSET, null, (flags & FLAG_WORKAROUND_IGNORE_EDIT_LISTS) != 0, isQuickTime); if (track == null) { continue; } diff --git a/library/core/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 index 7ac3158794..3adc5a8972 100644 --- a/library/core/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 @@ -81,12 +81,12 @@ public final class Track { /** * Durations of edit list segments in the movie timescale. Null if there is no edit list. */ - public final long[] editListDurations; + @Nullable public final long[] editListDurations; /** * Media times for edit list segments in the track timescale. Null if there is no edit list. */ - public final long[] editListMediaTimes; + @Nullable public final long[] editListMediaTimes; /** * For H264 video tracks, the length in bytes of the NALUnitLength field in each sample. 0 for @@ -99,7 +99,7 @@ public final class Track { public Track(int id, int type, long timescale, long movieTimescale, long durationUs, Format format, @Transformation int sampleTransformation, @Nullable TrackEncryptionBox[] sampleDescriptionEncryptionBoxes, int nalUnitLengthFieldLength, - long[] editListDurations, long[] editListMediaTimes) { + @Nullable long[] editListDurations, @Nullable long[] editListMediaTimes) { this.id = id; this.type = type; this.timescale = timescale; From f6d0dae50e4dd21ad6c10a3774db4da6ba1f5f1e Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 13 Oct 2017 09:31:00 -0700 Subject: [PATCH 0553/2472] Workaround/Fix #3351 1. Ignore edit list where the sequence doesn't contain a sync sample, rather than failing. 2. Make Mp4Extractor.readAtomPayload so it doesn't try and read the same payload twice if a failure occurs parsing it. 3. Make processAtomEnded so that it doesn't pop the moov if parsing it fails. Issue: #3351 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=172106244 --- .../android/exoplayer2/extractor/mp4/AtomParsers.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/library/core/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 index 84538b8122..e527f310b3 100644 --- a/library/core/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 @@ -395,7 +395,11 @@ import java.util.List; hasSyncSample |= (editedFlags[i] & C.BUFFER_FLAG_KEY_FRAME) != 0; } if (!hasSyncSample) { - throw new ParserException("The edited sample sequence does not contain a sync sample."); + // We don't support edit lists where the edited sample sequence doesn't contain a sync sample. + // Such edit lists are often (although not always) broken, so we ignore it and continue. + Log.w(TAG, "Ignoring edit list: Edited sample sequence does not contain a sync sample."); + Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale); + return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags); } return new TrackSampleTable(editedOffsets, editedSizes, editedMaximumSize, editedTimestamps, From aa42d3c910fbee0497d94d1896c0748d57794ab5 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 17 Oct 2017 03:49:49 -0700 Subject: [PATCH 0554/2472] We're not playing an ad if the timeline is empty. Issue: #3334 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=172447125 --- .../java/com/google/android/exoplayer2/ExoPlayerImpl.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index b53cce1f74..cc63d57cae 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -356,17 +356,17 @@ import java.util.concurrent.CopyOnWriteArraySet; @Override public boolean isPlayingAd() { - return pendingSeekAcks == 0 && playbackInfo.periodId.isAd(); + return !timeline.isEmpty() && pendingSeekAcks == 0 && playbackInfo.periodId.isAd(); } @Override public int getCurrentAdGroupIndex() { - return pendingSeekAcks == 0 ? playbackInfo.periodId.adGroupIndex : C.INDEX_UNSET; + return isPlayingAd() ? playbackInfo.periodId.adGroupIndex : C.INDEX_UNSET; } @Override public int getCurrentAdIndexInAdGroup() { - return pendingSeekAcks == 0 ? playbackInfo.periodId.adIndexInAdGroup : C.INDEX_UNSET; + return isPlayingAd() ? playbackInfo.periodId.adIndexInAdGroup : C.INDEX_UNSET; } @Override From ce0a03dbdf8f0d83ac98001805f0c6cea47f6722 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 17 Oct 2017 07:33:19 -0700 Subject: [PATCH 0555/2472] Add an extractor flag for ignoring edit lists Issue: #3358 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=172464053 --- .../extractor/DefaultExtractorsFactory.java | 15 +++++++++- .../exoplayer2/extractor/mp4/AtomParsers.java | 16 +++++++--- .../extractor/mp4/FragmentedMp4Extractor.java | 8 +++-- .../extractor/mp4/Mp4Extractor.java | 30 ++++++++++++++++++- .../exoplayer2/extractor/mp4/Track.java | 6 ++-- 5 files changed, 64 insertions(+), 11 deletions(-) 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 index ccc5c0eb3e..87165e7a9b 100644 --- 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 @@ -67,6 +67,7 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory { } private @MatroskaExtractor.Flags int matroskaFlags; + private @Mp4Extractor.Flags int mp4Flags; private @FragmentedMp4Extractor.Flags int fragmentedMp4Flags; private @Mp3Extractor.Flags int mp3Flags; private @TsExtractor.Mode int tsMode; @@ -89,6 +90,18 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory { return this; } + /** + * Sets flags for {@link Mp4Extractor} instances created by the factory. + * + * @see Mp4Extractor#Mp4Extractor(int) + * @param flags The flags to use. + * @return The factory, for convenience. + */ + public synchronized DefaultExtractorsFactory setMp4ExtractorFlags(@Mp4Extractor.Flags int flags) { + this.mp4Flags = flags; + return this; + } + /** * Sets flags for {@link FragmentedMp4Extractor} instances created by the factory. * @@ -145,7 +158,7 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory { 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[2] = new Mp4Extractor(mp4Flags); extractors[3] = new Mp3Extractor(mp3Flags); extractors[4] = new AdtsExtractor(); extractors[5] = new Ac3Extractor(); diff --git a/library/core/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 index e527f310b3..52149f10ef 100644 --- a/library/core/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 @@ -60,11 +60,13 @@ import java.util.List; * @param duration The duration in units of the timescale declared in the mvhd atom, or * {@link C#TIME_UNSET} if the duration should be parsed from the tkhd atom. * @param drmInitData {@link DrmInitData} to be included in the format. + * @param ignoreEditLists Whether to ignore any edit lists in the trak box. * @param isQuickTime True for QuickTime media. False otherwise. * @return A {@link Track} instance, or {@code null} if the track's type isn't supported. */ public static Track parseTrak(Atom.ContainerAtom trak, Atom.LeafAtom mvhd, long duration, - DrmInitData drmInitData, boolean isQuickTime) throws ParserException { + DrmInitData drmInitData, boolean ignoreEditLists, boolean isQuickTime) + throws ParserException { Atom.ContainerAtom mdia = trak.getContainerAtomOfType(Atom.TYPE_mdia); int trackType = parseHdlr(mdia.getLeafAtomOfType(Atom.TYPE_hdlr).data); if (trackType == C.TRACK_TYPE_UNKNOWN) { @@ -88,11 +90,17 @@ import java.util.List; Pair mdhdData = parseMdhd(mdia.getLeafAtomOfType(Atom.TYPE_mdhd).data); StsdData stsdData = parseStsd(stbl.getLeafAtomOfType(Atom.TYPE_stsd).data, tkhdData.id, tkhdData.rotationDegrees, mdhdData.second, drmInitData, isQuickTime); - Pair edtsData = parseEdts(trak.getContainerAtomOfType(Atom.TYPE_edts)); + long[] editListDurations = null; + long[] editListMediaTimes = null; + if (!ignoreEditLists) { + Pair edtsData = parseEdts(trak.getContainerAtomOfType(Atom.TYPE_edts)); + editListDurations = edtsData.first; + editListMediaTimes = edtsData.second; + } return stsdData.format == null ? null : new Track(tkhdData.id, trackType, mdhdData.first, movieTimescale, durationUs, stsdData.format, stsdData.requiredSampleTransformation, stsdData.trackEncryptionBoxes, - stsdData.nalUnitLengthFieldLength, edtsData.first, edtsData.second); + stsdData.nalUnitLengthFieldLength, editListDurations, editListMediaTimes); } /** @@ -783,7 +791,7 @@ import java.util.List; * * @param edtsAtom edts (edit box) atom to decode. * @return Pair of edit list durations and edit list media times, or a pair of nulls if they are - * not present. + * not present. */ private static Pair parseEdts(Atom.ContainerAtom edtsAtom) { Atom.LeafAtom elst; diff --git a/library/core/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 index c3f2a9fb38..3f33a4dd95 100644 --- a/library/core/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 @@ -74,7 +74,7 @@ public final class FragmentedMp4Extractor implements Extractor { @Retention(RetentionPolicy.SOURCE) @IntDef(flag = true, value = {FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME, FLAG_WORKAROUND_IGNORE_TFDT_BOX, FLAG_ENABLE_EMSG_TRACK, FLAG_ENABLE_CEA608_TRACK, - FLAG_SIDELOADED}) + FLAG_SIDELOADED, FLAG_WORKAROUND_IGNORE_EDIT_LISTS}) public @interface Flags {} /** * Flag to work around an issue in some video streams where every frame is marked as a sync frame. @@ -103,6 +103,10 @@ public final class FragmentedMp4Extractor implements Extractor { * container. */ private static final int FLAG_SIDELOADED = 16; + /** + * Flag to ignore any edit lists in the stream. + */ + public static final int FLAG_WORKAROUND_IGNORE_EDIT_LISTS = 32; private static final String TAG = "FragmentedMp4Extractor"; private static final int SAMPLE_GROUP_TYPE_seig = Util.getIntegerCodeForString("seig"); @@ -426,7 +430,7 @@ public final class FragmentedMp4Extractor implements Extractor { Atom.ContainerAtom atom = moov.containerChildren.get(i); if (atom.type == Atom.TYPE_trak) { Track track = AtomParsers.parseTrak(atom, moov.getLeafAtomOfType(Atom.TYPE_mvhd), duration, - drmInitData, false); + drmInitData, (flags & FLAG_WORKAROUND_IGNORE_EDIT_LISTS) != 0, false); if (track != null) { tracks.put(track.id, track); } diff --git a/library/core/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 index d3fe9e0d05..473e17e3e0 100644 --- a/library/core/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 @@ -28,6 +28,7 @@ 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.mp4.Atom.ContainerAtom; +import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor.Flags; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.NalUnitUtil; @@ -57,6 +58,17 @@ public final class Mp4Extractor implements Extractor, SeekMap { }; + /** + * Flags controlling the behavior of the extractor. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(flag = true, value = {FLAG_WORKAROUND_IGNORE_EDIT_LISTS}) + public @interface Flags {} + /** + * Flag to ignore any edit lists in the stream. + */ + public static final int FLAG_WORKAROUND_IGNORE_EDIT_LISTS = 1; + /** * Parser states. */ @@ -76,6 +88,8 @@ public final class Mp4Extractor implements Extractor, SeekMap { */ private static final long RELOAD_MINIMUM_SEEK_DISTANCE = 256 * 1024; + private final @Flags int flags; + // Temporary arrays. private final ParsableByteArray nalStartCode; private final ParsableByteArray nalLength; @@ -98,7 +112,21 @@ public final class Mp4Extractor implements Extractor, SeekMap { private long durationUs; private boolean isQuickTime; + /** + * Creates a new extractor for unfragmented MP4 streams. + */ public Mp4Extractor() { + this(0); + } + + /** + * Creates a new extractor for unfragmented MP4 streams, using the specified flags to control the + * extractor's behavior. + * + * @param flags Flags that control the extractor's behavior. + */ + public Mp4Extractor(@Flags int flags) { + this.flags = flags; atomHeader = new ParsableByteArray(Atom.LONG_HEADER_SIZE); containerAtoms = new Stack<>(); nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE); @@ -345,7 +373,7 @@ public final class Mp4Extractor implements Extractor, SeekMap { } Track track = AtomParsers.parseTrak(atom, moov.getLeafAtomOfType(Atom.TYPE_mvhd), - C.TIME_UNSET, null, isQuickTime); + C.TIME_UNSET, null, (flags & FLAG_WORKAROUND_IGNORE_EDIT_LISTS) != 0, isQuickTime); if (track == null) { continue; } diff --git a/library/core/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 index 7ac3158794..3adc5a8972 100644 --- a/library/core/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 @@ -81,12 +81,12 @@ public final class Track { /** * Durations of edit list segments in the movie timescale. Null if there is no edit list. */ - public final long[] editListDurations; + @Nullable public final long[] editListDurations; /** * Media times for edit list segments in the track timescale. Null if there is no edit list. */ - public final long[] editListMediaTimes; + @Nullable public final long[] editListMediaTimes; /** * For H264 video tracks, the length in bytes of the NALUnitLength field in each sample. 0 for @@ -99,7 +99,7 @@ public final class Track { public Track(int id, int type, long timescale, long movieTimescale, long durationUs, Format format, @Transformation int sampleTransformation, @Nullable TrackEncryptionBox[] sampleDescriptionEncryptionBoxes, int nalUnitLengthFieldLength, - long[] editListDurations, long[] editListMediaTimes) { + @Nullable long[] editListDurations, @Nullable long[] editListMediaTimes) { this.id = id; this.type = type; this.timescale = timescale; From 4f8f87221ead66dab7610c5c2aa2796f33b3b336 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8B=97=E5=8D=8E=E6=A0=8B=28Rabbit=29?= Date: Wed, 18 Oct 2017 23:03:26 +0800 Subject: [PATCH 0556/2472] Fix FLV AVCVIDEOPACKET -> compositionTimeMs Type from UI 24 to SI 24 --- .../exoplayer2/extractor/flv/VideoTagPayloadReader.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/library/core/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 index 8a4d314ee0..6bf91e4824 100644 --- a/library/core/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 @@ -80,6 +80,9 @@ import com.google.android.exoplayer2.video.AvcConfig; protected void parsePayload(ParsableByteArray data, long timeUs) throws ParserException { int packetType = data.readUnsignedByte(); int compositionTimeMs = data.readUnsignedInt24(); + // compositionTimeMs is signed int 24, change unsigned int 24 to signed int 24 + compositionTimeMs = (compositionTimeMs & 0x800000) >> 23 == 1 ? (compositionTimeMs & 0xff000000) : compositionTimeMs; + timeUs += compositionTimeMs * 1000L; // Parse avc sequence header in case this was not done before. if (packetType == AVC_PACKET_TYPE_SEQUENCE_HEADER && !hasOutputFormat) { From 5895884c53a33c8417f231696b27dd9ef30495f7 Mon Sep 17 00:00:00 2001 From: miaohuadong Date: Thu, 19 Oct 2017 14:10:18 +0800 Subject: [PATCH 0557/2472] Fix bug --- .../android/exoplayer2/extractor/flv/VideoTagPayloadReader.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/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 index 6bf91e4824..22a8b57ef7 100644 --- a/library/core/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 @@ -81,7 +81,7 @@ import com.google.android.exoplayer2.video.AvcConfig; int packetType = data.readUnsignedByte(); int compositionTimeMs = data.readUnsignedInt24(); // compositionTimeMs is signed int 24, change unsigned int 24 to signed int 24 - compositionTimeMs = (compositionTimeMs & 0x800000) >> 23 == 1 ? (compositionTimeMs & 0xff000000) : compositionTimeMs; + compositionTimeMs = (compositionTimeMs & 0x800000L) >>> 23 == 1 ? (compositionTimeMs | 0xff000000) : compositionTimeMs; timeUs += compositionTimeMs * 1000L; // Parse avc sequence header in case this was not done before. From 276087c532500a191b1559f424cd400e59667e22 Mon Sep 17 00:00:00 2001 From: Mike Date: Thu, 19 Oct 2017 14:23:50 +0300 Subject: [PATCH 0558/2472] Removed useless parentheses --- .../java/com/google/android/exoplayer2/demo/PlayerActivity.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index 1f92473bc9..08c5bddb09 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -176,7 +176,7 @@ public class PlayerActivity extends Activity implements OnClickListener, @Override public void onResume() { super.onResume(); - if ((Util.SDK_INT <= 23 || player == null)) { + if (Util.SDK_INT <= 23 || player == null) { initializePlayer(); } } From 69e50a43a8104723fdf2f37d477f27f1f1ee40a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8B=97=E5=8D=8E=E6=A0=8B=28Rabbit=29?= Date: Thu, 19 Oct 2017 23:18:12 +0800 Subject: [PATCH 0559/2472] add readSignedInt24 in ParsableByteArray --- .../extractor/flv/VideoTagPayloadReader.java | 4 +--- .../exoplayer2/util/ParsableByteArray.java | 8 ++++++++ .../exoplayer2/util/ParsableByteArrayTest.java | 16 ++++++++++++++++ 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/library/core/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 index 22a8b57ef7..7fa45a2a94 100644 --- a/library/core/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 @@ -79,9 +79,7 @@ import com.google.android.exoplayer2.video.AvcConfig; @Override protected void parsePayload(ParsableByteArray data, long timeUs) throws ParserException { int packetType = data.readUnsignedByte(); - int compositionTimeMs = data.readUnsignedInt24(); - // compositionTimeMs is signed int 24, change unsigned int 24 to signed int 24 - compositionTimeMs = (compositionTimeMs & 0x800000L) >>> 23 == 1 ? (compositionTimeMs | 0xff000000) : compositionTimeMs; + int compositionTimeMs = data.readSignedInt24(); timeUs += compositionTimeMs * 1000L; // Parse avc sequence header in case this was not done before. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java b/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java index 70cb584085..9de86cc3bc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java @@ -256,6 +256,14 @@ public final class ParsableByteArray { | (data[position++] & 0xFF); } + /** + * Reads the next three bytes as an signed value. + */ + public int readSignedInt24() { + int ui24 = readUnsignedInt24(); + return (ui24 & 0x800000L) >>> 23 == 1 ? (ui24 | 0xff000000) : ui24; + } + /** * Reads the next three bytes as a signed value in little endian order. */ diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/ParsableByteArrayTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/ParsableByteArrayTest.java index 504a58b4a8..56a4740464 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/util/ParsableByteArrayTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/ParsableByteArrayTest.java @@ -334,6 +334,22 @@ public final class ParsableByteArrayTest { assertThat(byteArray.getPosition()).isEqualTo(3); } + @Test + public void testReadPositiveSignedInt24() { + byte[] data = { 0x01, 0x02, (byte) 0xFF }; + ParsableByteArray byteArray = new ParsableByteArray(data); + assertThat(byteArray.readSignedInt24()).isEqualTo(0x0102FF); + assertThat(byteArray.getPosition()).isEqualTo(3); + } + + @Test + public void testReadNegativeSignedInt24() { + byte[] data = { (byte)0xFF, 0x02, (byte) 0x01 }; + ParsableByteArray byteArray = new ParsableByteArray(data); + assertThat(byteArray.readSignedInt24()).isEqualTo(0xFFFF0201); + assertThat(byteArray.getPosition()).isEqualTo(3); + } + @Test public void testReadLittleEndianUnsignedShort() { ParsableByteArray byteArray = new ParsableByteArray(new byte[] { From fc5e8ee5162757d4a52142f59ec5bf771791af34 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 18 Oct 2017 04:46:05 -0700 Subject: [PATCH 0560/2472] Add some additional device specific workarounds Issue: #3355 Issue: #3257 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=172587141 --- .../android/exoplayer2/mediacodec/MediaCodecRenderer.java | 2 +- .../android/exoplayer2/video/MediaCodecVideoRenderer.java | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java index 1ad6c1eed7..d965b662be 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java @@ -1142,7 +1142,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { */ private @AdaptationWorkaroundMode int codecAdaptationWorkaroundMode(String name) { if (Util.SDK_INT <= 24 && "OMX.Exynos.avc.dec.secure".equals(name) - && Util.MODEL.startsWith("SM-T585")) { + && (Util.MODEL.startsWith("SM-T585") || Util.MODEL.startsWith("SM-A520"))) { return ADAPTATION_WORKAROUND_MODE_ALWAYS; } else if (Util.SDK_INT < 24 && ("OMX.Nvidia.h264.decode".equals(name) || "OMX.Nvidia.h264.decode.secure".equals(name)) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index 40f2a7b081..25e507d984 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -1071,9 +1071,11 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { * If true is returned then we fall back to releasing and re-instantiating the codec instead. */ private static boolean codecNeedsSetOutputSurfaceWorkaround(String name) { - // Work around https://github.com/google/ExoPlayer/issues/3236. - return ("deb".equals(Util.DEVICE) || "flo".equals(Util.DEVICE)) - && "OMX.qcom.video.decoder.avc".equals(name); + // Work around https://github.com/google/ExoPlayer/issues/3236 and + // https://github.com/google/ExoPlayer/issues/3355. + return (("deb".equals(Util.DEVICE) || "flo".equals(Util.DEVICE)) + && "OMX.qcom.video.decoder.avc".equals(name)) + || ("tcl_eu".equals(Util.DEVICE) && "OMX.MTK.VIDEO.DECODER.AVC".equals(name)); } /** From 1e79d6eb849a7e404e7682f24bbdcbd720ab6eda Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 18 Oct 2017 05:59:14 -0700 Subject: [PATCH 0561/2472] Fix seeking with repeated periods newPlayingPeriodHolder could be set then updated if seeking to a repeated period that was loaded more than once. This led to MediaPeriodHolders leaking. Only set newPlayingPeriodHolder once so that any later holders with the same period identifier get released. Also add a regression test. FakeMediaSource checks that all created MediaPeriods were released when it is released. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=172591937 --- .../google/android/exoplayer2/ExoPlayerTest.java | 16 ++++++++++++++++ .../exoplayer2/ExoPlayerImplInternal.java | 3 ++- .../exoplayer2/testutil/FakeMediaSource.java | 1 + 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index 2971aaf779..eb9dc6f9d8 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -261,6 +261,22 @@ public final class ExoPlayerTest extends TestCase { assertTrue(renderer.isEnded); } + public void testPeriodHoldersReleasedAfterSeekWithRepeatModeAll() throws Exception { + Timeline fakeTimeline = new FakeTimeline(new TimelineWindowDefinition(true, false, 100000)); + FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); + ActionSchedule actionSchedule = new ActionSchedule.Builder("testPeriodHoldersReleased") + .setRepeatMode(Player.REPEAT_MODE_ALL) + .waitForPositionDiscontinuity() + .seek(0) // Seek with repeat mode set to REPEAT_MODE_ALL. + .waitForPositionDiscontinuity() + .setRepeatMode(Player.REPEAT_MODE_OFF) // Turn off repeat so that playback can finish. + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(fakeTimeline).setRenderers(renderer).setActionSchedule(actionSchedule) + .build().start().blockUntilEnded(TIMEOUT_MS); + assertTrue(renderer.isEnded); + } + public void testSeekProcessedCallback() throws Exception { Timeline timeline = new FakeTimeline( new TimelineWindowDefinition(true, false, 100000), diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index a0ca448de7..79c92aeb67 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -753,7 +753,8 @@ import java.io.IOException; // Clear the timeline, but keep the requested period if it is already prepared. MediaPeriodHolder periodHolder = playingPeriodHolder; while (periodHolder != null) { - if (shouldKeepPeriodHolder(periodId, periodPositionUs, periodHolder)) { + if (newPlayingPeriodHolder == null + && shouldKeepPeriodHolder(periodId, periodPositionUs, periodHolder)) { newPlayingPeriodHolder = periodHolder; } else { periodHolder.release(); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java index 9e7b498269..a827fb80c6 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java @@ -116,4 +116,5 @@ public class FakeMediaSource implements MediaSource { } return new TrackGroupArray(trackGroups); } + } From b8ef1dcc78ed240c7f7535fea193e53d1b5a3867 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 18 Oct 2017 06:08:51 -0700 Subject: [PATCH 0562/2472] Fix MediaPeriod position param Javadoc. prepare and selectTracks receive the position from which any loading should start, where-as continueLoading receives the actual playback position. These are different in the case that a previous period is still being played out. Also removed "relative to the start of the period" from prepare documentation because it couldn't really be relative to anything else. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=172592769 --- .../google/android/exoplayer2/source/MediaPeriod.java | 10 ++++++---- .../android/exoplayer2/source/SequenceableLoader.java | 4 +++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java index 514b96ae8d..7d16f794cd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java @@ -58,8 +58,7 @@ public interface MediaPeriod extends SequenceableLoader { * * @param callback Callback to receive updates from this period, including being notified when * preparation completes. - * @param positionUs The position in microseconds relative to the start of the period at which to - * start loading data. + * @param positionUs The expected starting position, in microseconds. */ void prepare(Callback callback, long positionUs); @@ -103,7 +102,8 @@ public interface MediaPeriod extends SequenceableLoader { * selections. * @param streamResetFlags Will be updated to indicate new sample streams, and sample streams that * have been retained but with the requirement that the consuming renderer be reset. - * @param positionUs The current playback position in microseconds. + * @param positionUs The current playback position in microseconds. If playback of this period has + * not yet started, the value will be the starting position. * @return The actual position at which the tracks were enabled, in microseconds. */ long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, @@ -176,7 +176,9 @@ public interface MediaPeriod extends SequenceableLoader { * called when the period is permitted to continue loading data. A period may do this both during * and after preparation. * - * @param positionUs The current playback position. + * @param positionUs The current playback position in microseconds. If playback of this period has + * not yet started, the value will be the starting position minus the duration of any media in + * previous periods still to be played. * @return True if progress was made, meaning that {@link #getNextLoadPositionUs()} will return * a different value than prior to the call. False otherwise. */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SequenceableLoader.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SequenceableLoader.java index 26cb9a2666..e40ceab976 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SequenceableLoader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SequenceableLoader.java @@ -52,7 +52,9 @@ public interface SequenceableLoader { /** * Attempts to continue loading. * - * @param positionUs The current playback position. + * @param positionUs The current playback position in microseconds. If playback of the period to + * which this loader belongs has not yet started, the value will be the period's starting + * position minus the duration of any media in previous periods still to be played. * @return True if progress was made, meaning that {@link #getNextLoadPositionUs()} will return * a different value than prior to the call. False otherwise. */ From eb08e1a5c3481057c2672446b50b9e0e565b7895 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 18 Oct 2017 07:46:54 -0700 Subject: [PATCH 0563/2472] Work around AudioTrack Bluetooth connection issue If connecting a Bluetooth audio device fails, the AudioTrack may be left in a bad state, where it is not actually playing and its position has jumped back to zero. Detect and work around this case by resetting the track. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=172600912 --- .../exoplayer2/audio/DefaultAudioSink.java | 44 ++++++++++++++++++- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java index a5173db284..73c0bc20be 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java @@ -293,8 +293,6 @@ public final class DefaultAudioSink implements AudioSink { @Override public long getCurrentPositionUs(boolean sourceEnded) { - // If the device supports it, use the playback timestamp from AudioTrack.getTimestamp. - // Otherwise, derive a smoothed position by sampling the track's frame position. if (!hasCurrentPositionUs()) { return CURRENT_POSITION_NOT_SET; } @@ -303,6 +301,8 @@ public final class DefaultAudioSink implements AudioSink { maybeSampleSyncParams(); } + // If the device supports it, use the playback timestamp from AudioTrack.getTimestamp. + // Otherwise, derive a smoothed position by sampling the track's frame position. long systemClockUs = System.nanoTime() / 1000; long positionUs; if (audioTimestampSet) { @@ -638,6 +638,13 @@ public final class DefaultAudioSink implements AudioSink { inputBuffer = null; return true; } + + if (audioTrackUtil.needsReset(getWrittenFrames())) { + Log.w(TAG, "Resetting stalled audio track"); + reset(); + return true; + } + return false; } @@ -1292,6 +1299,8 @@ public final class DefaultAudioSink implements AudioSink { */ private static class AudioTrackUtil { + private static final long FORCE_RESET_WORKAROUND_TIMEOUT_MS = 200; + protected AudioTrack audioTrack; private boolean needsPassthroughWorkaround; private int sampleRate; @@ -1300,6 +1309,7 @@ public final class DefaultAudioSink implements AudioSink { private long passthroughWorkaroundPauseOffset; private long stopTimestampUs; + private long forceResetWorkaroundTimeMs; private long stopPlaybackHeadPosition; private long endPlaybackHeadPosition; @@ -1314,6 +1324,7 @@ public final class DefaultAudioSink implements AudioSink { this.audioTrack = audioTrack; this.needsPassthroughWorkaround = needsPassthroughWorkaround; stopTimestampUs = C.TIME_UNSET; + forceResetWorkaroundTimeMs = C.TIME_UNSET; lastRawPlaybackHeadPosition = 0; rawPlaybackHeadWrapCount = 0; passthroughWorkaroundPauseOffset = 0; @@ -1348,6 +1359,17 @@ public final class DefaultAudioSink implements AudioSink { audioTrack.pause(); } + /** + * Returns whether the track is in an invalid state and must be reset. + * + * @see #getPlaybackHeadPosition() + */ + public boolean needsReset(long writtenFrames) { + return forceResetWorkaroundTimeMs != C.TIME_UNSET && writtenFrames > 0 + && SystemClock.elapsedRealtime() - forceResetWorkaroundTimeMs + >= FORCE_RESET_WORKAROUND_TIMEOUT_MS; + } + /** * {@link AudioTrack#getPlaybackHeadPosition()} returns a value intended to be interpreted as an * unsigned 32 bit integer, which also wraps around periodically. This method returns the @@ -1380,6 +1402,24 @@ public final class DefaultAudioSink implements AudioSink { } rawPlaybackHeadPosition += passthroughWorkaroundPauseOffset; } + + if (Util.SDK_INT <= 26) { + if (rawPlaybackHeadPosition == 0 && lastRawPlaybackHeadPosition > 0 + && state == PLAYSTATE_PLAYING) { + // If connecting a Bluetooth audio device fails, the AudioTrack may be left in a state + // where its Java API is in the playing state, but the native track is stopped. When this + // happens the playback head position gets stuck at zero. In this case, return the old + // playback head position and force the track to be reset after + // {@link #FORCE_RESET_WORKAROUND_TIMEOUT_MS} has elapsed. + if (forceResetWorkaroundTimeMs == C.TIME_UNSET) { + forceResetWorkaroundTimeMs = SystemClock.elapsedRealtime(); + } + return lastRawPlaybackHeadPosition; + } else { + forceResetWorkaroundTimeMs = C.TIME_UNSET; + } + } + if (lastRawPlaybackHeadPosition > rawPlaybackHeadPosition) { // The value must have wrapped around. rawPlaybackHeadWrapCount++; From daf9c8d19a2b6e1d95425af910641896a944e606 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 19 Oct 2017 03:00:44 -0700 Subject: [PATCH 0564/2472] Update HLS sample streams ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=172722536 --- demos/main/src/main/assets/media.exolist.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/demos/main/src/main/assets/media.exolist.json b/demos/main/src/main/assets/media.exolist.json index 59d8259d37..38a0c577ae 100644 --- a/demos/main/src/main/assets/media.exolist.json +++ b/demos/main/src/main/assets/media.exolist.json @@ -344,11 +344,11 @@ "samples": [ { "name": "Apple 4x3 basic stream", - "uri": "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/bipbop_4x3_variant.m3u8" + "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/bipbop_4x3_variant.m3u8" }, { "name": "Apple 16x9 basic stream", - "uri": "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8" + "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8" }, { "name": "Apple master playlist advanced (TS)", @@ -360,11 +360,11 @@ }, { "name": "Apple TS media playlist", - "uri": "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/gear1/prog_index.m3u8" + "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear1/prog_index.m3u8" }, { "name": "Apple AAC media playlist", - "uri": "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/gear0/prog_index.m3u8" + "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear0/prog_index.m3u8" }, { "name": "Apple ID3 metadata", @@ -381,11 +381,11 @@ }, { "name": "Apple AAC 10s", - "uri": "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/gear0/fileSequence0.aac" + "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear0/fileSequence0.aac" }, { "name": "Apple TS 10s", - "uri": "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/gear1/fileSequence0.ts" + "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear1/fileSequence0.ts" }, { "name": "Android screens (Matroska)", From 49aca6e9ce745f36443e1c5388f0572b4ee1fd58 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 19 Oct 2017 03:21:09 -0700 Subject: [PATCH 0565/2472] Fix typo in variable name ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=172724250 --- .../exoplayer2/source/dash/manifest/DashManifestParser.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/library/dash/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 index 72faf21b57..410fd7e41e 100644 --- a/library/dash/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 @@ -524,10 +524,10 @@ public class DashManifestParser extends DefaultHandler } format = format.copyWithDrmInitData(drmInitData); } - ArrayList inbandEventStremas = representationInfo.inbandEventStreams; - inbandEventStremas.addAll(extraInbandEventStreams); + ArrayList inbandEventStreams = representationInfo.inbandEventStreams; + inbandEventStreams.addAll(extraInbandEventStreams); return Representation.newInstance(contentId, Representation.REVISION_ID_DEFAULT, format, - representationInfo.baseUrl, representationInfo.segmentBase, inbandEventStremas); + representationInfo.baseUrl, representationInfo.segmentBase, inbandEventStreams); } // SegmentBase, SegmentList and SegmentTemplate parsing. From 2cfc478c3ebf3662727cbe38ad5fef7bcddc8a38 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 19 Oct 2017 03:57:51 -0700 Subject: [PATCH 0566/2472] Allow extractor injection for HLS Issue:#2748 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=172726367 --- .../extractor/mp3/Mp3Extractor.java | 11 +- .../exoplayer2/extractor/ts/Ac3Extractor.java | 5 +- .../extractor/ts/AdtsExtractor.java | 5 +- .../ts/DefaultTsPayloadReaderFactory.java | 2 +- .../hls/DefaultHlsExtractorFactory.java | 106 ++++++++++++ .../exoplayer2/source/hls/HlsChunkSource.java | 20 ++- .../source/hls/HlsExtractorFactory.java | 53 ++++++ .../exoplayer2/source/hls/HlsMediaChunk.java | 162 +++++------------- .../exoplayer2/source/hls/HlsMediaPeriod.java | 11 +- .../exoplayer2/source/hls/HlsMediaSource.java | 53 ++++-- .../source/hls/HlsSampleStreamWrapper.java | 19 +- .../hls/playlist/HlsPlaylistTracker.java | 5 +- 12 files changed, 287 insertions(+), 165 deletions(-) create mode 100644 library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java create mode 100644 library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsExtractorFactory.java diff --git a/library/core/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 index 8d33f95640..a4349ada09 100644 --- a/library/core/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 @@ -112,16 +112,11 @@ public final class Mp3Extractor implements Extractor { private long samplesRead; private int sampleBytesRemaining; - /** - * Constructs a new {@link Mp3Extractor}. - */ public Mp3Extractor() { this(0); } /** - * Constructs a new {@link Mp3Extractor}. - * * @param flags Flags that control the extractor's behavior. */ public Mp3Extractor(@Flags int flags) { @@ -129,8 +124,6 @@ public final class Mp3Extractor implements Extractor { } /** - * Constructs a new {@link Mp3Extractor}. - * * @param flags Flags that control the extractor's behavior. * @param forcedFirstSampleTimestampUs A timestamp to force for the first sample, or * {@link C#TIME_UNSET} if forcing is not required. @@ -144,6 +137,8 @@ public final class Mp3Extractor implements Extractor { basisTimeUs = C.TIME_UNSET; } + // Extractor implementation. + @Override public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { return synchronize(input, true); @@ -195,6 +190,8 @@ public final class Mp3Extractor implements Extractor { return readSample(input); } + // Internal methods. + private int readSample(ExtractorInput extractorInput) throws IOException, InterruptedException { if (sampleBytesRemaining == 0) { extractorInput.resetPeekPosition(); diff --git a/library/core/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 index 8bab6b7ed1..4d54600c6d 100644 --- a/library/core/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 @@ -29,8 +29,7 @@ import com.google.android.exoplayer2.util.Util; import java.io.IOException; /** - * Facilitates the extraction of AC-3 samples from elementary audio files formatted as AC-3 - * bitstreams. + * Extracts samples from (E-)AC-3 bitstreams. */ public final class Ac3Extractor implements Extractor { @@ -71,6 +70,8 @@ public final class Ac3Extractor implements Extractor { sampleData = new ParsableByteArray(MAX_SYNC_FRAME_SIZE); } + // Extractor implementation. + @Override public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { // Skip any ID3 headers. diff --git a/library/core/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 index a1851aa0ea..5ce15952a5 100644 --- a/library/core/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 @@ -29,8 +29,7 @@ import com.google.android.exoplayer2.util.Util; import java.io.IOException; /** - * Facilitates the extraction of AAC samples from elementary audio files formatted as AAC with ADTS - * headers. + * Extracts samples from AAC bit streams with ADTS framing. */ public final class AdtsExtractor implements Extractor { @@ -70,6 +69,8 @@ public final class AdtsExtractor implements Extractor { packetBuffer = new ParsableByteArray(MAX_PACKET_SIZE); } + // Extractor implementation. + @Override public boolean sniff(ExtractorInput input) throws IOException, InterruptedException { // Skip any ID3 headers. diff --git a/library/core/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 index bd013f96a3..2d16b46895 100644 --- a/library/core/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 @@ -28,7 +28,7 @@ import java.util.Collections; import java.util.List; /** - * Default implementation for {@link TsPayloadReader.Factory}. + * Default {@link TsPayloadReader.Factory} implementation. */ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Factory { diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java new file mode 100644 index 0000000000..9f0989e444 --- /dev/null +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java @@ -0,0 +1,106 @@ +/* + * 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.hls; + +import android.net.Uri; +import android.text.TextUtils; +import android.util.Pair; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.drm.DrmInitData; +import com.google.android.exoplayer2.extractor.Extractor; +import com.google.android.exoplayer2.extractor.mp3.Mp3Extractor; +import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor; +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.TsExtractor; +import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.TimestampAdjuster; +import java.util.Collections; +import java.util.List; + +/** + * Default {@link HlsExtractorFactory} implementation. + * + *

          This class can be extended to override {@link TsExtractor} instantiation.

          + */ +public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { + + public static final String AAC_FILE_EXTENSION = ".aac"; + public static final String AC3_FILE_EXTENSION = ".ac3"; + public static final String EC3_FILE_EXTENSION = ".ec3"; + public static final String MP3_FILE_EXTENSION = ".mp3"; + public static final String MP4_FILE_EXTENSION = ".mp4"; + public static final String M4_FILE_EXTENSION_PREFIX = ".m4"; + public static final String VTT_FILE_EXTENSION = ".vtt"; + public static final String WEBVTT_FILE_EXTENSION = ".webvtt"; + + @Override + public Pair createExtractor(Extractor previousExtractor, Uri uri, + Format format, List muxedCaptionFormats, DrmInitData drmInitData, + TimestampAdjuster timestampAdjuster) { + String lastPathSegment = uri.getLastPathSegment(); + boolean isPackedAudioExtractor = false; + Extractor extractor; + if (MimeTypes.TEXT_VTT.equals(format.sampleMimeType) + || lastPathSegment.endsWith(WEBVTT_FILE_EXTENSION) + || lastPathSegment.endsWith(VTT_FILE_EXTENSION)) { + extractor = new WebvttExtractor(format.language, timestampAdjuster); + } else if (lastPathSegment.endsWith(AAC_FILE_EXTENSION)) { + isPackedAudioExtractor = true; + extractor = new AdtsExtractor(); + } else if (lastPathSegment.endsWith(AC3_FILE_EXTENSION) + || lastPathSegment.endsWith(EC3_FILE_EXTENSION)) { + isPackedAudioExtractor = true; + extractor = new Ac3Extractor(); + } else if (lastPathSegment.endsWith(MP3_FILE_EXTENSION)) { + isPackedAudioExtractor = true; + extractor = new Mp3Extractor(0, 0); + } else if (previousExtractor != null) { + // Only reuse TS and fMP4 extractors. + extractor = previousExtractor; + } else if (lastPathSegment.endsWith(MP4_FILE_EXTENSION) + || lastPathSegment.startsWith(M4_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 4)) { + extractor = new FragmentedMp4Extractor(0, timestampAdjuster, null, drmInitData); + } else { + // For any other file extension, we assume TS format. + @DefaultTsPayloadReaderFactory.Flags + int esReaderFactoryFlags = DefaultTsPayloadReaderFactory.FLAG_IGNORE_SPLICE_INFO_STREAM; + if (muxedCaptionFormats != null) { + // The playlist declares closed caption renditions, we should ignore descriptors. + esReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_OVERRIDE_CAPTION_DESCRIPTORS; + } else { + muxedCaptionFormats = Collections.emptyList(); + } + String codecs = format.codecs; + if (!TextUtils.isEmpty(codecs)) { + // Sometimes AAC and H264 streams are declared in TS chunks even though they don't really + // exist. If we know from the codec attribute that they don't exist, then we can + // explicitly ignore them even if they're declared. + if (!MimeTypes.AUDIO_AAC.equals(MimeTypes.getAudioMediaMimeType(codecs))) { + esReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_IGNORE_AAC_STREAM; + } + if (!MimeTypes.VIDEO_H264.equals(MimeTypes.getVideoMediaMimeType(codecs))) { + esReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_IGNORE_H264_STREAM; + } + } + extractor = new TsExtractor(TsExtractor.MODE_HLS, timestampAdjuster, + new DefaultTsPayloadReaderFactory(esReaderFactoryFlags, muxedCaptionFormats)); + } + return Pair.create(extractor, isPackedAudioExtractor); + } + +} diff --git a/library/hls/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 index 8aa4e057a2..b8a0c3ddb7 100644 --- a/library/hls/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 @@ -80,6 +80,7 @@ import java.util.List; } + private final HlsExtractorFactory extractorFactory; private final DataSource mediaDataSource; private final DataSource encryptionDataSource; private final TimestampAdjusterProvider timestampAdjusterProvider; @@ -106,6 +107,8 @@ import java.util.List; private long liveEdgeTimeUs; /** + * @param extractorFactory An {@link HlsExtractorFactory} from which to obtain the extractors for + * media chunks. * @param playlistTracker The {@link HlsPlaylistTracker} from which to obtain media playlists. * @param variants The available variants. * @param dataSourceFactory An {@link HlsDataSourceFactory} to create {@link DataSource}s for the @@ -116,9 +119,10 @@ import java.util.List; * @param muxedCaptionFormats List of muxed caption {@link Format}s. Null if no closed caption * information is available in the master playlist. */ - public HlsChunkSource(HlsPlaylistTracker playlistTracker, HlsUrl[] variants, - HlsDataSourceFactory dataSourceFactory, TimestampAdjusterProvider timestampAdjusterProvider, - List muxedCaptionFormats) { + public HlsChunkSource(HlsExtractorFactory extractorFactory, HlsPlaylistTracker playlistTracker, + HlsUrl[] variants, HlsDataSourceFactory dataSourceFactory, + TimestampAdjusterProvider timestampAdjusterProvider, List muxedCaptionFormats) { + this.extractorFactory = extractorFactory; this.playlistTracker = playlistTracker; this.variants = variants; this.timestampAdjusterProvider = timestampAdjusterProvider; @@ -321,11 +325,11 @@ import java.util.List; Uri chunkUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.url); DataSpec dataSpec = new DataSpec(chunkUri, segment.byterangeOffset, segment.byterangeLength, null); - out.chunk = new HlsMediaChunk(mediaDataSource, dataSpec, initDataSpec, selectedUrl, - muxedCaptionFormats, trackSelection.getSelectionReason(), trackSelection.getSelectionData(), - startTimeUs, startTimeUs + segment.durationUs, chunkMediaSequence, discontinuitySequence, - isTimestampMaster, timestampAdjuster, previous, mediaPlaylist.drmInitData, encryptionKey, - encryptionIv); + out.chunk = new HlsMediaChunk(extractorFactory, mediaDataSource, dataSpec, initDataSpec, + selectedUrl, muxedCaptionFormats, trackSelection.getSelectionReason(), + trackSelection.getSelectionData(), startTimeUs, startTimeUs + segment.durationUs, + chunkMediaSequence, discontinuitySequence, isTimestampMaster, timestampAdjuster, previous, + mediaPlaylist.drmInitData, encryptionKey, encryptionIv); } /** diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsExtractorFactory.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsExtractorFactory.java new file mode 100644 index 0000000000..3ed6a549db --- /dev/null +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsExtractorFactory.java @@ -0,0 +1,53 @@ +/* + * 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.hls; + +import android.net.Uri; +import android.util.Pair; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.drm.DrmInitData; +import com.google.android.exoplayer2.extractor.Extractor; +import com.google.android.exoplayer2.util.TimestampAdjuster; +import java.util.List; + +/** + * Factory for HLS media chunk extractors. + */ +public interface HlsExtractorFactory { + + HlsExtractorFactory DEFAULT = new DefaultHlsExtractorFactory(); + + /** + * Creates an {@link Extractor} for extracting HLS media chunks. + * + * @param previousExtractor A previously used {@link Extractor} which can be reused if the current + * chunk is a continuation of the previously extracted chunk, or null otherwise. It is the + * responsibility of implementers to only reuse extractors that are suited for reusage. + * @param uri The URI of the media chunk. + * @param format A {@link Format} associated with the chunk to extract. + * @param muxedCaptionFormats List of muxed caption {@link Format}s. Null if no closed caption + * information is available in the master playlist. + * @param drmInitData {@link DrmInitData} associated with the chunk. + * @param timestampAdjuster Adjuster corresponding to the provided discontinuity sequence number. + * @return A pair containing the {@link Extractor} and a boolean that indicates whether it is a + * packed audio extractor. The first element may be {@code previousExtractor} if the factory + * has determined it can be re-used. + */ + Pair createExtractor(Extractor previousExtractor, Uri uri, Format format, + List muxedCaptionFormats, DrmInitData drmInitData, + TimestampAdjuster timestampAdjuster); + +} diff --git a/library/hls/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 index 91513b536e..5ca8675dd9 100644 --- a/library/hls/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 @@ -15,19 +15,13 @@ */ package com.google.android.exoplayer2.source.hls; -import android.text.TextUtils; +import android.util.Pair; 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.DefaultExtractorInput; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; -import com.google.android.exoplayer2.extractor.mp3.Mp3Extractor; -import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor; -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.TsExtractor; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.id3.Id3Decoder; import com.google.android.exoplayer2.metadata.id3.PrivFrame; @@ -35,12 +29,10 @@ import com.google.android.exoplayer2.source.chunk.MediaChunk; import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; -import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.TimestampAdjuster; import com.google.android.exoplayer2.util.Util; import java.io.IOException; -import java.util.Collections; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; @@ -49,19 +41,11 @@ import java.util.concurrent.atomic.AtomicInteger; */ /* package */ final class HlsMediaChunk extends MediaChunk { - private static final AtomicInteger UID_SOURCE = new AtomicInteger(); private static final String PRIV_TIMESTAMP_FRAME_OWNER = "com.apple.streaming.transportStreamTimestamp"; - private static final String AAC_FILE_EXTENSION = ".aac"; - private static final String AC3_FILE_EXTENSION = ".ac3"; - private static final String EC3_FILE_EXTENSION = ".ec3"; - private static final String MP3_FILE_EXTENSION = ".mp3"; - private static final String MP4_FILE_EXTENSION = ".mp4"; - private static final String M4_FILE_EXTENSION_PREFIX = ".m4"; - private static final String VTT_FILE_EXTENSION = ".vtt"; - private static final String WEBVTT_FILE_EXTENSION = ".webvtt"; + private static final AtomicInteger uidSource = new AtomicInteger(); /** * A unique identifier for the chunk. @@ -83,26 +67,24 @@ import java.util.concurrent.atomic.AtomicInteger; private final boolean isEncrypted; private final boolean isMasterTimestampSource; private final TimestampAdjuster timestampAdjuster; - private final String lastPathSegment; - private final Extractor previousExtractor; private final boolean shouldSpliceIn; - private final boolean needNewExtractor; - private final List muxedCaptionFormats; - private final DrmInitData drmInitData; - - private final boolean isPackedAudio; + private final Extractor extractor; + private final boolean isPackedAudioExtractor; + private final boolean reusingExtractor; private final Id3Decoder id3Decoder; private final ParsableByteArray id3Data; - private Extractor extractor; + private HlsSampleStreamWrapper output; private int initSegmentBytesLoaded; private int bytesLoaded; + private boolean id3TimestampPeeked; private boolean initLoadCompleted; - private HlsSampleStreamWrapper extractorOutput; private volatile boolean loadCanceled; private volatile boolean loadCompleted; /** + * @param extractorFactory A {@link HlsExtractorFactory} from which the HLS media chunk + * extractor is obtained. * @param dataSource The source from which the data should be loaded. * @param dataSpec Defines the data to be loaded. * @param initDataSpec Defines the initialization data to be fed to new extractors. May be null. @@ -124,10 +106,10 @@ import java.util.concurrent.atomic.AtomicInteger; * @param encryptionIv The AES initialization vector, or null if the segment is not fully * encrypted. */ - public HlsMediaChunk(DataSource dataSource, DataSpec dataSpec, DataSpec initDataSpec, - HlsUrl hlsUrl, List muxedCaptionFormats, int trackSelectionReason, - Object trackSelectionData, long startTimeUs, long endTimeUs, int chunkIndex, - int discontinuitySequenceNumber, boolean isMasterTimestampSource, + public HlsMediaChunk(HlsExtractorFactory extractorFactory, DataSource dataSource, + DataSpec dataSpec, DataSpec initDataSpec, HlsUrl hlsUrl, List muxedCaptionFormats, + int trackSelectionReason, Object trackSelectionData, long startTimeUs, long endTimeUs, + int chunkIndex, int discontinuitySequenceNumber, boolean isMasterTimestampSource, TimestampAdjuster timestampAdjuster, HlsMediaChunk previousChunk, DrmInitData drmInitData, byte[] fullSegmentEncryptionKey, byte[] encryptionIv) { super(buildDataSource(dataSource, fullSegmentEncryptionKey, encryptionIv), dataSpec, @@ -136,33 +118,34 @@ import java.util.concurrent.atomic.AtomicInteger; this.discontinuitySequenceNumber = discontinuitySequenceNumber; this.initDataSpec = initDataSpec; this.hlsUrl = hlsUrl; - this.muxedCaptionFormats = muxedCaptionFormats; this.isMasterTimestampSource = isMasterTimestampSource; this.timestampAdjuster = timestampAdjuster; // Note: this.dataSource and dataSource may be different. this.isEncrypted = this.dataSource instanceof Aes128DataSource; - this.drmInitData = drmInitData; - lastPathSegment = dataSpec.uri.getLastPathSegment(); - isPackedAudio = lastPathSegment.endsWith(AAC_FILE_EXTENSION) - || lastPathSegment.endsWith(AC3_FILE_EXTENSION) - || lastPathSegment.endsWith(EC3_FILE_EXTENSION) - || lastPathSegment.endsWith(MP3_FILE_EXTENSION); + Extractor previousExtractor = null; if (previousChunk != null) { - id3Decoder = previousChunk.id3Decoder; - id3Data = previousChunk.id3Data; - previousExtractor = previousChunk.extractor; shouldSpliceIn = previousChunk.hlsUrl != hlsUrl; - needNewExtractor = previousChunk.discontinuitySequenceNumber != discontinuitySequenceNumber - || shouldSpliceIn; + previousExtractor = previousChunk.discontinuitySequenceNumber != discontinuitySequenceNumber + || shouldSpliceIn ? null : previousChunk.extractor; } else { - id3Decoder = isPackedAudio ? new Id3Decoder() : null; - id3Data = isPackedAudio ? new ParsableByteArray(Id3Decoder.ID3_HEADER_LENGTH) : null; - previousExtractor = null; shouldSpliceIn = false; - needNewExtractor = true; + } + Pair extractorData = extractorFactory.createExtractor(previousExtractor, + dataSpec.uri, trackFormat, muxedCaptionFormats, drmInitData, timestampAdjuster); + extractor = extractorData.first; + isPackedAudioExtractor = extractorData.second; + reusingExtractor = extractor == previousExtractor; + initLoadCompleted = reusingExtractor && initDataSpec != null; + if (isPackedAudioExtractor) { + id3Decoder = previousChunk != null ? previousChunk.id3Decoder : new Id3Decoder(); + id3Data = previousChunk != null ? previousChunk.id3Data + : new ParsableByteArray(Id3Decoder.ID3_HEADER_LENGTH); + } else { + id3Decoder = null; + id3Data = null; } initDataSource = dataSource; - uid = UID_SOURCE.getAndIncrement(); + uid = uidSource.getAndIncrement(); } /** @@ -172,8 +155,11 @@ import java.util.concurrent.atomic.AtomicInteger; * @param output The output that will receive the loaded samples. */ public void init(HlsSampleStreamWrapper output) { - extractorOutput = output; + this.output = output; output.init(uid, shouldSpliceIn); + if (!reusingExtractor) { + extractor.init(output); + } } @Override @@ -200,10 +186,6 @@ import java.util.concurrent.atomic.AtomicInteger; @Override public void load() throws IOException, InterruptedException { - if (extractor == null && !isPackedAudio) { - // See HLS spec, version 20, Section 3.4 for more information on packed audio extraction. - extractor = createExtractor(); - } maybeLoadInitData(); if (!loadCanceled) { loadMedia(); @@ -213,8 +195,8 @@ import java.util.concurrent.atomic.AtomicInteger; // Internal loading methods. private void maybeLoadInitData() throws IOException, InterruptedException { - if (previousExtractor == extractor || initLoadCompleted || initDataSpec == null) { - // According to spec, for packed audio, initDataSpec is expected to be null. + if (initLoadCompleted || initDataSpec == null) { + // Note: The HLS spec forbids initialization segments for packed audio. return; } DataSpec initSegmentDataSpec = initDataSpec.subrange(initSegmentBytesLoaded); @@ -258,10 +240,10 @@ import java.util.concurrent.atomic.AtomicInteger; try { ExtractorInput input = new DefaultExtractorInput(dataSource, loadDataSpec.absoluteStreamPosition, dataSource.open(loadDataSpec)); - if (extractor == null) { - // Media segment format is packed audio. + if (isPackedAudioExtractor && !id3TimestampPeeked) { long id3Timestamp = peekId3PrivTimestamp(input); - extractor = buildPackedAudioExtractor(id3Timestamp != C.TIME_UNSET + id3TimestampPeeked = true; + output.setSampleOffsetUs(id3Timestamp != C.TIME_UNSET ? timestampAdjuster.adjustTsTimestamp(id3Timestamp) : startTimeUs); } if (skipLoadedBytes) { @@ -345,68 +327,4 @@ import java.util.concurrent.atomic.AtomicInteger; return dataSource; } - private Extractor createExtractor() { - // Select the extractor that will read the chunk. - Extractor extractor; - boolean usingNewExtractor = true; - if (MimeTypes.TEXT_VTT.equals(hlsUrl.format.sampleMimeType) - || lastPathSegment.endsWith(WEBVTT_FILE_EXTENSION) - || lastPathSegment.endsWith(VTT_FILE_EXTENSION)) { - extractor = new WebvttExtractor(trackFormat.language, timestampAdjuster); - } else if (!needNewExtractor) { - // Only reuse TS and fMP4 extractors. - usingNewExtractor = false; - extractor = previousExtractor; - } else if (lastPathSegment.endsWith(MP4_FILE_EXTENSION) - || lastPathSegment.startsWith(M4_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 4)) { - extractor = new FragmentedMp4Extractor(0, timestampAdjuster, null, drmInitData); - } else { - // MPEG-2 TS segments, but we need a new extractor. - // This flag ensures the change of pid between streams does not affect the sample queues. - @DefaultTsPayloadReaderFactory.Flags - int esReaderFactoryFlags = DefaultTsPayloadReaderFactory.FLAG_IGNORE_SPLICE_INFO_STREAM; - List closedCaptionFormats = muxedCaptionFormats; - if (closedCaptionFormats != null) { - // The playlist declares closed caption renditions, we should ignore descriptors. - esReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_OVERRIDE_CAPTION_DESCRIPTORS; - } else { - closedCaptionFormats = Collections.emptyList(); - } - String codecs = trackFormat.codecs; - if (!TextUtils.isEmpty(codecs)) { - // Sometimes AAC and H264 streams are declared in TS chunks even though they don't really - // exist. If we know from the codec attribute that they don't exist, then we can - // explicitly ignore them even if they're declared. - if (!MimeTypes.AUDIO_AAC.equals(MimeTypes.getAudioMediaMimeType(codecs))) { - esReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_IGNORE_AAC_STREAM; - } - if (!MimeTypes.VIDEO_H264.equals(MimeTypes.getVideoMediaMimeType(codecs))) { - esReaderFactoryFlags |= DefaultTsPayloadReaderFactory.FLAG_IGNORE_H264_STREAM; - } - } - extractor = new TsExtractor(TsExtractor.MODE_HLS, timestampAdjuster, - new DefaultTsPayloadReaderFactory(esReaderFactoryFlags, closedCaptionFormats)); - } - if (usingNewExtractor) { - extractor.init(extractorOutput); - } - return extractor; - } - - private Extractor buildPackedAudioExtractor(long startTimeUs) { - Extractor extractor; - if (lastPathSegment.endsWith(AAC_FILE_EXTENSION)) { - extractor = new AdtsExtractor(startTimeUs); - } else if (lastPathSegment.endsWith(AC3_FILE_EXTENSION) - || lastPathSegment.endsWith(EC3_FILE_EXTENSION)) { - extractor = new Ac3Extractor(startTimeUs); - } else if (lastPathSegment.endsWith(MP3_FILE_EXTENSION)) { - extractor = new Mp3Extractor(0, startTimeUs); - } else { - throw new IllegalArgumentException("Unknown extension for audio file: " + lastPathSegment); - } - extractor.init(extractorOutput); - return extractor; - } - } diff --git a/library/hls/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 index 003b38efef..ea9e52e62e 100644 --- a/library/hls/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 @@ -44,6 +44,7 @@ import java.util.List; public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper.Callback, HlsPlaylistTracker.PlaylistEventListener { + private final HlsExtractorFactory extractorFactory; private final HlsPlaylistTracker playlistTracker; private final HlsDataSourceFactory dataSourceFactory; private final int minLoadableRetryCount; @@ -60,8 +61,10 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper private HlsSampleStreamWrapper[] enabledSampleStreamWrappers; private CompositeSequenceableLoader sequenceableLoader; - public HlsMediaPeriod(HlsPlaylistTracker playlistTracker, HlsDataSourceFactory dataSourceFactory, - int minLoadableRetryCount, EventDispatcher eventDispatcher, Allocator allocator) { + public HlsMediaPeriod(HlsExtractorFactory extractorFactory, HlsPlaylistTracker playlistTracker, + HlsDataSourceFactory dataSourceFactory, int minLoadableRetryCount, + EventDispatcher eventDispatcher, Allocator allocator) { + this.extractorFactory = extractorFactory; this.playlistTracker = playlistTracker; this.dataSourceFactory = dataSourceFactory; this.minLoadableRetryCount = minLoadableRetryCount; @@ -344,8 +347,8 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper private HlsSampleStreamWrapper buildSampleStreamWrapper(int trackType, HlsUrl[] variants, Format muxedAudioFormat, List muxedCaptionFormats, long positionUs) { - HlsChunkSource defaultChunkSource = new HlsChunkSource(playlistTracker, variants, - dataSourceFactory, timestampAdjusterProvider, muxedCaptionFormats); + HlsChunkSource defaultChunkSource = new HlsChunkSource(extractorFactory, playlistTracker, + variants, dataSourceFactory, timestampAdjusterProvider, muxedCaptionFormats); return new HlsSampleStreamWrapper(trackType, this, defaultChunkSource, allocator, positionUs, muxedAudioFormat, minLoadableRetryCount, eventDispatcher); } diff --git a/library/hls/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 index 10a0536612..f7f26bb37c 100644 --- a/library/hls/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 @@ -20,6 +20,7 @@ import android.os.Handler; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; +import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener; import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.MediaPeriod; @@ -51,6 +52,7 @@ public final class HlsMediaSource implements MediaSource, */ public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 3; + private final HlsExtractorFactory extractorFactory; private final Uri manifestUri; private final HlsDataSourceFactory dataSourceFactory; private final int minLoadableRetryCount; @@ -60,32 +62,57 @@ public final class HlsMediaSource implements MediaSource, private HlsPlaylistTracker playlistTracker; private Listener sourceListener; + /** + * @param manifestUri The {@link Uri} of the HLS manifest. + * @param dataSourceFactory An {@link HlsDataSourceFactory} for {@link DataSource}s for manifests, + * segments and keys. + * @param eventHandler A handler for events. May be null if delivery of events is not required. + * @param eventListener An {@link AdaptiveMediaSourceEventListener}. May be null if delivery of + * events is not required. + */ public HlsMediaSource(Uri manifestUri, DataSource.Factory dataSourceFactory, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { this(manifestUri, dataSourceFactory, DEFAULT_MIN_LOADABLE_RETRY_COUNT, eventHandler, eventListener); } + /** + * @param manifestUri The {@link Uri} of the HLS manifest. + * @param dataSourceFactory An {@link HlsDataSourceFactory} for {@link DataSource}s for manifests, + * segments and keys. + * @param minLoadableRetryCount The minimum number of times loads must be retried before + * errors are propagated. + * @param eventHandler A handler for events. May be null if delivery of events is not required. + * @param eventListener An {@link AdaptiveMediaSourceEventListener}. May be null if delivery of + * events is not required. + */ public HlsMediaSource(Uri manifestUri, DataSource.Factory dataSourceFactory, int minLoadableRetryCount, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { - this(manifestUri, new DefaultHlsDataSourceFactory(dataSourceFactory), minLoadableRetryCount, - eventHandler, eventListener); + this(manifestUri, new DefaultHlsDataSourceFactory(dataSourceFactory), + HlsExtractorFactory.DEFAULT, minLoadableRetryCount, eventHandler, eventListener, + new HlsPlaylistParser()); } + /** + * @param manifestUri The {@link Uri} of the HLS manifest. + * @param dataSourceFactory An {@link HlsDataSourceFactory} for {@link DataSource}s for manifests, + * segments and keys. + * @param extractorFactory An {@link HlsExtractorFactory} for {@link Extractor}s for the segments. + * @param minLoadableRetryCount The minimum number of times loads must be retried before + * errors are propagated. + * @param eventHandler A handler for events. May be null if delivery of events is not required. + * @param eventListener An {@link AdaptiveMediaSourceEventListener}. May be null if delivery of + * events is not required. + * @param playlistParser A {@link ParsingLoadable.Parser} for HLS playlists. + */ public HlsMediaSource(Uri manifestUri, HlsDataSourceFactory dataSourceFactory, - int minLoadableRetryCount, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { - this(manifestUri, dataSourceFactory, minLoadableRetryCount, eventHandler, eventListener, - new HlsPlaylistParser()); - } - - public HlsMediaSource(Uri manifestUri, HlsDataSourceFactory dataSourceFactory, - int minLoadableRetryCount, Handler eventHandler, + HlsExtractorFactory extractorFactory, int minLoadableRetryCount, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener, - ParsingLoadable.Parser playlistParser) { + ParsingLoadable.Parser playlistParser) { this.manifestUri = manifestUri; this.dataSourceFactory = dataSourceFactory; + this.extractorFactory = extractorFactory; this.minLoadableRetryCount = minLoadableRetryCount; this.playlistParser = playlistParser; eventDispatcher = new EventDispatcher(eventHandler, eventListener); @@ -108,8 +135,8 @@ public final class HlsMediaSource implements MediaSource, @Override public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { Assertions.checkArgument(id.periodIndex == 0); - return new HlsMediaPeriod(playlistTracker, dataSourceFactory, minLoadableRetryCount, - eventDispatcher, allocator); + return new HlsMediaPeriod(extractorFactory, playlistTracker, dataSourceFactory, + minLoadableRetryCount, eventDispatcher, allocator); } @Override diff --git a/library/hls/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 index b844988588..946ae24d17 100644 --- a/library/hls/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 @@ -103,6 +103,7 @@ import java.util.LinkedList; private boolean[] trackGroupEnabledStates; private boolean[] trackGroupIsAudioVideoFlags; + private long sampleOffsetUs; private long lastSeekPositionUs; private long pendingResetPositionUs; private boolean pendingResetUpstreamFormats; @@ -369,16 +370,16 @@ import java.util.LinkedList; // SampleStream implementation. - /* package */ boolean isReady(int trackGroupIndex) { + public boolean isReady(int trackGroupIndex) { return loadingFinished || (!isPendingReset() && sampleQueues[trackGroupIndex].hasNextSample()); } - /* package */ void maybeThrowError() throws IOException { + public void maybeThrowError() throws IOException { loader.maybeThrowError(); chunkSource.maybeThrowError(); } - /* package */ int readData(int trackGroupIndex, FormatHolder formatHolder, + public int readData(int trackGroupIndex, FormatHolder formatHolder, DecoderInputBuffer buffer, boolean requireFormat) { if (isPendingReset()) { return C.RESULT_NOTHING_READ; @@ -402,7 +403,7 @@ import java.util.LinkedList; lastSeekPositionUs); } - /* package */ int skipData(int trackGroupIndex, long positionUs) { + public int skipData(int trackGroupIndex, long positionUs) { SampleQueue sampleQueue = sampleQueues[trackGroupIndex]; if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) { return sampleQueue.advanceToEnd(); @@ -573,6 +574,7 @@ import java.util.LinkedList; } } SampleQueue trackOutput = new SampleQueue(allocator); + trackOutput.setSampleOffsetUs(sampleOffsetUs); trackOutput.setUpstreamFormatChangeListener(this); sampleQueueTrackIds = Arrays.copyOf(sampleQueueTrackIds, trackCount + 1); sampleQueueTrackIds[trackCount] = id; @@ -599,6 +601,15 @@ import java.util.LinkedList; handler.post(maybeFinishPrepareRunnable); } + // Called by the loading thread. + + public void setSampleOffsetUs(long sampleOffsetUs) { + this.sampleOffsetUs = sampleOffsetUs; + for (SampleQueue sampleQueue : sampleQueues) { + sampleQueue.setSampleOffsetUs(sampleOffsetUs); + } + } + // Internal methods. private void maybeFinishPrepare() { diff --git a/library/hls/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 index da73aa3996..355a8575ca 100644 --- a/library/hls/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 @@ -134,8 +134,9 @@ public final class HlsPlaylistTracker implements Loader.Callback Date: Thu, 19 Oct 2017 06:30:50 -0700 Subject: [PATCH 0567/2472] Fix positions passed to TrackSelection - When transitioning to a new period, the value of bufferedDurationUs passed to TrackSelection.updateSelectedTrack was incorrectly set to 0. It should have been set to correctly reflect buffered media in previous periods still being played out. - This change fixes the issue described above, and also propagates the playback position through to this method. The position of the next load within the period can be calculated by adding the position and bufferedDurationUs. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=172736101 --- .../exoplayer2/source/MediaPeriod.java | 4 +-- .../exoplayer2/source/SequenceableLoader.java | 4 +-- .../source/chunk/ChunkSampleStream.java | 13 ++++++-- .../exoplayer2/source/chunk/ChunkSource.java | 13 +++++--- .../AdaptiveTrackSelection.java | 3 +- .../trackselection/FixedTrackSelection.java | 3 +- .../trackselection/RandomTrackSelection.java | 3 +- .../trackselection/TrackSelection.java | 30 +++++++++++++------ .../source/dash/DefaultDashChunkSource.java | 9 +++--- .../exoplayer2/source/hls/HlsChunkSource.java | 24 +++++++++------ .../source/hls/HlsSampleStreamWrapper.java | 16 +++++++--- .../smoothstreaming/DefaultSsChunkSource.java | 9 +++--- .../exoplayer2/testutil/FakeChunkSource.java | 7 +++-- 13 files changed, 91 insertions(+), 47 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java index 7d16f794cd..211da1d666 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java @@ -177,8 +177,8 @@ public interface MediaPeriod extends SequenceableLoader { * and after preparation. * * @param positionUs The current playback position in microseconds. If playback of this period has - * not yet started, the value will be the starting position minus the duration of any media in - * previous periods still to be played. + * not yet started, the value will be the starting position in this period minus the duration + * of any media in previous periods still to be played. * @return True if progress was made, meaning that {@link #getNextLoadPositionUs()} will return * a different value than prior to the call. False otherwise. */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SequenceableLoader.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SequenceableLoader.java index e40ceab976..6daa1e847a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SequenceableLoader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SequenceableLoader.java @@ -53,8 +53,8 @@ public interface SequenceableLoader { * Attempts to continue loading. * * @param positionUs The current playback position in microseconds. If playback of the period to - * which this loader belongs has not yet started, the value will be the period's starting - * position minus the duration of any media in previous periods still to be played. + * which this loader belongs has not yet started, the value will be the starting position + * in the period minus the duration of any media in previous periods still to be played. * @return True if progress was made, meaning that {@link #getNextLoadPositionUs()} will return * a different value than prior to the call. False otherwise. */ diff --git a/library/core/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 index f2609a0ffd..9d5a405c2f 100644 --- a/library/core/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 @@ -351,9 +351,16 @@ public class ChunkSampleStream implements SampleStream, S return false; } - chunkSource.getNextChunk(mediaChunks.isEmpty() ? null : mediaChunks.getLast(), - pendingResetPositionUs != C.TIME_UNSET ? pendingResetPositionUs : positionUs, - nextChunkHolder); + MediaChunk previousChunk; + long loadPositionUs; + if (isPendingReset()) { + previousChunk = null; + loadPositionUs = pendingResetPositionUs; + } else { + previousChunk = mediaChunks.getLast(); + loadPositionUs = previousChunk.endTimeUs; + } + chunkSource.getNextChunk(previousChunk, positionUs, loadPositionUs, nextChunkHolder); boolean endOfStream = nextChunkHolder.endOfStream; Chunk loadable = nextChunkHolder.chunk; nextChunkHolder.clear(); diff --git a/library/core/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 index 00865822e1..6dffc457d6 100644 --- a/library/core/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 @@ -54,12 +54,17 @@ public interface ChunkSource { * end of the stream has not been reached, the {@link ChunkHolder} is not modified. * * @param previous The most recently loaded media chunk. - * @param playbackPositionUs The current playback position. If {@code previous} is null then this - * parameter is the position from which playback is expected to start (or restart) and hence - * should be interpreted as a seek position. + * @param playbackPositionUs The current playback position in microseconds. If playback of the + * period to which this chunk source belongs has not yet started, the value will be the + * starting position in the period minus the duration of any media in previous periods still + * to be played. + * @param loadPositionUs The current load position in microseconds. If {@code previous} is null, + * this is the starting position from which chunks should be provided. Else it's equal to + * {@code previous.endTimeUs}. * @param out A holder to populate. */ - void getNextChunk(MediaChunk previous, long playbackPositionUs, ChunkHolder out); + void getNextChunk(MediaChunk previous, long playbackPositionUs, long loadPositionUs, + ChunkHolder out); /** * Called when the {@link ChunkSampleStream} has finished loading a chunk obtained from this diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java index b999164f00..f9eddab286 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java @@ -201,7 +201,8 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { } @Override - public void updateSelectedTrack(long bufferedDurationUs, long availableDurationUs) { + public void updateSelectedTrack(long playbackPositionUs, long bufferedDurationUs, + long availableDurationUs) { long nowMs = SystemClock.elapsedRealtime(); // Stash the current selection, then make a new one. int currentSelectedIndex = selectedIndex; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/FixedTrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/FixedTrackSelection.java index ca43258e3f..50873d372d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/FixedTrackSelection.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/FixedTrackSelection.java @@ -78,7 +78,8 @@ public final class FixedTrackSelection extends BaseTrackSelection { } @Override - public void updateSelectedTrack(long bufferedDurationUs, long availableDurationUs) { + public void updateSelectedTrack(long playbackPositionUs, long bufferedDurationUs, + long availableDurationUs) { // Do nothing. } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/RandomTrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/RandomTrackSelection.java index b70cc8e0d5..d11344a6f6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/RandomTrackSelection.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/RandomTrackSelection.java @@ -88,7 +88,8 @@ public final class RandomTrackSelection extends BaseTrackSelection { } @Override - public void updateSelectedTrack(long bufferedDurationUs, long availableDurationUs) { + public void updateSelectedTrack(long playbackPositionUs, long bufferedDurationUs, + long availableDurationUs) { // Count the number of non-blacklisted formats. long nowMs = SystemClock.elapsedRealtime(); int nonBlacklistedFormatCount = 0; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java index aeb1d1d6e3..ad02b6c775 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java @@ -26,7 +26,7 @@ import java.util.List; * {@link TrackGroup}, and a possibly varying individual selected track from the subset. *

          * Tracks belonging to the subset are exposed in decreasing bandwidth order. The individual selected - * track may change as a result of calling {@link #updateSelectedTrack(long, long)}. + * track may change as a result of calling {@link #updateSelectedTrack(long, long, long)}. */ public interface TrackSelection { @@ -125,12 +125,21 @@ public interface TrackSelection { /** * Updates the selected track. * - * @param bufferedDurationUs The duration of media currently buffered in microseconds. + * @param playbackPositionUs The current playback position in microseconds. If playback of the + * period to which this track selection belongs has not yet started, the value will be the + * starting position in the period minus the duration of any media in previous periods still + * to be played. + * @param bufferedDurationUs The duration of media currently buffered from the current playback + * position, in microseconds. Note that the next load position can be calculated as + * {@code (playbackPositionUs + bufferedDurationUs)}. * @param availableDurationUs The duration of media available for buffering from the current * playback position, in microseconds, or {@link C#TIME_UNSET} if media can be buffered - * to the end of the current period. + * to the end of the current period. Note that if not set to {@link C#TIME_UNSET}, the + * position up to which media is available for buffering can be calculated as + * {@code (playbackPositionUs + availableDurationUs)}. */ - void updateSelectedTrack(long bufferedDurationUs, long availableDurationUs); + void updateSelectedTrack(long playbackPositionUs, long bufferedDurationUs, + long availableDurationUs); /** * May be called periodically by sources that load media in discrete {@link MediaChunk}s and @@ -143,7 +152,10 @@ public interface TrackSelection { * significantly higher quality. Discarding chunks may allow faster switching to a higher quality * track in this case. * - * @param playbackPositionUs The current playback position in microseconds. + * @param playbackPositionUs The current playback position in microseconds. If playback of the + * period to which this track selection belongs has not yet started, the value will be the + * starting position in the period minus the duration of any media in previous periods still + * to be played. * @param queue The queue of buffered {@link MediaChunk}s. Must not be modified. * @return The number of chunks to retain in the queue. */ @@ -151,10 +163,10 @@ public interface TrackSelection { /** * Attempts to blacklist the track at the specified index in the selection, making it ineligible - * for selection by calls to {@link #updateSelectedTrack(long, long)} for the specified period of - * time. Blacklisting will fail if all other tracks are currently blacklisted. If blacklisting the - * currently selected track, note that it will remain selected until the next call to - * {@link #updateSelectedTrack(long, long)}. + * for selection by calls to {@link #updateSelectedTrack(long, long, long)} for the specified + * period of time. Blacklisting will fail if all other tracks are currently blacklisted. If + * blacklisting the currently selected track, note that it will remain selected until the next + * call to {@link #updateSelectedTrack(long, long, long)}. * * @param index The index of the track in the selection. * @param blacklistDurationMs The duration of time for which the track should be blacklisted, in diff --git a/library/dash/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 index bb798b0af9..1eac1b5616 100644 --- a/library/dash/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 @@ -175,14 +175,15 @@ public class DefaultDashChunkSource implements DashChunkSource { } @Override - public void getNextChunk(MediaChunk previous, long playbackPositionUs, ChunkHolder out) { + public void getNextChunk(MediaChunk previous, long playbackPositionUs, long loadPositionUs, + ChunkHolder out) { if (fatalError != null) { return; } - long bufferedDurationUs = previous != null ? (previous.endTimeUs - playbackPositionUs) : 0; + long bufferedDurationUs = loadPositionUs - playbackPositionUs; long timeToLiveEdgeUs = resolveTimeToLiveEdgeUs(playbackPositionUs); - trackSelection.updateSelectedTrack(bufferedDurationUs, timeToLiveEdgeUs); + trackSelection.updateSelectedTrack(playbackPositionUs, bufferedDurationUs, timeToLiveEdgeUs); RepresentationHolder representationHolder = representationHolders[trackSelection.getSelectedIndex()]; @@ -237,7 +238,7 @@ public class DefaultDashChunkSource implements DashChunkSource { int segmentNum; if (previous == null) { - segmentNum = Util.constrainValue(representationHolder.getSegmentNum(playbackPositionUs), + segmentNum = Util.constrainValue(representationHolder.getSegmentNum(loadPositionUs), firstAvailableSegmentNum, lastAvailableSegmentNum); } else { segmentNum = previous.getNextChunkIndex(); diff --git a/library/hls/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 index b8a0c3ddb7..2b1ece4eee 100644 --- a/library/hls/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 @@ -204,17 +204,22 @@ import java.util.List; * contain the {@link HlsUrl} that refers to the playlist that needs refreshing. * * @param previous The most recently loaded media chunk. - * @param playbackPositionUs The current playback position. If {@code previous} is null then this - * parameter is the position from which playback is expected to start (or restart) and hence - * should be interpreted as a seek position. + * @param playbackPositionUs The current playback position in microseconds. If playback of the + * period to which this chunk source belongs has not yet started, the value will be the + * starting position in the period minus the duration of any media in previous periods still + * to be played. + * @param loadPositionUs The current load position in microseconds. If {@code previous} is null, + * this is the starting position from which chunks should be provided. Else it's equal to + * {@code previous.endTimeUs}. * @param out A holder to populate. */ - public void getNextChunk(HlsMediaChunk previous, long playbackPositionUs, HlsChunkHolder out) { + public void getNextChunk(HlsMediaChunk previous, long playbackPositionUs, long loadPositionUs, + HlsChunkHolder out) { int oldVariantIndex = previous == null ? C.INDEX_UNSET : trackGroup.indexOf(previous.trackFormat); expectedPlaylistUrl = null; - long bufferedDurationUs = previous == null ? 0 : previous.endTimeUs - playbackPositionUs; + long bufferedDurationUs = loadPositionUs - playbackPositionUs; long timeToLiveEdgeUs = resolveTimeToLiveEdgeUs(playbackPositionUs); if (previous != null && !independentSegments) { // Unless segments are known to be independent, switching variant will require downloading @@ -231,7 +236,7 @@ import java.util.List; } // Select the variant. - trackSelection.updateSelectedTrack(bufferedDurationUs, timeToLiveEdgeUs); + trackSelection.updateSelectedTrack(playbackPositionUs, bufferedDurationUs, timeToLiveEdgeUs); int selectedVariantIndex = trackSelection.getSelectedIndexInTrackGroup(); boolean switchingVariant = oldVariantIndex != selectedVariantIndex; @@ -250,8 +255,8 @@ import java.util.List; // Select the chunk. int chunkMediaSequence; if (previous == null || switchingVariant) { - long targetPositionUs = previous == null ? playbackPositionUs - : independentSegments ? previous.endTimeUs : previous.startTimeUs; + long targetPositionUs = (previous == null || independentSegments) ? loadPositionUs + : previous.startTimeUs; if (!mediaPlaylist.hasEndTag && targetPositionUs >= mediaPlaylist.getEndTimeUs()) { // If the playlist is too old to contain the chunk, we need to refresh it. chunkMediaSequence = mediaPlaylist.mediaSequence + mediaPlaylist.segments.size(); @@ -437,7 +442,8 @@ import java.util.List; } @Override - public void updateSelectedTrack(long bufferedDurationUs, long availableDurationUs) { + public void updateSelectedTrack(long playbackPositionUs, long bufferedDurationUs, + long availableDurationUs) { long nowMs = SystemClock.elapsedRealtime(); if (!isBlacklisted(selectedIndex, nowMs)) { return; diff --git a/library/hls/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 index 946ae24d17..3eae83624b 100644 --- a/library/hls/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 @@ -255,7 +255,8 @@ import java.util.LinkedList; // may need to be discarded. boolean primarySampleQueueDirty = false; if (!seenFirstTrackSelection) { - primaryTrackSelection.updateSelectedTrack(0, C.TIME_UNSET); + long bufferedDurationUs = positionUs < 0 ? -positionUs : 0; + primaryTrackSelection.updateSelectedTrack(positionUs, bufferedDurationUs, C.TIME_UNSET); int chunkIndex = chunkSource.getTrackGroup().indexOf(mediaChunks.getLast().trackFormat); if (primaryTrackSelection.getSelectedIndexInTrackGroup() != chunkIndex) { // This is the first selection and the chunk loaded during preparation does not match @@ -438,9 +439,16 @@ import java.util.LinkedList; return false; } - chunkSource.getNextChunk(mediaChunks.isEmpty() ? null : mediaChunks.getLast(), - pendingResetPositionUs != C.TIME_UNSET ? pendingResetPositionUs : positionUs, - nextChunkHolder); + HlsMediaChunk previousChunk; + long loadPositionUs; + if (isPendingReset()) { + previousChunk = null; + loadPositionUs = pendingResetPositionUs; + } else { + previousChunk = mediaChunks.getLast(); + loadPositionUs = previousChunk.endTimeUs; + } + chunkSource.getNextChunk(previousChunk, positionUs, loadPositionUs, nextChunkHolder); boolean endOfStream = nextChunkHolder.endOfStream; Chunk loadable = nextChunkHolder.chunk; HlsMasterPlaylist.HlsUrl playlistToLoad = nextChunkHolder.playlist; diff --git a/library/smoothstreaming/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 index d9743649c5..5a6493b702 100644 --- a/library/smoothstreaming/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 @@ -149,7 +149,8 @@ public class DefaultSsChunkSource implements SsChunkSource { } @Override - public final void getNextChunk(MediaChunk previous, long playbackPositionUs, ChunkHolder out) { + public final void getNextChunk(MediaChunk previous, long playbackPositionUs, long loadPositionUs, + ChunkHolder out) { if (fatalError != null) { return; } @@ -163,7 +164,7 @@ public class DefaultSsChunkSource implements SsChunkSource { int chunkIndex; if (previous == null) { - chunkIndex = streamElement.getChunkIndex(playbackPositionUs); + chunkIndex = streamElement.getChunkIndex(loadPositionUs); } else { chunkIndex = previous.getNextChunkIndex() - currentManifestChunkOffset; if (chunkIndex < 0) { @@ -179,9 +180,9 @@ public class DefaultSsChunkSource implements SsChunkSource { return; } - long bufferedDurationUs = previous != null ? (previous.endTimeUs - playbackPositionUs) : 0; + long bufferedDurationUs = loadPositionUs - playbackPositionUs; long timeToLiveEdgeUs = resolveTimeToLiveEdgeUs(playbackPositionUs); - trackSelection.updateSelectedTrack(bufferedDurationUs, timeToLiveEdgeUs); + trackSelection.updateSelectedTrack(playbackPositionUs, bufferedDurationUs, timeToLiveEdgeUs); long chunkStartTimeUs = streamElement.getStartTimeUs(chunkIndex); long chunkEndTimeUs = chunkStartTimeUs + streamElement.getChunkDurationUs(chunkIndex); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeChunkSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeChunkSource.java index 40c91a5a81..28f5926bfa 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeChunkSource.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeChunkSource.java @@ -82,9 +82,10 @@ public final class FakeChunkSource implements ChunkSource { } @Override - public void getNextChunk(MediaChunk previous, long playbackPositionUs, ChunkHolder out) { - long bufferedDurationUs = previous != null ? (previous.endTimeUs - playbackPositionUs) : 0; - trackSelection.updateSelectedTrack(bufferedDurationUs, C.TIME_UNSET); + public void getNextChunk(MediaChunk previous, long playbackPositionUs, long loadPositionUs, + ChunkHolder out) { + long bufferedDurationUs = loadPositionUs - playbackPositionUs; + trackSelection.updateSelectedTrack(playbackPositionUs, bufferedDurationUs, C.TIME_UNSET); int chunkIndex = previous == null ? dataSet.getChunkIndexByPosition(playbackPositionUs) : previous.getNextChunkIndex(); if (chunkIndex >= dataSet.getChunkCount()) { From 08706f9bfb8d5283d019f55515795677aecb990d Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 19 Oct 2017 06:34:02 -0700 Subject: [PATCH 0568/2472] Retain playback position on re-preparation ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=172736350 --- .../com/google/android/exoplayer2/ExoPlayerImplInternal.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 79c92aeb67..40268653b0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -443,6 +443,10 @@ import java.io.IOException; loadControl.onPrepared(); if (resetPosition) { playbackInfo = new PlaybackInfo(0, C.TIME_UNSET); + } else { + // The new start position is the current playback position. + playbackInfo = new PlaybackInfo(playbackInfo.periodId, playbackInfo.positionUs, + playbackInfo.contentPositionUs); } this.mediaSource = mediaSource; mediaSource.prepareSource(player, true, this); From bb3dea5191088ce13e82f2b614f58f713f38a580 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 19 Oct 2017 06:39:00 -0700 Subject: [PATCH 0569/2472] Remove USE_CHORD_PITCH We have been using USE_CHORD_PITCH == false for a while and the quality of pitch changes seems fine. It's now possible to set the sample rate too, but this only works if USE_CHORD_PITCH is false, so remove the constant. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=172736631 --- .../android/exoplayer2/audio/Sonic.java | 58 +------------------ 1 file changed, 1 insertion(+), 57 deletions(-) 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 index dfd122c397..daab04e4ab 100644 --- 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 @@ -27,7 +27,6 @@ import java.util.Arrays; */ /* 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; @@ -326,32 +325,6 @@ import java.util.Arrays; 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]; short right = in[inPos + numChannels]; @@ -462,11 +435,7 @@ import java.util.Arrays; copyToOutput(inputBuffer, 0, numInputSamples); numInputSamples = 0; } - if (USE_CHORD_PITCH) { - if (pitch != 1.0f) { - adjustPitch(originalNumOutputSamples); - } - } else if (r != 1.0f) { + if (r != 1.0f) { adjustRate(r, originalNumOutputSamples); } } @@ -486,29 +455,4 @@ import java.util.Arrays; } } - 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; - } - } - } - } From 7d0ec68d86dbb0a64365d3ab12fd497c03ed8b4e Mon Sep 17 00:00:00 2001 From: eguven Date: Thu, 19 Oct 2017 07:35:51 -0700 Subject: [PATCH 0570/2472] Put DownloadTasks on hold until preceding conflicting tasks are complete Tasks conflict if both of them work on the same media and at least one of them is remove action. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=172741795 --- .../offline/DownloadManagerTest.java | 352 ++++++++++++++++++ .../exoplayer2/offline/DownloadException.java | 5 + .../exoplayer2/util/ConditionVariable.java | 21 +- 3 files changed, 376 insertions(+), 2 deletions(-) create mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java new file mode 100644 index 0000000000..cfc5e1593e --- /dev/null +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java @@ -0,0 +1,352 @@ +/* + * 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.offline; + +import android.os.ConditionVariable; +import android.support.annotation.Nullable; +import android.test.InstrumentationTestCase; +import com.google.android.exoplayer2.offline.DownloadManager.DownloadListener; +import com.google.android.exoplayer2.offline.DownloadManager.DownloadTask; +import com.google.android.exoplayer2.offline.DownloadManager.DownloadTask.State; +import com.google.android.exoplayer2.upstream.DummyDataSource; +import com.google.android.exoplayer2.upstream.cache.Cache; +import com.google.android.exoplayer2.util.ClosedSource; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.concurrent.Executors; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +/** Tests {@link DownloadManager}. */ +@ClosedSource(reason = "Not ready yet") +public class DownloadManagerTest extends InstrumentationTestCase { + + /* Used to check if condition becomes true in this time interval. */ + private static final int ASSERT_TRUE_TIMEOUT = 1000; + /* Used to check if condition stays false for this time interval. */ + private static final int ASSERT_FALSE_TIME = 1000; + + private DownloadManager downloadManager; + private ConditionVariable downloadFinishedCondition; + private Throwable downloadError; + + @Override + public void setUp() throws Exception { + super.setUp(); + setUpMockito(this); + + downloadManager = new DownloadManager( + new DownloaderConstructorHelper(Mockito.mock(Cache.class), DummyDataSource.FACTORY), + Executors.newCachedThreadPool()); + + downloadFinishedCondition = new ConditionVariable(); + downloadManager.setListener(new TestDownloadListener()); + } + + class TestDownloadListener implements DownloadListener { + @Override + public void onStateChange(DownloadManager downloadManager, DownloadTask downloadTask, int state, + Throwable error) { + if (state == DownloadTask.STATE_ERROR && downloadError == null) { + downloadError = error; + } + ((FakeDownloadAction) downloadTask.getDownloadAction()).onStateChange(); + } + + @Override + public void onTasksFinished(DownloadManager downloadManager) { + downloadFinishedCondition.open(); + } + } + + public void testDownloadActionRuns() throws Throwable { + doTestActionRuns(createDownloadAction("media 1")); + } + + public void testRemoveActionRuns() throws Throwable { + doTestActionRuns(createRemoveAction("media 1")); + } + + public void testDifferentMediaDownloadActionsStartInParallel() throws Throwable { + doTestActionsRunInParallel(createDownloadAction("media 1"), + createDownloadAction("media 2")); + } + + public void testDifferentMediaDifferentActionsStartInParallel() throws Throwable { + doTestActionsRunInParallel(createDownloadAction("media 1"), + createRemoveAction("media 2")); + } + + public void testSameMediaDownloadActionsStartInParallel() throws Throwable { + doTestActionsRunInParallel(createDownloadAction("media 1"), + createDownloadAction("media 1")); + } + + public void testSameMediaRemoveActionWaitsDownloadAction() throws Throwable { + doTestActionsRunSequentially(createDownloadAction("media 1"), + createRemoveAction("media 1")); + } + + public void testSameMediaDownloadActionWaitsRemoveAction() throws Throwable { + doTestActionsRunSequentially(createRemoveAction("media 1"), + createDownloadAction("media 1")); + } + + public void testSameMediaRemoveActionWaitsRemoveAction() throws Throwable { + doTestActionsRunSequentially(createRemoveAction("media 1"), + createRemoveAction("media 1")); + } + + public void testSameMediaMultipleActions() throws Throwable { + FakeDownloadAction downloadAction1 = createDownloadAction("media 1").ignoreInterrupts(); + FakeDownloadAction downloadAction2 = createDownloadAction("media 1").ignoreInterrupts(); + FakeDownloadAction removeAction1 = createRemoveAction("media 1"); + FakeDownloadAction downloadAction3 = createDownloadAction("media 1"); + FakeDownloadAction removeAction2 = createRemoveAction("media 1"); + + // Two download actions run in parallel. + downloadAction1.post().assertStarted(); + downloadAction2.post().assertStarted(); + // removeAction1 is added. It interrupts the two download actions' threads but they are + // configured to ignore it so removeAction1 doesn't start. + removeAction1.post().assertDoesNotStart(); + + // downloadAction2 finishes but it isn't enough to start removeAction1. + downloadAction2.finishAndAssertFinished(); + removeAction1.assertDoesNotStart(); + // downloadAction3 is post to DownloadManager but it waits for removeAction1 to finish. + downloadAction3.post().assertDoesNotStart(); + + // When downloadAction1 finishes, removeAction1 starts. + downloadAction1.finishAndAssertFinished(); + removeAction1.assertStarted(); + // downloadAction3 still waits removeAction1 + downloadAction3.assertDoesNotStart(); + + // removeAction2 is posted. removeAction1 and downloadAction3 is canceled so removeAction2 + // starts immediately. + removeAction2.post().assertStarted().finishAndAssertFinished(); + blockUntilTasksCompleteAndThrowAnyDownloadError(); + } + + public void testMultipleWaitingAction() throws Throwable { + FakeDownloadAction removeAction1 = createRemoveAction("media 1").ignoreInterrupts(); + FakeDownloadAction removeAction2 = createRemoveAction("media 1"); + FakeDownloadAction removeAction3 = createRemoveAction("media 1"); + + removeAction1.post().assertStarted(); + removeAction2.post().assertDoesNotStart(); + removeAction3.post().assertDoesNotStart(); + + removeAction1.finishAndAssertFinished(); + removeAction2.assertTaskState(DownloadTask.STATE_CANCELLED); + removeAction3.assertStarted().finishAndAssertFinished(); + + blockUntilTasksCompleteAndThrowAnyDownloadError(); + } + + private void doTestActionRuns(FakeDownloadAction action) throws Throwable { + action.post().assertStarted().finishAndAssertFinished(); + blockUntilTasksCompleteAndThrowAnyDownloadError(); + } + + private void doTestActionsRunSequentially(FakeDownloadAction action1, + FakeDownloadAction action2) throws Throwable { + action1.ignoreInterrupts().post().assertStarted(); + action2.post().assertDoesNotStart(); + + action1.finishAndAssertFinished(); + action2.assertStarted(); + + action2.finishAndAssertFinished(); + blockUntilTasksCompleteAndThrowAnyDownloadError(); + } + + private void doTestActionsRunInParallel(FakeDownloadAction action1, + FakeDownloadAction action2) throws Throwable { + action1.post().assertStarted(); + action2.post().assertStarted(); + action1.finishAndAssertFinished(); + action2.finishAndAssertFinished(); + blockUntilTasksCompleteAndThrowAnyDownloadError(); + } + + private FakeDownloadAction createDownloadAction(String mediaId) { + return new FakeDownloadAction(downloadManager, mediaId, false); + } + + private FakeDownloadAction createRemoveAction(String mediaId) { + return new FakeDownloadAction(downloadManager, mediaId, true); + } + + private void blockUntilTasksCompleteAndThrowAnyDownloadError() throws Throwable { + assertTrue(downloadFinishedCondition.block(ASSERT_TRUE_TIMEOUT)); + downloadFinishedCondition.close(); + if (downloadError != null) { + throw downloadError; + } + } + + /** + * Sets up Mockito for an instrumentation test. + */ + private static void setUpMockito(InstrumentationTestCase instrumentationTestCase) { + // Workaround for https://code.google.com/p/dexmaker/issues/detail?id=2. + System.setProperty("dexmaker.dexcache", + instrumentationTestCase.getInstrumentation().getTargetContext().getCacheDir().getPath()); + MockitoAnnotations.initMocks(instrumentationTestCase); + } + + private static class FakeDownloadAction extends DownloadAction { + + private final DownloadManager downloadManager; + private final String mediaId; + private final boolean removeAction; + private final FakeDownloader downloader; + private final ConditionVariable stateChanged; + private DownloadTask downloadTask; + + private FakeDownloadAction(DownloadManager downloadManager, String mediaId, + boolean removeAction) { + this.downloadManager = downloadManager; + this.mediaId = mediaId; + this.removeAction = removeAction; + this.downloader = new FakeDownloader(removeAction); + this.stateChanged = new ConditionVariable(); + } + + @Override + protected String getType() { + return "FakeDownloadAction"; + } + + @Override + protected void writeToStream(DataOutputStream output) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + protected boolean isRemoveAction() { + return removeAction; + } + + @Override + protected boolean isSameMedia(DownloadAction other) { + return other instanceof FakeDownloadAction + && mediaId.equals(((FakeDownloadAction) other).mediaId); + } + + @Override + protected Downloader createDownloader(DownloaderConstructorHelper downloaderConstructorHelper) { + return downloader; + } + + private FakeDownloadAction post() throws DownloadException { + downloadTask = downloadManager.handleAction(this); + return this; + } + + private FakeDownloadAction assertDoesNotStart() { + assertFalse(downloader.started.block(ASSERT_FALSE_TIME)); + return this; + } + + private FakeDownloadAction assertStarted() { + assertTrue(downloader.started.block(ASSERT_TRUE_TIMEOUT)); + assertTaskState(DownloadTask.STATE_STARTED); + return this; + } + + private FakeDownloadAction assertTaskState(@State int state) { + assertTrue(stateChanged.block(ASSERT_TRUE_TIMEOUT)); + stateChanged.close(); + assertEquals(state, downloadTask.getState()); + return this; + } + + private FakeDownloadAction finishAndAssertFinished() { + downloader.finish.open(); + assertTaskState(DownloadTask.STATE_ENDED); + return this; + } + + private FakeDownloadAction ignoreInterrupts() { + downloader.ignoreInterrupts = true; + return this; + } + + private void onStateChange() { + stateChanged.open(); + } + } + + private static class FakeDownloader implements Downloader { + private final ConditionVariable started; + private final com.google.android.exoplayer2.util.ConditionVariable finish; + private final boolean removeAction; + private boolean ignoreInterrupts; + + private FakeDownloader(boolean removeAction) { + this.removeAction = removeAction; + this.started = new ConditionVariable(); + this.finish = new com.google.android.exoplayer2.util.ConditionVariable(); + } + + @Override + public void init() throws InterruptedException, IOException { + // do nothing. + } + + @Override + public void download(@Nullable ProgressListener listener) + throws InterruptedException, IOException { + assertFalse(removeAction); + started.open(); + blockUntilFinish(); + } + + @Override + public void remove() throws InterruptedException { + assertTrue(removeAction); + started.open(); + blockUntilFinish(); + } + + private void blockUntilFinish() throws InterruptedException { + while (true){ + try { + finish.block(); + break; + } catch (InterruptedException e) { + if (!ignoreInterrupts) { + throw e; + } + } + } + } + + @Override + public long getDownloadedBytes() { + return 0; + } + + @Override + public float getDownloadPercentage() { + return 0; + } + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadException.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadException.java index 730ce2d3e8..983727c14d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadException.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadException.java @@ -25,4 +25,9 @@ public final class DownloadException extends IOException { super(message); } + /** @param cause The cause for the exception. */ + public DownloadException(Throwable cause) { + super(cause); + } + } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/ConditionVariable.java b/library/core/src/main/java/com/google/android/exoplayer2/util/ConditionVariable.java index b6c6914c50..550cdfbb47 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/ConditionVariable.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/ConditionVariable.java @@ -16,8 +16,8 @@ package com.google.android.exoplayer2.util; /** - * A condition variable whose {@link #open()} and {@link #close()} methods return whether they - * resulted in a change of state. + * An interruptible condition variable whose {@link #open()} and {@link #close()} methods return + * whether they resulted in a change of state. */ public final class ConditionVariable { @@ -59,4 +59,21 @@ public final class ConditionVariable { } } + /** + * Blocks until the condition is opened or until timeout milliseconds have passed. + * + * @param timeout The maximum time to wait in milliseconds. + * @return true If the condition was opened, false if the call returns because of the timeout. + * @throws InterruptedException If the thread is interrupted. + */ + public synchronized boolean block(int timeout) throws InterruptedException { + long now = System.currentTimeMillis(); + long end = now + timeout; + while (!isOpen && now < end) { + wait(end - now); + now = System.currentTimeMillis(); + } + return isOpen; + } + } From 64b928e77f605e65a1154eec4d683806e08e7272 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 19 Oct 2017 17:28:24 +0100 Subject: [PATCH 0571/2472] Stylistic cleanup --- .../extractor/flv/VideoTagPayloadReader.java | 2 +- .../android/exoplayer2/util/ParsableByteArray.java | 13 +++++++------ .../exoplayer2/util/ParsableByteArrayTest.java | 14 +++++++------- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/library/core/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 index 7fa45a2a94..92db91e20b 100644 --- a/library/core/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 @@ -79,7 +79,7 @@ import com.google.android.exoplayer2.video.AvcConfig; @Override protected void parsePayload(ParsableByteArray data, long timeUs) throws ParserException { int packetType = data.readUnsignedByte(); - int compositionTimeMs = data.readSignedInt24(); + int compositionTimeMs = data.readInt24(); timeUs += compositionTimeMs * 1000L; // Parse avc sequence header in case this was not done before. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java b/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java index 9de86cc3bc..57313ea895 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/ParsableByteArray.java @@ -233,7 +233,7 @@ public final class ParsableByteArray { } /** - * Reads the next two bytes as an signed value. + * Reads the next two bytes as a signed value. */ public short readShort() { return (short) ((data[position++] & 0xFF) << 8 @@ -257,11 +257,12 @@ public final class ParsableByteArray { } /** - * Reads the next three bytes as an signed value. + * Reads the next three bytes as a signed value. */ - public int readSignedInt24() { - int ui24 = readUnsignedInt24(); - return (ui24 & 0x800000L) >>> 23 == 1 ? (ui24 | 0xff000000) : ui24; + public int readInt24() { + return ((data[position++] & 0xFF) << 24) >> 8 + | (data[position++] & 0xFF) << 8 + | (data[position++] & 0xFF); } /** @@ -313,7 +314,7 @@ public final class ParsableByteArray { } /** - * Reads the next four bytes as an signed value in little endian order. + * Reads the next four bytes as a signed value in little endian order. */ public int readLittleEndianInt() { return (data[position++] & 0xFF) diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/ParsableByteArrayTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/ParsableByteArrayTest.java index 56a4740464..947a692647 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/util/ParsableByteArrayTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/ParsableByteArrayTest.java @@ -328,25 +328,25 @@ public final class ParsableByteArrayTest { @Test public void testReadLittleEndianUnsignedInt24() { - byte[] data = { 0x01, 0x02, (byte) 0xFF }; + byte[] data = {0x01, 0x02, (byte) 0xFF}; ParsableByteArray byteArray = new ParsableByteArray(data); assertThat(byteArray.readLittleEndianUnsignedInt24()).isEqualTo(0xFF0201); assertThat(byteArray.getPosition()).isEqualTo(3); } @Test - public void testReadPositiveSignedInt24() { - byte[] data = { 0x01, 0x02, (byte) 0xFF }; + public void testReadInt24Positive() { + byte[] data = {0x01, 0x02, (byte) 0xFF}; ParsableByteArray byteArray = new ParsableByteArray(data); - assertThat(byteArray.readSignedInt24()).isEqualTo(0x0102FF); + assertThat(byteArray.readInt24()).isEqualTo(0x0102FF); assertThat(byteArray.getPosition()).isEqualTo(3); } @Test - public void testReadNegativeSignedInt24() { - byte[] data = { (byte)0xFF, 0x02, (byte) 0x01 }; + public void testReadInt24Negative() { + byte[] data = {(byte) 0xFF, 0x02, (byte) 0x01}; ParsableByteArray byteArray = new ParsableByteArray(data); - assertThat(byteArray.readSignedInt24()).isEqualTo(0xFFFF0201); + assertThat(byteArray.readInt24()).isEqualTo(0xFFFF0201); assertThat(byteArray.getPosition()).isEqualTo(3); } From 006263e9836ad929f6cfa169a299407f613fdb1c Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 19 Oct 2017 06:34:02 -0700 Subject: [PATCH 0572/2472] Retain playback position on re-preparation ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=172736350 --- .../com/google/android/exoplayer2/ExoPlayerImplInternal.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index b8274126b5..1e486a06d0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -430,6 +430,10 @@ import java.io.IOException; loadControl.onPrepared(); if (resetPosition) { playbackInfo = new PlaybackInfo(0, C.TIME_UNSET); + } else { + // The new start position is the current playback position. + playbackInfo = new PlaybackInfo(playbackInfo.periodId, playbackInfo.positionUs, + playbackInfo.contentPositionUs); } this.mediaSource = mediaSource; mediaSource.prepareSource(player, true, this); From 9ea8a8a7148cdf9adefcbfee109d54e711d1baac Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 19 Oct 2017 03:00:44 -0700 Subject: [PATCH 0573/2472] Update HLS sample streams ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=172722536 --- demo/src/main/assets/media.exolist.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/demo/src/main/assets/media.exolist.json b/demo/src/main/assets/media.exolist.json index 59d8259d37..38a0c577ae 100644 --- a/demo/src/main/assets/media.exolist.json +++ b/demo/src/main/assets/media.exolist.json @@ -344,11 +344,11 @@ "samples": [ { "name": "Apple 4x3 basic stream", - "uri": "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/bipbop_4x3_variant.m3u8" + "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/bipbop_4x3_variant.m3u8" }, { "name": "Apple 16x9 basic stream", - "uri": "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8" + "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8" }, { "name": "Apple master playlist advanced (TS)", @@ -360,11 +360,11 @@ }, { "name": "Apple TS media playlist", - "uri": "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/gear1/prog_index.m3u8" + "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear1/prog_index.m3u8" }, { "name": "Apple AAC media playlist", - "uri": "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/gear0/prog_index.m3u8" + "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear0/prog_index.m3u8" }, { "name": "Apple ID3 metadata", @@ -381,11 +381,11 @@ }, { "name": "Apple AAC 10s", - "uri": "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/gear0/fileSequence0.aac" + "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear0/fileSequence0.aac" }, { "name": "Apple TS 10s", - "uri": "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/gear1/fileSequence0.ts" + "uri": "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/gear1/fileSequence0.ts" }, { "name": "Android screens (Matroska)", From e54841436b185007924d5515c00bd2b1ff169bf3 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 19 Sep 2017 09:57:07 -0700 Subject: [PATCH 0574/2472] Workaround Samsung tablet reboot playing adaptive secure content ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=169256059 --- .../mediacodec/MediaCodecRenderer.java | 57 ++++++++++++++----- 1 file changed, 42 insertions(+), 15 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java index 01229c1104..ffe6313360 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java @@ -158,9 +158,26 @@ public abstract class MediaCodecRenderer extends BaseRenderer { */ private static final int REINITIALIZATION_STATE_WAIT_END_OF_STREAM = 2; + @Retention(RetentionPolicy.SOURCE) + @IntDef({ADAPTATION_WORKAROUND_MODE_NEVER, ADAPTATION_WORKAROUND_MODE_SAME_RESOLUTION, + ADAPTATION_WORKAROUND_MODE_ALWAYS}) + private @interface AdaptationWorkaroundMode {} + /** + * The adaptation workaround is never used. + */ + private static final int ADAPTATION_WORKAROUND_MODE_NEVER = 0; + /** + * The adaptation workaround is used when adapting between formats of the same resolution only. + */ + private static final int ADAPTATION_WORKAROUND_MODE_SAME_RESOLUTION = 1; + /** + * The adaptation workaround is always used when adapting between formats. + */ + private static final int ADAPTATION_WORKAROUND_MODE_ALWAYS = 2; + /** * H.264/AVC buffer to queue when using the adaptation workaround (see - * {@link #codecNeedsAdaptationWorkaround(String)}. Consists of three NAL units with start codes: + * {@link #codecAdaptationWorkaroundMode(String)}. Consists of three NAL units with start codes: * Baseline sequence/picture parameter sets and a 32 * 32 pixel IDR slice. This stream can be * queued to force a resolution change when adapting to a new format. */ @@ -182,9 +199,9 @@ public abstract class MediaCodecRenderer extends BaseRenderer { private DrmSession pendingDrmSession; private MediaCodec codec; private MediaCodecInfo codecInfo; + private @AdaptationWorkaroundMode int codecAdaptationWorkaroundMode; private boolean codecNeedsDiscardToSpsWorkaround; private boolean codecNeedsFlushWorkaround; - private boolean codecNeedsAdaptationWorkaround; private boolean codecNeedsEosPropagationWorkaround; private boolean codecNeedsEosFlushWorkaround; private boolean codecNeedsEosOutputExceptionWorkaround; @@ -355,9 +372,9 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } String codecName = codecInfo.name; + codecAdaptationWorkaroundMode = codecAdaptationWorkaroundMode(codecName); codecNeedsDiscardToSpsWorkaround = codecNeedsDiscardToSpsWorkaround(codecName, format); codecNeedsFlushWorkaround = codecNeedsFlushWorkaround(codecName); - codecNeedsAdaptationWorkaround = codecNeedsAdaptationWorkaround(codecName); codecNeedsEosPropagationWorkaround = codecNeedsEosPropagationWorkaround(codecName); codecNeedsEosFlushWorkaround = codecNeedsEosFlushWorkaround(codecName); codecNeedsEosOutputExceptionWorkaround = codecNeedsEosOutputExceptionWorkaround(codecName); @@ -458,7 +475,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { codecReceivedBuffers = false; codecNeedsDiscardToSpsWorkaround = false; codecNeedsFlushWorkaround = false; - codecNeedsAdaptationWorkaround = false; + codecAdaptationWorkaroundMode = ADAPTATION_WORKAROUND_MODE_NEVER; codecNeedsEosPropagationWorkaround = false; codecNeedsEosFlushWorkaround = false; codecNeedsMonoChannelCountWorkaround = false; @@ -802,8 +819,10 @@ public abstract class MediaCodecRenderer extends BaseRenderer { && canReconfigureCodec(codec, codecInfo.adaptive, oldFormat, format)) { codecReconfigured = true; codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING; - codecNeedsAdaptationWorkaroundBuffer = codecNeedsAdaptationWorkaround - && format.width == oldFormat.width && format.height == oldFormat.height; + codecNeedsAdaptationWorkaroundBuffer = + codecAdaptationWorkaroundMode == ADAPTATION_WORKAROUND_MODE_ALWAYS + || (codecAdaptationWorkaroundMode == ADAPTATION_WORKAROUND_MODE_SAME_RESOLUTION + && format.width == oldFormat.width && format.height == oldFormat.height); } else { if (codecReceivedBuffers) { // Signal end of stream and wait for any final output buffers before re-initialization. @@ -989,7 +1008,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { */ private void processOutputFormat() throws ExoPlaybackException { MediaFormat format = codec.getOutputFormat(); - if (codecNeedsAdaptationWorkaround + if (codecAdaptationWorkaroundMode != ADAPTATION_WORKAROUND_MODE_NEVER && format.getInteger(MediaFormat.KEY_WIDTH) == ADAPTATION_WORKAROUND_SLICE_WIDTH_HEIGHT && format.getInteger(MediaFormat.KEY_HEIGHT) == ADAPTATION_WORKAROUND_SLICE_WIDTH_HEIGHT) { // We assume this format changed event was caused by the adaptation workaround. @@ -1122,22 +1141,30 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } /** - * Returns whether the decoder is known to get stuck during some adaptations where the resolution - * does not change. + * Returns a mode that specifies when the adaptation workaround should be enabled. *

          - * If true is returned, the renderer will work around the issue by queueing and discarding a blank - * frame at a different resolution, which resets the codec's internal state. + * When enabled, the workaround queues and discards a blank frame with a resolution whose width + * and height both equal {@link #ADAPTATION_WORKAROUND_SLICE_WIDTH_HEIGHT}, to reset the codec's + * internal state when a format change occurs. *

          * See [Internal: b/27807182]. + * See GitHub issue #3257. * * @param name The name of the decoder. - * @return True if the decoder is known to get stuck during some adaptations. + * @return The mode specifying when the adaptation workaround should be enabled. */ - private static boolean codecNeedsAdaptationWorkaround(String name) { - return Util.SDK_INT < 24 + private @AdaptationWorkaroundMode int codecAdaptationWorkaroundMode(String name) { + if (Util.SDK_INT <= 24 && "OMX.Exynos.avc.dec.secure".equals(name) + && Util.MODEL.startsWith("SM-T585")) { + return ADAPTATION_WORKAROUND_MODE_ALWAYS; + } else if (Util.SDK_INT < 24 && ("OMX.Nvidia.h264.decode".equals(name) || "OMX.Nvidia.h264.decode.secure".equals(name)) && ("flounder".equals(Util.DEVICE) || "flounder_lte".equals(Util.DEVICE) - || "grouper".equals(Util.DEVICE) || "tilapia".equals(Util.DEVICE)); + || "grouper".equals(Util.DEVICE) || "tilapia".equals(Util.DEVICE))) { + return ADAPTATION_WORKAROUND_MODE_SAME_RESOLUTION; + } else { + return ADAPTATION_WORKAROUND_MODE_NEVER; + } } /** From 5357726a8cc4765d3d72da61c6f8d0a864b075bd Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 18 Oct 2017 04:46:05 -0700 Subject: [PATCH 0575/2472] Add some additional device specific workarounds Issue: #3355 Issue: #3257 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=172587141 --- .../android/exoplayer2/mediacodec/MediaCodecRenderer.java | 2 +- .../android/exoplayer2/video/MediaCodecVideoRenderer.java | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java index ffe6313360..f556269e3a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java @@ -1155,7 +1155,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { */ private @AdaptationWorkaroundMode int codecAdaptationWorkaroundMode(String name) { if (Util.SDK_INT <= 24 && "OMX.Exynos.avc.dec.secure".equals(name) - && Util.MODEL.startsWith("SM-T585")) { + && (Util.MODEL.startsWith("SM-T585") || Util.MODEL.startsWith("SM-A520"))) { return ADAPTATION_WORKAROUND_MODE_ALWAYS; } else if (Util.SDK_INT < 24 && ("OMX.Nvidia.h264.decode".equals(name) || "OMX.Nvidia.h264.decode.secure".equals(name)) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index 9d769b2050..1215fb00f3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -973,9 +973,11 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { * If true is returned then we fall back to releasing and re-instantiating the codec instead. */ private static boolean codecNeedsSetOutputSurfaceWorkaround(String name) { - // Work around https://github.com/google/ExoPlayer/issues/3236 - return ("deb".equals(Util.DEVICE) || "flo".equals(Util.DEVICE)) - && "OMX.qcom.video.decoder.avc".equals(name); + // Work around https://github.com/google/ExoPlayer/issues/3236 and + // https://github.com/google/ExoPlayer/issues/3355. + return (("deb".equals(Util.DEVICE) || "flo".equals(Util.DEVICE)) + && "OMX.qcom.video.decoder.avc".equals(name)) + || ("tcl_eu".equals(Util.DEVICE) && "OMX.MTK.VIDEO.DECODER.AVC".equals(name)); } /** From 69ec60e0f1406c25705940013c82d80dc4faa158 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 18 Oct 2017 05:59:14 -0700 Subject: [PATCH 0576/2472] Fix seeking with repeated periods newPlayingPeriodHolder could be set then updated if seeking to a repeated period that was loaded more than once. This led to MediaPeriodHolders leaking. Only set newPlayingPeriodHolder once so that any later holders with the same period identifier get released. Also add a regression test. FakeMediaSource checks that all created MediaPeriods were released when it is released. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=172591937 --- .../com/google/android/exoplayer2/ExoPlayerImplInternal.java | 3 ++- .../google/android/exoplayer2/testutil/FakeMediaSource.java | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 1e486a06d0..38b9d2afcd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -723,7 +723,8 @@ import java.io.IOException; // Clear the timeline, but keep the requested period if it is already prepared. MediaPeriodHolder periodHolder = playingPeriodHolder; while (periodHolder != null) { - if (shouldKeepPeriodHolder(periodId, periodPositionUs, periodHolder)) { + if (newPlayingPeriodHolder == null + && shouldKeepPeriodHolder(periodId, periodPositionUs, periodHolder)) { newPlayingPeriodHolder = periodHolder; } else { periodHolder.release(); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java index a2c1e9879e..93134bf312 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java @@ -111,4 +111,5 @@ public class FakeMediaSource implements MediaSource { } return new TrackGroupArray(trackGroups); } + } From 9a52d63271d5fcd7924a3f503eb19713bda472d2 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 19 Oct 2017 17:48:07 +0100 Subject: [PATCH 0577/2472] Fix build --- .../android/exoplayer2/mediacodec/MediaCodecRenderer.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java index f556269e3a..070c59a80a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java @@ -23,6 +23,7 @@ import android.media.MediaCrypto; import android.media.MediaFormat; import android.os.Looper; import android.os.SystemClock; +import android.support.annotation.IntDef; import android.support.annotation.Nullable; import android.util.Log; import com.google.android.exoplayer2.BaseRenderer; @@ -43,6 +44,9 @@ import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.NalUnitUtil; import com.google.android.exoplayer2.util.TraceUtil; import com.google.android.exoplayer2.util.Util; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; From feff4d3e0272a9c3594fbb3144ed4453202192ac Mon Sep 17 00:00:00 2001 From: Drew Hill Date: Thu, 19 Oct 2017 12:49:09 -0400 Subject: [PATCH 0578/2472] fix missing cause of display refresh rate changing between videoframereleasetimehelper constructor and enable being called --- .../exoplayer2/video/VideoFrameReleaseTimeHelper.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java index af06432261..596e1046d1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java @@ -74,7 +74,7 @@ public final class VideoFrameReleaseTimeHelper { */ public VideoFrameReleaseTimeHelper(Context context) { this(getDefaultDisplayRefreshRate(context)); - this.context = context; + this.context = context.getApplicationContext(); registerDisplayListener(); } @@ -89,8 +89,9 @@ public final class VideoFrameReleaseTimeHelper { haveSync = false; if (useDefaultDisplayVsync) { vsyncSampler.addObserver(); + setSync(getDefaultDisplayRefreshRate(context)); + registerDisplayListener(); } - registerDisplayListener(); } /** From c2d05f44050c6fb0f068cbfae08667c146f3bcd3 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 19 Oct 2017 09:52:16 -0700 Subject: [PATCH 0579/2472] Bump to 2.5.4 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=172758309 --- .hgignore | 71 +++++++++++++++++++ RELEASENOTES.md | 18 +++++ constants.gradle | 2 +- demos/cast/src/main/AndroidManifest.xml | 4 +- demos/main/src/main/AndroidManifest.xml | 4 +- .../exoplayer2/ExoPlayerLibraryInfo.java | 6 +- 6 files changed, 97 insertions(+), 8 deletions(-) create mode 100644 .hgignore diff --git a/.hgignore b/.hgignore new file mode 100644 index 0000000000..f7c3656f65 --- /dev/null +++ b/.hgignore @@ -0,0 +1,71 @@ +# Mercurial's .hgignore files can only be used in the root directory. +# You can still apply these rules by adding +# include:path/to/this/directory/.hgignore to the top-level .hgignore file. + +# Ensure same syntax as in .gitignore can be used +syntax:glob + +# Android generated +bin +gen +libs +obj +lint.xml + +# IntelliJ IDEA +.idea +*.iml +*.ipr +*.iws +classes +gen-external-apklibs + +# Eclipse +.project +.classpath +.settings +.checkstyle +.cproject + +# Gradle +.gradle +build +buildout +out + +# Maven +target +release.properties +pom.xml.* + +# Ant +ant.properties +local.properties +proguard.cfg +proguard-project.txt + +# Other +.DS_Store +cmake-build-debug +dist +tmp + +# VP9 extension +extensions/vp9/src/main/jni/libvpx +extensions/vp9/src/main/jni/libvpx_android_configs +extensions/vp9/src/main/jni/libyuv + +# Opus extension +extensions/opus/src/main/jni/libopus + +# FLAC extension +extensions/flac/src/main/jni/flac + +# FFmpeg extension +extensions/ffmpeg/src/main/jni/ffmpeg + +# Cronet extension +extensions/cronet/jniLibs/* +!extensions/cronet/jniLibs/README.md +extensions/cronet/libs/* +!extensions/cronet/libs/README.md diff --git a/RELEASENOTES.md b/RELEASENOTES.md index c63a20ba94..90b3d15e08 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,23 @@ # Release notes # +### r2.5.4 ### + +* Remove unnecessary media playlist fetches during playback of live HLS streams. +* Add the ability to inject a HLS playlist parser through `HlsMediaSource`. +* Fix potential `IndexOutOfBoundsException` when using `ImaMediaSource` + ([#3334](https://github.com/google/ExoPlayer/issues/3334)). +* Fix an issue parsing MP4 content containing non-CENC sinf boxes. +* Fix memory leak when seeking with repeated periods. +* Fix playback position when `ExoPlayer.prepare` is called with `resetPosition` + set to false. +* Ignore MP4 edit lists that seem invalid + ([#3351](https://github.com/google/ExoPlayer/issues/3351)). +* Add extractor flag for ignoring all MP4 edit lists + ([#3358](https://github.com/google/ExoPlayer/issues/3358)). +* Improve extensibility by exposing public constructors for + `FrameworkMediaCrypto` and by making `DefaultDashChunkSource.getNextChunk` + non-final. + ### r2.5.3 ### * IMA extension: Support skipping of skippable ads on AndroidTV and other diff --git a/constants.gradle b/constants.gradle index 4778a0c66f..282addf7aa 100644 --- a/constants.gradle +++ b/constants.gradle @@ -28,7 +28,7 @@ project.ext { junitVersion = '4.12' truthVersion = '0.35' robolectricVersion = '3.4.2' - releaseVersion = 'r2.5.3' + releaseVersion = 'r2.5.4' modulePrefix = ':' if (gradle.ext.has('exoplayerModulePrefix')) { modulePrefix += gradle.ext.exoplayerModulePrefix diff --git a/demos/cast/src/main/AndroidManifest.xml b/demos/cast/src/main/AndroidManifest.xml index 95dfb5f8e0..8f0aa69e8c 100644 --- a/demos/cast/src/main/AndroidManifest.xml +++ b/demos/cast/src/main/AndroidManifest.xml @@ -15,8 +15,8 @@ --> + android:versionCode="2504" + android:versionName="2.5.4"> diff --git a/demos/main/src/main/AndroidManifest.xml b/demos/main/src/main/AndroidManifest.xml index 4a9eec4002..f70d6152e8 100644 --- a/demos/main/src/main/AndroidManifest.xml +++ b/demos/main/src/main/AndroidManifest.xml @@ -16,8 +16,8 @@ + android:versionCode="2504" + android:versionName="2.5.4"> diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java index 90385ed6c0..62ee8c4873 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java @@ -31,13 +31,13 @@ public final class ExoPlayerLibraryInfo { * The version of the library expressed as a string, for example "1.2.3". */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. - public static final String VERSION = "2.5.3"; + public static final String VERSION = "2.5.4"; /** * The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final String VERSION_SLASHY = "ExoPlayerLib/2.5.3"; + public static final String VERSION_SLASHY = "ExoPlayerLib/2.5.4"; /** * The version of the library expressed as an integer, for example 1002003. @@ -47,7 +47,7 @@ public final class ExoPlayerLibraryInfo { * integer version 123045006 (123-045-006). */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final int VERSION_INT = 2005003; + public static final int VERSION_INT = 2005004; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} From 04862bccc88d5f1c53fe8394c707fcd1fae488a1 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 19 Oct 2017 09:52:16 -0700 Subject: [PATCH 0580/2472] Bump to 2.5.4 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=172758309 --- RELEASENOTES.md | 18 ++++++++++++++++++ constants.gradle | 2 +- demo/src/main/AndroidManifest.xml | 4 ++-- .../exoplayer2/ExoPlayerLibraryInfo.java | 6 +++--- 4 files changed, 24 insertions(+), 6 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index c63a20ba94..90b3d15e08 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,23 @@ # Release notes # +### r2.5.4 ### + +* Remove unnecessary media playlist fetches during playback of live HLS streams. +* Add the ability to inject a HLS playlist parser through `HlsMediaSource`. +* Fix potential `IndexOutOfBoundsException` when using `ImaMediaSource` + ([#3334](https://github.com/google/ExoPlayer/issues/3334)). +* Fix an issue parsing MP4 content containing non-CENC sinf boxes. +* Fix memory leak when seeking with repeated periods. +* Fix playback position when `ExoPlayer.prepare` is called with `resetPosition` + set to false. +* Ignore MP4 edit lists that seem invalid + ([#3351](https://github.com/google/ExoPlayer/issues/3351)). +* Add extractor flag for ignoring all MP4 edit lists + ([#3358](https://github.com/google/ExoPlayer/issues/3358)). +* Improve extensibility by exposing public constructors for + `FrameworkMediaCrypto` and by making `DefaultDashChunkSource.getNextChunk` + non-final. + ### r2.5.3 ### * IMA extension: Support skipping of skippable ads on AndroidTV and other diff --git a/constants.gradle b/constants.gradle index db7b12acf0..3402375e87 100644 --- a/constants.gradle +++ b/constants.gradle @@ -24,7 +24,7 @@ project.ext { supportLibraryVersion = '25.4.0' dexmakerVersion = '1.2' mockitoVersion = '1.9.5' - releaseVersion = 'r2.5.3' + releaseVersion = 'r2.5.4' modulePrefix = ':' if (gradle.ext.has('exoplayerModulePrefix')) { modulePrefix += gradle.ext.exoplayerModulePrefix diff --git a/demo/src/main/AndroidManifest.xml b/demo/src/main/AndroidManifest.xml index 612044762f..3fec020c3b 100644 --- a/demo/src/main/AndroidManifest.xml +++ b/demo/src/main/AndroidManifest.xml @@ -16,8 +16,8 @@ + android:versionCode="2504" + android:versionName="2.5.4"> diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java index 90385ed6c0..62ee8c4873 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java @@ -31,13 +31,13 @@ public final class ExoPlayerLibraryInfo { * The version of the library expressed as a string, for example "1.2.3". */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. - public static final String VERSION = "2.5.3"; + public static final String VERSION = "2.5.4"; /** * The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final String VERSION_SLASHY = "ExoPlayerLib/2.5.3"; + public static final String VERSION_SLASHY = "ExoPlayerLib/2.5.4"; /** * The version of the library expressed as an integer, for example 1002003. @@ -47,7 +47,7 @@ public final class ExoPlayerLibraryInfo { * integer version 123045006 (123-045-006). */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final int VERSION_INT = 2005003; + public static final int VERSION_INT = 2005004; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} From 84afad0748125142b3612c5fcd24e31b8d59f2a0 Mon Sep 17 00:00:00 2001 From: Drew Hill Date: Thu, 19 Oct 2017 16:24:07 -0400 Subject: [PATCH 0581/2472] adjustments --- .../video/VideoFrameReleaseTimeHelper.java | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java index 596e1046d1..408cd1a09a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java @@ -44,10 +44,10 @@ public final class VideoFrameReleaseTimeHelper { private DisplayManager.DisplayListener displayListener = null; private Context context = null; - private VSyncSampler vsyncSampler; - private boolean useDefaultDisplayVsync; - private long vsyncDurationNs; - private long vsyncOffsetNs; + private VSyncSampler vsyncSampler = null; + private final boolean useDefaultDisplayVsync; + private long vsyncDurationNs = -1; // Value unused. + private long vsyncOffsetNs = -1; // Value unused. private long lastFramePresentationTimeUs; private long adjustedLastFrameTimeNs; @@ -75,11 +75,10 @@ public final class VideoFrameReleaseTimeHelper { public VideoFrameReleaseTimeHelper(Context context) { this(getDefaultDisplayRefreshRate(context)); this.context = context.getApplicationContext(); - registerDisplayListener(); } private VideoFrameReleaseTimeHelper(double defaultDisplayRefreshRate) { - setSync(defaultDisplayRefreshRate); + useDefaultDisplayVsync = defaultDisplayRefreshRate != DISPLAY_REFRESH_RATE_UNKNOWN; } /** @@ -100,8 +99,8 @@ public final class VideoFrameReleaseTimeHelper { public void disable() { if (useDefaultDisplayVsync) { vsyncSampler.removeObserver(); + unregisterDisplayListener(); } - unregisterDisplayListener(); } private void registerDisplayListener() { @@ -128,15 +127,10 @@ public final class VideoFrameReleaseTimeHelper { private void setSync(double defaultDisplayRefreshRate) { - useDefaultDisplayVsync = defaultDisplayRefreshRate != DISPLAY_REFRESH_RATE_UNKNOWN; if (useDefaultDisplayVsync) { vsyncSampler = VSyncSampler.getInstance(); vsyncDurationNs = (long) (C.NANOS_PER_SECOND / defaultDisplayRefreshRate); vsyncOffsetNs = (vsyncDurationNs * VSYNC_OFFSET_PERCENTAGE) / 100; - } else { - vsyncSampler = null; - vsyncDurationNs = -1; // Value unused. - vsyncOffsetNs = -1; // Value unused. } } From 3830307cd3c012ed4e6e5a764793b640ead00e2b Mon Sep 17 00:00:00 2001 From: Drew Hill Date: Thu, 19 Oct 2017 20:28:17 -0400 Subject: [PATCH 0582/2472] fix not initialized error --- .../android/exoplayer2/video/VideoFrameReleaseTimeHelper.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java index 408cd1a09a..b3ce65daa4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java @@ -79,6 +79,9 @@ public final class VideoFrameReleaseTimeHelper { private VideoFrameReleaseTimeHelper(double defaultDisplayRefreshRate) { useDefaultDisplayVsync = defaultDisplayRefreshRate != DISPLAY_REFRESH_RATE_UNKNOWN; + if (useDefaultDisplayVsync) { + vsyncSampler = VSyncSampler.getInstance(); + } } /** @@ -128,7 +131,6 @@ public final class VideoFrameReleaseTimeHelper { private void setSync(double defaultDisplayRefreshRate) { if (useDefaultDisplayVsync) { - vsyncSampler = VSyncSampler.getInstance(); vsyncDurationNs = (long) (C.NANOS_PER_SECOND / defaultDisplayRefreshRate); vsyncOffsetNs = (vsyncDurationNs * VSYNC_OFFSET_PERCENTAGE) / 100; } From 208166759124075727fa5fc319b860f1be4561cd Mon Sep 17 00:00:00 2001 From: Drew Hill Date: Tue, 24 Oct 2017 11:19:26 -0400 Subject: [PATCH 0583/2472] cleanups for videoframereleasetimehelper --- .../video/VideoFrameReleaseTimeHelper.java | 84 ++++++++----------- 1 file changed, 35 insertions(+), 49 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java index b3ce65daa4..c5ca212a36 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java @@ -24,6 +24,7 @@ import android.os.HandlerThread; import android.os.Message; import android.view.Choreographer; import android.view.Choreographer.FrameCallback; +import android.view.Display; import android.view.WindowManager; import com.google.android.exoplayer2.C; @@ -33,7 +34,6 @@ import com.google.android.exoplayer2.C; @TargetApi(16) public final class VideoFrameReleaseTimeHelper { - private static final int DISPLAY_ID_UNKNOWN = -1; private static final double DISPLAY_REFRESH_RATE_UNKNOWN = -1; private static final long CHOREOGRAPHER_SAMPLE_DELAY_MILLIS = 500; private static final long MAX_ALLOWED_DRIFT_NS = 20000000; @@ -41,10 +41,10 @@ public final class VideoFrameReleaseTimeHelper { private static final long VSYNC_OFFSET_PERCENTAGE = 80; private static final int MIN_FRAMES_FOR_ADJUSTMENT = 6; - private DisplayManager.DisplayListener displayListener = null; private Context context = null; - private VSyncSampler vsyncSampler = null; + private final DefaultDisplayListener defaultDisplayListener; + private final VSyncSampler vsyncSampler; private final boolean useDefaultDisplayVsync; private long vsyncDurationNs = -1; // Value unused. private long vsyncOffsetNs = -1; // Value unused. @@ -63,7 +63,10 @@ public final class VideoFrameReleaseTimeHelper { * the default display's vsync signal. */ public VideoFrameReleaseTimeHelper() { - this(DISPLAY_REFRESH_RATE_UNKNOWN); + defaultDisplayListener = null; + useDefaultDisplayVsync = false; + vsyncSampler = null; + context = null; } /** @@ -73,16 +76,13 @@ public final class VideoFrameReleaseTimeHelper { * @param context A context from which information about the default display can be retrieved. */ public VideoFrameReleaseTimeHelper(Context context) { - this(getDefaultDisplayRefreshRate(context)); this.context = context.getApplicationContext(); + defaultDisplayListener = Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 ? + new DefaultDisplayListener(context) : null; + useDefaultDisplayVsync = true; + vsyncSampler = VSyncSampler.getInstance(); } - private VideoFrameReleaseTimeHelper(double defaultDisplayRefreshRate) { - useDefaultDisplayVsync = defaultDisplayRefreshRate != DISPLAY_REFRESH_RATE_UNKNOWN; - if (useDefaultDisplayVsync) { - vsyncSampler = VSyncSampler.getInstance(); - } - } /** * Enables the helper. @@ -92,7 +92,8 @@ public final class VideoFrameReleaseTimeHelper { if (useDefaultDisplayVsync) { vsyncSampler.addObserver(); setSync(getDefaultDisplayRefreshRate(context)); - registerDisplayListener(); + if (defaultDisplayListener != null) + defaultDisplayListener.register(); } } @@ -102,38 +103,14 @@ public final class VideoFrameReleaseTimeHelper { public void disable() { if (useDefaultDisplayVsync) { vsyncSampler.removeObserver(); - unregisterDisplayListener(); - } - } - - private void registerDisplayListener() { - if (displayListener == null && context != null && - Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - DisplayManager displayManager = context.getSystemService(DisplayManager.class); - if (displayManager != null) { - displayListener = new DefaultDisplayListener(context); - displayManager.registerDisplayListener(displayListener, null); - } - } - } - - private void unregisterDisplayListener() { - if (context != null && displayListener != null && - Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - DisplayManager displayManager = context.getSystemService(DisplayManager.class); - if (displayManager != null) { - displayManager.unregisterDisplayListener(displayListener); - displayListener = null; - } + if (defaultDisplayListener != null) + defaultDisplayListener.unregister(); } } private void setSync(double defaultDisplayRefreshRate) { - - if (useDefaultDisplayVsync) { - vsyncDurationNs = (long) (C.NANOS_PER_SECOND / defaultDisplayRefreshRate); - vsyncOffsetNs = (vsyncDurationNs * VSYNC_OFFSET_PERCENTAGE) / 100; - } + vsyncDurationNs = (long) (C.NANOS_PER_SECOND / defaultDisplayRefreshRate); + vsyncOffsetNs = (vsyncDurationNs * VSYNC_OFFSET_PERCENTAGE) / 100; } /** @@ -234,12 +211,6 @@ public final class VideoFrameReleaseTimeHelper { return snappedAfterDiff < snappedBeforeDiff ? snappedAfterNs : snappedBeforeNs; } - private static int getDefaultDisplayId(Context context) { - WindowManager manager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); - return manager != null && manager.getDefaultDisplay() != null ? - manager.getDefaultDisplay().getDisplayId() : DISPLAY_ID_UNKNOWN; - } - private static double getDefaultDisplayRefreshRate(Context context) { WindowManager manager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); return manager != null && manager.getDefaultDisplay() != null ? @@ -341,12 +312,16 @@ public final class VideoFrameReleaseTimeHelper { } - @TargetApi(17) + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) private class DefaultDisplayListener implements DisplayManager.DisplayListener { private final Context context; + private final DisplayManager displayManager; + DefaultDisplayListener(Context context) { this.context = context; + displayManager = context != null ? + (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE) : null; } @Override @@ -359,12 +334,23 @@ public final class VideoFrameReleaseTimeHelper { @Override public void onDisplayChanged(int displayId) { - final int defaultDisplayId = getDefaultDisplayId(context); - if (displayId == defaultDisplayId || defaultDisplayId == DISPLAY_ID_UNKNOWN) { + if (displayId == Display.DEFAULT_DISPLAY) { setSync(getDefaultDisplayRefreshRate(context)); } } + public void register() { + if (displayManager != null && context != null) { // context is used on callback + displayManager.registerDisplayListener(this, null); + } + } + + public void unregister() { + if (displayManager != null) { + displayManager.unregisterDisplayListener(this); + } + } + } } From 9568802c6d3c1bbd337eae13aac7326c92fd635c Mon Sep 17 00:00:00 2001 From: eguven Date: Fri, 20 Oct 2017 06:12:01 -0700 Subject: [PATCH 0584/2472] Persist DownloadActions in DownloadManager ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=172875067 --- .../offline/DownloadManagerTest.java | 45 ++++++++++++------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java index cfc5e1593e..b835ea6a0e 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java @@ -24,7 +24,9 @@ import com.google.android.exoplayer2.offline.DownloadManager.DownloadTask.State; import com.google.android.exoplayer2.upstream.DummyDataSource; import com.google.android.exoplayer2.upstream.cache.Cache; import com.google.android.exoplayer2.util.ClosedSource; +import com.google.android.exoplayer2.util.Util; import java.io.DataOutputStream; +import java.io.File; import java.io.IOException; import java.util.concurrent.Executors; import org.mockito.Mockito; @@ -40,6 +42,7 @@ public class DownloadManagerTest extends InstrumentationTestCase { private static final int ASSERT_FALSE_TIME = 1000; private DownloadManager downloadManager; + private File actionFile; private ConditionVariable downloadFinishedCondition; private Throwable downloadError; @@ -48,28 +51,20 @@ public class DownloadManagerTest extends InstrumentationTestCase { super.setUp(); setUpMockito(this); + actionFile = Util.createTempFile(getInstrumentation().getContext(), "ExoPlayerTest"); downloadManager = new DownloadManager( new DownloaderConstructorHelper(Mockito.mock(Cache.class), DummyDataSource.FACTORY), - Executors.newCachedThreadPool()); + Executors.newCachedThreadPool(), actionFile.getAbsolutePath()); downloadFinishedCondition = new ConditionVariable(); downloadManager.setListener(new TestDownloadListener()); } - - class TestDownloadListener implements DownloadListener { - @Override - public void onStateChange(DownloadManager downloadManager, DownloadTask downloadTask, int state, - Throwable error) { - if (state == DownloadTask.STATE_ERROR && downloadError == null) { - downloadError = error; - } - ((FakeDownloadAction) downloadTask.getDownloadAction()).onStateChange(); - } - - @Override - public void onTasksFinished(DownloadManager downloadManager) { - downloadFinishedCondition.open(); - } + + @Override + public void tearDown() throws Exception { + downloadManager.release(); + actionFile.delete(); + super.tearDown(); } public void testDownloadActionRuns() throws Throwable { @@ -200,6 +195,22 @@ public class DownloadManagerTest extends InstrumentationTestCase { } } + private class TestDownloadListener implements DownloadListener { + @Override + public void onStateChange(DownloadManager downloadManager, DownloadTask downloadTask, int state, + Throwable error) { + if (state == DownloadTask.STATE_ERROR && downloadError == null) { + downloadError = error; + } + ((FakeDownloadAction) downloadTask.getDownloadAction()).onStateChange(); + } + + @Override + public void onTasksFinished(DownloadManager downloadManager) { + downloadFinishedCondition.open(); + } + } + /** * Sets up Mockito for an instrumentation test. */ @@ -235,7 +246,7 @@ public class DownloadManagerTest extends InstrumentationTestCase { @Override protected void writeToStream(DataOutputStream output) throws IOException { - throw new UnsupportedOperationException(); + // do nothing. } @Override From 9306b24c6566c9dd5013aba1c310a88d0f3b2114 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 23 Oct 2017 01:23:24 -0700 Subject: [PATCH 0585/2472] Fix some Android Studio inspection warnings ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=173085316 --- .../main/java/com/google/android/exoplayer2/Renderer.java | 2 +- .../java/com/google/android/exoplayer2/RenderersFactory.java | 2 +- .../java/com/google/android/exoplayer2/audio/AudioSink.java | 3 +-- .../com/google/android/exoplayer2/drm/DefaultDrmSession.java | 3 +-- .../java/com/google/android/exoplayer2/drm/ExoMediaDrm.java | 2 +- .../android/exoplayer2/extractor/mkv/MatroskaExtractor.java | 4 ++-- .../android/exoplayer2/extractor/mp4/Mp4Extractor.java | 1 - .../android/exoplayer2/metadata/id3/ChapterTocFrame.java | 4 ++-- .../android/exoplayer2/source/ClippingMediaSource.java | 4 +--- .../android/exoplayer2/source/ConcatenatingMediaSource.java | 2 +- .../android/exoplayer2/source/ExtractorMediaSource.java | 5 +---- .../java/com/google/android/exoplayer2/text/cea/CeaUtil.java | 2 +- .../exoplayer2/trackselection/DefaultTrackSelector.java | 2 +- .../com/google/android/exoplayer2/upstream/cache/Cache.java | 2 +- .../source/smoothstreaming/manifest/SsManifestTest.java | 2 +- .../main/res/drawable-anydpi-v21/exo_controls_shuffle.xml | 2 +- .../exoplayer2/playbacktests/gts/DashDownloadTest.java | 2 +- .../android/exoplayer2/testutil/FakeSimpleExoPlayer.java | 4 ++-- 18 files changed, 20 insertions(+), 28 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java b/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java index e16caec980..6def1591da 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java @@ -226,7 +226,7 @@ public interface Renderer extends ExoPlayerComponent { /** * Whether the renderer is ready for the {@link ExoPlayer} instance to transition to - * {@link ExoPlayer#STATE_ENDED}. The player will make this transition as soon as {@code true} is + * {@link Player#STATE_ENDED}. The player will make this transition as soon as {@code true} is * returned by all of its {@link Renderer}s. *

          * This method may be called when the renderer is in the following states: 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 index a08ba448a4..944a6a9e5e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/RenderersFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/RenderersFactory.java @@ -31,7 +31,7 @@ public interface RenderersFactory { * * @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 audioRendererEventListener 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}. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java index 8b39a28182..5408032907 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java @@ -202,8 +202,7 @@ public interface AudioSink { * @param trimStartSamples The number of audio samples to trim from the start of data written to * the sink after this call. * @param trimEndSamples The number of audio samples to trim from data written to the sink - * immediately preceding the next call to {@link #reset()} or - * {@link #configure(String, int, int, int, int, int[], int, int)}. + * immediately preceding the next call to {@link #reset()} or this method. * @throws ConfigurationException If an error occurs configuring the sink. */ void configure(String inputMimeType, int inputChannelCount, int inputSampleRate, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java index 4e5696ef1f..290c1877de 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java @@ -86,6 +86,7 @@ import java.util.UUID; /* package */ final MediaDrmCallback callback; /* package */ final UUID uuid; + /* package */ final PostResponseHandler postResponseHandler; private @DrmSession.State int state; private int openCount; @@ -96,8 +97,6 @@ import java.util.UUID; private byte[] sessionId; private byte[] offlineLicenseKeySetId; - /* package */ PostResponseHandler postResponseHandler; - /** * Instantiates a new DRM session. * diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java index 1930e11f06..e9ee1ce90b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java @@ -205,7 +205,7 @@ public interface ExoMediaDrm { * * @param initData Opaque initialization data specific to the crypto scheme. * @return An object extends {@link ExoMediaCrypto}, using opaque crypto scheme specific data. - * @throws MediaCryptoException + * @throws MediaCryptoException If the instance can't be created. */ T createMediaCrypto(byte[] initData) throws MediaCryptoException; diff --git a/library/core/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 index a5fe6ae35b..5aefd041c4 100644 --- a/library/core/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 @@ -242,7 +242,7 @@ public final class MatroskaExtractor implements Extractor { * The value by which to divide a time in microseconds to convert it to the unit of the last value * in a subrip timecode (milliseconds). */ - private static long SUBRIP_TIMECODE_LAST_VALUE_SCALING_FACTOR = 1000; + private static final long SUBRIP_TIMECODE_LAST_VALUE_SCALING_FACTOR = 1000; /** * The format of a subrip timecode. */ @@ -270,7 +270,7 @@ public final class MatroskaExtractor implements Extractor { * The value by which to divide a time in microseconds to convert it to the unit of the last value * in an SSA timecode (1/100ths of a second). */ - private static long SSA_TIMECODE_LAST_VALUE_SCALING_FACTOR = 10000; + private static final long SSA_TIMECODE_LAST_VALUE_SCALING_FACTOR = 10000; /** * A special end timecode indicating that an SSA subtitle should be displayed until the next * subtitle, or until the end of the media in the case of the last subtitle. diff --git a/library/core/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 index 473e17e3e0..f23af98e7f 100644 --- a/library/core/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 @@ -28,7 +28,6 @@ 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.mp4.Atom.ContainerAtom; -import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor.Flags; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.NalUnitUtil; diff --git a/library/core/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 index d71d0863c7..939c00b9db 100644 --- a/library/core/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 @@ -101,8 +101,8 @@ public final class ChapterTocFrame extends Id3Frame { dest.writeByte((byte) (isOrdered ? 1 : 0)); dest.writeStringArray(children); dest.writeInt(subFrames.length); - for (int i = 0; i < subFrames.length; i++) { - dest.writeParcelable(subFrames[i], 0); + for (Id3Frame subFrame : subFrames) { + dest.writeParcelable(subFrame, 0); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java index 21de83524a..161fbb4871 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java @@ -37,7 +37,6 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste private final ArrayList mediaPeriods; private MediaSource.Listener sourceListener; - private ClippingTimeline clippingTimeline; /** * Creates a new clipping source that wraps the specified source. @@ -117,8 +116,7 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste @Override public void onSourceInfoRefreshed(Timeline timeline, Object manifest) { - clippingTimeline = new ClippingTimeline(timeline, startUs, endUs); - sourceListener.onSourceInfoRefreshed(clippingTimeline, manifest); + sourceListener.onSourceInfoRefreshed(new ClippingTimeline(timeline, startUs, endUs), manifest); int count = mediaPeriods.size(); for (int i = 0; i < count; i++) { mediaPeriods.get(i).setClipping(startUs, endUs); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java index 4cf3843ea1..bafdebf6cb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java @@ -40,10 +40,10 @@ public final class ConcatenatingMediaSource implements MediaSource { private final Map sourceIndexByMediaPeriod; private final boolean[] duplicateFlags; private final boolean isAtomic; + private final ShuffleOrder shuffleOrder; private Listener listener; private ConcatenatedTimeline timeline; - private ShuffleOrder shuffleOrder; /** * @param mediaSources The {@link MediaSource}s to concatenate. It is valid for the same diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java index 51e9757165..be8b3595db 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java @@ -19,7 +19,6 @@ import android.net.Uri; import android.os.Handler; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; -import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorsFactory; @@ -84,7 +83,6 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe private final int minLoadableRetryCount; private final Handler eventHandler; private final EventListener eventListener; - private final Timeline.Period period; private final String customCacheKey; private final int continueLoadingCheckIntervalBytes; @@ -149,7 +147,6 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe this.eventListener = eventListener; this.customCacheKey = customCacheKey; this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes; - period = new Timeline.Period(); } @Override @@ -187,7 +184,7 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe public void onSourceInfoRefreshed(long durationUs, boolean isSeekable) { // If we already have the duration from a previous source info refresh, use it. durationUs = durationUs == C.TIME_UNSET ? timelineDurationUs : durationUs; - if (timelineDurationUs == durationUs && timelineIsSeekable == isSeekable + if ((timelineDurationUs == durationUs && timelineIsSeekable == isSeekable) || (timelineDurationUs != C.TIME_UNSET && durationUs == C.TIME_UNSET)) { // Suppress no-op source info changes. return; diff --git a/library/core/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 index 130c7461f9..ddb3804c3e 100644 --- a/library/core/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 @@ -82,7 +82,7 @@ public final class CeaUtil { * number of 0xFF bytes and T is the value of the terminating byte. * * @param buffer The buffer from which to read the value. - * @returns The read value, or -1 if the end of the buffer is reached before a value is read. + * @return The read value, or -1 if the end of the buffer is reached before a value is read. */ private static int readNon255TerminatedValue(ParsableByteArray buffer) { int b; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index 42eeebde11..c789caded4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -1094,7 +1094,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { * * @param other The other score to compare to. * @return A positive integer if this score is better than the other. Zero if they are - * equal. A negative integer if this score is worse than the other. + * equal. A negative integer if this score is worse than the other. */ @Override public int compareTo(AudioTrackScore other) { diff --git a/library/core/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 index 0265ef83ea..57cbcafb53 100644 --- a/library/core/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 @@ -140,7 +140,7 @@ public interface Cache { * @param key The key of the data being requested. * @param position The position of the data being requested. * @return The {@link CacheSpan}. - * @throws InterruptedException + * @throws InterruptedException If the thread was interrupted. */ CacheSpan startReadWrite(String key, long position) throws InterruptedException, CacheException; diff --git a/library/smoothstreaming/src/androidTest/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestTest.java b/library/smoothstreaming/src/androidTest/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestTest.java index 0a221b6932..f172293c22 100644 --- a/library/smoothstreaming/src/androidTest/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestTest.java +++ b/library/smoothstreaming/src/androidTest/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestTest.java @@ -32,7 +32,7 @@ import junit.framework.TestCase; */ public class SsManifestTest extends TestCase { - private static ProtectionElement DUMMY_PROTECTION_ELEMENT = + private static final ProtectionElement DUMMY_PROTECTION_ELEMENT = new ProtectionElement(C.WIDEVINE_UUID, new byte[] {0, 1, 2}); public void testCopy() throws Exception { diff --git a/library/ui/src/main/res/drawable-anydpi-v21/exo_controls_shuffle.xml b/library/ui/src/main/res/drawable-anydpi-v21/exo_controls_shuffle.xml index 28ac6a5786..123c06c43e 100644 --- a/library/ui/src/main/res/drawable-anydpi-v21/exo_controls_shuffle.xml +++ b/library/ui/src/main/res/drawable-anydpi-v21/exo_controls_shuffle.xml @@ -21,6 +21,6 @@ diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashDownloadTest.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashDownloadTest.java index 7818ddcf12..8e02204c26 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashDownloadTest.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashDownloadTest.java @@ -170,7 +170,7 @@ public final class DashDownloadTest extends ActivityInstrumentationTestCase2 Date: Mon, 23 Oct 2017 04:16:48 -0700 Subject: [PATCH 0586/2472] Re-use single session when multiSession disabled ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=173098862 --- .../android/exoplayer2/drm/DefaultDrmSessionManager.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java index 52bb084281..677c784fe5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java @@ -367,10 +367,8 @@ public class DefaultDrmSessionManager implements DrmSe } } - DefaultDrmSession session = null; byte[] initData = null; String mimeType = null; - if (offlineLicenseKeySetId == null) { SchemeData data = getSchemeData(drmInitData, uuid); if (data == null) { @@ -389,8 +387,12 @@ public class DefaultDrmSessionManager implements DrmSe } } + DefaultDrmSession session; if (!multiSession) { - // Look for an existing session to use. + session = sessions.isEmpty() ? null : sessions.get(0); + } else { + // Only use an existing session if it has matching init data. + session = null; for (DefaultDrmSession existingSession : sessions) { if (existingSession.hasInitData(initData)) { session = existingSession; From 93423aa8d4cdd10d2e90f8e770abbae30f1e1d0b Mon Sep 17 00:00:00 2001 From: pavlotsky Date: Mon, 23 Oct 2017 07:23:17 -0700 Subject: [PATCH 0587/2472] Moved Exo IMA Demo to ExoPlayer. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=173112608 --- demos/ima/README.md | 4 + demos/ima/build.gradle | 39 +++++++ demos/ima/src/main/AndroidManifest.xml | 21 ++++ .../exoplayer2/imademo/DemoPlayer.java | 97 ++++++++++++++++++ .../exoplayer2/imademo/MainActivity.java | 57 ++++++++++ .../ima/src/main/res/layout/activity_main.xml | 14 +++ .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3418 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 4208 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2206 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 2555 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4842 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 6114 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 7718 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 10056 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 10486 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 14696 bytes demos/ima/src/main/res/values/colors.xml | 6 ++ demos/ima/src/main/res/values/strings.xml | 5 + demos/ima/src/main/res/values/styles.xml | 11 ++ settings.gradle | 2 + 20 files changed, 256 insertions(+) create mode 100644 demos/ima/README.md create mode 100644 demos/ima/build.gradle create mode 100644 demos/ima/src/main/AndroidManifest.xml create mode 100644 demos/ima/src/main/java/com/google/android/exoplayer2/imademo/DemoPlayer.java create mode 100644 demos/ima/src/main/java/com/google/android/exoplayer2/imademo/MainActivity.java create mode 100644 demos/ima/src/main/res/layout/activity_main.xml create mode 100644 demos/ima/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 demos/ima/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 demos/ima/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 demos/ima/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 demos/ima/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 demos/ima/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 demos/ima/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 demos/ima/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 demos/ima/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 demos/ima/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 demos/ima/src/main/res/values/colors.xml create mode 100644 demos/ima/src/main/res/values/strings.xml create mode 100644 demos/ima/src/main/res/values/styles.xml diff --git a/demos/ima/README.md b/demos/ima/README.md new file mode 100644 index 0000000000..8002b56667 --- /dev/null +++ b/demos/ima/README.md @@ -0,0 +1,4 @@ +# IMA demo application # + +This folder contains a demo application that showcases ExoPlayer integration +with the IMA SDK. diff --git a/demos/ima/build.gradle b/demos/ima/build.gradle new file mode 100644 index 0000000000..d93d826b67 --- /dev/null +++ b/demos/ima/build.gradle @@ -0,0 +1,39 @@ +// 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 from: '../../constants.gradle' +apply plugin: 'com.android.application' + +android { + compileSdkVersion project.ext.compileSdkVersion + buildToolsVersion project.ext.buildToolsVersion + defaultConfig { + applicationId "com.google.android.exoplayer2.imademo" + minSdkVersion 16 + targetSdkVersion project.ext.targetSdkVersion + versionCode 1 + versionName "1.0" + } + buildTypes { + release { + minifyEnabled false + } + } +} + +dependencies { + compile 'com.android.support:appcompat-v7:' + supportLibraryVersion + compile project(modulePrefix + 'library-core') + compile project(modulePrefix + 'library-ui') + compile project(modulePrefix + 'extension-ima') +} diff --git a/demos/ima/src/main/AndroidManifest.xml b/demos/ima/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..d6dfe4571e --- /dev/null +++ b/demos/ima/src/main/AndroidManifest.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + diff --git a/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/DemoPlayer.java b/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/DemoPlayer.java new file mode 100644 index 0000000000..d127304437 --- /dev/null +++ b/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/DemoPlayer.java @@ -0,0 +1,97 @@ +/* + * 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.imademo; + +import android.content.Context; +import android.net.Uri; +import com.google.android.exoplayer2.ExoPlayerFactory; +import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.ext.ima.ImaAdsLoader; +import com.google.android.exoplayer2.ext.ima.ImaAdsMediaSource; +import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; +import com.google.android.exoplayer2.extractor.ExtractorsFactory; +import com.google.android.exoplayer2.source.ExtractorMediaSource; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; +import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; +import com.google.android.exoplayer2.trackselection.TrackSelection; +import com.google.android.exoplayer2.trackselection.TrackSelector; +import com.google.android.exoplayer2.ui.SimpleExoPlayerView; +import com.google.android.exoplayer2.upstream.BandwidthMeter; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; +import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; +import com.google.android.exoplayer2.util.Util; + +/** + * This class deals with ExoPlayer, the IMA plugin, and all video playback. + */ +class DemoPlayer { + + private final ImaAdsLoader mAdsLoader; + private SimpleExoPlayer mPlayer; + private long mContentPosition; + + DemoPlayer(Context context) { + String adTag = context.getString(R.string.ad_tag_url); + mAdsLoader = new ImaAdsLoader(context, Uri.parse(adTag)); + } + + void init(Context context, SimpleExoPlayerView simpleExoPlayerView) { + // Create a default track selector. + BandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(); + TrackSelection.Factory videoTrackSelectionFactory = + new AdaptiveTrackSelection.Factory(bandwidthMeter); + TrackSelector trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory); + + // Create a simple ExoPlayer instance. + mPlayer = ExoPlayerFactory.newSimpleInstance(context, trackSelector); + + // Bind the player to the view. + simpleExoPlayerView.setPlayer(mPlayer); + + // Produces DataSource instances through which media data is loaded. + DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(context, + Util.getUserAgent(context, context.getString(R.string.app_name))); + // Produces Extractor instances for parsing the media data. + ExtractorsFactory extractorsFactory = new DefaultExtractorsFactory(); + // This is the MediaSource representing the non-ad, content media to be played. + String contentUrl = context.getString(R.string.content_url); + MediaSource contentMediaSource = new ExtractorMediaSource( + Uri.parse(contentUrl), dataSourceFactory, extractorsFactory, null, null); + // Compose the content media source into a new ImaAdMediaSource with both ads and content. + MediaSource mediaSourceWithAds = new ImaAdsMediaSource( + contentMediaSource, + dataSourceFactory, + mAdsLoader, + simpleExoPlayerView.getOverlayFrameLayout()); + // Prepare the player with the source. + mPlayer.seekTo(mContentPosition); + mPlayer.prepare(mediaSourceWithAds); + mPlayer.setPlayWhenReady(true); + } + + void reset() { + if (mPlayer != null) { + mContentPosition = mPlayer.getContentPosition(); + mPlayer.release(); + } + } + + void release() { + mAdsLoader.release(); + } +} diff --git a/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/MainActivity.java b/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/MainActivity.java new file mode 100644 index 0000000000..6cacdf252f --- /dev/null +++ b/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/MainActivity.java @@ -0,0 +1,57 @@ +/* + * 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.imademo; + +import android.os.Bundle; +import android.support.v7.app.AppCompatActivity; +import com.google.android.exoplayer2.ui.SimpleExoPlayerView; + +/** + * Main Activity for the ExoPlayer IMA plugin example. ExoPlayer objects are created by DemoPlayer, + * which this class instantiates. + */ +public class MainActivity extends AppCompatActivity { + + private DemoPlayer mPlayer; + private SimpleExoPlayerView mView; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + mView = (SimpleExoPlayerView) findViewById(R.id.simpleExoPlayerView); + mPlayer = new DemoPlayer(this); + } + + @Override + public void onResume() { + super.onResume(); + mPlayer.init(this, mView); + } + + @Override + public void onPause() { + super.onPause(); + mPlayer.reset(); + } + + @Override + public void onDestroy() { + mPlayer.release(); + super.onDestroy(); + } +} diff --git a/demos/ima/src/main/res/layout/activity_main.xml b/demos/ima/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000000..180ab3223f --- /dev/null +++ b/demos/ima/src/main/res/layout/activity_main.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/demos/ima/src/main/res/mipmap-hdpi/ic_launcher.png b/demos/ima/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..cde69bcccec65160d92116f20ffce4fce0b5245c GIT binary patch literal 3418 zcmZ{nX*|@A^T0p5j$I+^%FVhdvMbgt%d+mG98ubwNv_tpITppba^GiieBBZGI>I89 zGgm8TA>_)DlEu&W;s3#ZUNiH4&CF{a%siTjzG;eOzQB6{003qKeT?}z_5U*{{kgZ; zdV@U&tqa-&4FGisjMN8o=P}$t-`oTM2oeB5d9mHPgTYJx4jup)+5a;Tke$m708DocFzDL>U$$}s6FGiy_I1?O zHXq`q884|^O4Q*%V#vwxqCz-#8i`Gu)2LeB0{%%VKunOF%9~JcFB9MM>N00M`E~;o zBU%)O5u-D6NF~OQV7TV#JAN;=Lylgxy0kncoQpGq<<_gxw`FC=C-cV#$L|(47Hatl ztq3Jngq00x#}HGW@_tj{&A?lwOwrVX4@d66vLVyj1H@i}VD2YXd)n03?U5?cKtFz4 zW#@+MLeDVP>fY0F2IzT;r5*MAJ2}P8Z{g3utX0<+ZdAC)Tvm-4uN!I7|BTw&G%RQn zR+A5VFx(}r<1q9^N40XzP=Jp?i=jlS7}T~tB4CsWx!XbiHSm zLu}yar%t>-3jlutK=wdZhES->*1X({YI;DN?6R=C*{1U6%wG`0>^?u}h0hhqns|SeTmV=s;Gxx5F9DtK>{>{f-`SpJ`dO26Ujk?^%ucsuCPe zIUk1(@I3D^7{@jmXO2@<84|}`tDjB}?S#k$ik;jC))BH8>8mQWmZ zF#V|$gW|Xc_wmmkoI-b5;4AWxkA>>0t4&&-eC-J_iP(tLT~c6*(ZnSFlhw%}0IbiJ ztgnrZwP{RBd(6Ds`dM~k;rNFgkbU&Yo$KR#q&%Kno^YXF5ONJwGwZ*wEr4wYkGiXs z$&?qX!H5sV*m%5t@3_>ijaS5hp#^Pu>N_9Q?2grdNp({IZnt|P9Xyh);q|BuoqeUJ zfk(AGX4odIVADHEmozF|I{9j>Vj^jCU}K)r>^%9#E#Y6B0i#f^iYsNA!b|kVS$*zE zx7+P?0{oudeZ2(ke=YEjn#+_cdu_``g9R95qet28SG>}@Me!D6&}un*e#CyvlURrg8d;i$&-0B?4{eYEgzwotp*DOQ_<=Ai21Kzb0u zegCN%3bdwxj!ZTLvBvexHmpTw{Z3GRGtvkwEoKB1?!#+6h1i2JR%4>vOkPN_6`J}N zk}zeyY3dPV+IAyn;zRtFH5e$Mx}V(|k+Ey#=nMg-4F#%h(*nDZDK=k1snlh~Pd3dA zV!$BoX_JfEGw^R6Q2kpdKD_e0m*NX?M5;)C zb3x+v?J1d#jRGr=*?(7Habkk1F_#72_iT7{IQFl<;hkqK83fA8Q8@(oS?WYuQd4z^ z)7eB?N01v=oS47`bBcBnKvI&)yS8`W8qHi(h2na?c6%t4mU(}H(n4MO zHIpFdsWql()UNTE8b=|ZzY*>$Z@O5m9QCnhOiM%)+P0S06prr6!VET%*HTeL4iu~!y$pN!mOo5t@1 z?$$q-!uP(+O-%7<+Zn5i=)2OftC+wOV;zAU8b`M5f))CrM6xu94e2s78i&zck@}%= zZq2l!$N8~@63!^|`{<=A&*fg;XN*7CndL&;zE(y+GZVs-IkK~}+5F`?ergDp=9x1w z0hkii!N(o!iiQr`k`^P2LvljczPcM`%7~2n#|K7nJq_e0Ew;UsXV_~3)<;L?K9$&D zUzgUOr{C6VLl{Aon}zp`+fH3>$*~swkjCw|e>_31G<=U0@B*~hIE)|WSb_MaE41Prxp-2eEg!gcon$fN6Ctl7A_lV8^@B9B+G~0=IYgc%VsprfC`e zoBn&O3O)3MraW#z{h3bWm;*HPbp*h+I*DoB%Y~(Fqp9+x;c>K2+niydO5&@E?SoiX_zf+cI09%%m$y=YMA~rg!xP*>k zmYxKS-|3r*n0J4y`Nt1eO@oyT0Xvj*E3ssVNZAqQnj-Uq{N_&3e45Gg5pna+r~Z6^ z>4PJ7r(gO~D0TctJQyMVyMIwmzw3rbM!};>C@8JA<&6j3+Y9zHUw?tT_-uNh^u@np zM?4qmcc4MZjY1mWLK!>1>7uZ*%Pe%=DV|skj)@OLYvwGXuYBoZvbB{@l}cHK!~UHm z4jV&m&uQAOLsZUYxORkW4|>9t3L@*ieU&b0$sAMH&tKidc%;nb4Z=)D7H<-`#%$^# zi`>amtzJ^^#zB2e%o*wF!gZBqML9>Hq9jqsl-|a}yD&JKsX{Op$7)_=CiZvqj;xN& zqb@L;#4xW$+icPN?@MB|{I!>6U(h!Wxa}14Z0S&y|A5$zbH(DXuE?~WrqNv^;x}vI z0PWfSUuL7Yy``H~*?|%z zT~ZWYq}{X;q*u-}CT;zc_NM|2MKT8)cMy|d>?i^^k)O*}hbEcCrU5Bk{Tjf1>$Q=@ zJ9=R}%vW$~GFV_PuXqE4!6AIuC?Tn~Z=m#Kbj3bUfpb82bxsJ=?2wL>EGp=wsj zAPVwM=CffcycEF; z@kPngVDwPM>T-Bj4##H9VONhbq%=SG;$AjQlV^HOH7!_vZk=}TMt*8qFI}bI=K9g$fgD9$! zO%cK1_+Wbk0Ph}E$BR2}4wO<_b0{qtIA1ll>s*2^!7d2e`Y>$!z54Z4FmZ*vyO}EP z@p&MG_C_?XiKBaP#_XrmRYszF;Hyz#2xqG%yr991pez^qN!~gT_Jc=PPCq^8V(Y9K zz33S+Mzi#$R}ncqe!oJ3>{gacj44kx(SOuC%^9~vT}%7itrC3b;ZPfX;R`D2AlGgN zw$o4-F77!eWU0$?^MhG9zxO@&zDcF;@w2beXEa3SL^htWYY{5k?ywyq7u&)~Nys;@ z8ZNIzUw$#ci&^bZ9mp@A;7y^*XpdWlzy%auO1hU=UfNvfHtiPM@+99# z!uo2`>!*MzphecTjN4x6H)xLeeDVEO#@1oDp`*QsBvmky=JpY@fC0$yIexO%f>c-O zAzUA{ch#N&l;RClb~;`@dqeLPh?e-Mr)T-*?Sr{32|n(}m>4}4c3_H3*U&Yj)grth z{%F0z7YPyjux9hfqa+J|`Y%4gwrZ_TZCQq~0wUR8}9@Jj4lh( z#~%AcbKZ++&f1e^G8LPQ)*Yy?lp5^z4pDTI@b^hlv06?GC%{ZywJcy}3U@zS3|M{M zGPp|cq4Zu~9o_cEZiiNyU*tc73=#Mf>7uzue|6Qo_e!U;oJ)Z$DP~(hOcRy&hR{`J zP7cNIgc)F%E2?p%{%&sxXGDb0yF#zac5fr2x>b)NZz8prv~HBhw^q=R$nZ~@&zdBi z)cEDu+cc1?-;ZLm?^x5Ov#XRhw9{zr;Q#0*wglhWD={Pn$Qm$;z?Vx)_f>igNB!id zmTlMmkp@8kP212#@jq=m%g4ZEl$*a_T;5nHrbt-6D0@eqFP7u+P`;X_Qk68bzwA0h zf{EW5xAV5fD)il-cV&zFmPG|KV4^Z{YJe-g^>uL2l7Ep|NeA2#;k$yerpffdlXY<2 znDODl8(v(24^8Cs3wr(UajK*lY*9yAqcS>92eF#8&Yxa2Dcw(Xv69J_N zk;D>XMA4`aM3i10k4LkBNK-;@A|OZ;#K7a*d%yYSG4Jup%tK1DbI$+FD>GmD&As=# z-?RrF=*NW+GKk5>gy{bd{J$)$!-GM#xR$V=ZlB*AFlGtZIU5uI4+V_?jR8H!G=}{) z)S5DXEnw(TH~8&w&`i)~kRK=sR0yi=?Cfj--DASfwd}tnw(Tcu-^UHglw^$q0gSEC z4dC;Wpw*yrplawiL20#GN#ggzGC;ws%qI=p*LI*=jE&&?bkGl=+Xhgy9c*DAwQT7$ zke2<|A=tiC2n@?+bxb#Kzrh2}Y6PDhK+)KG0hA5_3DQIHR67h{VVw@f+SK0x*oJ)` z4+;>1F+A$MpiWkY5EQmyykYzL1CE{G^M62h8JNyK0AmUitrM0uY?HCJ_9+}#KMYVp z1QyfYhfs`)Zv%^aq1eVgg(QG88B~G|VU5!EHyndF#e*ujckkYdeFBLOeC_S+v(StM zaL7QEplxk;?%er%uLf_PK2*8@om>!v$v_t0Mp%)ChK9wxVo7{~U^(xIfrE|d2M}f< zp|wN%Nli`7ocjuiH%ahgj5%$V;MCu#A=hpukh^UyeFmo$>dLN+C-u$M79l}D+KP*d z|9oHEO_1Z*W3Xc}$0Qs)LUBL)k#CZhkmSNZ^2;y3^g0}@BO(7Z@k&q-Rqhem21}4y zT3SjoGcz9*_OVBRpxh8K0T~;6H8+KPleB^yNLfiLYm0i--LUM6+5+N}w1jxaFQ9c> zIw*V}>gwvkp=*Pz2E>~mRQR#j(Fz+}RaHd-61}Mv1!cI9*1N41_d(&27mEMgtZPBp z0qIWEdi*sWv~H0Hq#az1l$DkJ*D6=zCwq7A-W>;UTKU{UR6J;HB{|o#$ak85QAinO zs%~bF-?4#Bcj`&Wt!$E25l2#r&XD+gKdR)SK=@5f|7(P8a9d+#q?g7JuS6yJR=tYW z3GEe~C*fez+}zxno}T`DVV@-df}?R-YOaGv@b>N7B9`6MhOX?ZGIm$hdB zu%8I{%9SgxTZ~1#i9viA<9U^r$-b2365vR)9&>>9B*@8L2;4tcUNSq~Fc++0jur+Cx}WstFViF^CqD+; z-jwQIH1}z&ft=@``cQOm78Ad;jU?deb_!68^%w)>1JF;WZzaB|8;k-%9ZXqG+ahs_ zL){E!`qf@uUZaFe^hPg;KQsCB%2G$H$ZPwJfZ;4AxiEm#H`L?#7*bY~M-E?FF98k* z==+On=)PD6mX%m=$|xXIc(xCXg;H}O9L-cJl_RoTP&2W=s zMf`A|o11%DFAfQAF&PYzJV6Q|I+v*{2kUvyAn{G3i#8MlQ6*#Ddc#I`<$2Z_0WQ5GpAzQ1pm~ea1jkSy@>)Y0{+O zxS7|CijZ{FOM zF!F%H!^6h`phhWx>Kksuu)V@85HVoPxt8(F*)kkY%{<797ST3J%&42Zy}c)O0~8t> zIuQW1ik+aMZx`IiG-)xGfJlQQ-Fgtv9*vCT-^dUfhdLRcRsb}m8=&Ce;7L*dp>JO) zQb__~9?X4&!vLYu3S-5_Asrx3PtTXS0XlKw!~`g)Nvw3oSmIVK|!K}H0BsFS-!+evp}TYrP>p3sQG&GL}}PM zUMY}*NlrYBN=DpK>UnyK%KSlWKBNoM>({RzCmh8npb;ZR42Os>dYH#b!%`2CttS=a zQ$IP`;wK}Y!TPh~OeZ*f{v+rl=#-3XJtZgGPJ{gACzo&~2-XpxNKUSiaxJpO6A5GV>618&CCo;u5MPI|0DX^Pmt;&M4Y>fIvI1WF1$KT~SI- z(Mqx#6{93>u?n(Vr66t~cPen5I9RK3Ei>v`?j~HzjcP6l&kzp?N4vDNw4acL-YE|@ zF&hH&kgZ}Ts}xYyp{~FRal;j?K;J4ji*ThD!2}N)W^w&>o08 z2m)h|m{H3^PXH+MfY=z+fk|a#WTXq5YIK{d+D1e~IEuYR*AS2nQiMJrSDm|XfObbI zsKxMrcE@rSqYnt-$SELC3I_pLhT~}fM=T(;99$Y38_E9t`xhY#!_yt;Yc@-lE*%RL zE5(dtJRp8J<{|AtNRiBX5D;1rxYjNTNTCC?J4Qj_@PK%ia*vZ!KpyB;YPnHBmf=VS zL<4kLSy|PbIddkm*}VQE4~*EuRaI5z#l#^)KtkcwPK1GQTy%gi?#Oj6wkt*bp}q@{(gY+WagFMV zL9Pf#0En|5Ilz(Y0YW&O70J5*SqaBo<0uLcgcU8GO+0n#)ThV*K-n365(idxix)5c zV{2<`jU_kJ2V`6b34!Rt;f8HPIBqH#6>mL;?qv-eF@SjYs;H=_ef#aV@y04UlTQ@+ z`}+@p)nobj`4-PCa>M+0W&u%18h{eR3JB;X6NEg=1$=200}0Lri75(Vp+mRB?CY*21#bpdJs%c;JC-nF$)ND zL$sc{x;nCT>(&L>ccbw~xNO+40iV%&sd zz!3+C_U-cJ%L&luQLOLg7e;WnkB`qnJRxt&is)1W0GXOu8=Y+v_{X5cAEW<^?Kb1|uax*#z?ah%-a z=21X6ukwI7ln{=Gm2liBpzgDIe&m8M(j=3~W@2BRoSdZHrwBVB(Wioff}HR!EP&Ku zc)~0tCmcGg5D!LgsOBuD3l4M~Cz@zE43If6V&J&NJCbB*qws_odIa_bFC85@a>Nz; zxN+mghpf5Lb%xXs=36tU8>eFGdh|=h#l?k&k33=anR6|N1jqT2 zW6`_F(I^+m@{JVAnG^o5lXKVaCbiQ*E+klWjJ8d9dmgqO!$nqBR?(kBW^&`k4N_QGNFc!+5W==#n-C6vMWcgF*^7#b znqjse$3C&X^?X^jY?(c*o^f_|UUlo%Ev*m|?`~+e7z_u3ur0zX89W@APG}(^TnBv_ z!}@gJUQ#efp-?;m>v3LQUK^^btF`PV&-VU!vPa6DC+Jo@95}!mu@8=pj*s3?IQ(KW zW5x_Dcml+x56jET8`(^FKtkdJGR7QmtEMemwxH!qm_B_vo{;ag2YqeceDh6w^TGJ# z%a_ZpU%y_&vTdz3_cZn*94)p9-7O;{qiEs6g-UEQYkRLh1#L5H)+{^QdOI*x1+@XyY_&D{FI~Jt98nt+(F7r-?^{CLcb0*tw*nqydju ze}EE#!8Slj(s1CwfnCrxe3*AMYipmsHD=J%sZ)oI9Xl3pdYm|O=FC~q(a|9_H8peu zVW2vC)AjgQSFlkPuZrSTiBJaz2Yi5cBDM|N*dK6&i|w>&)6ln{1-$@i`v-}MiSann zVSHkX?u`;Xu`Jw|m4Q&Syv1N$SSQrI8ry(vVQm^PFFT>uG=BVed>hLI(3ExS)-4YU z3-gDhtqL!v@K(iMUC|+Y#|iwWWgXW^@EhG0_u==)vYMKjFd?kMI@YXNgQqL-mX!(E zhJj!;rk264yz+`Yb2|j}0xUCqe0;X4)#^ydax3uc9cH-v1k%!i!!&N&($YeoLn|mK zsDOD?1eS?qGmDvkbz=W8<&GtU-}>|S$M5}kyxz~p>-~Pb{(irc?QF~icx8A201&Xin%Hxx@kekd zw>yHjlemC*8(JFz05gs6x7#7EM|xoGtpVVs0szqB0bqwaqAdVG7&rLc6#(=y0YEA! z=jFw}xeKVfmAMI*+}bv7qH=LK2#X5^06wul0s+}M(f|O@&WMyG9frlGyLb z&Eix=47rL84J+tEWcy_XTyc*xw9uOQy`qmHCjAeJ?d=dUhm;P}^F=LH42AEMIh6X8 z*I7Q1jK%gVlL|8w?%##)xSIY`Y+9$SC8!X*_A*S0SWOKNUtza(FZHahoC2|6f=*oD zxJ8-RZk!+YpG+J}Uqnq$y%y>O^@e5M3SSw^29PMwt%8lX^9FT=O@VX$FCLBdlj#<{ zJWWH<#iU!^E7axvK+`u;$*sGq1SmGYc&{g03Md&$r@btQSUIjl&yJXA&=79FdJ+D< z4K^ORdM{M0b2{wRROvjz1@Rb>5dFb@gfkYiIOAKM(NR3*1JpeR_Hk3>WGvU&>}D^HXZ02JUnM z@1s_HhX#rG7;|FkSh2#agJ_2fREo)L`ws+6{?IeWV(>Dy8A(6)IjpSH-n_uO=810y z#4?ez9NnERv6k)N13sXmx)=sv=$$i_QK`hp%I2cyi*J=ihBWZLwpx9Z#|s;+XI!0s zLjYRVt!1KO;mnb7ZL~XoefWU02f{jcY`2wZ4QK+q7gc4iz%d0)5$tPUg~$jVI6vFO zK^wG7t=**T40km@TNUK+WTx<1mL|6Tn6+kB+E$Gpt8SauF9E-CR9Uui_EHn_nmBqS z>o#G}58nHFtICqJPx<_?UZ;z0_(0&UqMnTftMKW@%AxYpa!g0fxGe060^xkRtYguj ze&fPtC!?RgE}FsE0*^2lnE>42K#jp^nJDyzp{JV*jU?{+%KzW37-q|d3i&%eooE6C8Z2t2 z9bBL;^fzVhdLxCQh1+Ms5P)ilz9MYFKdqYN%*u^ch(Fq~QJASr5V_=szAKA4Xm5M} z(Kka%r!noMtz6ZUbjBrJ?Hy&c+mHB{OFQ}=41Irej{0N90`E*~_F1&7Du+zF{Dky) z+KN|-mmIT`Thcij!{3=ibyIn830G zN{kI3d`NgUEJ|2If}J!?@w~FV+v?~tlo8ps3Nl`3^kI)WfZ0|ms6U8HEvD9HIDWkz6`T_QSewYZyzkRh)!g~R>!jaR9;K|#82kfE5^;R!~}H4C?q{1AG?O$5kGp)G$f%VML%aPD?{ zG6)*KodSZRXbl8OD=ETxQLJz)KMI7xjArKUNh3@0f|T|75?Yy=pD7056ja0W)O;Td zCEJ=7q?d|$3rZb+8Cvt6mybV-#1B2}Jai^DOjM2<90tpql|M5tmheg){2NyZR}x3w zL6u}F+C-PIzZ56q0x$;mVJXM1V0;F}y9F29ob51f;;+)t&7l30gloMMHPTuod530FC}j^4#qOJV%5!&e!H9#!N&XQvs5{R zD_FOomd-uk@?_JiWP%&nQ_myBlM6so1Ffa1aaL7B`!ZTXPg_S%TUS*>M^8iJRj1*~ e{{%>Z1YfTk|3C04d;8A^0$7;Zm{b|L#{L(;l>}-4 literal 0 HcmV?d00001 diff --git a/demos/ima/src/main/res/mipmap-mdpi/ic_launcher_round.png b/demos/ima/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..efc028a636dd690a51db5a525cf781a5a7daba68 GIT binary patch literal 2555 zcmVDi>vW`@Y|P=j^x3Ifn%y?#weBmhZgZ z^Srn3`_5s_nkW1KfDd9V!jFD>F_Mc=&(D`S9F8`G9j`|SbWPvU-)IaU`}$WdghKD(z^U%DuFl=dhBq1 zV2N08FaBOdb12Qd668Nb;&Z~}bITyD2yV;4Q;V)Yd}0yejcD*w$?M!}^D9N(BLyEz zzdw5PC}r6q#BPAbGB|lDe_=J@3Wft_XJ;=W1)n8}5Q_(meMaO(qlBrMNwAM~()TMt z7``0qU^YGKgUvTFF>zWD;p2?}U+(!oOP=>E(#D=LI9;^|21mP}Sb%-B3r<$-f`)GE zf+ENH9giPBhLMqxk3?>Z_Ib>|pGpO*ls1Edc1SPZ4+Zs6n5(m@o)w`qhVIR+3x!nc z2QWA^sF+UVL`bPYG*m}z-@eUAx}Y&)U4(ZX!1ID&B)9UZ-m)SmI=x*&DX z(4U0VQSCNkV`Ff+G6~M!-Uofd_rTVE5zbccg%jm(Lo!1!!}0Rp$Ve*N38}aK2$p*n zpm(?p)9??FQ;`7UThq+UOtDt(yU340PTgTf-cvxbAYdW+ zodS8MfJB=CGHd^~s0fLZ-EJ=tYQaZdAO;5qU&BEYQVUZvM7db#>3OfcuPlI&kC9O8 zXc8ynO6$TzSy@?tytqki3G?eco<8$hd0*Xm)s6T`#OF=Nz|?XUQmTHh=zTGLKE-+| z`R_lmJHKZj zYHDgW;R5zROF(6Nf!D;<$-4^>$-4vuLPcAirU0zhk=)$eH)H`8i{&*f0hE))jVY>R zmqT9B`&@vr{-k0Zhyu=?I~O1eC@L!YJ}zQ*H377xy<8iOlOj14B;uwl(JEnwjAJr_ zIFPu-00|bojChNVBak8YiwHKSngDD7gUQLsn`8k84<3AZYHCWgh-vZ4u!X_jGYxR) zq8|Q1$V6o6;p0n)Y&{&#F~E^rJsc(EAuj77G#^obxT1%!D>?`(A_PMCRVU~=tY|yO zHVEaoPJAc#i9+(48VAl77nID%R4M5zcJ#F_)$kX3y|RRI0$?(VKa z&d-Y*IbZCp=~@DEYr|PSAG7R$NTWpBz(_|H8#rMDBOQAaVG81;4G>?7DO1YR#;Tn6 zgm{iiHR=MWHX0flE+A(=#+`2^eCq4#-GFC! z6M$q(^=<;x$j4i^s|lc;#5~q2T)%#OKVOMmTZ!}M&%cE?jVW#BSPIpK3EjjgBC41R zU=h$eBj6^$nKJQasbF=Bl6MMNSOesJ+RS09kH^Hs{G2bqzT$RzJ?=lyi2lg=rilsXN0U$-dvIO{gZQWn5CwY0QYkn1i@vBQ*i6ms==x^iJG#36RN40+4*XRgHY0OkPO<9mtU5JZ^U&KR=(+$Jgyx zDIL$YY}xWX3{k7+k&+4cB2-?0JVEIZU7}-f3eXAOclCI0$TI=e3k0wuC3c^-&6_uG zR6N*oMPDbVp?Du@1oKFGD6fK=08A@$~dMVygPvL8+hkiK{R{*ed% zA|nNnV>ylomVT*i&f`G~^78Uxh|{8v7Nyn{92`s``gUbyWd@x=@k0-m99ZD=a0z;Q zdshWyo93XoXijn<_WCU1LY%yQYs2e-LiK8Ob#)<+1PkeEKVFy8hUToOsJMz8en4DQ z^L~*R9P1F9Y&P3P+^sSZR1(zHR^hz>d%;0-P}*QOB+vhlIItCWIUjx_iP%Vah~b^# zk7wprN{B$5*%}@mp2^C}ilsT9h`g9i0RaKeQXb;D;hnp8@77Q>s6z=t97}xdB)!pO z#K{)fY;JC@IdI^>ZkmhcTyolI6*d|p5%eVB&CJZqu#S$7Rthzb2>VEHRu*~1>JY}W zbRkF@9VldW5~{?cGD{E9%= z^d0?;k9mdPCi)Wq~U2RobsvA@Q0MM$dq4lq5{hy#9 zzgp+B{O(-=?1<7r0l>Q?>N6X%s~lmgrmqD6fjj_!c?AF`S0&6U06Z51fWOuNAe#jM z%pSN#J-Mp}`ICpL=qp~?u~Jj$6(~K_%)9}Bn(;pY0&;M00H9x2N23h=CpR7kr8A9X zU%oh4-E@i!Ac}P+&%vOPQ3warO9l!SCN)ixGW54Jsh!`>*aU)#&Mg7;#O_6xd5%I6 zneGSZL3Kn-4B^>#T7pVaIHs3^PY-N^v1!W=%gzfioIWosZ!BN?_M)OOux&6HCyyMf z3ToZ@_h75A33KyC!T)-zYC-bp`@^1n;w3~N+vQ0#4V7!f|JPMlWWJ@+Tg~8>1$GzLlHGuxS)w&NAF*&Y;ef`T^w4HP7GK%6UA8( z{&ALM(%!w2U7WFWwq8v4H3|0cOjdt7$JLh(;U8VcTG;R-vmR7?21nA?@@b+XPgJbD z*Y@v&dTqo5Bcp-dIQQ4@?-m{=7>`LZ{g4jvo$CE&(+7(rp#WShT9&9y>V#ikmXFau03*^{&d(AId0Jg9G;tc7K_{ivzBjqHuJx08cx<8U`z2JjtOK3( zvtuduBHha>D&iu#))5RKXm>(|$m=_;e?7ZveYy=J$3wjL>xPCte-MDcVW<;ng`nf= z9);CVVZjI-&UcSAlhDB{%0v$wPd=w6MBwsVEaV!hw~8G(rs`lw@|#AAHbyA&(I-7Y zFE&1iIGORsaskMqSYfX33U%&17oTszdHPjr&Sx(`IQzoccST*}!cU!ZnJ+~duBM6f z{Lf8PITt%uWZ zTY09Jm5t<2+Un~yC-%DYEP>c-7?=+|reXO4Cd^neCQ{&aP@yODLN8}TQAJ8ogsnkb zM~O>~3&n6d+ee`V_m@$6V`^ltL&?uwt|-afgd7BQ9Kz|g{B@K#qQ#$o4ut`9lQsYfHofccNoqE+`V zQ&UXP{X4=&Z16O_wCk9SFBQPKyu?<&B2zDVhI6%B$12c^SfcRYIIv!s1&r|8;xw5t zF~*-cE@V$vaB;*+91`CiN~1l8w${?~3Uy#c|D{S$I? zb!9y)DbLJ3pZ>!*+j=n@kOLTMr-T2>Hj^I~lml-a26UP1_?#!5S_a&v zeZ86(21wU0)4(h&W0iE*HaDlw+-LngX=}es#X$u*1v9>qR&qUGfADc7yz6$WN`cx9 zzB#!5&F%AK=ed|-eV6kb;R>Atp2Rk=g3lU6(IVEP3!;0YNAmqz=x|-mE&8u5W+zo7 z-QfwS6uzp9K4wC-Te-1~u?zPb{RjjIVoL1bQ=-HK_a_muB>&3I z*{e{sE_sI$CzyK-x>7abBc+uIZf?#e8;K_JtJexgpFEBMq92+Fm0j*DziUMras`o= zTzby8_XjyCYHeE@q&Q_7x?i|V9XY?MnSK;cLV?k>vf?!N87)gFPc9#XB?p)bEWGs$ zH>f$8?U7In{9@vsd%#sY5u!I$)g^%ZyutkNBBJ0eHQeiR5!DlQbYZJ-@09;c?IP7A zx>P=t*xm1rOqr@ec>|ziw@3e$ymK7YSXtafMk30i?>>1lC>LLK1~JV1n6EJUGJT{6 zWP4A(129xkvDP09j<3#1$T6j6$mZaZ@vqUBBM4Pi!H>U8xvy`bkdSNTGVcfkk&y8% z=2nfA@3kEaubZ{1nwTV1gUReza>QX%_d}x&2`jE*6JZN{HZtXSr{{6v6`r47MoA~R zejyMpeYbJ$F4*+?*=Fm7E`S_rUC0v+dHTlj{JnkW-_eRa#9V`9o!8yv_+|lB4*+p1 zUI-t)X$J{RRfSrvh80$OW_Wwp>`4*iBr|oodPt*&A9!SO(x|)UgtVvETLuLZ<-vRp z&zAubgm&J8Pt647V?Qxh;`f6E#Zgx5^2XV($YMV7;Jn2kx6aJn8T>bo?5&;GM4O~| zj>ksV0U}b}wDHW`pgO$L@Hjy2`a)T}s@(0#?y3n zj;yjD76HU&*s!+k5!G4<3{hKah#gBz8HZ6v`bmURyDi(wJ!C7+F%bKnRD4=q{(Fl0 zOp*r}F`6~6HHBtq$afFuXsGAk58!e?O(W$*+3?R|cDO88<$~pg^|GRHN}yml3WkbL zzSH*jmpY=`g#ZX?_XT`>-`INZ#d__BJ)Ho^&ww+h+3>y8Z&T*EI!mtgEqiofJ@5&E z6M6a}b255hCw6SFJ4q(==QN6CUE3GYnfjFNE+x8T(+J!C!?v~Sbh`Sl_0CJ;vvXsP z5oZRiPM-Vz{tK(sJM~GI&VRbBOd0JZmGzqDrr9|?iPT(qD#M*RYb$>gZi*i)xGMD`NbmZt;ky&FR_2+YqpmFb`8b`ry;}D+y&WpUNd%3cfuUsb8 z7)1$Zw?bm@O6J1CY9UMrle_BUM<$pL=YI^DCz~!@p25hE&g62n{j$?UsyYjf#LH~b z_n!l6Z(J9daalVYSlA?%=mfp(!e+Hk%%oh`t%0`F`KR*b-Zb=7SdtDS4`&&S@A)f>bKC7vmRWwT2 zH}k+2Hd7@>jiHwz^GrOeU8Y#h?YK8>a*vJ#s|8-uX_IYp*$9Y=W_Edf%$V4>w;C3h z&>ZDGavV7UA@0QIQV$&?Z_*)vj{Q%z&(IW!b-!MVDGytRb4DJJV)(@WG|MbhwCx!2 z6QJMkl^4ju9ou8Xjb*pv=Hm8DwYsw23wZqQFUI)4wCMjPB6o8yG7@Sn^5%fmaFnfD zSxp8R-L({J{p&cR7)lY+PA9#8Bx87;mB$zXCW8VDh0&g#@Z@lktyArvzgOn&-zerA zVEa9h{EYvWOukwVUGWUB5xr4{nh}a*$v^~OEasKj)~HyP`YqeLUdN~f!r;0dV7uho zX)iSYE&VG67^NbcP5F*SIE@T#=NVjJ1=!Mn!^oeCg1L z?lv_%(ZEe%z*pGM<(UG{eF1T(#PMw}$n0aihzGoJAP^UceQMiBuE8Y`lZ|sF2_h_6 zQw*b*=;2Ey_Flpfgsr4PimZ~8G~R(vU}^Zxmri5)l?N>M_dWyCsjZw<+a zqjmL0l*}PXNGUOh)YxP>;ENiJTd|S^%BARx9D~%7x?F6u4K(Bx0`KK2mianotlX^9 z3z?MW7Coqy^ol0pH)Z3+GwU|Lyuj#7HCrqs#01ZF&KqEg!olHc$O#Wn>Ok_k2`zoD z+LYbxxVMf<(d2OkPIm8Xn>bwFsF6m8@i7PA$sdK~ZA4|ic?k*q2j1YQ>&A zjPO%H@H(h`t+irQqx+e)ll9LGmdvr1zXV;WTi}KCa>K82n90s|K zi`X}C*Vb12p?C-sp5maVDP5{&5$E^k6~BuJ^UxZaM=o+@(LXBWChJUJ|KEckEJTZL zI2K&Nd$U65YoF3_J6+&YU4uKGMq2W6ZQ%BG>4HnIM?V;;Ohes{`Ucs56ue^7@D7;4 z+EsFB)a_(%K6jhxND}n!UBTuF3wfrvll|mp7)3wi&2?LW$+PJ>2)2C-6c@O&lKAn zOm=$x*dn&dI8!QCb(ul|t3oDY^MjHqxl~lp{p@#C%Od-U4y@NQ4=`U!YjK$7b=V}D z%?E40*f8DVrvV2nV>`Z3f5yuz^??$#3qR#q6F($w>kmKK`x21VmX=9kb^+cPdBY2l zGkIZSf%C+`2nj^)j zo}g}v;5{nk<>%xj-2OqDbJ3S`7|tQWqdvJdgiL{1=w0!qS9$A`w9Qm7>N0Y*Ma%P_ zr@fR4>5u{mKwgZ33Xs$RD6(tcVH~Mas-87Fd^6M6iuV^_o$~ql+!eBIw$U)lzl`q9 z=L6zVsZzi0IIW=DT&ES9HajKhb5lz4yQxT-NRBLv_=2sn7WFX&Wp6Y!&}P+%`!A;s zrCwXO3}jrdA7mB`h~N~HT64TM{R$lNj*~ekqSP^n9P~z;P zWPlRPz0h6za8-P>!ARb+A1-r>8VF*xhrGa8W6J$p*wy`ULrD$CmYV7Gt^scLydQWbo7XN-o9X1i7;l+J_8Ncu zc=EX&dg`GRo4==cz2d_Rz28oLS`Suf6OCp~f{0-aQ`t5YZ=!CAMc6-RZw#}A%;s44 znf2`6gcgm=0SezTH9h+JzeR3Lcm;8?*@+?FDfguK^9)z(Z`I!RKrSAI?H~4et6GTkz07Qgq4B6%Q*8Y0yPc4x z8(^YwtZjYIeOvVLey#>@$UzIciJ#x0pJLFg=8UaZv%-&?Yzp7gWNIo_x^(d75=x2c zv|LQ`HrKP(8TqFxTiP5gdT2>aTN0S7XW*pilASS$UkJ2*n+==D)0mgTGxv43t61fr z47GkfMnD-zSH@|mZ26r*d3WEtr+l-xH@L}BM)~ThoMvKqGw=Ifc}BdkL$^wC}=(XSf4YpG;sA9#OSJf)V=rs#Wq$?Wj+nTlu$YXn yn3SQon5>kvtkl(BT2@T#Mvca!|08g9w{vm``2PjZHg=b<1c17-HkzPl9sXa)&-Ts$ literal 0 HcmV?d00001 diff --git a/demos/ima/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/demos/ima/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..3af2608a4492ef9ae63a77ec3305aedda89594cb GIT binary patch literal 6114 zcmV<87aiz{P)QBg$Z&8YKy<2dSjG6I2&!iu7JRdT!gcBlJx2NL9-^PTGD_Ptf# z_t*dbRdw&}d+xcr-QAko7-Mb(cL9%PAop{-%ba$?L0~%p4=0Y}p*W8FU1n`tILPv} zML2!uMd(K8O&CZREHF@fhVQ(Z5yVrJcYBD!LfyzFt;&e2oN5Pm5Z@1b~qKj96+4}@|h;R-VA2(=2-37BtnR`#_JMV#vgaqj!A)$dLw zzAqt=kf%brlHdkMtlkP5%mgwQBTv+&?;R(E^s|ch{RoQ*)slEY&`lQ-Zm%FW<@tmV z)uL|w%v_~goAvXG*IfwH2{j7hrMtKlq}vjs(Nzf{YD8VTsI{f7SiPs>{X2v+3gRt% zb1Q)~2q^^WJXX;T&sN_Xm~Vh zb#=9En0OP&wxC@%Z{GYqE-tQJs}Mm3TMTBXa{GnLsc$2`UQ2AK7a~NTIdi77l7ri6 z`43X1QUv+6ZQSM9m9|2JpMU;2wWOq^>uu=?@`M*IT!7^#gZw+m<=EqrAj0+Q*Hg$H zJ$Oq+P^6h2REa1@$fx}f$avWbNp+}hvdvenT!~)3e7WZ>$&QpcFrEB6N8An?S5|d~ zB^5-n^6EnVzO|5VtXly~JQKl6t4`ZnH?qHmS_oEMUA;k(9l5u-^-~3>C<3lsKL5sz z8*E#~Y!;d{mW8E%&1x=JwThmAI-oA!r+v=m8+=*h@o#ut?Trbv)l*PrWo2c7E!qoY zv?ucapvd#>&UUU|y~?7Ft!1Hy#&Qu1ry?9_Xo~@Lh|Ar;$)A_t%k~~!$?NJ!b|m5f zD<~+?wMb?p0}NHHJDsdpOP+u2+BKGS@&sFv@K-LtvgALql8XG>>WXmgqKZ7WIB_f& zU}@aPypE`=gT1H@oRBLjNl8iR<+gNF7DT_{uWTA=gaS^s< z%wkurUa`v+VILVNZ9(p5&+%~X&FO)h{Q2?zEb7oEUPshb%hUyrC1qui#Fe{(H`iD{ zRqAcU+)jfQUrQMS%gf7S-|N5O0)!^L%Z?YuT5Yf-9N%BNewEc+xx~t=irJa+43>S) zz%q&ta%7!LpwEu;@37DH>(}^iY-Kh0{%FB|wjj};3$QLWfY%M~M`LW_lSb%0be!=n z=>;;NR8>`VrY@E*Tu+@dUH;<5i!9}cfh{roiHor2@c*#Ns?tVRBuR&FuDMdhPL?LI znB3KD)A6ZndFr3ox5@9Z#Yu0oMTf?4EIjlk$D*XSSZFf2wv-7hB0Ye9vyz=WpTq+! zj-?a>uPZK{XDd?v%;qQhv4#3^RHsB@%l79i<(6Z#^lR)?X&T#`y^t+W`7gHk(A$K!h-@XsSO{Q_ z1&MDE-egNtK45#Y=JR7-yLJ`R2>e{TGZ%95=NtUkj`-EQPNk!V64;&s^jD12Z2L5d8ftq zyOG5#aFz8-zzQoWDwsZbKMOUyPa?cS*8WGfB+2Mr8lh1DQ}T@ha9>YYm^g+69%r=v z__uf+P#4t6m8)x_7c3LKpq-|`OA);fS^h;=S--LuAlT)cq+Ve7k_#Z=dI9`R1ZaXE zTN(c;%gN1hCh%JA1>lTg$|Z^gPk_rKM~-+p?EA?l1}H|n%#}T$>{1bnI5thh0oRf5 zhyW?TQ78(VIKDpAD{DT0|E=TTVVd^}lVCZ>RO!CxE{d0Zhr4 zKq633p6N<=REuMsI(2F@aq7|R=va0U@>@OV$LCxXeEATae15ZT$0qqLXZ;fM3_ffX zxudd6u9+^EDQS6mdFj%nOZ$M^O`A4(G&kevMmg-8u5v%dIhV^U@_3+a;vH~3EhzvH zerz(Yv$L6z(hVghCVl{J$++7$m;JcYNby@&SU(zo(Pezz59)-Qkso^K9k!GPWv;P) zO92*B#)Z$D69CZXZRB-#L3&z`xI)CQ5tDQtHr>yN5hFawZ>70H0O|KJ(zQiAM!xa+ z8(8I~Qbr?h^1~-+L_EnM@@-i^M!+~Gj*WA~o%)U+ODTYod;sSyD04m@NDd1N3)6e{ z?CE9I4aw{$H#c`6{h(U;W3ASI`O1%cg{e7L6PLG+Ro7H=f+Wf>7PB>JpV;kstO>CC z@L%XyB__wlxngoxS+#zNh+_fdihgve7sxnJSy@@LapT6};8=A~CIz6p)lcF7>z%Rw ztYQOqE9QhNf$vKy^GyhnIGDTAY3o0jyF&HY#g%z%fx*wF0GO!DEJ|>;7jOYE{}mGx z^S;$|RQms_s;aLQ%Z&}rSbxN^DK^QM?x&2bU5zBTCCAA(6(Ii92GwJi(&%?#;+s~< zm)Lk@BDKY-fZQNQ#c642(^cbuB0p_M5qq_>qhDA|-npa3Sxqa%D+6psajXSF)zwvO z)A4|2$+u{kLd}ek4`)t&f|q+W6j- z0PM_|$J^x0>?nE=#aBIX>}4@6A>O!+88fESjT<+PE9Ww_xSxwv6>LSyhjt49D_@d4 zj_t^t&7w~(WgCuu$v=0Nd#hD8qeFL)eT85DHFdl`B_vr><7ui~v0N7AEpW8vVEJ0hJn>BfdHEZ4SI_DI}ALlgP-T0h7K zHXi<(x6K&=Dk>^!LPJCU-69i`0_@wjZy5dHvQ`1m(ZtGVFFh9YMw@u3| zsZxMNix&M>Oifz~5E&Uc*clguAeCE~ZdV55O5$DRdaPN$5kBlBwM|PPR=S{|prEI% z3b10uipNP|%|RH0jr7xTMBJDbB3=XePP!h6ISD#;^i-^-6*DP7X=!QY#EBE1v?{56WdhMqlpwur`B{lT@#wL)Sb=014v;I1?hKJJVF ziCMeZ)CgZT@jD+Q*6Y|m2w$)FG2(j#Hu$hfz(yZ7`3D`FM40>oy$X+~mWiZq^wQN!a4U%W09`Y}ytox6)@@>Gjsp1aB6&4H(@B9+rxsS>y9hrkD{m+6AQ@Wv75@>#&X6UUn0?$%>?%Ou~~$fQB>|XVzxj~G?mf5Z1w?P7Icu_AM|CxK#VU7 ziKQ}@Tni!CCUh*w1m0G0D93RDK)jrcOG!xyCywt2*A|QOVv)d$y2(_5}*ufmkC#VvUv_!U^}|q|YVN zdC;W*Y$RUCQ^@AC9-Ud%V-9Ts$OW0|>T0%j?b;8)G5P=Y)>g#YFI>2A1f`;vw4|bH z0&tKBuwo1HRRowV+)7ZiQGj3z@_kjv_q8NH!2$9O&6BTH0GWcGJ9n=7^Uptj5gc1v zl7vsf7Y|*&d^ydf0*IcV6rqv)C|UY(%-*jqKoGf`phlOY6u`$!0O4M22w;o+xmL(` zMgWwVnVA{H?IYmWBmgTn8YbUMMVF$YqUBnyifD`hs)HjT0ukD1{rgM>Fel&WddM9e z^i>hS7+{qG%!$)+zi&$b$H;eH0Nlok-^9ekU^T3Z;8=azyLT_X>~!$p!4DL1puuGV z$e3`@Pn~?}|D%0G3{WHAw~2hE04SRgz!~yG5=J>JfV?mZlX%OQFaImJr8sb(RRP4{ zpu>Cbz4x2z*RK~l>W1tRK!|`$W@c2A8{(M{h*ywrDu7HIeND)hutvTVz!~zL5PRXyfA!T@F%8{8r2E#l*Is)Ky`WoRVPTl^nF#g^u*-5TMhym|dzooYzJ>MsD9ASz z06Bbf0=SBNM+Ff1e=YWpjg8$-oOT!7+TKVZq(~2L-@bjkV(z=acKP3Kjy9E%|Uyn;*HgDd% z2wVzI?c0PKdSLwc@z2tjpxoY+)ENN)xEG`A(KW&$^2zE$5_FaVxPW{I1(3nFQm51X z4qSfv>8JNPa-$@_Mu^IuM~@y|CYIq^OaNt`4sy-OHy1!H`>`ND!IF4QQP>DY54gkoLBjT`qL)Riji=><{%TdPj?fX`6c>3Tx+O_OP+0(d(WaLvhg zKmcz2d3kvk$ohW|4kt{QaG#c&<=sY(9EnG}_ew}em@5_{ZixT@+>tHv8&|CKX5_~^ zZuRz%Z;t@d`Z4hq78bSy+zAe~JvD{84q`!9%7})Pl$7K)H!g6c09=GPQ}To3nxIO) zezb)Et|C9!z8=6AUdV0d_wL;r1Fx=j<^HyM0d*rN_{geNt3JVnNw#j>MlVS|xyNM! zND;6YqDsCLK!tpJh znl)3RwZ3Th`#ocJ*~5?s0b>4~1hh7IdRW&f>Pw+5p! zYViPF6n-#0J)IrU?_rzvuVUf*mTSPWTY|8CORXXzY6Xjq+s)g8HkrF0#f{i(&6+g} zz>VOjMV=?^Mt-eB$BrFwUCR@(v9aM8Y(N7Hz0L0p#w66)vuANv2+PUI!F{rA3aB&c zjy9kz=JyQC=?2X8M@B|&0Vm)_+=|*_|Fq%WzkmM+#M0W(>2yR;ZA2vKF(C~QR>FGH0JZzw5qOy;dm)D4tl$2!Yj_%O^4p931dU4P1 z;SL=-JPQs47wuZo^{9y;gYsj9r}TRL0U4N4(bo8cbZ74RS3Hc5?b)*jZU>i{Kc)z} zxBMTLaKiROh77?!4B=nsp4_{4?+I(BdH*rUgJo3oD zb?)35A`G51Y0{r*R9FCC*%o_)((2KM)YR0oUwrWe23dpAMzr;IxgDD#bm`Kib06C1 z^`OTefBc2ryLWGw!*@*6))}|fZuNDduDGw4ZP~JA=YRnNu&Ol(ZF`Wm)<(Wk1f*dd z`}OPhD3t?{A5Wh?{fi?P3)lXhp;~2zSE+E$T{EpBESy_`f2@A0XP) zQM9pD|D_=YBKJM^*kj$hb?b(ICjCvP6-x%LaS@ltE?m-Jm>{bTRTd|41uQ zht;cBFM8&gXZ|4E%|O%@brx3d(H6LfFb5-hhTK4$NNMZLHW^QvKA?TDuaazO=@1&@6gpQS&WUqV9i9^wKM-|89fhxN z*Vc(wiw)??9pO_&wglHSm`HeX;J|^u4+seOf(AMpl9G~+;;Mr3@^ZewE&p3UtUNJm zn^>dZSr?w~!ynRDSy`W-pI@1roO~3=#yM~lW29pNtM``b5s=k5x!TRq|b4{^B1?GF9`<{9 literal 0 HcmV?d00001 diff --git a/demos/ima/src/main/res/mipmap-xxhdpi/ic_launcher.png b/demos/ima/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..324e72cdd7480cb983fa1bcc7ce686e51ef87fe7 GIT binary patch literal 7718 zcmZ{JWl)?=u?hpbj?h-6mfK3P*Eck~k0Tzeg5-hkABxtZea0_k$f-mlF z0S@Qqtva`>x}TYzc}9LrO?P#qj+P1@HZ?W?0C;Muih9o&|G$cb@ocx1*PEUJ%~tM} z901hB;rx4#{@jOHs_MN00ADr$2n+#$yJuJ64gh!x0KlF(07#?(0ENrf7G3D`0EUHz zisCaq%dJ9dz%zhdRNuG*01nCjDhiPCl@b8xIMfv7^t~4jVRrSTGYyZUWqY@yW=)V_ z&3sUP1SK9v1f{4lDSN(agrKYULc;#EGDVeU*5b@#MOSY5JBn#QG8wqxQh+mdR638{mo5f>O zLUdZIPSjFk0~F26zDrM3y_#P^P91oWtLlPaZrhnM$NR%qsbHHK#?fN?cX?EvAhY1Sr9A(1;Kw4@87~|;2QP~ z(kKOGvCdB}qr4m#)1DwQFlh^NdBZvNLkld&yg%&GU`+boBMsoj5o?8tVuY^b0?4;E zsxoLxz8?S$y~a~x0{?dqk+6~Dd(EG7px_yH(X&NX&qEtHPUhu*JHD258=5$JS12rQ zcN+7p>R>tbFJ3NzEcRIpS98?}YEYxBIA8}1Y8zH9wq0c{hx+EXY&ZQ!-Hvy03X zLTMo4EZwtKfwb294-cY5XhQRxYJSybphcrNJWW2FY+b?|QB^?$5ZN=JlSs9Og(;8+ z*~-#CeeEOxt~F#aWn8wy-N_ilDDe_o+SwJD>4y?j5Lpj z2&!EX)RNxnadPBAa?fOj5D1C{l1E0X?&G3+ckcVfk`?%2FTsoUf4@~eaS#th=zq7v zMEJR@1T?Pi4;$xiPv`3)9rsrbVUH&b0e2{YTEG%;$GGzKUKEim;R6r>F@Q-}9JR-< zOPpQI>W0Vt6&7d?~$d&}chKTr_rELu} zWY;KTvtpJFr?P~ReHL4~2=ABn1`GN4Li%OI_1{mMRQi1Bf?+^Va?xdn4>h)Bq#ZRK zYo%R_h5etrv|!$1QF8fu80fN?1oXe(Jx#e6H^$+>C}N{*i$bNbELsXDA>cxlh|iFq zh~$yJ?1lTdcFd1Yv+Hr^PP!yupP!0H@Y6(wFcaVE+0?qjDJ1;*-Q8qL{NNPc{GAoi z_kBH`kw^(^7ShmzArk^A-!3_$W%!M-pGaZC=K`p-ch&iT%CV0>ofS74aPd7oT&cRr zXI30fVV6#PR*Z?c*orR0!$K6SUl9!H>hG+%`LdifNk`!Sw7Hon{Wn=|qV{a%v9nEq zAdBW*5kq6il=yA}x8cZQt^c+RBS|TRn;!?$ue?@jIV~0w1dt1FJRYI-K5>z-^01)R z)r}A&QXp^?-?}Uj`}ZPqB#}xO-?{0wrmi|eJOEjzdXbey4$rtKNHz)M*o?Ov+;S=K z-l~`)xV`%7Gvzy5wfvwqc0|80K29k0G~1nuBO+y-6)w11Kz2{>yD{HTt-uybe2pe? zUZK*Eij7TT4NwF1Jr@6R7gMuu^@qn#zPIgRtF?-SJL83LBDrh7k#{F^222EXPg}S0d4Lf0!|1 z|2k$^b~)^8$Z-yH{B-vo%7sVU@ZCvXN+Am)-fy$afZ_4HAUpK}j4p`UyXRel-+(VS z#K>-=-oA1pH+Lo$&|!lYB|M7Y&&bF##Oi@y_G3p1X$0I{jS1!NEdTz#x0`H`d*l%X z*8Y3>L*>j@ZQGOdPqwY(GzbA4nxqT(UAP<-tBf{_cb&Hn8hO5gEAotoV;tF6K4~wr2-M0v|2acQ!E@G*g$J z)~&_lvwN%WW>@U_taX5YX@a~pnG7A~jGwQwd4)QKk|^d_x9j+3JYmI5H`a)XMKwDt zk(nmso_I$Kc5m+8iVbIhY<4$34Oz!sg3oZF%UtS(sc6iq3?e8Z;P<{OFU9MACE6y( zeVprnhr!P;oc8pbE%A~S<+NGI2ZT@4A|o9bByQ0er$rYB3(c)7;=)^?$%a${0@70N zuiBVnAMd|qX7BE)8})+FAI&HM|BIb3e=e`b{Do8`J0jc$H>gl$zF26=haG31FDaep zd~i}CHSn$#8|WtE06vcA%1yxiy_TH|RmZ5>pI5*8pJZk0X54JDQQZgIf1Pp3*6hepV_cXe)L2iW$Ov=RZ4T)SP^a_8V} z+Nl?NJL7fAi<)Gt98U+LhE>x4W=bfo4F>5)qBx@^8&5-b>y*Wq19MyS(72ka8XFr2 zf*j(ExtQkjwN|4B?D z7+WzS*h6e_Po+Iqc-2n)gTz|de%FcTd_i9n+Y5*Vb=E{8xj&|h`CcUC*(yeCf~#Mf zzb-_ji&PNcctK6Xhe#gB0skjFFK5C4=k%tQQ}F|ZvEnPcH=#yH4n%z78?McMh!vek zVzwC0*OpmW2*-A6xz0=pE#WdXHMNxSJ*qGY(RoV9)|eu)HSSi_+|)IgT|!7HRx~ zjM$zp%LEBY)1AKKNI?~*>9DE3Y2t5p#jeqeq`1 zsjA-8eQKC*!$%k#=&jm+JG?UD(}M!tI{wD*3FQFt8jgv2xrRUJ}t}rWx2>XWz9ndH*cxl()ZC zoq?di!h6HY$fsglgay7|b6$cUG-f!U4blbj(rpP^1ZhHv@Oi~;BBvrv<+uC;%6QK!nyQ!bb3i3D~cvnpDAo3*3 zXRfZ@$J{FP?jf(NY7~-%Kem>jzZ2+LtbG!9I_fdJdD*;^T9gaiY>d+S$EdQrW9W62 z6w8M&v*8VWD_j)fmt?+bdavPn>oW8djd zRnQ}{XsIlwYWPp;GWLXvbSZ8#w25z1T}!<{_~(dcR_i1U?hyAe+lL*(Y6c;j2q7l! zMeN(nuA8Z9$#w2%ETSLjF{A#kE#WKus+%pal;-wx&tTsmFPOcbJtT?j&i(#-rB}l@ zXz|&%MXjD2YcYCZ3h4)?KnC*X$G%5N)1s!0!Ok!F9KLgV@wxMiFJIVH?E5JcwAnZF zU8ZPDJ_U_l81@&npI5WS7Y@_gf3vTXa;511h_(@{y1q-O{&bzJ z*8g>?c5=lUH6UfPj3=iuuHf4j?KJPq`x@en2Bp>#zIQjX5(C<9-X4X{a^S znWF1zJ=7rEUwQ&cZgyV4L12f&2^eIc^dGIJP@ToOgrU_Qe=T)utR;W$_2Vb7NiZ+d z$I0I>GFIutqOWiLmT~-Q<(?n5QaatHWj**>L8sxh1*pAkwG>siFMGEZYuZ)E!^Hfs zYBj`sbMQ5MR;6=1^0W*qO*Zthx-svsYqrUbJW)!vTGhWKGEu8c+=Yc%xi}Rncu3ph zTT1j_>={i3l#~$!rW!%ZtD9e6l6k-k8l{2w53!mmROAD^2yB^e)3f9_Qyf&C#zk`( z|5RL%r&}#t(;vF4nO&n}`iZpIL=p9tYtYv3%r@GzLWJ6%y_D(icSF^swYM`e8-n43iwo$C~>G<)dd0ze@5}n(!^YD zHf#OVbQ$Li@J}-qcOYn_iWF=_%)EXhrVuaYiai|B<1tXwNsow(m;XfL6^x~|Tr%L3~cs0@c) zDvOFU-AYn1!A;RBM0S}*EhYK49H$mBAxus)CB*KW(87#!#_C0wDr<0*dZ+GN&(3wR z6)cFLiDvOfs*-7Q75ekTAx)k!dtENUKHbP|2y4=tf*d_BeZ(9kR*m;dVzm&0fkKuD zVw5y9N>pz9C_wR+&Ql&&y{4@2M2?fWx~+>f|F%8E@fIfvSM$Dsk26(UL32oNvTR;M zE?F<7<;;jR4)ChzQaN((foV z)XqautTdMYtv<=oo-3W-t|gN7Q43N~%fnClny|NNcW9bIPPP5KK7_N8g!LB8{mK#! zH$74|$b4TAy@hAZ!;irT2?^B0kZ)7Dc?(7xawRUpO~AmA#}eX9A>+BA7{oDi)LA?F ze&CT`Cu_2=;8CWI)e~I_65cUmMPw5fqY1^6v))pc_TBArvAw_5Y8v0+fFFT`T zHP3&PYi2>CDO=a|@`asXnwe>W80%%<>JPo(DS}IQiBEBaNN0EF6HQ1L2i6GOPMOdN zjf3EMN!E(ceXhpd8~<6;6k<57OFRs;mpFM6VviPN>p3?NxrpNs0>K&nH_s ze)2#HhR9JHPAXf#viTkbc{-5C7U`N!`>J-$T!T6%=xo-)1_WO=+BG{J`iIk%tvxF39rJtK49Kj#ne;WG1JF1h7;~wauZ)nMvmBa2PPfrqREMKWX z@v}$0&+|nJrAAfRY-%?hS4+$B%DNMzBb_=Hl*i%euVLI5Ts~UsBVi(QHyKQ2LMXf` z0W+~Kz7$t#MuN|X2BJ(M=xZDRAyTLhPvC8i&9b=rS-T{k34X}|t+FMqf5gwQirD~N1!kK&^#+#8WvcfENOLA`Mcy@u~ zH10E=t+W=Q;gn}&;`R1D$n(8@Nd6f)9=F%l?A>?2w)H}O4avWOP@7IMVRjQ&aQDb) zzj{)MTY~Nk78>B!^EbpT{&h zy{wTABQlVVQG<4;UHY?;#Je#-E;cF3gVTx520^#XjvTlEX>+s{?KP#Rh@hM6R;~DE zaQY16$Axm5ycukte}4FtY-VZHc>=Ps8mJDLx3mwVvcF<^`Y6)v5tF`RMXhW1kE-;! z7~tpIQvz5a6~q-8@hTfF9`J;$QGQN%+VF#`>F4K3>h!tFU^L2jEagQ5Pk1U_I5&B> z+i<8EMFGFO$f7Z?pzI(jT0QkKnV)gw=j74h4*jfkk3UsUT5PemxD`pO^Y#~;P2Cte zzZ^pr>SQHC-576SI{p&FRy36<`&{Iej&&A&%>3-L{h(fUbGnb)*b&eaXj>i>gzllk zLXjw`pp#|yQIQ@;?mS=O-1Tj+ZLzy+aqr7%QwWl?j=*6dw5&4}>!wXqh&j%NuF{1q zzx$OXeWiAue+g#nkqQ#Uej@Zu;D+@z^VU*&HuNqqEm?V~(Z%7D`W5KSy^e|yF6kM7 z8Z9fEpcs^ElF9Vnolfs7^4b0fsNt+i?LwUX8Cv|iJeR|GOiFV!JyHdq+XQ&dER(KSqMxW{=M)lA?Exe&ZEB~6SmHg`zkcD7x#myq0h61+zhLr_NzEIjX zr~NGX_Uh~gdcrvjGI(&5K_zaEf}1t*)v3uT>~Gi$r^}R;H+0FEE5El{y;&DniH2@A z@!71_8mFHt1#V8MVsIYn={v&*0;3SWf4M$yLB^BdewOxz;Q=+gakk`S{_R_t!z2b| z+0d^C?G&7U6$_-W9@eR6SH%+qLx_Tf&Gu5%pn*mOGU0~kv~^K zhPeqYZMWWoA(Y+4GgQo9nNe6S#MZnyce_na@78ZnpwFenVafZC3N2lc5Jk-@V`{|l zhaF`zAL)+($xq8mFm{7fXtHru+DANoGz-A^1*@lTnE;1?03lz8kAnD{zQU=Pb^3f` zT5-g`z5|%qOa!WTBed-8`#AQ~wb9TrUZKU)H*O7!LtNnEd!r8!Oda)u!Gb5P`9(`b z`lMP6CLh4OzvXC#CR|@uo$EcHAyGr=)LB7)>=s3 zvU;aR#cN3<5&CLMFU@keW^R-Tqyf4fdkOnwI(H$x#@I1D6#dkUo@YW#7MU0@=NV-4 zEh2K?O@+2e{qW^7r?B~QTO)j}>hR$q9*n$8M(4+DOZ00WXFonLlk^;os8*zI>YG#? z9oq$CD~byz>;`--_NMy|iJRALZ#+qV8OXn=AmL^GL&|q1Qw-^*#~;WNNNbk(96Tnw zGjjscNyIyM2CYwiJ2l-}u_7mUGcvM+puPF^F89eIBx27&$|p_NG)fOaafGv|_b9G$;1LzZ-1aIE?*R6kHg}dy%~K(Q5S2O6086 z{lN&8;0>!pq^f*Jlh=J%Rmaoed<=uf@$iKl+bieC83IT!09J&IF)9H)C?d!eW1UQ}BQwxaqQY47DpOk@`zZ zo>#SM@oI^|nrWm~Ol7=r`!Bp9lQNbBCeHcfN&X$kjj0R(@?f$OHHt|fWe6jDrYg3(mdEd$8P2Yzjt9*EM zLE|cp-Tzsdyt(dvLhU8}_IX&I?B=|yoZ!&<`9&H5PtApt=VUIB4l0a1NH v0SQqt3DM`an1p};^>=lX|A*k@Y-MNT^ZzF}9G-1G696?OEyXH%^Pv9$0dR%J literal 0 HcmV?d00001 diff --git a/demos/ima/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/demos/ima/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..9bec2e623103ac9713b00cad8502a057c1efda61 GIT binary patch literal 10056 zcmV-OC%4#%P)f{b8~La&ABzzjS$j|sySB+3lg7e=Ipr#6B0nslBeFh90 zSSvo;k;;{-H`UWrL#ckvHI)CYH~&mWOOQywast)FplM+W82a~aRKuwzQB9{>M-@hu zN|i@dN_B^-lB$~2Zq@v6clc-W_;w$o0*U~HsH7SRTub^rz-g7#hsU6Ec|iLuRk{&0*aR?Y!eR?l3@CnX($h`nZRl-$kvK*5?~ zZ16HwhzvM2O&AfiDtMnXb6O*rSV!{y6<#yBUtN{Gt}WTft+ja2;c=0? zpD8ihO(mmpSmuU{Nzy+v<@)e}D+u!UeW{|1td0{J)A5n$D)d=jxl+e{e+xpqud1qg zgZ{f*Vs&bqkXUwW5^Gfc%P+sYDc83TLcHVSv^vUIqsq!kU)rV3?(4Wnl4Z4`4c{$E z&7HB1eVH1|`tRPoyXVZAGp+B-R9^&o6%`d-__PYA%TmFm-Me=$Av-&}>wOhmi>u+z zojWKDW^s7#IR{>G-9yLHnCNstK|%lf!V-xF&_)fS?~9!9I1Hkq!otEKO&TI$LTO{3 zrSGrufX4}sgCL?7zvSGxb3>b?JCnFA%-Ol^?c0q!osAUQcX;~Q0G zCTOO97KOrVN=*Pmr_n5qT)K3L?1=RvOJc|CA=+~MD{`gea+7yu!gXD_c8RP{{69TB z{?T4!TZ}Jldy!HA=_ja_(oL(?KGi6KYNNO(O353e!UA2se3`@_k0vXlKG6fTG;Sh^ z$lAhOSyQ$`a8GDMSms*ly1exOE!9jW3CUX4b_D@qV}oN}ym&E=j#-NakB4||p&1>- z8A`=HQsL^P7YsRl`ZU=WwUz{EC+Q&yOqfj06`f*Mswr9_VPSJGX0QuFz_T!NEZGye znq+5Zv$iW8>tT!lEp=t{cs$gyL4#)Mzh6=+?vaZR(AWzXE|8?;V`Oc_cY1)JJ*hsV zwESAVU757zf@47#Fmn>0v!`AoTvusX3E7c6or2?~2WVB;m#nSSN~mRFSv+*@+BK4t zl=ORyVMIhk%Z74Y&8b;TP;*WXI-15;BsVvggvA^nOQYVab!G7rN%FZPsJL3y(Nb6d z1NIFUfgtwgtsA7`Mj0usxI(U$6_Mi7LYf8TGvPh{c8&fYK7-HVJNPd4A;7X0C~;vV z=7x};V#bn%F*<;L(o7^_+F;gJv>E$Wqfdn^qZei}9YYs~yE5Ur=t)df!*v-CItHt_ zxR|7;r<3iP#WbLvpoa*-=fx{|CSwI-Xy7&gKv_izxo|a?q!nmL)R`@;Jh1oVT(b4V zH*}w$l2wWCQ#bi86W*^){09j-@iqI*;jCr!JDW&azJ~7OEZZ0MiG5pwNyK)A#b?Q? zgumXqRnc$W{lbO>(@zUX6CmJb!EJg*{rCj=m|=4DR*7fYNxtr zY<_+|iBF6nD&8Cj9=SN8qIv2SpV zGti>gznImMxHrkNgty5$3fG~`0Fs<{h!kJDz>Z}MleF4gUQtdCo(#~#11$~zh_$Vt zpn#>@4oD8zY9cgHFAEM1ev(7f+)=SlbJ`iJ9W@t`@M*;0n&aa++we*Hd@&39DekS_p8| z0!XSQ6sFaQAJTJJN6#gjStXoX(Up9%>G(eltj~s{vq@@d3TvB#3#2TdzH;SCH4UWI z52(3`gZ0_d5R>6?1ygv*`Sa(AHZGC`XeLW)LlcPR)FzTsm_m-6T1nOAk4+|rPc0`o1*zm{`dVtK#?}I)d56TrN3k}cZH~T0BW`nKXJ?0^Hl&&x z6V``j2d{|<@eNfwxq9^~Id$q3*{xZ_1M0V!;G)*T;>1rd1V;uQr2vw%K2m_7g?I%> z3AiOQQ4%ty?!6bg~?7fU^uSElt^sOw@g7kk!*sbstOc zWE94-!k$&GtDf%55daAVCcMw4s9*pa5F%C=%FoX)U%h(u0F3#L9XnbmRdsGo2kwi8 zTB}FEbK}N!l5{piSI?1wr{S$n{QzR~e`4Pv$Ib?`HZ}xAI3C@qa0?|qK7KmJ{P^+X zE=t_IaX*-Pc&#t&apCoh5pcXmhsHHaCbR zV!<@#A%%p5jKtX66-;vz*5dZ<+kTFAU(%Q-A$Py+Zp#kqJ zM?wTQhDv@?Qql^HeZAe7a9>N8F6}^foayM`S=_ov%Zng^$KG!O@Yv_Rr1IB#kY#a` zNNS#@A?AKp1K2ZX&SX!XJh@A~-I#D+mo8m;P2#>B1`p~Y=PqTCbxEJt2961Mni@b* zVEkm(2j~k&LL_QJ`}XZ~ueTfHUusFs=p07|&tkS-N$C}`E%{s9z;O^f^><&E0TS>C zZ9e`la;@x&LmwbOsDkM;adB}0V8CX8B-vLh>Vsn(1&}^yrdde%sWp~iF$>R|7T{6W z`bYuN%{sI${xJp!I-0r4p+PkO!m%%3?PXIbHXQ%V0oF$jpt02b{)2>PuOabgcd@A@o06w-uq?YT zsTOMgLNfE?92pO>Y%DJ??*@&5hk*r~ii#rpqUqdQJpQS6lh+86-H2?0HhM|SmVB6{UUNUuwzTl1?LujZa14PU<*LdhQz6)xa6Wk zTp2GaR^xtSXlUq%V1WYE%GUVDh5A8%meXc^f4-Xo6T_!s<^ny%gRa(227~5 z>>4?mwUQ0296U-|AI$Z^v2aYebHO>r=H%oQO`JHf7r#T_+*pY!y}T9fc`y#P9T zdWG2m6WVohrpke{H`$do!>V&RbZUvs@GvVBuX`d_Z7W3g%>wBQ7cNw;UAy*oU}ELU zl`hr>&@J=x^Zz1Q$XV6Q3%)iYYqLS>ZH+`wyyxT`8laY#9k8pVm&xW6UnuChdDy)gS%gfpiT5>0P^aO$HNI1=1X#RwX4RU-S4! zRriIg;?k8uvN35YgTWeLjD<<-dBvG#2QBkL3|SukwyN-;))NpnfgUT??75t~oKBX} zbEzLd?$lC$LW*dgsrBTl00_1N=X><%(Yav4DuDQhT31w5ELA&z7Wcc3pFK(g<_TsB zewKw*y{=p?uveCMk35f=6g;%GdPj*XnCQa3v}EVPyUB zDK>*sUwDMpCjEmR`>5WXp(d1G7{xNi`UKAc9-*I4%wqdhIhd}3l}k)a#AN$+oDK8a z?|=V$e5l=>J9myDfL6Tn~!r$1r)(0LrfR@Mol@t`6RW+E#*kj+RbfZjkSwHz>D zKqpFemYM(w_myF^#R9T>tpSGuliaa=Ek&MB=O8a)`w~W1O_rPGIG0j z?~bK{TXIHB#y>6ihq}`NE>yDy1c2})W=Lv)O+Y+o@R$N?=(0xO$r_fKucoYBzc8r zRC_2<6ch9E@^1d{!w)Z54G?`DOyRksCO|BG&(W~?zYPhE>hP#!eV~O}Z<3T9u38)< z04gXbxI1&^%$LE2S%7${8u|V(3ePWU0VEcT(qwF5nTnDiCJMB zl@{!t5y$^SfG1W0mRKy z>kS(=459GcRudqsHnt;iPLqPCL0y*#fVL&fWPPb7K>7LkcfR@N8@RC6AAb0ui$#D| ztXT0Z-NAJ=vM~MX>{qUk4RQZ$WZ*O{c>Ji=#!h2>sYWJ-IuOsoZhY~@7cW{3(5zXr zo}^#Csun<~p5n2Qz}OEP5jYCDEj!_{6`*C&?S|U_Uzef@4fflP>TSGnTYSc z`|jhE=mNC>LfVOiw3o)d)2P8w3Ldqr540$HJbr~otyG=?bn4WpqLCv<4g?$gc7}O? zs2-(6pHkyih5!gFjQK~rNftzmB?~lTi67SjONy{8KOv2`74p(4qE-tc4F4@JPkCuP zY89b-oi8hQSFFJUhbTB>XV0!8XnCg3~ zAL!rp+QzjV^3dzwJGg!}mM8hoPOe=ZOw*y=y4M-vJ=Kgo678+k%zYB=hurm=B}4~s zHr31nZcMX+sSfBgJ7kQkW*v~z=sKEtU{qa&;P0c^>+I0cWbP3U)|V;)#MVxXjEux| zjxL-H^8nExsU3ZNm*%o5t~NukwgR%WS$%L!i=cuQFe2;n%-!M-y zFWiF(133>0ch~)m#WU6kv5dUN7{~_-=i+~xAE7Eh)u=IT-@bi5n6L$)PFk&Yyc(;q z)&VHmn`$iaj~Ywng?a0M*yqVyn_j^tbU;8tbq0=SOnU0fqb`t<(HScX>s))zLg-MUEkU zQSPb%gh}%c4mPH|0U;u@? zPIO=wSdbr+TU|v$V+=H3PEliMO0Sv)s^K-DyI+0v)t|w{-~RTuHWmTmd4Bs>UU{WA z4WP~|ory^S!X0(FMG5?PT%@-y%))rq(Hsdl0A&srtPHa>uq=9)s>UwGjK7fS$PYvJnZ+Md3;mX(zqvGbo=giQ0QpA=fIJKUQmSBR5g@HP07)`1Jlg!L9zA-r6Th=+X=^@i+_(<( zwd?uw=NBrSiCGH}gbYm%9y#kXSI+t{ad^xCgcwH$k7r$Y^ZClH#uxw(P1E*g#I9i;;tqI`Iu40xp0 z$5#RmQ@E#ICIQk1#dQHDg1CWgM@#Vp^JUjv*Ps4jwM)0sqE5f}FK$hYkHQ<4;4>bTn{1XuofhF#q01MUz z(E31n#E20c>1+2>r%w4a27n;k#GHG`3V0*{`5cjEVLEtB15_6t1ArnpJT?NP7CdSI zBnpUl+9N0^C=kiiOE10D$=U!~9|!&EPk%xt)^**wb#92rm8u8X1CSIVIe2P|gdTNk zKPIe?4j>PU0O{Xzcx2-r8GzJ;XMXf(H2`AupWNKss_(x0ZXy_bho z=wYfp)QzPnWrgeoNDt9rncEP&XsCzB2%x&w$FNXn3Lpb`%mHK+|0n~Gn@M=o00;w& z>9Ja^_B0)P{F?K_oCTW}8)rYT^6IOvK7u$XBO}9K9f1B~dSaFZ&8HB}IqYe=>TK5f zc<5zVX*Qg*gZosb0J7x1)PzSZfTZqg^XAQKF!nFM{4!RnZ)qz)(m3d`g$ozHPO~vZ zp3+bXAV^puDLlpi)xzV!WC|WBK;kB+tOc^*zD$Cn0z4`JRKp)-zDG0gH!=40iGTEQ z5N4ot?AY;9xUu5mVnrsHDG87sq9dkUmj}CRE(edC^)bFnZoB((EIdjB1nYzBD?B_L zt8w(_W8d1=_($r-T(}AAsnKY@!R$19*Nj#gARR=W92|F@01b!76hH!=+V}330g|cz z=x>ZF3Xhvr@GyX)l>tbs4UOXAvSrJBFy_OD4+lUl^>JT%H#TU{AVlDg(MWt)d3pII zdy9&OcjL$ECY{#@9HU9=3nBoGb?^viYTvutWqsHk^k~P!qXWoIDGS8LG$|?R%5Q%2 zo0l-=0|yT5SYP*L;KrVR{&}no(>paabq#-nwn|Ze6cQ@LzG3F!@d(T3Xt@_uqft8)MzCU%$@v&A#fm zF|3)`w{Krp`r0omD{G%UR!D7tAPlrIIQ4<24nR>lt78n00YLSF$2Pa6BtX(T?|b&_ z!Q}aVe5~8r>%I(vX&MV5nC>-e)-2EK*RNOBH>Ee2(kkc84EWu;m`nc=i zsbhVj&4Z&BJPKJLW_{Ar)2pUTnS#o5ucx1W+V0@l7$A_?u6OU=c(`mpN=nLZ{w#Kt zy#U$r$gi!ELS$>)BLEU}l>MS)020=x-tdgE3m$s`64r+;bg^T{A&e~_V=;M55r9N6 z-KtlwUa&$>eER99ua}gR+^UZiawI?kqWZY5`GCg=pgPtkN?EI8D?E^&eHMsWpA#oe z+@3UP(pZdb&z?PDeOlQYJe#sY?Voz;sh%KJtJSW>!)&%%Ax8sL3z2oMYhHxpi3oGn z#{xi(fX5zyg!RF~3>!9VK;}hrr2+U+mG(*n&$1~!C-jLI=~hrsa1keBOLe*-01^`w^0Y*ha^Tb#o_Y3JAokdDOiaw>VZ(-D@u(+y^ytx5iPYU}N)JLgsr|QZ z-TEz}cm9juHUoq;{u~96Nr)oc>%wCM(EO;n@W=t=Xn5wa_qGEhs?NE&xx~-U??;TK z+SbP)7Q!w5wr$%!PG6r+OG}I9uB_75#T6Dsz2Q)R7(`LEPl8$l4?wX5k6#191NldJ z+qAd>cU_gZ@b~ZEpGe2>89tT|s}cK{%*gum>C+uGgAYFVU`%0Q;cb5M)z&WWf_pA& zwf}SoG{(0V0ER_)B6Sb=&6fd432>Bv2U-(7&DP~z*cc@yCf*r8emnx_erjc2=ByBE z1f3{Eedz1JojZ5VMH$?h8?6E$tWXvlx0?7zd#MVGDM=wReuUT@JOUs`TOB!g@M!b? z_|>d0tpP~P_sPl0AxoAl`3Ymk$FLJ0)8-F3U=vn|ts~UAb7w4p|7=`bTo_hzuqG=* z4GEK$Qcs>B%QTD-4tYiin6PdghsD z{u^UP$F7GX0%uDBb!XwqX3UuJE)D3aEyY8^jTILcWBol69TQ2mg#JX9g#Ls47~)N4 zA9Pn#v-EP4SBM*#8SJKCBx+^|*MTuQ@qe58{>+duR%o=WW-yJC*8xLeVXL1Gd`vcl z`m;Vm-=Pn!a9`{>uhi7k>S@!aeS)!~aSyCdXGa9imRuQbx;@&fSFZsui(9sAnU5tw z_;0P&m|Ly>=FOXIfkl~jyf1Y(p zdU`sh72s-dN+R?L`UW86<>j$HL*H5By72k+>(}qc*zhrWtRY>ODOc99UAuNY_@f|$ z>D3Z};0_J21QBW&h>7rdfQPICSC><@LZ6^-&`0PixGiho!FPA;*bzg=1nWFM*|u$4 z+=}YhkgiM43N_~?@Q3Nv8$On5SZr);G745GT$%IH0wiP-=oqI=3w?yXvecjGb7Wk5 z_wGGO#{xgqG?0(Y!;;$-%^qqbn=~Hk;_B+!4^`>`0|vaDkdTmr9|N%jk!ZM6mSs() zxwNzti({Vc*RS8J7z;ioT^d8&V<{d&MYAgp)SekJV#I3{qI1F$srei954xoA96EF; z|HT(y{3FJIjs?Psu6%4-Hb!_1W-sypt((Zq08va#Otz(%$SM05g+g#mEl)0oM`T>x z_?WmfW_XNmb+E^QIQ`G|@85q!SXfvx=AUqgYMcYF+=7_sQ`{5VwQE;e-@bi+%i(#F zXIvc|d8@%|q&nlG`oV+xSyEC`)q({J z7Nbwmx4e&Cn>svl5Wx?3YtyDp-!5Ic45IIcOr1LQeXUkofC3q2$T?k_)h??VvE-2> zM=pHy(MKNx9`q^g+kQM??$DSDg-XUm?Rh%+MECC90nuR8DR%GP9gaCFD3Uo-ee)?g zUUADOC@3hhPoF-&Lmxi=_~Xx^PkG#q*9I zKYkO{Qv`*$(wx@FFi=JrBqk>2=Dd0H{LyFVJANTP&il08{Rod-u@Ti!tbW#`W55RrsJmBl&>gozJ43M7p_4WNvbaZqf(tVMsp)Vf_2hh#9d?_9Hc4%Qd5RWa{kO!0UX4D$;rugH*VZ`VC2Y=UNTmv zJMXKu_j|l!t2JuPYZu5QdbMud`l-hrdu#~OeRSf)i4!Mm-MaN44YY5;tRpT!VA&Mi zo77DqC5M~F&!8tICEeP*d2{Ia@#80PaE71{&==h5bme{2`a!ii)>@;^+`m5olTAAj zMY5sjR0NT$SFhd_6%};>)oe^CN34Kgn?F|6C}HB(riNP^Hb)snRNR63aVN@@S9Xob>KtRCC(9qDd)YQ~F$lhR?_`?VWKuMvpH-<8r z=vBiPnJ@qb))AHl(40JZ@(#`s=j!e4Jpt#=>p9F-af{Q3x3vpzduvI0?u17HkeEe6 zTtEZM!89|0Yh&&WccLdunDF+ZMT?g1*|R4$E-tPZH6_do22hAKB%2uMDv7nK77&Q{ za(@#Xitl1yVyA!!z#!m1bLI@eIqcoLHwNcKK0f{eO{1?+7_L#5Q85|rOzir#L5bVR(*VhO8#J*d$Z22-j*7N+>%+g4p>CeygSNz;N^R~2d zg5y|_TJVfSSf$Pqm~d~XFLezAX;Atc29LgqxXBo*UvmrbA_l)_&z`SQt1)u;@ZqCh zef3p02=DPX{2vEoINYV=`+8V-AUuR0^EsRY&V`?o6dK{CTzFfY;4}b8##TuR)1y57 z?ZK~j0QDr#<``5Ih+#;VCDux+VMa3ee{NNV@_jH^ux}iL1M>twwktmuDKy5`#tBX% zg{d7cygkf=({4Oa?a3`dZ$8+FMfzj#VKD##*Rx#Da5x5XK>G9V^yT|_obR(cKSmdR z%#QpVoX|8;m|E~bbK${hTV7M?z~d(Y)}!3DbmIZ7D~CZUSN?z9_-7xLfYOQYvpqjX zYktg@M()W8O%n%73Y7q>6(8_6eDK?Ht05=x|84kpT1h~W!r}zx0fEXGuI5IdNhS9g ek+^GN3bv-?^>(QkVinb zlU9`mfQEQnq$S4VGrg6fmMQ=QFarQQ0ss(?uiys&;LQU7M-~7engIZmZaH5x#UC3m z-zvYBd&I}<`b3rPHj1tDgVv1x| zQss$ELI?W?E(!7PKk$lm@;7PwPX3o43{Ccd9@_BUsL4kQzSMa&=g{>4wj9#)9wgYw;=H@gH9KK{s?Be8N1_8W< z1Rh%Lm&PAfyYb*rGB%E#3q+}riOBB~+@@X<`9mgIiAex!QP8vg-XT>=+N&y*jC-f< zGihyr7XAly+G)|_e)qA?rnKZGG(x?=lLM7nrPk&93@5eX#7I_$g8kMX`0h=}l`HH) z=bpOkBCx=z*-fyr{yp7A9F=%o*qm93t_#tB2lAM@O{fX9ju%X#0~)nRUMvrXClh9w ze8|a0|0}JJg(_@$2wItI?LUY{zF78o(P2BR7;aC^@(jOp{8RE%U3m>MV5%Lu*46b@ zw*c?Nweu!TULS~}*9mi!ejNfNa=`po1*!jiYK)osxi%b59(thEyUZ>#lX@uEXSb_x?3)0kvB?8*TAh)7}IbzSm}5Ia;_?10{}M; z7vq-OS;Ayk8%_c-gg1Ee0FsrRU5phNs#H9Lp!1t+hwyK~9W0bWCxuG$LM~wQuumEw z=fbBD@sQE%1^j z`T@`PZLRVyWjX@*tjc7r;w$H~aW&7vu?|war?84^sg!{J*RH|mhq?KTsCVQBC1~fR z>99jeR=g-Q2b=d;pKwzXwYjrG>?pd3tFSsHN4in{usYLdK;01X2BdRLFI`cuB9yI) zI_ZX?7_(bz`MX2@^mCknx7 z*f}KV@}TBBc}CXMR8T_5yInD3p`KrNROSA;HoJJtlNG3weri%utO$eeY0 z+w-NEn;(;UCBk=OM$f%=%ma24wV7$idelqyNWI>sz1>BlGwr_3UugqVjY+UYyi9P) zxCB?&rPUetoZN?|*D%=hOOJ_${JU3GRjppY%&8Ws^G6>iokr^Bmv1&*@#2#5mXu05 zhPVXaQ`qe5i0lP-1^XL45x`ertKU5d-8b_?*1+tSU!qCeqD9gZP_>ZLq9p)RKtV(B zOh&^x>gV^eqb&c~Oi0|HgGG|gjpbR`9aRdZhOimvS2Y3e?eCFiw+L#_mi9j z;nU}gih+zTn{nv_|L}IllD1Dr3~@yitI}+4C&+;SR+cEfelqJ?eUjZ%&Qz)W8S750 z+vG8Lvo}xXz2C}S-m|9*uE?NWQWT#W+p@$DkH8wVn#=gLKa13M!Yva9qsfE(5Z#0V`A0pN)Ok zP*Eq0(~e$~m@iej0#Av_z703y-7|W6`UuGDS8fpy2rUgINZs#`33@@0(S%~%XUO5G zscEp&x^dU`8syC67USOswNLq>Z_}q#gLh2x`zR)0wvor72-IW@oDpnT0x zWn%LZ_yvR*7geY6<}MC~SViD+4`S9XC|L}N0ANpsUU;50sAjL zb5h>&s<-wcdf2>}P91QgeAu~ZnB7;;FkfKJp^8ne8!-`jK0+O(^`s~#RE0@)=IWiQ z@(vh6D^4jN5ih;*c4J48FMC9MwoN(cXk1Wiq55Vi-^X#p8R_(!y81}YDdMefwdl2F zNA0n}-!P4!FaCe-jnf{^I#?5W=%9T1C|$ z`+tq*x!rEx)Bkv-eO9$mWML9_yId)A_OltKIH-X=0eJ`Opqqj&s^T;PLIZXJ!pEi!=3ZLHPGi*~?<(L&m6;{M(636VC<08tan>&c6fW z%KEuUN9x|i7Wc^-0l&Vf20kI~_XfD4hEac=&}5n&MoYL`Xsx=1po#V*6wUpwB@pu* z*@2n|zglL~zr$9&uOd9_%)GWk&0UN`<&GAm8=Ba-@MT&TH*`NHlt+CMi2Ag;LgGpm zm+ybGL-!1Z$kBYk66=39zAsErw1}|-l1npj-?3g1LE#PXU%%_{8kO=5!W!6pQ?z&i zc_MuV(xKMXSA0ga@IsiwYspm&d4|n@L_zji`zUWxsM}|=@R}BFfT2P!uJcrQf81WG z;7~y_$uMK=ih(2hrfqIGOzb(81e}^7h$dQ*w9&zG_k*kV{ml>Dkn2!p9tb_+Sa82P zf!TC+{4a(i^7UC$53;w?sleb~lFWqeCjv5msi}#JQ!wJtA>=k~`WL0M{^a9PG3%vT z6x=jB0{7wX7$gs%H}xJ&s+hHnzrl#L*=KB8OZd%sPoxKs(`;%|I$(^;nFYa4Cg|3D zmbQ)m6I_Y@t)A~{YBRo!2sYI^n!q)$tPp|m&n1BkYVmX22Z+nY#4N{Bb0!Ko=DOhh z8)8*=>e(W&-%LSWUN;u45Wex{{R747!a~45S>12$wNc{9N95&r%gU+b#-B7PcF%`_ zbDPAsmvpVBsQpf}s{igh23+1)`QSj71!|zjij@kvxgob&J{E97Lwu==Z)RY-lujF1 zts{7+jfS(K5+clZ(CY~%ks(F!=cb)YtqEu(dp_7=A?O!zz8KONrrma{eU-54%}Dm| zMb0!-=YUH?S7JzBX|TVr;=fB(8}a+Mcip|v&=pAeFMCaHj_Nkl!sWeZSb#k<%oczm z#`lGsgJHo7RywsRYYQs4O`J_C=fARQ$)B1peZk)|&ULCaa#RJ45lrml54sxO!CCv< zACe-^PSoZc!)x$#iZa*NuMlS%Jd!_x9|UdgLzlGyF0cI$EUFG4O;L+8*+s;KNL-ld z?R+O)guOt(>{+*e-+_A{1MBbRn&>53j=33ngVZ*A9^^??x8!ww@-m%DVVPmliJh;B zA?gVg!0|Rs7)?hBD^!lSxbI8;-8Q65B4DKw29-K9_w0glvBA&vz=a(hBCWqSnbKS0 zUg%$!iEY%1jOqivHBW;uSX*e&(J!Yr7cborEc&_4TQAAt(Hs@99pynWwVQc-PD)!b zEAfVEq-cX>10nj+=mUt(v;j?>9`bLJayfOcTYEOojVJwg!qg=XHGMAonnJPa; zUJ!+pYTulTHW%^S;&|h~V3suNSc{q3^zg~L0z(5QQ;Fz}<5*7QiE`G{EY!_Bq6Tf3 z#Y6<%5EL^6+vT44<%^2!TOb&Drb?#eUqR@vqcvAd=l_6n*oWcLU38eLio z&XA9a$>+}PoZ&n7&1;j$MfqAp&SK~ziPsl|%{|CWXWM9wxyVKXe0%lk}rDC8g z8X@%6X|;SG;muLTK4d!cPgVxqjvaX=-$(Q65p5S*rI%=0cH7U(J{e1RPLJ7=nOmA) zMlRB`!r37ZXhzV+&X?quSyu}sbAn^a+S992*Te=%QW1izNzH-(Fc!u`0^%jIwx-q{ zjJ$P>vDS90xVX3yM??JQE(8|%*Ent^LOWJSOM1DpOGR5rG_7xH(O_SiI zQPhe?AtaSr$aWQDFB=s4vG}6A7sKS9#`*O?Gvb$VpNFveZ{M$e6gN?k zBAf6x8lMv8irB7O2F*?SxjQ+G9(Zzcf(-v6B#Che%7km*jk@ z)2}#vcILe$u75B8OqP#aD^OyEpX+8%bA;T*9+xPtBOA56r>VBH?W|l@4D*s*oHF7b zKiEI(=9Q&zzKDNu(c_-(iYp|O=RX90e|T*1D)Vi}F|XXxwzlFY%vI5oyr@gp+zfor zE{L0=4=<&pTg$Vb2&yaL(=zg-A=-V)<6G@}QKeym;mw^FzryGI(YX6E{x5!pKKNFb zX2wUTC}&?H`qv0{Ouyp!O!9>BD+&bp+x5*hFxlEJ|Jlx!dC36CiNWcOOOUw5NPT2n zckQz+nHS7$v`1`e33@@emu_-PmpnE%>A~wldBhO+8|uKd(CXF1LguU>p-iuo+6+#A(zwt<~}iz8;e zi$`F>cJ*M;o0PM7dMP=uB26set3i}BC!lE@>Gk`4oZQIG&&(O{wh_khwAz^jz zLMdgg*JfCk1{LlNW)C?WLX_!#5OsEIb3ZPWV7*KBWoBhmt&{(fw|eI)9LZTDrF;Cm zrRI0DXcArT*)L<`{Gy!R-`j)ca2)6Ks~48Jcl^Qg{XgWYyo6RpJj`Aq>-T>){#|lR zRPY`?<2vJ#s7v8mNz1zwnz@<9ofov5TnYTqj(PJN^Hv0N1N6rZY2Q2ixJ9IY`5B)j z?o!|2DLA8bc-{QD-^}@UP_JB`BjVr};f3o#5P`$++U2>eVvNM%RKxPV7J0hzme%(z zR7M~;#x=}vL&%^k)1dkFp)ApEinI%CXma_IcfN1= zghNTqbv$mD$mXwAWysU;hUAFR0^jhAYjE}TV=j$O0>v_@{)|7er^HCFN$j4D(Rxa+ zr>@Me?gS|zVlda*cn+sM7^g8|~YJlBlxK`p<| zo$B!mr$%Z4An3pBbh@BK4Hi-E7l^3GMOiG?^~~z1Oxn$0PAR&}&*9D$O)(_>aB04e z*{ihG%K2UZE9c%O@J$1R+qtuhVW+Li7>Bw~LBLxQ_2GJ6dWmr`sMzGzRfiKQrm?9I zR~`S8uz0=lw5lTY3!?lQ|2LJNx(Ly%0Hkj_Q0C+f8>^@`ot4vM)#Bo9*u)9;#4lPQ zkD$dnQJ;T3;cR_9pRiRuc^MkgYiS>6*;09uV{z*IYw3#i;TH$m(R{*3w>BS-cM7T<{u?6<8}o91iDU^B)<6wJwL{eG{=U+MNz z>#f)F`15Bnp|A(04!41E4ixt89MvouKW88SEk-A`6{3;V9M)Ips3VNFol3u5WiBmL ze0Uor5Z+x~NDGz=5gd!i#D5L)gN!7;`5bPc*8~;4hQOzIJ_RM07TD_cA!r1XISg_x z%9r&%6tsJq$>~|UQ1|7AZe{Oeu!2V&rjYX=>T-qb@S?3(7FC=Z^XOYf24G=+FJR;^ z&+s!YCtoncOWkA~zS!&wfYTiV$WJeR&@pINr7!v$Vw3}H92S?Mj>$ckH9eSoqhxli^L9 zl6?;LH$mT|@_S}#35}P!_7@h%=&u7n2PH0zl8K6L4SX!;*Nkxnnt~qhgVoG_|@w$t9uwee?p`9loMG zr|Qqo!ws?ZaVp;+zT!zH^@xtf^zzvEF*EJK-3hdBe&e4hTya+V7cwy9k?-&u+1W$J9MsjiXQu0{sN!(0)p=yn;5R~ zm8G1M$wClU4oHZeWuEucT>8fj9@#M0kY>Zjx}{F%fX>qa5#{2}lM>g}Xnjo}l|ew8 zkXA5h=I9hvEufUW_wOT8b^(DlBKCuM+=VI>J`Ua;1OioQTVInOmu*pv>=0&M>MOS| z%x%82SVXH|##aK|&I9wXCi2Kuz8@~`}P*VwE0=zPr%s5aHvFP`FsjEx2cBo)6ex*A zWp5GPoq0Vy74R>2aPlQP>~oZKw3$U(jAdy#E}=(clqiqe%$7=zb#t-GOC`@<-LJz{!m%n21KVT2lg4>F^Qyl9E2SvvZNE^Kq<8~8z*~izg_2G$e)DWZ z&r)^t$fjc4=0*E2GgW8V@;;-uQTLpkoe4G&6_Gi{=*bj1demc_{W*z@M)N3w-y!I2 zxt>0g2bLTSCr87lvU@@?w=y0(8-&vH2iDYp1oVatM3hj{k zTI09~y|)(A+XuR&rxolH&~6OyHuw;ulgO_ zPuTLyiVw)P|B03nB7klGZ1SdadQT)(_wcJpUd5Dw*Tl^3%=>G;G`B&%wwFm(MjZi# zMzuQuU>R1Zq8as9MkmM~4%8aV4m60Cl4X`?$zw27Nx(x@)C3hiNs$loyeJV|;3R`m z=2BoxiLeZq;~pUpKfO}+8=>;xkRT&Wh?xRT*$vA=e1-1-a(LQ&8&RQ!R;p| z0{dFY6Iuv97U8}VgGV$6PB!6w5}-jehsz>M8R?2d0-?1=c9Ek)8Yhh)!3TZPk1>d^py>9{d~my1NBGJ)ypHC;!FbEqzyVi zu?k`sqbi!2$c8~?{{=5xCd5}QNx$~UD2(hV0{VWx-}##X2uo*=a!4(~o_<3lOh;=1 zGWy!R&!cXBeOPdKzslPq+FOzt2P)Y6SL*2}8s1q7(#-PEp*Wm`{7r`W-T4WD{gKfb zL=!WtyH86@TGc=5%hW+QVgF5lmp6`bUz|y3kvDq8cEX#Zcon0xK`W6icDQ>?Gb=4k zx9`mayKC`XvhQ;fwwljzxg#~7>oUV^PafLCvQ3GNmYh3%udW9gpP}zdP01_?V#F|} zu+6A+v$!2@w>!LQS}Htz#xrDTMCHF(viHn9B@`r*AN^Uh^K1dYX%OU(L;QO-NS7sm zB}n&5G=+cvZdostKMXC?^Pljs93+p|U_TbCD$_YFH_al)C6D--qOJJg^-4S{e(_Bh(hqonQpIAR3 zLn22yQovcP8^(~lYa;Iw1iN45bC1LAyPgyMn!Us#kC~Od)l{8iBF=vyb{%q5Uo|At z`GioU@7{~W>87(`5`y7oUan|z+y9y6kLnnMdpTsuWXtd+^OE@Rc1&DlS#6q{VJQ~^2R25csGlWAI6%1)G(k1hy(%a6 zP8;j(?t{iGcAAzn*N4^9x1BG`9YQD?lsKuJE}E(!LRb-C04hKL&@?*uDt+rmq#F+E zy;MAG%p~MH`3$_n9%+YIg%-3+vV)5OcqKaeQuCmrhtqvaxZ!JAr|$dSF%)+`Yvoou zOSNuZL?Y9b&gUmyj|pfc5HOzcO#wTn_4)qhXWH?-2h*_V$bXFzOAO}R;U0Utm6jK1 zARXYF88&Au<4|bU zjIqU6CietjeFXz>A`VLxAln~?Tc3Z$!7ZUwvHhxe6;yAIYyV5DChijA_*mxgWa1Hf zpMe^m_ zi=Br9$|jmRXy`ALU7%BL%h!;kp0u2jEG>Y(3_SumS4~Ap=R2K`FOb*E9xFaK2xw@q5)FC9ki5__UGG^ChH* zg8T@CWK(2ZAhn)tl(@xrQ|@?sJZYbg?wPRykjvXSzBgO!5l;~}n=Vx=*>!3~hpG!QO_vZ7nOf(H%X8Zyf5zQI9<;&VgO`J^g!d%ci*Gayzi9E zzV{ggWXFUOwfXv^Cu9g;LXloZZQq$>osapDJ&dlE+FA zOAq0EeuKAV6~J_=V4ai?3X&T(A2S-Y-bb`Ai`xZ-D`VrnQ>pAdiPR0)l-S!eWp};M zhdf*YpjTWa+F;wAvaF(x6TW7LroZ>f%xX1B>ku{kHy23f4Gr*{SyBzch&H417J0V$b=yDLEIl7<2;YbKQ&{=ZOVvMR0}AxP zsmR+tme$kQHP;7Yn9&3eFJljv567buHH|D~F|nOk<45BcE*rk)#MT#RvWplVxMlzpi*dmU?7Pzz{?ICX{O>V+&4<<0nM?7@q6?=qp|+- z^F2j+>w(o9IZ#i9MKt?we*u>AF^=)GwlEo-<8)ZNsl`DO9Ts^3mN?;` zpu-&&=Gn~8C2og^of_Emg!Z)!`}l6?zCnvZ2)$RRO7E_te3B9iY#R5%#LUxR2a$64 zRNuv={A!3W0>=Vd9-Gygqi!GqnO4Wu*hSIx$FOH*78(*CzB@93|C9L^)cR86oytQX zz(VBa;uz&eA4;0&+0T7h>1okMFU4QmpaK8N1A2wlN0S5ncCO%AcYgA${c!kFQ+TiA zSE{2T+HSjei*$%Ai4A}4W1S3}-mXNa1B^jTL+Biw<*SD;pmpz7SdmFu%Z231W zkED`=rBr|FkuV%mCW~b>XQTCw%K0Clxj&QGIm4o%6lpuc4OgwWW^N>I z$CiUaixkCEQf)R*DBF6P&%z|)%AGchvGhBH3v_5YPKL6o6gDG~@`ZoTScT$`HQPz7 zQiqtq$|yTKXN%7 zSaCG2Ucn>50Z`>XxJnz6%(tPlqY9dGm@zHtV2!nWMmS!~Ac!e66nI-(6fh>Qh>8n)+v%wQv>T#tc54h zB%~5--xs;qRhX+bIms&XJP;?K$K2_5H1EpFn-*GyZaD5sGDZ&n5P~FndmWj1xxfxb zSocm{R9OVmD?CfFE;Oebf@%V^7{ZETZUhZ?GM(@uT|gImuIH#AeMtxlE^*teXWH`b z$LnM8?Q_|vjv^u(kO-Y$cB1?ICmH@j5PY(q zaPxf3LgA{hO>D7{M2?XnUpAsX?0!P#eL3cHStcyY4^PB2N&Y`}U05UvjiREStj@u{ z|B)ET)+LPVvkvTJySZz%p9yT>L006*KQC84JeD?kCg^7-M*WGZz006}JRTO0P{npNd zG5qumV7)CN`i{&RgxVgioKN$1J|8zAKUGzbbc}RN6lZ;Ky0~oQ8NKB$i@Y%-vQlJ} zl`p?}r=`eoGKI1dl4@h-zxvPQ3w9zN|BbbX?`$6W7gEW+^STtfeERnAG~Ic)>6IMt zBl`dQWW!)8qf+#WBd6t^ig*+cQW9)cT$Dd%#c(vk`n|T@HT2MuhN(an9q^u~L{xOg zU1n*TG?)`zM?&_B=T|%_zfSk~74hq8Gu#*b3evyT_D-I*igRI*U8lV~b;}Vb5VC6* zN5E;X4OjRQ!JNdLy-WMcE{=v&^o^U|29wVS-Ai*G+?VeLGPYm%B?5ea`$ETmbLsMV zuiJFZNk})jLMuRt{=Zje`76#}#&Q3V26Dc8!}UHik>2-WLx2j8wjJtgf9=)R>8Fj` zFE*av-r!J0xiIKZ=FWHHmEwf_i<&;MI?)S0?HXsgeSf|Vdwciep&c%GwK}|@Gd1%C zPx_Dvy-tOWYC)cc%IxU5hWFRahFgTL`MW-E!fSGl4@u&*L&JnyUU@iw$)zbe=evjM zt%9xm6Y?gZ!w#c*4uAcV=SSq{@2c~b~PFc zrLk+YJ%voE`Km;35;%G)d%LORdN*Eq60==n7~OlR zeDy~0r+Q1hk8Yr?MxH*mAXicCi|m|AtCD8chU&|oBob+$`#`K>Z&%JO`Y%R7uDyRE zF5g9&e~dLD2ZIEeBG%T{e2<*tRN=!ovhEesu24}&nrdk1yHcs8dDLSfh#?!OG*Y`- zl)1>&QXhz7mtv_3w+Onw5moujv|FvvhWr@An6%|*_K+6y-Et^B2k5EJNa(4G6u+gZ#%FB$c>Z9t9-&I7gqC#_q%IHKMfPBUyrTeUAED`RyOHZ*lE3cF^YT^w=3_J}LVz_1$5uS^En^FgP{+ zwZh3iSKY!RJ$~CpQSq1M;=4*dXx_~juMzBpA``A*hPr_NET{O^Posj26|k4(rt zAHc=6#1`I^bRXZ6#FoV)T^cauCunE63*X{8+)QyR!F=o9Dh$t05}au@6(& z@P4%cYqyp7>VNlWtN+2Ii47Yf^_R^*o!eLUA@OZ@@tb#S1I2#JB@0elUXbp6r|42{ z>Up3u^Vvfrg^Il+stJvBXid@+&EVSOgR-g$BQby8*NSE(u*Tl&f2`!tbTR?=6uY^L zPmV1#CiH?yp9-)(yE+Z_^%o?|+{o#gn*KyKpZlws&guK|@#kd)uQ)L)!OY!Knx&P| zNp@L_L}5{}qGnN=&T5asB{T@XK=76W~DvO7em~fhn=gC4PSSYs4SoaDl z4SR_*-mpJaj#5&eNM^1s-C8E<%k98o<@`+7sc%qs*IIQqXIvO>K%p$Ngxw?&ke>v| zQcU2egr?SLxJr8NTG$4G?Ck6`0s>$-n!L!VquRp0WfWOX$)?iO$Ajpk z>7n<33vGN>qFeBio7xoe*0`-?PzmjX)HUP(Z8P<4deLYHj`)OsKl5>O`J@HzDTb{>)gRHJ*Y$4Gs??reV-nqI>o2 z(XleS1}kr_l4fnJdXlE(83<#vCA@UpZwSVI(iaMo<3Y( zhf!9!Wn^ckZ)}(o6Va(IMQB!vVxOu1rxZ7Rn3G9(3iJ)iX8e$aZ(di)O2MC<+B8nA zt6QMvIrA%RZ?}|{*_{Gw`j1S~Cw?}N$<0_Xt`_=MjXx`6AeLBGb5g|NCF>X)P-S}6 zSl7H@Q0njQ{*6l%c_D8^F+_7@;f8$aaG_JZNf^3CeT~BiV|W$E`tBMjBEK&7)0DkR z?z>hY-|gMqd9^Y3P&>pyQ~XmU@z*beD)dzp<>lo(Oj4w6nKcOkTJCP!ABl5Xv&?I_ zJ`cSkJ-$`pFA3ocK~Fx*R>Y$jr@`v(xq>dG?61*zt%i?D-~m)N?sNZb>o+|vyj z-P1A~|56bKm-o#W{_6P!q7YoBA?8Tah)qBGticj0=B(_p0}|mjGyRel%+YI>KwJ@n z^qRZ{oO<;bewX{$Tg(ztZtb2DUTkJ;Ry;NPRh5(23IsUxyxtqT+s;{WQv9+Mt@Qnn zwOx4AP_7(>wYZd6?ZAelWHhVc@(q>`FjOO!A^mLr>aOJ5g1s_}q}0vHBDLpFiR2;j zOAerCR@xs&%hW_H2B&Pxnz-P2VweWj@N#%B09O_hrLaqC2c=2;PHngFTyZxpNcoK< z#tIb^`g3OeZ)c)X8zmJX6PkwtK4|I2SVhV)tB4e~U?b0!Ptjea5!rx$zBKs7R9$^i zZQB%4^xSN0y;FX>r-#a?wlzGahK5R>o}S9uL)J|qXXyck4j60(CW@6y*ea5eCEKme zkd&$kva){zSj6%yjlOHkJU^XBUnND6@Z+g`p6E798cw4GM^A^H&~p+e`9?j!-{uP4#( zb2j-bBwJC$yC)}3BE{)hSxWa&b#RgYzr&HN}Y z7Ku~xdvis{1PCP~Z7|A9mtqU;tUl_D(q?ktNfV-~ud8FW=J0K}TuOYQ|1@)Dz$(m} z*-B&|oVY5BAvH_Dt)vnZ1jpFUAN(8xOed*0)^dv6r9`S*FlVyM)=V$kmGNY>C2v*9eaBUU8IB93V++|Aux;(T>}Q9T z%~-`gM2_p~%GaYUXQK z6PXG&_M+yM(zm%?ZkJOon=X)?uop!c=pM`cN8p1RvK;K_r7Y`6uEHZBcV7`a!ZXap zS|9d^O%X!cL4UbWzuLN2IL*2__5+%{NCa?ti5~o#UQ@%fB$8AG&1<9+uhwK^Wras` z4DsP7zU=JmoFB)QuLhKV7ryu^cPpdO`Qt|nE9-D-EtA*iNsccovR@v1^ktf4<(4-1 zmB@r8@llgA#O}<8w$)ciOBov1yWA=@;c&Y}EELbm{;OFebqSvNQwp1m>6V4Aw&`%D zaO*$u6mtCdm)lRIbkBFSgv4(il@~f$Y?&S8;FVc$Pmixi3&3vxL)zCEg}l4FuT*behEKMYV~DPF_4H!3MgyAO9k?H)N>5*- zuIwNe&4JxVO_$Jft`ze)-(CrKC?J>0XliQaR#!V?bR{DPvDb+uQvS_nf}QfCgv{_t z>Zzu^D;b;aVDRQi=_!HSp}uWPW$80+l7u;@WzcK%yizT(-y2`LPsI^>l8-Cakh{9I zuUf18fv_c#BTW-Om&f<t)e9l<2>wEz%eMmV3ayckm_V0v zKFd zE$!H$nT!BKw35QcH#@e(;PJv%ytPpk1rM4-V_jWOK}N>y`mfcPU+Ndb@UyEk&7r9u zU(9?8A__JTT`y>%W60>s+?FR2<~HbfJ71$FG2f0A@K9CdAfu+ffv&kGK|r`E&COlS zFBz&!|LpuN6rQXJ4}39Y4h{-yv3dLzV+j?!$@(B_Fw6cRXUc71(4?Y_}* zMdaZ%7=>5s!W%*^1pUU-IdheiHkRzvzZxe;oYIO zx9(9u&!D%#e4WMy6@El9pWaJKO6GgsSoA9W=$tA6J31b}t@=q_&i=m$7XC^2$JLHa z&P>oe&)aMwK$k!iNJ>egr8rFyfNyhA($Mhlb1n*;incWtZx>5x!V(0v`>DJ1L{ojQ zKYQdOBNWWNA zwRudxn3hl9E}7Rd?f8q2BCsf(0_ao`48#JMF(Y$V(qW5te)|I`Tj2eaf@_O*8cV`K zTo8ECnY7JySmSf9rK2K2#xks8>>_PYLV*GvI) znEV1m27uJ_JoyBH~+jV72 z-lkrB*eWrGGckj>1U%yw%Y@=JbY2nc@=)TK+^&%e5HtX+XfT%_brAb5+dswHh*MZv zZmD!r@7WyhQ7pl2Q9X(`-9yvH3qKHi<(yzMOMA5=yLMO3QBK;gV@I=l;}Xg0R*D+O z_bFwzTVrpe>K(M>d8>JRGbB`=G4yVi^!x#!FBufd#E#eeDevkHDD%N%!zBZ&U|w`q>1WzH$Uw$0>gV zACrR}e_6YXpy+Xl;xX-e7pb5U%OqLFA8k=yf~$C@YP_^~#9SHy0GHRCs-g(WErKK) zpQE`_;9*!-{@@g~!7GD+4JwZ|O)lWI4E2?Nyx@ntWmOHMcp9Vu8)^+!9rv1KCXx`Y zQbeE)fEz zd0RR4i2`G>k%~T$A@-;172D(;rocpUKna-J-TkunHk>RKfO84n*%fPg9ipvHVUVI1 z9k#VK@ly6~{FyNI-Yg!T`0X(auTwv`U;Qa-{GOy$AD~w9k?OwUxeum*)fu83(cIKD zj+p%-l(YpB{+`vt?0tM3n)#0`&$ESel1S`a(q{+JyB=*LOMYwC?t3*PUO~RH<2ZB z+j{q(;O9-%6uzYvH?_m=ip zu(NIOfP$xlJIdX{KKdAg+1?<1f;HZ?84C<&d&3s{ftnOasT~pDxYt(WNe@FbP3CEM zu1hUmmorNN6&?Kr6W@z3k0Zo-Fp3Go0T}$Py_CdC2iEOZ8Fr=uoo3&oNH@(9S}*vJ zsig1T7FF>>B0c}7N7&FDEmE>9acq70P&+#mEh00XcMUirmRM^!E?%h2taWZf6WR!A zZMf&x0^xoA9;Ctd(etb{vjgD7G&DLo3h>DBTJ=Uk3=#TM@IT;NKRc@E9AJ{u>=6 z6ciL{VhLufW?wY(43K@O-df3Ue8^`LP+45s{95*Gy%^t(Qlsap5@5#T+K_cA3It^F z1-c~w8oq1asxT}W;e%RETr)oX{rk5$;P&W?bcc)Kn+%+yI|6C=Y&@6Paw;-m>+5yA z-H>!}C$502{5`uoNL=xiO~;lpNQm49g z1`o34eh#gInycGeS|mPERe-Fl?93bi42|J{6RGdj7RTkaMOYIU9M@V zCOE3ss|p`^0gp|4ttdrhJb68wE@U~~c zD_%J-6yqLy*v=1~N_@#x@RK-iHed3^C-2j63N1r^d)ymxuz}oq^Y8!;O?&-`_)7M^ zch@9iCo8^}*w<#HP%^^j(0v{E1}PE}8+_8fME{$EMAYm~w09Z+c=kG-grCRzXPIc$ z{u1Pf_4VE6@Uf~6h_L@esnE43I}Bx_WF+ zWy`gP7thYl)Lx-8U<*L@l?zTYnoM+Z|H5GAdpUp&mV&>(*p-%zGT4rIC1B zl``%t4U1{S!D`Gax-le(Cj7J=P7w7UZ^*JGn2yByeAEB%8^{}T;!7Ez;qa+gpI^22 zN>d?deiX8?I_h2m=q@oI3*C#Xxuj(Sux?>tVSTp%LHB|E`$Q~CEdnNhU3<#7i{-kH zYTg-ux2a)f>-X%FZ1ID`slSR16>`um(2JnGjdw)$*b+R$%;>%_3;KAe<1I0pceoS9Ox-_z{7@g?+1$RiO_n^csRN`4c~@6f zid`rpS;^S}hg`1D`9!Z54UOKpHq$__IYh62Y5DoES-LG*QI8mzZR|A~(9ff_A=T}j zo>QwY4B*Voyt}0{Ta% z*an36!KOEnw*yiB45Kef9OLtOY38v4CbL@0;`%Rs{&8T3Oc41-6wkd)_q*5- z+ocoDn-o8hwSVkLcmLXzUhk_SGj^L8VYM{}o)|Er-@4q{-n03aI*@2RES2B2jeEhw2<-^hp=UfTIvwupO>zm2!zj+&6 zp5x<(J9su&`exW+=a?Wt1as<=W{}fl@`Hpf{R?s_r9A_cq67*s^_zeo;ufd^Rytv$ zsVpzsZx21y(zE4a=yr~rjRJ@)k~-d4aD_->HCI0WW5h}F*Bp548Q`sa`O|}hX>{j^Qo4VC>DcrN zgYi}|!8tEr$eDHf389(c{%_{7g^(jki|?ZREG<3#CX%I1kqG&H;62Z3-jPah=dc++ z=CzeV25~3f2j`MTeAG&Uag+#h!aX#5&&g|_&pGEDGGk*Q4rdj=Xz^u_#E^(-i9D8V zE_B*qm^I1%p>@=>rI+Cwqi{wTJ?4@XXqNK68M?dGZ%ZBNk6W5(r7t;&7WR(|+Vi(` z44yLg$*5Z%&Es(LKfzDyZLTYf?Gukzf5op3&2#twFd(JKhmoP7?g=!j<-|sB)D)pS zo`IMgu? zE4{$Id4GWZ+lXpXnti*!fpPR>JXEHE#)MG)HQ1a2C%Ma!P%eFwFn1-&sUd~E6K6Hh z2))}fX1QV53RlBC(Yi%~b?h=og*aj6Ml+}Xf4NIYV@pO(zG>3wxi8&sZDh2JZ;!LR zXk@8KcGNqSC;IwdRn_pOe@H$cODSm{IWt!*BcqvZZgqY}o+4Tde)<+jKy9N(I|t|- zHm91zxt&dc=AfI(%@bi6_gNldI5)@;;3VTD*cp@V_5*ALBb*wP&5(Y}Kwy8#G%Z6h zr>c$K*TW*5x5=#O$pt&cS!gL);uVpti5@JPxj@a z@J9(m$&T?v|B50s!MJ37!jXaHH*9Zje;WUT(ZBQZ{FEnwRY4ZALJ`w@&&kdGG`Bf} zk%DbyIqt&JT)9B3m|)91+b)=Ubis$C1lpNnQz+yJUD}M{@?L`Iy)>Gls(LUJGly(e}7nyrh*tZ%H&4#7g6WdgtD0C_wgxvK->Szk7_Z!LMQ9)?jHSbtC1Ag$!W zlZg9VUmCU%b2YEoehLQI2)^h%{E#b%QN#i$ko1M#&TAEx#d@SllI#p)%5aAuHF@7i9#nF6RBM`jXWOJr_tzOgF0>GwBzyRI|c z>O=XgR4}ZF*qecz)WFDyq4_iOhB4AYY@g8egc8`b)&f}&m9h3hh!fxn{r%?$Am!GS z`uSWDgn?a@#UI*7T?E>8tGDP`%hf|(d=qJ-CiYU)Sb&CxhI95GhA}fho;jseiuOa; zEJcVE6c5uXw5-5A7qFpD9Kr};Lw>6Y;x=W#zz%_egAS*^iHn9c=Xcdk@rIu0hgtaT zL{5)Z5HLu=@%LYN1NV_W*lBYCI$N*V*@pY+@5U_Mzb;`yHDX>Ed%s*yVD(M0BKeuf z0`3#w_>)LOZXT^(httov`E*i2e%ZtNA>LfF60t{8Uv`Izm+LLt&FHP-0P6k3hIH@v z0L_SnNU6P!cC7($%idO&!UUlx+_q`Z2DHV)htaGq{Q-?^0p8xXs|a}V?C;UmNXGb0 zfs(#TJ{tey@l!8CPsBKHWgRd@o{eK%xjy3mSY4|15{1U71u{X3IK}Q`gwha(l#W8) zJ7s)CV)`{egF7j(!3=auc-|%qzrhnnS>qj2fppNEtW-E;B`-7gA@RU0-I5- z7-8bMaC}05*=u@!zWMXj2t!v`wU)${!spmm_Y6Rbzs$qMpYvewkw~}?vWM-EXeL}2>BwE$1`kO{IS3*=->>#4khR&N=kJjl#_IF)X`B46b}#!iPW0)w&0sApO1H~z zqVJFAqgRV4EQ78bbG`RgJ?G5>v19~^9fE@BpdW<+J8XNR(y%;DkQZvmx8?2<9+qC- zF?Rwa<%d@+92{;c5tkLOZTrj3o-R|<7a@mm&JVcs5*-vS+D=XO?{dJNs4xr%>F8yBarda6AHdIz)i*J&QqO`4xF91VOGP*|E&v>2qTewcs^S6=UaaV05@$*`F6Q8crFJ( zOADo92CkU{Y>vI;*WwbJvjf#o;Bjkr)dv?9j;MTvPK zlvPz7KX->b-!p96APge`VR=hAa3>Gl8rzX1<)|lZ30-Y%!hT@rS_Ly;O1bFjmhlDt zx2}x?QC3#|GB3X>6u^-y^nsW%lW?2UK}5%3)4|6_qJV}?1-e>;PipbxO0Gs(lC9Q{ zk=EPYUn7!`4f$i&%m7U|_MBhuzpZMu-lQG4F{PCG?yVK=eF6KOg)3 z`(gI>c9Cp2?1&8_LKLF;PMs{8tR%Qt<^%T7)pw+&H90_F`sa6YYiVcb%kw}-WmjXs z5(lL5=#tEi`l{C2pIQxMh9#o_Ru6*0Ud9^xo;M5nl2|Pvc*)KJL3P7u!M?a9R9e( z3K2#tdYG&qZ{G}X=IN-Qcs5&0hr`%(?s*z97=kQ=}LX4&W5xI>uN~w^Yq4^ z;7~gaH$cLgFtJ1W3zJ!CsXozmCFicmPxf@_5;rgiL2{FX2&OO)jILzA-zxd8fPET1 zZsX!|HpLHt6X$)zJD@$SGJ<}I0h~Edc7qobj@{*vMyMWYtPR%XZu=CQ*t zA(u3yipVyJh$1dOn3JhU11FH*jk+_!0>!YPNSNZB{?X+G}4i65}5WFrlM2}AV zD=li$YS)FklOm?zmyaKOFB1GiqaD+()dKA8?RX;>kIGJe6=qNLB?V&Uol>%YbbHfc8c09$4Oj&MlQd{w@nVI!HlJ`PotRaXXAtSpxU8vNPM$6{>PJi%F z7B4Iv7xQvw7iWmh7n)Q;1%$GjBe{b2 z$%}GKgS3D5-yAJMD{1xHH>dEI_q!ifK~RAX{O@_wjuA>HfL z0+=B=r5OYDh$I20u?y%(Fua|>W{Qo949lLJ9A^bG2aR6$B^yVy(iBfIgTJ|2Yw5X! zz+p?kCqbY>FwU5?v zn=4^9reSg}$)CQL(>1d{bV@CzM@Qf5>FL=nC3!Lv^wn8*JO~O4XVT(4u$>}Tq(gyQ zvuABJqUlcH7!IzJREd%cXlFdyfKOrhgi=hy+?nLlf2kvBCpIl(#-sw{s0j;<8*j`(WaQ-G^Ec_YQx~+7?DFUE-Z4N1s-wVQq4T8-#_OF z#v~+k3n1{yOh481H;aI!?@&o>sS^{XjoNuc^=`D@JR;CAg^l0e2mB2YAJUNIZqI$} zW;q9|$HAc?g{7mGeq}$u_ie-4*1)2vx%(rOTQnGIaJZD5W$}!9>`NHDK~+UX<27-Oon6w18fKe+kBQJnt)-`z|=HuSis+1M~5gZa)2-v!q3UsHxIyS zHRQPlP=X9r=p9ZG++0H&kfDfwmg9)#HdQQ>p>c#q%K7hbB1S)vN2KQglgc9SYH4J} zModI@m_vYG(T0SUmNqU@we7R#5m~pXuqg#xvNSswi#b8BLwA<)PL#-{V52sh?&?b77cU)u5Il?AP}$^ zUdUw_3L-1~cj>3XYcCIJ9slC8X?fMA&dk)SD}Xj12)^*ejMW)xB*KTei`5IU=|e>^?TuPER-G_+iHHJAH>6ztc$yicfE(h-~G?i%F2ps+!leE z*69KzGRz{+=`AA|qw-9@UT%I92zvatJUh}8_%O`ejuf!3nO&g?>b!Ok2Zf`MAkh&Q zZsQ5%<7ZkUw1Q7KRW&_Vb=X}g5OO=+NlN!WKZSoHP}@wYJ3@kZ;b7al91!zZPO-dT zr>?|o5tFSptSwkY!0(I6Np+E)y12g1w2zZ3BO@c}KBr6PKugb=SJZY%*q-|r(bTOR zOk>U2POr~QVa3&mpa|XF`{O(7iUTz4L>Tj`qA))X&)IMo8ctR*!CZE?R^%b%bj)2D zm04i8&JyDF<%>1*<3XOg6b>F9ucC!ax~(w3cEi?4oHjx}Z`L~w?UiRJ;rFl9W9{aG zCbABfD6G{ZP9nVWb5NYfo*o!BU-%O6Z@b??Qmrfr9Xl3gjG3L5CfDY=PX4eP&!41F z=ySOl%xQ_Xp{095x=5c1S5jbPpIE^sk@ymjCUP?Gd`v_^;j2-@ZU96XQ3{rzKub6C zj_7Se6n)~xW&EcH>&<9Mzrszja!qHAET7#|xdx0q#uKJOLgvT4bS)`dOw7??Q|}t3 zq1&Gys8=LUwg$MgYyLi5U5%9oUkf1m<(VEC!AL5xA{Ms$@zE8Ud|&0kqg%FxuKIt1{dIFFYu(wY@L zVzD?ln|i7X-&{jnjeSg!uq8P+mx6K`J&`{W^YrJ!V3Dzz8GgJ}Oi`Pgr$hs$mF?mM zM(GPA8CNhu20#8E1m!qF*?G8}J460$se9}=^Q6rNW>I9UCHyne!`iGM^jm^Y2_>xnd9qlBcNr3$ws z7nGMLJ+8Z`bcndPLc;h1b@%<6bDdecnGSWaWuCX15gi+tq&T`pSlYba&veM+dVOfd|;{A6qI-MH;OVU%4_>fhegoxMiuwI*+=1s0rAE zjHn2)ozp4N&1&Az;zJKhE6_Kc^41k!!{f53ES7CzZf;KW>)8s?RIIf63SG;aHF8&; zD@4fptoL;9sr!7t?k`4zHprjxGqF+`7~?b$eeQP_uNnUQr%vK0qg@eo9Vs$BsD=S% z+LNzOMDn^TFgQkgo=q?6vMO*u#t9E1M}xUr z>e{hLG(;iw3Zm*NRSJ$Yj5GJ6stae8K4MWq#m-{!Msy&m0v7A+Y zRP2D$GA5b(?MY$il7$I`v01_A6glGWlG;l+6f>LrwAwGE10tq3N_!hlI@5joTdhv; zxDlZ(vLJ@OR3;+v@Y?UJ=O_$IN)$L*Fu!axdK1vGfa{-`#RhEm2HXObZ`0G#>Yz_g zg#*HqIRdsKJ?x?d3-5OS=0aPg$DE-9e;-6bAGx64j4}WCGe^UOmue)!Sd)oES6PAu zZZEgMs1@*@?ry{RIVRMyxTK`sIJ?y!x!X!~djuWN$?NPDcy5v{& z!LDd9Q_G>xXVD8dYv z85kIz-Y%CIXINf2C9g}WgxN~2t$M087;`7KU|B!Y?j!hA+tGo_Eg(jZy@4t15 z>-BN}4Gpj#@8fEzF`r%r-k(7^Rw~BQIlxNa(ht+v)Rx>3bi8!QRev}JNoC@=l6Qqv zcShO+EuHMRt*tHpF9bKG8)y*wfbeDR-yR-%9GY2KZNK5F;(?zdfMGJi7x;xiDjjrB z8-#I&`#ep-_6e-yX(1o!*V*H*pL`p9SJK1zId0F8?d2n51Ub4=B;UsCeMSN)P7d79G#XB(mxS>G zF0TaP3?K~11V!Gn#qN6H9EW%>&0$})XijA?@nMYD{-K06@p0g_^QjHvTDx{E_`x8t ztW?gKO2GS&yjb*MOjovn2ssPup~n*}nW1#B^>Dua@W5z~km(ENNMcO-wsr;onLMfo ziEw=ATF!d%BibpC0H+k*punkbRklp|*QyQZeDr6NuyqAm{*v!VU8F}c27KY3OI{ww z@QlC0pEsa66gSHd--B(AYo<1v1Rugf&!-T6MhGyTBpUr9}NwYYI zBY~zd6KSXg?eD_at<(P3Hu2Y*I(YNt->t<^u& + + #3F51B5 + #303F9F + #FF4081 + diff --git a/demos/ima/src/main/res/values/strings.xml b/demos/ima/src/main/res/values/strings.xml new file mode 100644 index 0000000000..9bf928a6b3 --- /dev/null +++ b/demos/ima/src/main/res/values/strings.xml @@ -0,0 +1,5 @@ + + Exo IMA Demo + + + diff --git a/demos/ima/src/main/res/values/styles.xml b/demos/ima/src/main/res/values/styles.xml new file mode 100644 index 0000000000..705be27764 --- /dev/null +++ b/demos/ima/src/main/res/values/styles.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/settings.gradle b/settings.gradle index f67f091650..d4530d67b7 100644 --- a/settings.gradle +++ b/settings.gradle @@ -20,9 +20,11 @@ if (gradle.ext.has('exoplayerModulePrefix')) { include modulePrefix + 'demo' include modulePrefix + 'demo-cast' +include modulePrefix + 'demo-ima' include modulePrefix + 'playbacktests' project(modulePrefix + 'demo').projectDir = new File(rootDir, 'demos/main') project(modulePrefix + 'demo-cast').projectDir = new File(rootDir, 'demos/cast') +project(modulePrefix + 'demo-ima').projectDir = new File(rootDir, 'demos/ima') project(modulePrefix + 'playbacktests').projectDir = new File(rootDir, 'playbacktests') apply from: 'core_settings.gradle' From 43d70bdde977bf4662bb9bee792e8acae2249074 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 23 Oct 2017 07:48:44 -0700 Subject: [PATCH 0588/2472] Ignore seekTo if an ad is playing Issue: #3309 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=173114842 --- .../com/google/android/exoplayer2/ExoPlayerImpl.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index 6bd6cd4795..d1c643c05b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -232,6 +232,18 @@ import java.util.concurrent.CopyOnWriteArraySet; if (windowIndex < 0 || (!timeline.isEmpty() && windowIndex >= timeline.getWindowCount())) { throw new IllegalSeekPositionException(timeline, windowIndex, positionMs); } + if (isPlayingAd()) { + // TODO: Investigate adding support for seeking during ads. This is complicated to do in + // general because the midroll ad preceding the seek destination must be played before the + // content position can be played, if a different ad is playing at the moment. + Log.w(TAG, "seekTo ignored because an ad is playing"); + if (pendingSeekAcks == 0) { + for (Player.EventListener listener : listeners) { + listener.onSeekProcessed(); + } + } + return; + } pendingSeekAcks++; maskingWindowIndex = windowIndex; if (timeline.isEmpty()) { From 3289e3e9cbcee9c00e6c2bde1b473841655ce1de Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 23 Oct 2017 08:28:30 -0700 Subject: [PATCH 0589/2472] Be robust against provideProvisionResponse throwing unchecked exceptions Other catch blocks in this class catch everything. This one should too. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=173118891 --- .../com/google/android/exoplayer2/drm/DefaultDrmSession.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java index 290c1877de..688fff48fb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSession.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.drm; import android.annotation.SuppressLint; import android.annotation.TargetApi; -import android.media.DeniedByServerException; import android.media.NotProvisionedException; import android.os.Handler; import android.os.HandlerThread; @@ -281,7 +280,7 @@ import java.util.UUID; try { mediaDrm.provideProvisionResponse((byte[]) response); - } catch (DeniedByServerException e) { + } catch (Exception e) { provisioningManager.onProvisionError(e); return; } From c4f3cad586bf8fc0ee7260058bf8050fb50d3cbf Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 23 Oct 2017 09:13:44 -0700 Subject: [PATCH 0590/2472] Better behavior if media doesn't support DRM scheme We don't expect this case to occur, since track selection is normally expected to check canAcquireSession before selecting a track. Nevertheless, if an attempt is made to acquire a session when the media doesn't support the manager's UUID, we should fail in a more graceful way. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=173124170 --- .../drm/DefaultDrmSessionManager.java | 17 ++++-- .../exoplayer2/drm/ErrorStateDrmSession.java | 57 +++++++++++++++++++ 2 files changed, 69 insertions(+), 5 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/drm/ErrorStateDrmSession.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java index 677c784fe5..2ec5040aef 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java @@ -26,6 +26,7 @@ import android.text.TextUtils; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.drm.DefaultDrmSession.ProvisioningManager; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; +import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException; import com.google.android.exoplayer2.drm.ExoMediaDrm.OnEventListener; import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil; import com.google.android.exoplayer2.util.Assertions; @@ -372,19 +373,20 @@ public class DefaultDrmSessionManager implements DrmSe if (offlineLicenseKeySetId == null) { SchemeData data = getSchemeData(drmInitData, uuid); if (data == null) { + final IllegalStateException error = new IllegalStateException( + "Media does not support uuid: " + uuid); if (eventHandler != null && eventListener != null) { eventHandler.post(new Runnable() { @Override public void run() { - eventListener.onDrmSessionManagerError(new IllegalStateException( - "Media does not support uuid: " + uuid)); + eventListener.onDrmSessionManagerError(error); } }); } - } else { - initData = getSchemeInitData(data, uuid); - mimeType = getSchemeMimeType(data, uuid); + return new ErrorStateDrmSession<>(new DrmSessionException(error)); } + initData = getSchemeInitData(data, uuid); + mimeType = getSchemeMimeType(data, uuid); } DefaultDrmSession session; @@ -414,6 +416,11 @@ public class DefaultDrmSessionManager implements DrmSe @Override public void releaseSession(DrmSession session) { + if (session instanceof ErrorStateDrmSession) { + // Do nothing. + return; + } + DefaultDrmSession drmSession = (DefaultDrmSession) session; if (drmSession.release()) { sessions.remove(drmSession); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/ErrorStateDrmSession.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/ErrorStateDrmSession.java new file mode 100644 index 0000000000..576f0a08a9 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/ErrorStateDrmSession.java @@ -0,0 +1,57 @@ +/* + * 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.drm; + +import com.google.android.exoplayer2.util.Assertions; +import java.util.Map; + +/** + * A {@link DrmSession} that's in a terminal error state. + */ +/* package */ final class ErrorStateDrmSession implements DrmSession { + + private final DrmSessionException error; + + public ErrorStateDrmSession(DrmSessionException error) { + this.error = Assertions.checkNotNull(error); + } + + @Override + public int getState() { + return STATE_ERROR; + } + + @Override + public DrmSessionException getError() { + return error; + } + + @Override + public T getMediaCrypto() { + return null; + } + + @Override + public Map queryKeyStatus() { + return null; + } + + @Override + public byte[] getOfflineLicenseKeySetId() { + return null; + } + +} From 60a81824f58c9bb19021d34e86cef288bce3c7c2 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 23 Oct 2017 11:41:11 -0700 Subject: [PATCH 0591/2472] Clean up IMA demo Also do some minor cleanup in other demo apps. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=173146425 --- demos/cast/src/main/AndroidManifest.xml | 1 - demos/cast/src/main/res/values/strings.xml | 4 +- demos/ima/build.gradle | 18 ++- demos/ima/src/main/AndroidManifest.xml | 40 +++++-- .../exoplayer2/imademo/DemoPlayer.java | 111 ++++++++++-------- .../exoplayer2/imademo/MainActivity.java | 57 ++++----- .../ima/src/main/res/layout/activity_main.xml | 14 --- .../src/main/res/layout/main_activity.xml} | 13 +- .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 3418 -> 3394 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 4208 -> 0 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 2206 -> 2184 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 2555 -> 0 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 4842 -> 4886 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 6114 -> 0 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 7718 -> 7492 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 10056 -> 0 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 10486 -> 10801 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 14696 -> 0 bytes demos/ima/src/main/res/values/colors.xml | 6 - demos/ima/src/main/res/values/strings.xml | 21 +++- demos/ima/src/main/res/values/styles.xml | 26 ++-- demos/main/src/main/res/values/strings.xml | 1 - demos/main/src/main/res/values/styles.xml | 1 - 23 files changed, 178 insertions(+), 135 deletions(-) delete mode 100644 demos/ima/src/main/res/layout/activity_main.xml rename demos/{cast/src/main/res/values/styles.xml => ima/src/main/res/layout/main_activity.xml} (71%) delete mode 100644 demos/ima/src/main/res/mipmap-hdpi/ic_launcher_round.png delete mode 100644 demos/ima/src/main/res/mipmap-mdpi/ic_launcher_round.png delete mode 100644 demos/ima/src/main/res/mipmap-xhdpi/ic_launcher_round.png delete mode 100644 demos/ima/src/main/res/mipmap-xxhdpi/ic_launcher_round.png delete mode 100644 demos/ima/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png delete mode 100644 demos/ima/src/main/res/values/colors.xml diff --git a/demos/cast/src/main/AndroidManifest.xml b/demos/cast/src/main/AndroidManifest.xml index 8f0aa69e8c..cd2b51513c 100644 --- a/demos/cast/src/main/AndroidManifest.xml +++ b/demos/cast/src/main/AndroidManifest.xml @@ -34,7 +34,6 @@ - diff --git a/demos/cast/src/main/res/values/strings.xml b/demos/cast/src/main/res/values/strings.xml index 503892da27..766e8972d9 100644 --- a/demos/cast/src/main/res/values/strings.xml +++ b/demos/cast/src/main/res/values/strings.xml @@ -16,9 +16,9 @@ - ExoCast Demo + Exo Cast Demo - ExoCast + Cast DRM scheme not supported by this device. diff --git a/demos/ima/build.gradle b/demos/ima/build.gradle index d93d826b67..c32228de28 100644 --- a/demos/ima/build.gradle +++ b/demos/ima/build.gradle @@ -17,22 +17,30 @@ apply plugin: 'com.android.application' android { compileSdkVersion project.ext.compileSdkVersion buildToolsVersion project.ext.buildToolsVersion + defaultConfig { - applicationId "com.google.android.exoplayer2.imademo" minSdkVersion 16 targetSdkVersion project.ext.targetSdkVersion - versionCode 1 - versionName "1.0" } + buildTypes { release { - minifyEnabled false + shrinkResources true + minifyEnabled true + proguardFiles getDefaultProguardFile('proguard-android.txt') } + debug { + jniDebuggable = true + } + } + + lintOptions { + // The demo app does not have translations. + disable 'MissingTranslation' } } dependencies { - compile 'com.android.support:appcompat-v7:' + supportLibraryVersion compile project(modulePrefix + 'library-core') compile project(modulePrefix + 'library-ui') compile project(modulePrefix + 'extension-ima') diff --git a/demos/ima/src/main/AndroidManifest.xml b/demos/ima/src/main/AndroidManifest.xml index d6dfe4571e..5c6db02417 100644 --- a/demos/ima/src/main/AndroidManifest.xml +++ b/demos/ima/src/main/AndroidManifest.xml @@ -1,21 +1,39 @@ - + + + + + + + + + - + diff --git a/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/DemoPlayer.java b/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/DemoPlayer.java index d127304437..67d96d7f68 100644 --- a/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/DemoPlayer.java +++ b/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/DemoPlayer.java @@ -17,14 +17,15 @@ package com.google.android.exoplayer2.imademo; import android.content.Context; import android.net.Uri; +import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.ext.ima.ImaAdsLoader; -import com.google.android.exoplayer2.ext.ima.ImaAdsMediaSource; import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; import com.google.android.exoplayer2.extractor.ExtractorsFactory; import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelection; @@ -37,61 +38,69 @@ import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; import com.google.android.exoplayer2.util.Util; /** - * This class deals with ExoPlayer, the IMA plugin, and all video playback. + * Manages the {@link ExoPlayer}, the IMA plugin and all video playback. */ -class DemoPlayer { +/* package */ final class DemoPlayer { - private final ImaAdsLoader mAdsLoader; - private SimpleExoPlayer mPlayer; - private long mContentPosition; + private final ImaAdsLoader adsLoader; - DemoPlayer(Context context) { - String adTag = context.getString(R.string.ad_tag_url); - mAdsLoader = new ImaAdsLoader(context, Uri.parse(adTag)); + private SimpleExoPlayer player; + private long contentPosition; + + public DemoPlayer(Context context) { + String adTag = context.getString(R.string.ad_tag_url); + adsLoader = new ImaAdsLoader(context, Uri.parse(adTag)); + } + + public void init(Context context, SimpleExoPlayerView simpleExoPlayerView) { + // Create a default track selector. + BandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(); + TrackSelection.Factory videoTrackSelectionFactory = + new AdaptiveTrackSelection.Factory(bandwidthMeter); + TrackSelector trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory); + + // Create a player instance. + player = ExoPlayerFactory.newSimpleInstance(context, trackSelector); + + // Bind the player to the view. + simpleExoPlayerView.setPlayer(player); + + // Produces DataSource instances through which media data is loaded. + DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(context, + Util.getUserAgent(context, context.getString(R.string.application_name))); + + // Produces Extractor instances for parsing the content media (i.e. not the ad). + ExtractorsFactory extractorsFactory = new DefaultExtractorsFactory(); + + // This is the MediaSource representing the content media (i.e. not the ad). + String contentUrl = context.getString(R.string.content_url); + MediaSource contentMediaSource = new ExtractorMediaSource( + Uri.parse(contentUrl), dataSourceFactory, extractorsFactory, null, null); + + // Compose the content media source into a new AdsMediaSource with both ads and content. + MediaSource mediaSourceWithAds = new AdsMediaSource(contentMediaSource, dataSourceFactory, + adsLoader, simpleExoPlayerView.getOverlayFrameLayout()); + + // Prepare the player with the source. + player.seekTo(contentPosition); + player.prepare(mediaSourceWithAds); + player.setPlayWhenReady(true); + } + + public void reset() { + if (player != null) { + contentPosition = player.getContentPosition(); + player.release(); + player = null; } + } - void init(Context context, SimpleExoPlayerView simpleExoPlayerView) { - // Create a default track selector. - BandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(); - TrackSelection.Factory videoTrackSelectionFactory = - new AdaptiveTrackSelection.Factory(bandwidthMeter); - TrackSelector trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory); - - // Create a simple ExoPlayer instance. - mPlayer = ExoPlayerFactory.newSimpleInstance(context, trackSelector); - - // Bind the player to the view. - simpleExoPlayerView.setPlayer(mPlayer); - - // Produces DataSource instances through which media data is loaded. - DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(context, - Util.getUserAgent(context, context.getString(R.string.app_name))); - // Produces Extractor instances for parsing the media data. - ExtractorsFactory extractorsFactory = new DefaultExtractorsFactory(); - // This is the MediaSource representing the non-ad, content media to be played. - String contentUrl = context.getString(R.string.content_url); - MediaSource contentMediaSource = new ExtractorMediaSource( - Uri.parse(contentUrl), dataSourceFactory, extractorsFactory, null, null); - // Compose the content media source into a new ImaAdMediaSource with both ads and content. - MediaSource mediaSourceWithAds = new ImaAdsMediaSource( - contentMediaSource, - dataSourceFactory, - mAdsLoader, - simpleExoPlayerView.getOverlayFrameLayout()); - // Prepare the player with the source. - mPlayer.seekTo(mContentPosition); - mPlayer.prepare(mediaSourceWithAds); - mPlayer.setPlayWhenReady(true); + public void release() { + if (player != null) { + player.release(); + player = null; } + adsLoader.release(); + } - void reset() { - if (mPlayer != null) { - mContentPosition = mPlayer.getContentPosition(); - mPlayer.release(); - } - } - - void release() { - mAdsLoader.release(); - } } diff --git a/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/MainActivity.java b/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/MainActivity.java index 6cacdf252f..b1e3a53694 100644 --- a/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/MainActivity.java +++ b/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/MainActivity.java @@ -15,43 +15,44 @@ */ package com.google.android.exoplayer2.imademo; +import android.app.Activity; import android.os.Bundle; -import android.support.v7.app.AppCompatActivity; +import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ui.SimpleExoPlayerView; /** - * Main Activity for the ExoPlayer IMA plugin example. ExoPlayer objects are created by DemoPlayer, - * which this class instantiates. + * Main Activity for the IMA plugin demo. {@link ExoPlayer} objects are created by + * {@link DemoPlayer}, which this class instantiates. */ -public class MainActivity extends AppCompatActivity { +public final class MainActivity extends Activity { - private DemoPlayer mPlayer; - private SimpleExoPlayerView mView; + private SimpleExoPlayerView playerView; + private DemoPlayer player; - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_main); + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.main_activity); + playerView = findViewById(R.id.player_view); + player = new DemoPlayer(this); + } - mView = (SimpleExoPlayerView) findViewById(R.id.simpleExoPlayerView); - mPlayer = new DemoPlayer(this); - } + @Override + public void onResume() { + super.onResume(); + player.init(this, playerView); + } - @Override - public void onResume() { - super.onResume(); - mPlayer.init(this, mView); - } + @Override + public void onPause() { + super.onPause(); + player.reset(); + } - @Override - public void onPause() { - super.onPause(); - mPlayer.reset(); - } + @Override + public void onDestroy() { + player.release(); + super.onDestroy(); + } - @Override - public void onDestroy() { - mPlayer.release(); - super.onDestroy(); - } } diff --git a/demos/ima/src/main/res/layout/activity_main.xml b/demos/ima/src/main/res/layout/activity_main.xml deleted file mode 100644 index 180ab3223f..0000000000 --- a/demos/ima/src/main/res/layout/activity_main.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - diff --git a/demos/cast/src/main/res/values/styles.xml b/demos/ima/src/main/res/layout/main_activity.xml similarity index 71% rename from demos/cast/src/main/res/values/styles.xml rename to demos/ima/src/main/res/layout/main_activity.xml index 1484a68a68..ad5da62f47 100644 --- a/demos/cast/src/main/res/values/styles.xml +++ b/demos/ima/src/main/res/layout/main_activity.xml @@ -13,10 +13,9 @@ See the License for the specific language governing permissions and limitations under the License. --> - - - - - + diff --git a/demos/ima/src/main/res/mipmap-hdpi/ic_launcher.png b/demos/ima/src/main/res/mipmap-hdpi/ic_launcher.png index cde69bcccec65160d92116f20ffce4fce0b5245c..adaa93220eb81c58e5c85874d1cf127a3188e419 100644 GIT binary patch delta 3392 zcmV-G4Zrf*8p0ZoBYzDENkld>%xF$xW+t8d`u>^S&KV-GPLkW5`RC5^;-~lC!ku)^Ohlv?a=C@{LON&3(GBWZ zt}HiPtth81qyN7FauI`bxyAo)XVqghW3>@#CO*5}T%G@AK!5UE*Qr#O)rIvcYX$2N zD~gp?T3VXVDu%>;n)u%#k@po*wywHPX<7dzKyKySpdo7_Ydb5M2TqXDXtYYQ9O__g z6LmH$=e};$8IWr^lkdP<%*rSS#i)oRZjB<9swMKNvn%*q4GeXQHae7ZKiBF8Nd3*d z%nDfmj)<#3~3d{qZws?HL9 z#FetLvV2)qS&}tyU^zW4hEte9%FW#n-)A}<60Dv3Pbn!WeOMMlve>QIhY@p%EP%B zr;w6xn1AyrPKA`8pMU-Q`SZ0|VvIHd;(Glp4aiFJDZnr(!;!a1$%=Mr;7(6Z58@*! zeboU8^D8MSnPbIl1q%TL^9me9h6}%uu_((KpbYG4bmPX2g)Ebrm0fK9-Q(KQ+00|lP&V+k9)YVv& zl$5j?u4WA&H(@<55+}L8u`?i{0c1GwF&T5?90G|`X+c2&UA%bFlO@J(9*~Pz^MuM- z%W;5&f`*Xc@)FF54p~GdCMNE_Rlu&)wU}yKVLjSfkbi$l`f)H_@f%847Y?Q5Uj~~7 zwSRLADc3`ZH&%8#1t3nm1)_@xV1lpb^wsdwd*Bc zD%#$^(sCDPd(qbWhf&-BFUtA)D>5V{*?(n5%gD$`U%7H+Q&yS9V+HHU$;p`@tVh7H z9Z>iiLjl61qTd@*rcCTdLn!DO`F{q~e<-C78AKVweJOva_he)DxI2@lu8&9Ku?{_eZ) zJXrrSQO{n8KV9UxGK;hVbaIS025Gg-0}>iKm~wymkql{RHm!*;o9*7cyDv+(XU`rt zmJT=n`Wy$USEPGs2MBI62$o?#Kz~9RBS%od=1pWQs?xEFg8kXqvuEE$sF6(6@yCS& z^a^Cw8W414ftO8y;`%TbJyNCV{_54M>(p^b`kJ0Ubmd5Xz)pZ5G1Z&a z7Phn_&-+k#x1ki(b0}SS$&1dt=!Jn>t?Uopqmq~ytImpIV`Gmni`yg@34ciU^2;yZ z!6p_VG8;7ngf$LU&Su@rpVvD#`Up$WO5Q^$@2@{e$;vVZ$Vi+L5)z^weDJ}0Svm>G z1%Q?>U*3Q-C5r-@0gC%_2!*t_%#ETwMVzsNGRBUk;xlK=&t#llef8C*EFIYV3n(Zk zs3il+)(TL@qCvF3kB`MMW`E~nz8HvIl`&=%DS3J3R@6D)fB*f1tg?Ut0s9r0p-~0V#i{$$$QBq8h`c6ar)BhE85s{ zxJ7`%9NWLC5W1TASh$E+1?ZZVAZKkY*o zd4YJfs~4T_=uIa(cvG6SaIio7=4&z}CtI~p3=a=K#w=<`F6ynuoyp0`tLz2nlhHrf zw$%*H#;H@MHiCtEs{y%TdCmKfw?R>BKz}no453Xe?7vyfoG^|G z!>W9St>DlsBqZchu&8Xa>J}Uv+*iIWrX`@ShJ0b);|^bM%3HRSl-%5^9+(kQc6K)X z_SD#E20sm|}vA>RJK%!@^l?QD;8{a;pI4eD(<$;%s}4CIU1bMJM&? z)8_%OD1TrEP=9cyZ`&$vAY^kt0XuNUB-^dVSt%vw#-9wy8{++3y9=;&$^Y0K$H%z~7&Y zCE7j9=dfbLY2wJB0_z#sL3=x^4F8}ljnP7;j|b|-}n27M*Mynd2Xri{jX7B%nj6U4=KEZpuKzd zZot^VfPbZvk~ff?&FqG; zHjfP|UZ$eA-+tQ*W0&!#LJX3cJ$v@;oUsuC}=pQgSxC_ottJ^5%z#Dl4)-pnqY@F4LmJ36x!+4#0M`9_-%fgAYC! zjWPf6#~8lH$fffy^s%+i&Y zyip7B$Q49~4(&zj04XmMr-UW1K&K{+%pcWfC_P#Yx|( zQ>T0R+M3@d4qLynh`KO%W;iPW^EU!o`IP7ot5pJf6karc9aQ z&eGjV;>M)z;))uM9Xqx!@6&RK$n4aAH7Sp#sUS^TA5QxtGf8ps?=^^1baZqs?-mAP zOc)zWXA>Z3k4G)*p@J9kFKtvNPL z;=}E1u@kPAkYqq+!L4%Mc;k(!=zE6_9qM2#EW0F^Zr!@oR9*jPt?|=4#qk`n=>4|Tl1wR^j{=k6)p_!SPq;k1ZCxSuS z2I7Q1?b@|#C-<=}`YQSiI1Wkr?%liBh4lm6LfpS`O^ixwVy|3H^6T|C3W!uTQ4`hdQOKDn`+pQJUlYSpSCW(w95Q>RWH6&M(}J}N3I36E2RRUyfJog$Kv z|A#-+Qh$cL3d)d3sEeNh6MyPP8Tp~%R{INnDEqfXQ@iqWGuDHw$NAfR!Ozcc($b|%zX}Zv-NcW1kML3McD}y@!aY92|B%O-0rwvu WdZo}W<(#De0000I89 zGgm8TA>_)DlEu&W;s3#ZUNiH4&CF{a%siTjzG;eOzQB6{003qKeT?}z_5U*{{kgZ; zdV@U&tqa-&4FGisjMN8o=P}$t-`oTM2oeB5d9mHPgTYJx4jup)+5a;Tke$m708DocFzDL>U$$}s6FGiy_I1?O zHXq`q884|^O4Q*%V#vwxqCz-#8i`Gu)2LeB0{%%VKunOF%9~JcFB9MM>N00M`E~;o zBU%)O5u-D6NF~OQV7TV#JAN;=Lylgxy0kncoQpGq<<_gxw`FC=C-cV#$L|(47Hatl ztq3Jngq00x#}HGW@_tj{&A?lwOwrVX4@d66vLVyj1H@i}VD2YXd)n03?U5?cKtFz4 zW#@+MLeDVP>fY0F2IzT;r5*MAJ2}P8Z{g3utX0<+ZdAC)Tvm-4uN!I7|BTw&G%RQn zR+A5VFx(}r<1q9^N40XzP=Jp?i=jlS7}T~tB4CsWx!XbiHSm zLu}yar%t>-3jlutK=wdZhES->*1X({YI;DN?6R=C*{1U6%wG`0>^?u}h0hhqns|SeTmV=s;Gxx5F9DtK>{>{f-`SpJ`dO26Ujk?^%ucsuCPe zIUk1(@I3D^7{@jmXO2@<84|}`tDjB}?S#k$ik;jC))BH8>8mQWmZ zF#V|$gW|Xc_wmmkoI-b5;4AWxkA>>0t4&&-eC-J_iP(tLT~c6*(ZnSFlhw%}0IbiJ ztgnrZwP{RBd(6Ds`dM~k;rNFgkbU&Yo$KR#q&%Kno^YXF5ONJwGwZ*wEr4wYkGiXs z$&?qX!H5sV*m%5t@3_>ijaS5hp#^Pu>N_9Q?2grdNp({IZnt|P9Xyh);q|BuoqeUJ zfk(AGX4odIVADHEmozF|I{9j>Vj^jCU}K)r>^%9#E#Y6B0i#f^iYsNA!b|kVS$*zE zx7+P?0{oudeZ2(ke=YEjn#+_cdu_``g9R95qet28SG>}@Me!D6&}un*e#CyvlURrg8d;i$&-0B?4{eYEgzwotp*DOQ_<=Ai21Kzb0u zegCN%3bdwxj!ZTLvBvexHmpTw{Z3GRGtvkwEoKB1?!#+6h1i2JR%4>vOkPN_6`J}N zk}zeyY3dPV+IAyn;zRtFH5e$Mx}V(|k+Ey#=nMg-4F#%h(*nDZDK=k1snlh~Pd3dA zV!$BoX_JfEGw^R6Q2kpdKD_e0m*NX?M5;)C zb3x+v?J1d#jRGr=*?(7Habkk1F_#72_iT7{IQFl<;hkqK83fA8Q8@(oS?WYuQd4z^ z)7eB?N01v=oS47`bBcBnKvI&)yS8`W8qHi(h2na?c6%t4mU(}H(n4MO zHIpFdsWql()UNTE8b=|ZzY*>$Z@O5m9QCnhOiM%)+P0S06prr6!VET%*HTeL4iu~!y$pN!mOo5t@1 z?$$q-!uP(+O-%7<+Zn5i=)2OftC+wOV;zAU8b`M5f))CrM6xu94e2s78i&zck@}%= zZq2l!$N8~@63!^|`{<=A&*fg;XN*7CndL&;zE(y+GZVs-IkK~}+5F`?ergDp=9x1w z0hkii!N(o!iiQr`k`^P2LvljczPcM`%7~2n#|K7nJq_e0Ew;UsXV_~3)<;L?K9$&D zUzgUOr{C6VLl{Aon}zp`+fH3>$*~swkjCw|e>_31G<=U0@B*~hIE)|WSb_MaE41Prxp-2eEg!gcon$fN6Ctl7A_lV8^@B9B+G~0=IYgc%VsprfC`e zoBn&O3O)3MraW#z{h3bWm;*HPbp*h+I*DoB%Y~(Fqp9+x;c>K2+niydO5&@E?SoiX_zf+cI09%%m$y=YMA~rg!xP*>k zmYxKS-|3r*n0J4y`Nt1eO@oyT0Xvj*E3ssVNZAqQnj-Uq{N_&3e45Gg5pna+r~Z6^ z>4PJ7r(gO~D0TctJQyMVyMIwmzw3rbM!};>C@8JA<&6j3+Y9zHUw?tT_-uNh^u@np zM?4qmcc4MZjY1mWLK!>1>7uZ*%Pe%=DV|skj)@OLYvwGXuYBoZvbB{@l}cHK!~UHm z4jV&m&uQAOLsZUYxORkW4|>9t3L@*ieU&b0$sAMH&tKidc%;nb4Z=)D7H<-`#%$^# zi`>amtzJ^^#zB2e%o*wF!gZBqML9>Hq9jqsl-|a}yD&JKsX{Op$7)_=CiZvqj;xN& zqb@L;#4xW$+icPN?@MB|{I!>6U(h!Wxa}14Z0S&y|A5$zbH(DXuE?~WrqNv^;x}vI z0PWfSUuL7Yy``H~*?|%z zT~ZWYq}{X;q*u-}CT;zc_NM|2MKT8)cMy|d>?i^^k)O*}hbEcCrU5Bk{Tjf1>$Q=@ zJ9=R}%vW$~GFV_PuXqE4!6AIuC?Tn~Z=m#Kbj3bUfpb82bxsJ=?2wL>EGp=wsj zAPVwM=CffcycEF; z@kPngVDwPM>T-Bj4##H9VONhbq%=SG;$AjQlV^HOH7!_vZk=}TMt*8qFI}bI=K9g$fgD9$! zO%cK1_+Wbk0Ph}E$BR2}4wO<_b0{qtIA1ll>s*2^!7d2e`Y>$!z54Z4FmZ*vyO}EP z@p&MG_C_?XiKBaP#_XrmRYszF;Hyz#2xqG%yr991pez^qN!~gT_Jc=PPCq^8V(Y9K zz33S+Mzi#$R}ncqe!oJ3>{gacj44kx(SOuC%^9~vT}%7itrC3b;ZPfX;R`D2AlGgN zw$o4-F77!eWU0$?^MhG9zxO@&zDcF;@w2beXEa3SL^htWYY{5k?ywyq7u&)~Nys;@ z8ZNIzUw$#ci&^bZ9mp@A;7y^*XpdWlzy%auO1hU=UfNvfHtiPM@+99# z!uo2`>!*MzphecTjN4x6H)xLeeDVEO#@1oDp`*QsBvmky=JpY@fC0$yIexO%f>c-O zAzUA{ch#N&l;RClb~;`@dqeLPh?e-Mr)T-*?Sr{32|n(}m>4}4c3_H3*U&Yj)grth z{%F0z7YPyjux9hfqa+J|`Y%4gwrZ_TZCQq~0wUR8}9@Jj4lh( z#~%AcbKZ++&f1e^G8LPQ)*Yy?lp5^z4pDTI@b^hlv06?GC%{ZywJcy}3U@zS3|M{M zGPp|cq4Zu~9o_cEZiiNyU*tc73=#Mf>7uzue|6Qo_e!U;oJ)Z$DP~(hOcRy&hR{`J zP7cNIgc)F%E2?p%{%&sxXGDb0yF#zac5fr2x>b)NZz8prv~HBhw^q=R$nZ~@&zdBi z)cEDu+cc1?-;ZLm?^x5Ov#XRhw9{zr;Q#0*wglhWD={Pn$Qm$;z?Vx)_f>igNB!id zmTlMmkp@8kP212#@jq=m%g4ZEl$*a_T;5nHrbt-6D0@eqFP7u+P`;X_Qk68bzwA0h zf{EW5xAV5fD)il-cV&zFmPG|KV4^Z{YJe-g^>uL2l7Ep|NeA2#;k$yerpffdlXY<2 znDODl8(v(24^8Cs3wr(UajK*lY*9yAqcS>92eF#8&Yxa2Dcw(Xv69J_N zk;D>XMA4`aM3i10k4LkBNK-;@A|OZ;#K7a*d%yYSG4Jup%tK1DbI$+FD>GmD&As=# z-?RrF=*NW+GKk5>gy{bd{J$)$!-GM#xR$V=ZlB*AFlGtZIU5uI4+V_?jR8H!G=}{) z)S5DXEnw(TH~8&w&`i)~kRK=sR0yi=?Cfj--DASfwd}tnw(Tcu-^UHglw^$q0gSEC z4dC;Wpw*yrplawiL20#GN#ggzGC;ws%qI=p*LI*=jE&&?bkGl=+Xhgy9c*DAwQT7$ zke2<|A=tiC2n@?+bxb#Kzrh2}Y6PDhK+)KG0hA5_3DQIHR67h{VVw@f+SK0x*oJ)` z4+;>1F+A$MpiWkY5EQmyykYzL1CE{G^M62h8JNyK0AmUitrM0uY?HCJ_9+}#KMYVp z1QyfYhfs`)Zv%^aq1eVgg(QG88B~G|VU5!EHyndF#e*ujckkYdeFBLOeC_S+v(StM zaL7QEplxk;?%er%uLf_PK2*8@om>!v$v_t0Mp%)ChK9wxVo7{~U^(xIfrE|d2M}f< zp|wN%Nli`7ocjuiH%ahgj5%$V;MCu#A=hpukh^UyeFmo$>dLN+C-u$M79l}D+KP*d z|9oHEO_1Z*W3Xc}$0Qs)LUBL)k#CZhkmSNZ^2;y3^g0}@BO(7Z@k&q-Rqhem21}4y zT3SjoGcz9*_OVBRpxh8K0T~;6H8+KPleB^yNLfiLYm0i--LUM6+5+N}w1jxaFQ9c> zIw*V}>gwvkp=*Pz2E>~mRQR#j(Fz+}RaHd-61}Mv1!cI9*1N41_d(&27mEMgtZPBp z0qIWEdi*sWv~H0Hq#az1l$DkJ*D6=zCwq7A-W>;UTKU{UR6J;HB{|o#$ak85QAinO zs%~bF-?4#Bcj`&Wt!$E25l2#r&XD+gKdR)SK=@5f|7(P8a9d+#q?g7JuS6yJR=tYW z3GEe~C*fez+}zxno}T`DVV@-df}?R-YOaGv@b>N7B9`6MhOX?ZGIm$hdB zu%8I{%9SgxTZ~1#i9viA<9U^r$-b2365vR)9&>>9B*@8L2;4tcUNSq~Fc++0jur+Cx}WstFViF^CqD+; z-jwQIH1}z&ft=@``cQOm78Ad;jU?deb_!68^%w)>1JF;WZzaB|8;k-%9ZXqG+ahs_ zL){E!`qf@uUZaFe^hPg;KQsCB%2G$H$ZPwJfZ;4AxiEm#H`L?#7*bY~M-E?FF98k* z==+On=)PD6mX%m=$|xXIc(xCXg;H}O9L-cJl_RoTP&2W=s zMf`A|o11%DFAfQAF&PYzJV6Q|I+v*{2kUvyAn{G3i#8MlQ6*#Ddc#I`<$2Z_0WQ5GpAzQ1pm~ea1jkSy@>)Y0{+O zxS7|CijZ{FOM zF!F%H!^6h`phhWx>Kksuu)V@85HVoPxt8(F*)kkY%{<797ST3J%&42Zy}c)O0~8t> zIuQW1ik+aMZx`IiG-)xGfJlQQ-Fgtv9*vCT-^dUfhdLRcRsb}m8=&Ce;7L*dp>JO) zQb__~9?X4&!vLYu3S-5_Asrx3PtTXS0XlKw!~`g)Nvw3oSmIVK|!K}H0BsFS-!+evp}TYrP>p3sQG&GL}}PM zUMY}*NlrYBN=DpK>UnyK%KSlWKBNoM>({RzCmh8npb;ZR42Os>dYH#b!%`2CttS=a zQ$IP`;wK}Y!TPh~OeZ*f{v+rl=#-3XJtZgGPJ{gACzo&~2-XpxNKUSiaxJpO6A5GV>618&CCo;u5MPI|0DX^Pmt;&M4Y>fIvI1WF1$KT~SI- z(Mqx#6{93>u?n(Vr66t~cPen5I9RK3Ei>v`?j~HzjcP6l&kzp?N4vDNw4acL-YE|@ zF&hH&kgZ}Ts}xYyp{~FRal;j?K;J4ji*ThD!2}N)W^w&>o08 z2m)h|m{H3^PXH+MfY=z+fk|a#WTXq5YIK{d+D1e~IEuYR*AS2nQiMJrSDm|XfObbI zsKxMrcE@rSqYnt-$SELC3I_pLhT~}fM=T(;99$Y38_E9t`xhY#!_yt;Yc@-lE*%RL zE5(dtJRp8J<{|AtNRiBX5D;1rxYjNTNTCC?J4Qj_@PK%ia*vZ!KpyB;YPnHBmf=VS zL<4kLSy|PbIddkm*}VQE4~*EuRaI5z#l#^)KtkcwPK1GQTy%gi?#Oj6wkt*bp}q@{(gY+WagFMV zL9Pf#0En|5Ilz(Y0YW&O70J5*SqaBo<0uLcgcU8GO+0n#)ThV*K-n365(idxix)5c zV{2<`jU_kJ2V`6b34!Rt;f8HPIBqH#6>mL;?qv-eF@SjYs;H=_ef#aV@y04UlTQ@+ z`}+@p)nobj`4-PCa>M+0W&u%18h{eR3JB;X6NEg=1$=200}0Lri75(Vp+mRB?CY*21#bpdJs%c;JC-nF$)ND zL$sc{x;nCT>(&L>ccbw~xNO+40iV%&sd zz!3+C_U-cJ%L&luQLOLg7e;WnkB`qnJRxt&is)1W0GXOu8=Y+v_{X5cAEW<^?Kb1|uax*#z?ah%-a z=21X6ukwI7ln{=Gm2liBpzgDIe&m8M(j=3~W@2BRoSdZHrwBVB(Wioff}HR!EP&Ku zc)~0tCmcGg5D!LgsOBuD3l4M~Cz@zE43If6V&J&NJCbB*qws_odIa_bFC85@a>Nz; zxN+mghpf5Lb%xXs=36tU8>eFGdh|=h#l?k&k33=anR6|N1jqT2 zW6`_F(I^+m@{JVAnG^o5lXKVaCbiQ*E+klWjJ8d9dmgqO!$nqBR?(kBW^&`k4N_QGNFc!+5W==#n-C6vMWcgF*^7#b znqjse$3C&X^?X^jY?(c*o^f_|UUlo%Ev*m|?`~+e7z_u3ur0zX89W@APG}(^TnBv_ z!}@gJUQ#efp-?;m>v3LQUK^^btF`PV&-VU!vPa6DC+Jo@95}!mu@8=pj*s3?IQ(KW zW5x_Dcml+x56jET8`(^FKtkdJGR7QmtEMemwxH!qm_B_vo{;ag2YqeceDh6w^TGJ# z%a_ZpU%y_&vTdz3_cZn*94)p9-7O;{qiEs6g-UEQYkRLh1#L5H)+{^QdOI*x1+@XyY_&D{FI~Jt98nt+(F7r-?^{CLcb0*tw*nqydju ze}EE#!8Slj(s1CwfnCrxe3*AMYipmsHD=J%sZ)oI9Xl3pdYm|O=FC~q(a|9_H8peu zVW2vC)AjgQSFlkPuZrSTiBJaz2Yi5cBDM|N*dK6&i|w>&)6ln{1-$@i`v-}MiSann zVSHkX?u`;Xu`Jw|m4Q&Syv1N$SSQrI8ry(vVQm^PFFT>uG=BVed>hLI(3ExS)-4YU z3-gDhtqL!v@K(iMUC|+Y#|iwWWgXW^@EhG0_u==)vYMKjFd?kMI@YXNgQqL-mX!(E zhJj!;rk264yz+`Yb2|j}0xUCqe0;X4)#^ydax3uc9cH-v1k%!i!!&N&($YeoLn|mK zsDOD?1eS?qGmDvkbzY5r`3xBYy}_Nkl;KEFc)3NPzrep1o z{^^JjiG{v$$j^J-@V^i?tSUk&(8}0g3tz_g@4dr3GMA|>DhkI>cYYM zqXoP9ZYWwPS~xShAL^!UF!l9Jf#d%XXrH1Tuv)EuSuB=XG&SDq$mpIvqwlBR0|Y7A*fZy;540h!#t0eldcmaRP1SIL;!)sI3#B8tRkx z3AP$RfdW53aDV?Uf-V*SCqQP)qYL_IqG$2{Vj9EM*nt4Q5`lAq!8sp5yEN99g-MI< zSNcst_#k$;kDqlmYt}%4caor)|KDUX9q^fiFdATIN(P?SFFZ+mycibf?d?5=5Q6$d zLg+l$*?s~q?9@I`a}k&d7NRWzt|1>RoH>w0?2~0M7=I25NCZhC5X@%&3x4KBPHNPF z>1+&`&djizxo`z=nzDeB!0|0+vl( z9i08o1b@=8`b2=YOY?whyoAqienlse65;|}g=H?K#52ObgK_q21hE$(&54n>SaC5F4jM>qSMDnA- zSiB2NnpU4k&?=xWKz)7v0*6%)hNhmLp7Wlx?|+WKRoNWK9HnruV!7P@eedZRQ1?v? zoOv@29?VIAt`mO%XEgfc3u?9cPk}E4*%O3DqZyBl415TaO2Cjl6Ea6DL&|ao-aGlE=BJ3b!Pj z&6$ABVq?Ite;;rVt;-dN-tl&Jk)43*7{6f-%Q9EF9v=pTmg4J zinRl5nYciQCNF+S*#Tef##|b^>K(Wv^kPc+gh@9mk^KhC&Wmqg~H@ z<+NByfZds4?;e)~+S}XJDJdyob^@iPrDHLHX{8b{X==c zQ=^Cg`l~J@BV(eSKw)9w_`VRRePDofE0my|yuc%Y2Qvdep!q+S1X5E|Bkct8^72Mu z5>rbha97<0@{}q_IB*NLCj0^?!xLQ+DF46@3C*4boktG4ln{n|&FJjq8$=lV29ra|3bH zpz@W#(#_}KT5mZr8v5c?3y_FL}0)GpBDsl@} z8MzQL!V)|Z=-9diBTbXkeSsBy6)s%3u$pD!$E8GCTG|xM1?^IbIU#|$drKf=bi4>W z0Z-aj0t>${fc+n?7ZK2}SO&c}Z}w?>NyQvWy<^9Y9q+LO94suShhjxqAf=pk_JN;g zY!wmsA!esTo7G=sL0Z%jP=6=Ph3>pux8;Jja=NCbrVz2d=&%*w{h+9*XgT+&y!9v} zX99{t<**II>Cv|qqdzQxElGPI?$-*3Ht}h<;i#$!O!{_Tc4St>XvJ~|va_>EKEV## zN`8Abb?VeNaEb1E+fq6c@MhaC-h)Q1(eEuKMxWZKsHiZP8+*p+`G10fg7tJ3W7|o) zSp}&CR%X>g>AiNpj~oTFzmYJyJRcG;3cY{-eyJy4@J3*s@($!xwt~sb1?*S_wSNn- zy-om6oH#MS)2ZCRUAuP4aD(5=juk~xs@1qA5SLm72UX3W>oNu8U<;MNQRLBTwY{4+ zZ+k2&--gJ!l7VXNbC@h=If4P{l7iXKb&Lh6J1qCO?X z`9Q1&8)OjLNVEwGh2j$|$2VXxn2s_irkVw}eJz-P+I7h2zNx9H7Ufq=ZKy3hgU?Q6 zeGH;LH*MPVOnx&#WQH*y_y$NYp}9s2#l3(SVF&qA2KL+F`6==^`kyys$3kdoH`0000004R>004l5008;`004mK z004C`008P>0026e000+ooVrmw00006VoOIv00000008+zyMF)x010qNS#tmY3ljhU z3ljkVnw%H_00*E+L_t(&-tAdkY!ufO{?475^{#iq-c;^Dmy#{tlF zn2UDq+?oI&B7XskqVt14KD48|Z|2gspW4!NjY-f0yVrcCZHvtAf9?KnMZp(|!29oH z21uZ${mz;f0I1iHfV!f0?jb3Sns?v0Fc8I9Q3%Mx2i|z^lXDZt;&r^?splVDAE6(G ze9B_k_1t~y<7uqB@b`&Ve|zw~(*U>@6A1>4^+}vU}5!#{kHC?rGQ7Y+rpxY>wQfCgoNV z##{y$v463{hy((3%FgE0mkO8pmxuQL_0*X&X9@tszTe&dY^o8DoH+W)6TkU!{|f-d zucik8K#j-at$U7ac&)yrw(G(v+1D^%X-_oKexc~|z&2o8`~(1inTzdgD5K*U-Ze2| z47GOCTVo$ho!tAtu}uIn018np8&wEEqPutLU4PpTbRPo<4CzExH39p7vh;rX*vY=L z?+*0?n2HKYsfNJv8hbJGL1hvZ>p(|GM{LcbZNFD*c)4xWqGdTKXsRIOeX_>WQs{Z` z@x`woLeZ>5K-qqvXHR@C^}2wEf_H+uvOKW+ChB;@_g>z#6BVMQTqEszc57GTXT=d8 zL1ChYk%#g27yiTmWxn;r8 zQ^yC#!p1Gx+V$jfPz7dN?& zCr=};q>z_WI5qkzE{vZH?WxAmmVejkpF4l@<4IpzfWBz!O0KH};#~Fw&Sg&&trVyo zP$TR&IeiSrhks!!mK_HGZtqI>y$u}pS|TWfltiO?UnyXmA1@e=2;RHZa4Cj@cGjC@ zv)B@3@E`(OGDTks8_@YLbm9Q;-jPDVhRB?`T5lu*D^_&WYH_M%aAd$v;D2G@1Iu2S z(D7D#rADNeE^SFvB0#O3^{I+TL%@ewx!TYPR0mMEqM;U^<<#ercuI}svV~zok`QFL zlqDjDm@E@z_FOmem(+`3@1;Dc^JK9vWG9aTVR))dSR>%&?)-GA1XUaMCFz8VkYb)BPL zi72XxP8O`x_T&K-mrdI%c0L3FF9Y5ptLw9s3t2#QFHb@-Yl4)e&>=fXsTBJprL&@y zjrSn%Y!B`cUkK>|`;AH>RBN+kZ86NHwX}lR;OSF=2lT?M^v&&k@wYz>U7FQ(-{Cn( zoQEA!7>Z(V7=PIHZG-n8OxZV982FhPIq{cc=P!FP_--Y}$7e|z+CLU{roXIe!oPnl zBV=vZ?1hKPT=&@#I#I3e)XGijldu15;2R&jf9Wg$-94vY6ae*_hMN~IYi?2$<%(A- z!zf(w1*u^eY=4iO{clFs@ezP509P1=rK|u@gKPUI)qkkMF#p_?_ku4+AZq*dBymIj zFXewDA7BDFxU!7^001R)MObuXVRU6WV{&C-bY%cCFflYOFgYzSHB>P$IyE;sH8?FW zH##sdyvITA0000bbVXQnWMOn=I&E)cX=ZrgFgH3d VFwL6zQVak9002ovPDHLkV1j2s3A_LR diff --git a/demos/ima/src/main/res/mipmap-mdpi/ic_launcher_round.png b/demos/ima/src/main/res/mipmap-mdpi/ic_launcher_round.png deleted file mode 100644 index efc028a636dd690a51db5a525cf781a5a7daba68..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2555 zcmVDi>vW`@Y|P=j^x3Ifn%y?#weBmhZgZ z^Srn3`_5s_nkW1KfDd9V!jFD>F_Mc=&(D`S9F8`G9j`|SbWPvU-)IaU`}$WdghKD(z^U%DuFl=dhBq1 zV2N08FaBOdb12Qd668Nb;&Z~}bITyD2yV;4Q;V)Yd}0yejcD*w$?M!}^D9N(BLyEz zzdw5PC}r6q#BPAbGB|lDe_=J@3Wft_XJ;=W1)n8}5Q_(meMaO(qlBrMNwAM~()TMt z7``0qU^YGKgUvTFF>zWD;p2?}U+(!oOP=>E(#D=LI9;^|21mP}Sb%-B3r<$-f`)GE zf+ENH9giPBhLMqxk3?>Z_Ib>|pGpO*ls1Edc1SPZ4+Zs6n5(m@o)w`qhVIR+3x!nc z2QWA^sF+UVL`bPYG*m}z-@eUAx}Y&)U4(ZX!1ID&B)9UZ-m)SmI=x*&DX z(4U0VQSCNkV`Ff+G6~M!-Uofd_rTVE5zbccg%jm(Lo!1!!}0Rp$Ve*N38}aK2$p*n zpm(?p)9??FQ;`7UThq+UOtDt(yU340PTgTf-cvxbAYdW+ zodS8MfJB=CGHd^~s0fLZ-EJ=tYQaZdAO;5qU&BEYQVUZvM7db#>3OfcuPlI&kC9O8 zXc8ynO6$TzSy@?tytqki3G?eco<8$hd0*Xm)s6T`#OF=Nz|?XUQmTHh=zTGLKE-+| z`R_lmJHKZj zYHDgW;R5zROF(6Nf!D;<$-4^>$-4vuLPcAirU0zhk=)$eH)H`8i{&*f0hE))jVY>R zmqT9B`&@vr{-k0Zhyu=?I~O1eC@L!YJ}zQ*H377xy<8iOlOj14B;uwl(JEnwjAJr_ zIFPu-00|bojChNVBak8YiwHKSngDD7gUQLsn`8k84<3AZYHCWgh-vZ4u!X_jGYxR) zq8|Q1$V6o6;p0n)Y&{&#F~E^rJsc(EAuj77G#^obxT1%!D>?`(A_PMCRVU~=tY|yO zHVEaoPJAc#i9+(48VAl77nID%R4M5zcJ#F_)$kX3y|RRI0$?(VKa z&d-Y*IbZCp=~@DEYr|PSAG7R$NTWpBz(_|H8#rMDBOQAaVG81;4G>?7DO1YR#;Tn6 zgm{iiHR=MWHX0flE+A(=#+`2^eCq4#-GFC! z6M$q(^=<;x$j4i^s|lc;#5~q2T)%#OKVOMmTZ!}M&%cE?jVW#BSPIpK3EjjgBC41R zU=h$eBj6^$nKJQasbF=Bl6MMNSOesJ+RS09kH^Hs{G2bqzT$RzJ?=lyi2lg=rilsXN0U$-dvIO{gZQWn5CwY0QYkn1i@vBQ*i6ms==x^iJG#36RN40+4*XRgHY0OkPO<9mtU5JZ^U&KR=(+$Jgyx zDIL$YY}xWX3{k7+k&+4cB2-?0JVEIZU7}-f3eXAOclCI0$TI=e3k0wuC3c^-&6_uG zR6N*oMPDbVp?Du@1oKFGD6fK=08A@$~dMVygPvL8+hkiK{R{*ed% zA|nNnV>ylomVT*i&f`G~^78Uxh|{8v7Nyn{92`s``gUbyWd@x=@k0-m99ZD=a0z;Q zdshWyo93XoXijn<_WCU1LY%yQYs2e-LiK8Ob#)<+1PkeEKVFy8hUToOsJMz8en4DQ z^L~*R9P1F9Y&P3P+^sSZR1(zHR^hz>d%;0-P}*QOB+vhlIItCWIUjx_iP%Vah~b^# zk7wprN{B$5*%}@mp2^C}ilsT9h`g9i0RaKeQXb;D;hnp8@77Q>s6z=t97}xdB)!pO z#K{)fY;JC@IdI^>ZkmhcTyolI6*d|p5%eVB&CJZqu#S$7Rthzb2>VEHRu*~1>JY}W zbRkF@9VldW5~{?cGD{E9%= z^d0?;k9mdPNZw)cXxMpfx5T2C&3}PVZZ;p-o4i2PAJ@j z+=Q8)wZ4!Axsm_BXYYOXxj|RrxHs+%BJu*>GCn21kMjaLPsdZ=m^Y{(+<2>%EO28%%wuqUDMu8OH$8%zKu7G4&}qFQk04YA1g*$9*T z-fAmCrB}H$Kq34SU>Gp7@Ek5m5M2v1I8?C_X8FB7VgIvuz5!mq7wBXR;06n?h|Qh^ zpAc-s4G;!&GQPme(+%+E*a@BkO92bdVTL>$4o@VHrSfOt+~Do02C(pgS3oS_4`l^Z zo>0t&81Rby&*~gy9`Es{e^8wD9OKI!Tp)18WVKp2jmH3M;1&be{m1d9 zjuM7eWu?2zR$)RxLBX-u*w`n5Q!~LS@l#V%#hHJEh}<7?G!99^gxuWRqXRF(=;(_=tajB`c+L4k@u+Sx9J@^Gl=@-gH zbtLBlx_R@aA5c{psY<%p+1bzG#bO+-^QBvxg}5s4BkSp3$(EDi>G?o{CL$EY9zA-r z3SiC3!~idWh;ewGB~Da4FkquP1DLI$BwbxbHcOsIw=OV5C|U`vyjeG=4PYlx_v#6Q zW1pr5FuUI*DgFSSOY+EkKo~hWIdtX9mA;@;qG0A=0pN&_+$2GE8k#f#5uZfT(GWp_6rYpFk-=oLzv*V}fW~yFWgb4PcD=P|8`jfTZkf%}Uawq@;u?Q>N4ay5-B4`v4syZWv*J zub3*RVMCPR$CU=e{Td|1QbqL0AaN|E&kCUZuju*rB^tn(-W*7IySeS{`Y?3x;K6=E zTb$e#fsPS33|Bwi=x0n)p8c*g;P&tk)dsAoZ4lKF&m7eTFy@GGD!6pfrR{2T5(xUv z7TPMSp+AJ5)~#FnB5s|~(txB1fh?Gls5gM~L)O^e$(mC3ZEx1__U+qeDpjiVAmA`m zGz4+=bNTY+`Z&M@v@!t6)1THi3UiwQjIpPIa+WV6DKD?g^FT^Uih0qZMJywMn(qh+O?~XvK&=*KE2U;{)V?3 zF$%RZAbUC68A6DLj#Q|19>{a*nYdzQV`uNEueJd~DO z+BhckhB^k?(?U-NTIp$56Fse|8Ae+h7$`L`&@ls;&|u1%GMOZ!(Ww@-NW^E)o?Qfu zeOYgyE0TaQDxw0~?hXUE`L?r*x>>NPu7NK9BZSfdiwXzf`FCZ4u}^QxUB8Y{(m2!| z#USL@z0mkUzI>&K0U|bqhK4?jHfkge(A)s?`~W%>&_k8@+Zq}u$v=oi(ggEIA5i{@ z6Atx-Pzjyy*s;Dj?}UiDUX}Q3Yw9WfbrxTH zGDnRhDLbpw1`r0WBaQy#lTWJse|NOV*5jHrYii@MNnAd;$^Z^G*MA76P0byP`NunQ za&;e>BS)wzMX|q(CdC=xy<^9YhIp{a)dnPv3#47$oU3?lzZC3N@fpkY>!mgz zIXT%papJ^QLTkGQY}vA<84k#Fl>u32w%qA@R(HOfGBDlMZItdC>mzvs$f5t~P-AVCXiQ+4&2H3^>xZ zhuaOvnlN5f5=NURB_blCd9enBhlf|eI^Y(UPOdb7=`s5}ef{ZTTGZq%b?xXH28wyb zO<^b`h_a_m6^k-eW$3M2x1zDusw1@KYlU>U;f0uYtY#JJ5>I+yG`; z_Xt%pVI3B5QQboSkdza!NJ|L_EHmLv@4Q9%`}aHAAr*3pUXW--JPEvUm4ra26F0&D z{1olzhmk1exXXavlTOgWX78}LszcKDMh4p56f>j#o!@P&YjD~GW0-+*mM$SFH`meq z0B*(+zBjNG8b2bmRyN~wVv|5LiZD%nOl%nafdzu)~bNU%tUaz0q4KNMtPkFm`lFe!@^DW7^xH!{i zpMBOlb<%JH2}wK6vtA(iO1-dz)`fadYCE$ z%%6Tt1*cDIa(yT+bR&F|{zIWH^1%H-$A}w7p7him_mC#m{pg?Cg1PNY# z`DHN+p2har68TB2T2w*51dT4W0V8^i7suFbYlMHfoE9{Hr-%W(xt={^I!R`;OGQ`I z|5g!xcJJQ37MeaMv;{gwz;_s7LPb3I@wIE$_7zXUVaU}6^u;1;kE!Rh z(|lYpRaGPn=|7p4*Xbh+po~F3lI7M-w_Ki%|0qLa$BunpXv*>5UJhV53_V!2YSr3^ zT^G17q{T{j;MhYntK(E&g7mY_QN$O^M1@0h+k7a_cpCreQ2H`rl?XAV z68_C4mudT@bh1j?imo{OZRo4fiWMuy3oV7FK&M39DB&>lXy3kl+hI+R#3iW)1~^67 zyo)sVXd-3im5^YyOTH<7etzAAmLGle(S1OtgzprAuMxt}N~cbpnrgkXGW2KzM(l{D z7^B<&mjowY8%|t89-SvNl(qEIsTsmCv2f_!yLTr{rDUE3o@T(OtFO_im`t||uRQzl zzamm`_WxaI*uQ`O`+?3L;>Iq*vCf@4KZ2iC_V3^SD|X7n3wVqH{TE%P4d+s+z^Y8J z?mzoQ<88^3C6k4AvW8wj{lL@_x!_4uL``s2*JWN8xYGdrTx{ALj3;A`R&iea#tXvs zwd&QYw-VZM?Oz;x$1cJ-Oe5i7<(V^QzMqwqMf}BWcNp-~mRocqHAibv)o#2Ku6+1H zXa)@*<+I}L?{wFJX#v?!HTPN=%$$@O+{6|>-FFE&H+fWBnZPd z#&T>M-D}(44PNMMW6jXnw!O9;INP>u1(|%heRnbt14b z%;Lo@rkE*HrZ6tZ?cTk6A5BrbgurH8$W7n8Nx<)a%hSQ~hu;gn{w};1_&F?ie(0fx zUWr5^1Hfwd@Zr|;X&s6kJa}-Lo{P^of zJw)x{Aa-#5_19krHil8cJ{~AWaV@1ohYsv(snrbMVg?%`n*RSiP4T)0OGht#?k~U} zvEl?QV@Ug|tcrm|EgWDaO&Gy75fbYi%YJ9S+CPZ$$ z@y2sCSo==oM$FWPHe2egZQ};S@251qB5eUAlC^2@Uh-&p%9~ z!k37Z{%eEZQZF00a_r^l)2APaZPUW*LBs^)q~fsU%&dzH{T4b zHc%6waF=?K>B_m@V8x0RYpbfNhGG-8brppTd`B_X4eL-*QBkDlDslAGQ%{|)t&h*> z4o#fk90C6nrmiJAp<5tASC#r_wKO{u8?eP}>R>ivYiKD}oLp8`mXn>GU7^?Y>FMeH zZ@&5FOP_e+iFfEJ;Rq29_`#g9$6`D>pbHAIh^{@H?`T@#a&$&_Y`_+5GTYh&xsAS^ z%FD}(tu{o43>h+R@#4id$PsT-%2;cL?7vtbN}^xTks(4RviXdzB|2#p>qf0$O+Z(4 zMt5xR8*==Iq~zS(+$^mS(LmRTqy75z8>>=wk*fKVv=KIvcFy<_qI5#eDR7g3Hn4x6 zmkTlOo`1TP=y2V2*InGFPoMGVhK}fp&gdTQkkf>bv$%t*bB-G4a&tsP!}z`n7cM+e zy9ZWj1JkPj!lH(&8IAzXy0kz1iGDQxl}JO{--GQ+I*w+XB3Z8>6WP+CAG)9ux}hVw zdKuYx0v2&c~z1(GLVH#Wc!ct;U{-SIo<&2pwgMf zfoA&Nga@l36&A8;0Mxk7vAwUcG!^`Y-&!8ICaE@0ri|jx?k-uv5rmFW@bA2pnr1XB_`0cAr~1`(4QCXBG>Tj>W)rr~m)}07*qo IM6N<$f{P}YasU7T literal 4842 zcmZ{oXE5C1x5t0WvTCfdv7&7fy$d2l*k#q|U5FAbL??P!61}%ovaIM)mL!5G(V|6J zAtDH(OY|Du^}l!K&fFLG%sJ2JIp@rG=9y>Ci)Wq~U2RobsvA@Q0MM$dq4lq5{hy#9 zzgp+B{O(-=?1<7r0l>Q?>N6X%s~lmgrmqD6fjj_!c?AF`S0&6U06Z51fWOuNAe#jM z%pSN#J-Mp}`ICpL=qp~?u~Jj$6(~K_%)9}Bn(;pY0&;M00H9x2N23h=CpR7kr8A9X zU%oh4-E@i!Ac}P+&%vOPQ3warO9l!SCN)ixGW54Jsh!`>*aU)#&Mg7;#O_6xd5%I6 zneGSZL3Kn-4B^>#T7pVaIHs3^PY-N^v1!W=%gzfioIWosZ!BN?_M)OOux&6HCyyMf z3ToZ@_h75A33KyC!T)-zYC-bp`@^1n;w3~N+vQ0#4V7!f|JPMlWWJ@+Tg~8>1$GzLlHGuxS)w&NAF*&Y;ef`T^w4HP7GK%6UA8( z{&ALM(%!w2U7WFWwq8v4H3|0cOjdt7$JLh(;U8VcTG;R-vmR7?21nA?@@b+XPgJbD z*Y@v&dTqo5Bcp-dIQQ4@?-m{=7>`LZ{g4jvo$CE&(+7(rp#WShT9&9y>V#ikmXFau03*^{&d(AId0Jg9G;tc7K_{ivzBjqHuJx08cx<8U`z2JjtOK3( zvtuduBHha>D&iu#))5RKXm>(|$m=_;e?7ZveYy=J$3wjL>xPCte-MDcVW<;ng`nf= z9);CVVZjI-&UcSAlhDB{%0v$wPd=w6MBwsVEaV!hw~8G(rs`lw@|#AAHbyA&(I-7Y zFE&1iIGORsaskMqSYfX33U%&17oTszdHPjr&Sx(`IQzoccST*}!cU!ZnJ+~duBM6f z{Lf8PITt%uWZ zTY09Jm5t<2+Un~yC-%DYEP>c-7?=+|reXO4Cd^neCQ{&aP@yODLN8}TQAJ8ogsnkb zM~O>~3&n6d+ee`V_m@$6V`^ltL&?uwt|-afgd7BQ9Kz|g{B@K#qQ#$o4ut`9lQsYfHofccNoqE+`V zQ&UXP{X4=&Z16O_wCk9SFBQPKyu?<&B2zDVhI6%B$12c^SfcRYIIv!s1&r|8;xw5t zF~*-cE@V$vaB;*+91`CiN~1l8w${?~3Uy#c|D{S$I? zb!9y)DbLJ3pZ>!*+j=n@kOLTMr-T2>Hj^I~lml-a26UP1_?#!5S_a&v zeZ86(21wU0)4(h&W0iE*HaDlw+-LngX=}es#X$u*1v9>qR&qUGfADc7yz6$WN`cx9 zzB#!5&F%AK=ed|-eV6kb;R>Atp2Rk=g3lU6(IVEP3!;0YNAmqz=x|-mE&8u5W+zo7 z-QfwS6uzp9K4wC-Te-1~u?zPb{RjjIVoL1bQ=-HK_a_muB>&3I z*{e{sE_sI$CzyK-x>7abBc+uIZf?#e8;K_JtJexgpFEBMq92+Fm0j*DziUMras`o= zTzby8_XjyCYHeE@q&Q_7x?i|V9XY?MnSK;cLV?k>vf?!N87)gFPc9#XB?p)bEWGs$ zH>f$8?U7In{9@vsd%#sY5u!I$)g^%ZyutkNBBJ0eHQeiR5!DlQbYZJ-@09;c?IP7A zx>P=t*xm1rOqr@ec>|ziw@3e$ymK7YSXtafMk30i?>>1lC>LLK1~JV1n6EJUGJT{6 zWP4A(129xkvDP09j<3#1$T6j6$mZaZ@vqUBBM4Pi!H>U8xvy`bkdSNTGVcfkk&y8% z=2nfA@3kEaubZ{1nwTV1gUReza>QX%_d}x&2`jE*6JZN{HZtXSr{{6v6`r47MoA~R zejyMpeYbJ$F4*+?*=Fm7E`S_rUC0v+dHTlj{JnkW-_eRa#9V`9o!8yv_+|lB4*+p1 zUI-t)X$J{RRfSrvh80$OW_Wwp>`4*iBr|oodPt*&A9!SO(x|)UgtVvETLuLZ<-vRp z&zAubgm&J8Pt647V?Qxh;`f6E#Zgx5^2XV($YMV7;Jn2kx6aJn8T>bo?5&;GM4O~| zj>ksV0U}b}wDHW`pgO$L@Hjy2`a)T}s@(0#?y3n zj;yjD76HU&*s!+k5!G4<3{hKah#gBz8HZ6v`bmURyDi(wJ!C7+F%bKnRD4=q{(Fl0 zOp*r}F`6~6HHBtq$afFuXsGAk58!e?O(W$*+3?R|cDO88<$~pg^|GRHN}yml3WkbL zzSH*jmpY=`g#ZX?_XT`>-`INZ#d__BJ)Ho^&ww+h+3>y8Z&T*EI!mtgEqiofJ@5&E z6M6a}b255hCw6SFJ4q(==QN6CUE3GYnfjFNE+x8T(+J!C!?v~Sbh`Sl_0CJ;vvXsP z5oZRiPM-Vz{tK(sJM~GI&VRbBOd0JZmGzqDrr9|?iPT(qD#M*RYb$>gZi*i)xGMD`NbmZt;ky&FR_2+YqpmFb`8b`ry;}D+y&WpUNd%3cfuUsb8 z7)1$Zw?bm@O6J1CY9UMrle_BUM<$pL=YI^DCz~!@p25hE&g62n{j$?UsyYjf#LH~b z_n!l6Z(J9daalVYSlA?%=mfp(!e+Hk%%oh`t%0`F`KR*b-Zb=7SdtDS4`&&S@A)f>bKC7vmRWwT2 zH}k+2Hd7@>jiHwz^GrOeU8Y#h?YK8>a*vJ#s|8-uX_IYp*$9Y=W_Edf%$V4>w;C3h z&>ZDGavV7UA@0QIQV$&?Z_*)vj{Q%z&(IW!b-!MVDGytRb4DJJV)(@WG|MbhwCx!2 z6QJMkl^4ju9ou8Xjb*pv=Hm8DwYsw23wZqQFUI)4wCMjPB6o8yG7@Sn^5%fmaFnfD zSxp8R-L({J{p&cR7)lY+PA9#8Bx87;mB$zXCW8VDh0&g#@Z@lktyArvzgOn&-zerA zVEa9h{EYvWOukwVUGWUB5xr4{nh}a*$v^~OEasKj)~HyP`YqeLUdN~f!r;0dV7uho zX)iSYE&VG67^NbcP5F*SIE@T#=NVjJ1=!Mn!^oeCg1L z?lv_%(ZEe%z*pGM<(UG{eF1T(#PMw}$n0aihzGoJAP^UceQMiBuE8Y`lZ|sF2_h_6 zQw*b*=;2Ey_Flpfgsr4PimZ~8G~R(vU}^Zxmri5)l?N>M_dWyCsjZw<+a zqjmL0l*}PXNGUOh)YxP>;ENiJTd|S^%BARx9D~%7x?F6u4K(Bx0`KK2mianotlX^9 z3z?MW7Coqy^ol0pH)Z3+GwU|Lyuj#7HCrqs#01ZF&KqEg!olHc$O#Wn>Ok_k2`zoD z+LYbxxVMf<(d2OkPIm8Xn>bwFsF6m8@i7PA$sdK~ZA4|ic?k*q2j1YQ>&A zjPO%H@H(h`t+irQqx+e)ll9LGmdvr1zXV;WTi}KCa>K82n90s|K zi`X}C*Vb12p?C-sp5maVDP5{&5$E^k6~BuJ^UxZaM=o+@(LXBWChJUJ|KEckEJTZL zI2K&Nd$U65YoF3_J6+&YU4uKGMq2W6ZQ%BG>4HnIM?V;;Ohes{`Ucs56ue^7@D7;4 z+EsFB)a_(%K6jhxND}n!UBTuF3wfrvll|mp7)3wi&2?LW$+PJ>2)2C-6c@O&lKAn zOm=$x*dn&dI8!QCb(ul|t3oDY^MjHqxl~lp{p@#C%Od-U4y@NQ4=`U!YjK$7b=V}D z%?E40*f8DVrvV2nV>`Z3f5yuz^??$#3qR#q6F($w>kmKK`x21VmX=9kb^+cPdBY2l zGkIZSf%C+`2nj^)j zo}g}v;5{nk<>%xj-2OqDbJ3S`7|tQWqdvJdgiL{1=w0!qS9$A`w9Qm7>N0Y*Ma%P_ zr@fR4>5u{mKwgZ33Xs$RD6(tcVH~Mas-87Fd^6M6iuV^_o$~ql+!eBIw$U)lzl`q9 z=L6zVsZzi0IIW=DT&ES9HajKhb5lz4yQxT-NRBLv_=2sn7WFX&Wp6Y!&}P+%`!A;s zrCwXO3}jrdA7mB`h~N~HT64TM{R$lNj*~ekqSP^n9P~z;P zWPlRPz0h6za8-P>!ARb+A1-r>8VF*xhrGa8W6J$p*wy`ULrD$CmYV7Gt^scLydQWbo7XN-o9X1i7;l+J_8Ncu zc=EX&dg`GRo4==cz2d_Rz28oLS`Suf6OCp~f{0-aQ`t5YZ=!CAMc6-RZw#}A%;s44 znf2`6gcgm=0SezTH9h+JzeR3Lcm;8?*@+?FDfguK^9)z(Z`I!RKrSAI?H~4et6GTkz07Qgq4B6%Q*8Y0yPc4x z8(^YwtZjYIeOvVLey#>@$UzIciJ#x0pJLFg=8UaZv%-&?Yzp7gWNIo_x^(d75=x2c zv|LQ`HrKP(8TqFxTiP5gdT2>aTN0S7XW*pilASS$UkJ2*n+==D)0mgTGxv43t61fr z47GkfMnD-zSH@|mZ26r*d3WEtr+l-xH@L}BM)~ThoMvKqGw=Ifc}BdkL$^wC}=(XSf4YpG;sA9#OSJf)V=rs#Wq$?Wj+nTlu$YXn yn3SQon5>kvtkl(BT2@T#Mvca!|08g9w{vm``2PjZHg=b<1c17-HkzPl9sXa)&-Ts$ diff --git a/demos/ima/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/demos/ima/src/main/res/mipmap-xhdpi/ic_launcher_round.png deleted file mode 100644 index 3af2608a4492ef9ae63a77ec3305aedda89594cb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6114 zcmV<87aiz{P)QBg$Z&8YKy<2dSjG6I2&!iu7JRdT!gcBlJx2NL9-^PTGD_Ptf# z_t*dbRdw&}d+xcr-QAko7-Mb(cL9%PAop{-%ba$?L0~%p4=0Y}p*W8FU1n`tILPv} zML2!uMd(K8O&CZREHF@fhVQ(Z5yVrJcYBD!LfyzFt;&e2oN5Pm5Z@1b~qKj96+4}@|h;R-VA2(=2-37BtnR`#_JMV#vgaqj!A)$dLw zzAqt=kf%brlHdkMtlkP5%mgwQBTv+&?;R(E^s|ch{RoQ*)slEY&`lQ-Zm%FW<@tmV z)uL|w%v_~goAvXG*IfwH2{j7hrMtKlq}vjs(Nzf{YD8VTsI{f7SiPs>{X2v+3gRt% zb1Q)~2q^^WJXX;T&sN_Xm~Vh zb#=9En0OP&wxC@%Z{GYqE-tQJs}Mm3TMTBXa{GnLsc$2`UQ2AK7a~NTIdi77l7ri6 z`43X1QUv+6ZQSM9m9|2JpMU;2wWOq^>uu=?@`M*IT!7^#gZw+m<=EqrAj0+Q*Hg$H zJ$Oq+P^6h2REa1@$fx}f$avWbNp+}hvdvenT!~)3e7WZ>$&QpcFrEB6N8An?S5|d~ zB^5-n^6EnVzO|5VtXly~JQKl6t4`ZnH?qHmS_oEMUA;k(9l5u-^-~3>C<3lsKL5sz z8*E#~Y!;d{mW8E%&1x=JwThmAI-oA!r+v=m8+=*h@o#ut?Trbv)l*PrWo2c7E!qoY zv?ucapvd#>&UUU|y~?7Ft!1Hy#&Qu1ry?9_Xo~@Lh|Ar;$)A_t%k~~!$?NJ!b|m5f zD<~+?wMb?p0}NHHJDsdpOP+u2+BKGS@&sFv@K-LtvgALql8XG>>WXmgqKZ7WIB_f& zU}@aPypE`=gT1H@oRBLjNl8iR<+gNF7DT_{uWTA=gaS^s< z%wkurUa`v+VILVNZ9(p5&+%~X&FO)h{Q2?zEb7oEUPshb%hUyrC1qui#Fe{(H`iD{ zRqAcU+)jfQUrQMS%gf7S-|N5O0)!^L%Z?YuT5Yf-9N%BNewEc+xx~t=irJa+43>S) zz%q&ta%7!LpwEu;@37DH>(}^iY-Kh0{%FB|wjj};3$QLWfY%M~M`LW_lSb%0be!=n z=>;;NR8>`VrY@E*Tu+@dUH;<5i!9}cfh{roiHor2@c*#Ns?tVRBuR&FuDMdhPL?LI znB3KD)A6ZndFr3ox5@9Z#Yu0oMTf?4EIjlk$D*XSSZFf2wv-7hB0Ye9vyz=WpTq+! zj-?a>uPZK{XDd?v%;qQhv4#3^RHsB@%l79i<(6Z#^lR)?X&T#`y^t+W`7gHk(A$K!h-@XsSO{Q_ z1&MDE-egNtK45#Y=JR7-yLJ`R2>e{TGZ%95=NtUkj`-EQPNk!V64;&s^jD12Z2L5d8ftq zyOG5#aFz8-zzQoWDwsZbKMOUyPa?cS*8WGfB+2Mr8lh1DQ}T@ha9>YYm^g+69%r=v z__uf+P#4t6m8)x_7c3LKpq-|`OA);fS^h;=S--LuAlT)cq+Ve7k_#Z=dI9`R1ZaXE zTN(c;%gN1hCh%JA1>lTg$|Z^gPk_rKM~-+p?EA?l1}H|n%#}T$>{1bnI5thh0oRf5 zhyW?TQ78(VIKDpAD{DT0|E=TTVVd^}lVCZ>RO!CxE{d0Zhr4 zKq633p6N<=REuMsI(2F@aq7|R=va0U@>@OV$LCxXeEATae15ZT$0qqLXZ;fM3_ffX zxudd6u9+^EDQS6mdFj%nOZ$M^O`A4(G&kevMmg-8u5v%dIhV^U@_3+a;vH~3EhzvH zerz(Yv$L6z(hVghCVl{J$++7$m;JcYNby@&SU(zo(Pezz59)-Qkso^K9k!GPWv;P) zO92*B#)Z$D69CZXZRB-#L3&z`xI)CQ5tDQtHr>yN5hFawZ>70H0O|KJ(zQiAM!xa+ z8(8I~Qbr?h^1~-+L_EnM@@-i^M!+~Gj*WA~o%)U+ODTYod;sSyD04m@NDd1N3)6e{ z?CE9I4aw{$H#c`6{h(U;W3ASI`O1%cg{e7L6PLG+Ro7H=f+Wf>7PB>JpV;kstO>CC z@L%XyB__wlxngoxS+#zNh+_fdihgve7sxnJSy@@LapT6};8=A~CIz6p)lcF7>z%Rw ztYQOqE9QhNf$vKy^GyhnIGDTAY3o0jyF&HY#g%z%fx*wF0GO!DEJ|>;7jOYE{}mGx z^S;$|RQms_s;aLQ%Z&}rSbxN^DK^QM?x&2bU5zBTCCAA(6(Ii92GwJi(&%?#;+s~< zm)Lk@BDKY-fZQNQ#c642(^cbuB0p_M5qq_>qhDA|-npa3Sxqa%D+6psajXSF)zwvO z)A4|2$+u{kLd}ek4`)t&f|q+W6j- z0PM_|$J^x0>?nE=#aBIX>}4@6A>O!+88fESjT<+PE9Ww_xSxwv6>LSyhjt49D_@d4 zj_t^t&7w~(WgCuu$v=0Nd#hD8qeFL)eT85DHFdl`B_vr><7ui~v0N7AEpW8vVEJ0hJn>BfdHEZ4SI_DI}ALlgP-T0h7K zHXi<(x6K&=Dk>^!LPJCU-69i`0_@wjZy5dHvQ`1m(ZtGVFFh9YMw@u3| zsZxMNix&M>Oifz~5E&Uc*clguAeCE~ZdV55O5$DRdaPN$5kBlBwM|PPR=S{|prEI% z3b10uipNP|%|RH0jr7xTMBJDbB3=XePP!h6ISD#;^i-^-6*DP7X=!QY#EBE1v?{56WdhMqlpwur`B{lT@#wL)Sb=014v;I1?hKJJVF ziCMeZ)CgZT@jD+Q*6Y|m2w$)FG2(j#Hu$hfz(yZ7`3D`FM40>oy$X+~mWiZq^wQN!a4U%W09`Y}ytox6)@@>Gjsp1aB6&4H(@B9+rxsS>y9hrkD{m+6AQ@Wv75@>#&X6UUn0?$%>?%Ou~~$fQB>|XVzxj~G?mf5Z1w?P7Icu_AM|CxK#VU7 ziKQ}@Tni!CCUh*w1m0G0D93RDK)jrcOG!xyCywt2*A|QOVv)d$y2(_5}*ufmkC#VvUv_!U^}|q|YVN zdC;W*Y$RUCQ^@AC9-Ud%V-9Ts$OW0|>T0%j?b;8)G5P=Y)>g#YFI>2A1f`;vw4|bH z0&tKBuwo1HRRowV+)7ZiQGj3z@_kjv_q8NH!2$9O&6BTH0GWcGJ9n=7^Uptj5gc1v zl7vsf7Y|*&d^ydf0*IcV6rqv)C|UY(%-*jqKoGf`phlOY6u`$!0O4M22w;o+xmL(` zMgWwVnVA{H?IYmWBmgTn8YbUMMVF$YqUBnyifD`hs)HjT0ukD1{rgM>Fel&WddM9e z^i>hS7+{qG%!$)+zi&$b$H;eH0Nlok-^9ekU^T3Z;8=azyLT_X>~!$p!4DL1puuGV z$e3`@Pn~?}|D%0G3{WHAw~2hE04SRgz!~yG5=J>JfV?mZlX%OQFaImJr8sb(RRP4{ zpu>Cbz4x2z*RK~l>W1tRK!|`$W@c2A8{(M{h*ywrDu7HIeND)hutvTVz!~zL5PRXyfA!T@F%8{8r2E#l*Is)Ky`WoRVPTl^nF#g^u*-5TMhym|dzooYzJ>MsD9ASz z06Bbf0=SBNM+Ff1e=YWpjg8$-oOT!7+TKVZq(~2L-@bjkV(z=acKP3Kjy9E%|Uyn;*HgDd% z2wVzI?c0PKdSLwc@z2tjpxoY+)ENN)xEG`A(KW&$^2zE$5_FaVxPW{I1(3nFQm51X z4qSfv>8JNPa-$@_Mu^IuM~@y|CYIq^OaNt`4sy-OHy1!H`>`ND!IF4QQP>DY54gkoLBjT`qL)Riji=><{%TdPj?fX`6c>3Tx+O_OP+0(d(WaLvhg zKmcz2d3kvk$ohW|4kt{QaG#c&<=sY(9EnG}_ew}em@5_{ZixT@+>tHv8&|CKX5_~^ zZuRz%Z;t@d`Z4hq78bSy+zAe~JvD{84q`!9%7})Pl$7K)H!g6c09=GPQ}To3nxIO) zezb)Et|C9!z8=6AUdV0d_wL;r1Fx=j<^HyM0d*rN_{geNt3JVnNw#j>MlVS|xyNM! zND;6YqDsCLK!tpJh znl)3RwZ3Th`#ocJ*~5?s0b>4~1hh7IdRW&f>Pw+5p! zYViPF6n-#0J)IrU?_rzvuVUf*mTSPWTY|8CORXXzY6Xjq+s)g8HkrF0#f{i(&6+g} zz>VOjMV=?^Mt-eB$BrFwUCR@(v9aM8Y(N7Hz0L0p#w66)vuANv2+PUI!F{rA3aB&c zjy9kz=JyQC=?2X8M@B|&0Vm)_+=|*_|Fq%WzkmM+#M0W(>2yR;ZA2vKF(C~QR>FGH0JZzw5qOy;dm)D4tl$2!Yj_%O^4p931dU4P1 z;SL=-JPQs47wuZo^{9y;gYsj9r}TRL0U4N4(bo8cbZ74RS3Hc5?b)*jZU>i{Kc)z} zxBMTLaKiROh77?!4B=nsp4_{4?+I(BdH*rUgJo3oD zb?)35A`G51Y0{r*R9FCC*%o_)((2KM)YR0oUwrWe23dpAMzr;IxgDD#bm`Kib06C1 z^`OTefBc2ryLWGw!*@*6))}|fZuNDduDGw4ZP~JA=YRnNu&Ol(ZF`Wm)<(Wk1f*dd z`}OPhD3t?{A5Wh?{fi?P3)lXhp;~2zSE+E$T{EpBESy_`f2@A0XP) zQM9pD|D_=YBKJM^*kj$hb?b(ICjCvP6-x%LaS@ltE?m-Jm>{bTRTd|41uQ zht;cBFM8&gXZ|4E%|O%@brx3d(H6LfFb5-hhTK4$NNMZLHW^QvKA?TDuaazO=@1&@6gpQS&WUqV9i9^wKM-|89fhxN z*Vc(wiw)??9pO_&wglHSm`HeX;J|^u4+seOf(AMpl9G~+;;Mr3@^ZewE&p3UtUNJm zn^>dZSr?w~!ynRDSy`W-pI@1roO~3=#yM~lW29pNtM``b5s=k5x!TRq|b4{^B1?GF9`<{9 diff --git a/demos/ima/src/main/res/mipmap-xxhdpi/ic_launcher.png b/demos/ima/src/main/res/mipmap-xxhdpi/ic_launcher.png index 324e72cdd7480cb983fa1bcc7ce686e51ef87fe7..223ec8bd1132aab759469bd5ad2232f35f7be2c2 100644 GIT binary patch literal 7492 zcmV-K9lPR*P)7bFjo3yR6hYmf3aeOcP}{a`bNw*d|0U;cCTESAhdF=p<#BND ze(#)nSIgQ+Boc{4BJn&=2L@r*gF)y(i#kyntnL#ek1(4lg3iwuHMt@n4EL&P$1W2%4KN`sv1kd9&jD}1l8iYZj<4mYg_}n>DmK;k!doC zK-8Ytd#%B2upQh1F)j+>AQoX0DsDZf*n~wmwTKm?d)ZF+)|%ZjwK)+eUDYNJOanW? zn;L|%l_)+zyns-GRb8`&D)ol$bt=dUuPTW^d~&;k)!;FcCKVEfWJxcs63RuGP>*R0 z-T9A11PV@^$>1O;W0l6DU%!{(q++pFS4Lx++;Sp`K)hAW0H;{BQI*EaQfwuY3XO3l zp9c}B;pl8_FcBQ(;;8nNBe+~78uKK!*3(6xI$+}T00;fvkT_nBW3h$OLC}NEkqA^r z)zLAD%kfz3Gl$w#bYMHUVww}3RU;9I_4i5OP5`YcS6bU$*4E6Z3zQEuVm(mt@2CU> z5?Q|aSL4y#5NK~Mg#jwlFZsOPqA&&6XANTp%zN^pJEs3?(0 z%mW&rxy@aHc+cM0GF3;ak!4w%G)>b%=w(ATBu}!%CsIgy2-ZR` zK7IPs8-!AcLXii98jQzpp~)i>ef#|R^Dq#&M1i&~l4lSef75~|BzZ(4RQx~h-n|uaCmfD&_xA;bbBL6fpgL3ffTX{bTL=(&!o5T$ReRGIh+p* z&-^E#W(q)kq$}0MM!=PkkpYpBks35%nVB%rjB%CERL7$UcYC{>eJ0L15>N5JQj0ins zbiIe*L6Nr7Xx+mxDP#))AkHpoK=;T4^ipoAgNS^TnVCtU?BC=&W9vgki{|X??B0Cc zqdpUw{8)5QUN`)lC(a9fDlYz29zjRfg}J?J8x zP#@)}T?^@O1Qzbzy<4P=%;35YimWD_x{u-jirH?7pg4eAbH4e1=l}_OERElh{3n7g zI!N_(q36$^r|#dszb*OP+1a@aniguRYLgZGKH%@q^XN-kNV2ttj2(_3B_^sa zjG>>Yuy_xBUXgrXSdYx0sxuav2FSY)6VHkSDD~Jd@NBQ)fWo)a-8GQuJk(%tQVtJ; zprQ8pfRzEV8b$`mBZot_rw3q)qb4y;?F~3{=FFGmdlJ*d7SXhYQCf%P?v4ELr3`?s zY3#^}M%mpdwuOG3Y`39sXOb<3J_c*S05MCMF&%QRT~$T>2$v2=Uuo21H~GG>Fji>z z+OcCtRs6&cnRr$zK-um?!LyylXn@Z2&_J%w>$AE?KU(A8Dd1x{05MBjwF0!ypDA8U zlODobJ|f1CA78t$Fji>z!rH(``N9_{6`+^DSdjyTSbPClS*)WaWo-P91AvyAwkCNKpWx-{HZ72RD#`O1v<}D;`>egoL!iPnO2AFGcS_^B6$F zO%OmFv$llv4eLRQic(ZI_UzfS4Aj6qi3KVu8eB+Z*%zjB4uFeYL#JSXsAZvAfS4uQ z+d|f{qoB*oP(o2rkL4RUckbLIVgb1*Zzw=a|CGZxxCwCSa48+2EZ3owR;wByW+@ZK zL5{y4=q2fmhA~R#gM)+j5eu)=K?Uj`TU%Soz$##i-%lj~kx|psBmiQTwqz0Hg}$-d zMo0$%%9vo%LBvEsI_S0fXXD0=b&;DFvi{Ml0icwltP*Vk05P+%f{e|ZK#Grl6U|jR zfEYb|`0%E3hn|cJ5DQ;ru&qz8yVi09v?6<9ROoYSVLAXYOa9IQvRs@&my`2P=^zr) zbaqTSc<|uAh>1OW_LM_2%Ww2Ueq#cDS=h)`5V9p!e(`Q6w-NOZ(w_&^~%pq5#p}fmaoRudnYqV!{xB$jXy|#-lqs zn1EIzK-V0^3F@;Q93b>V8%VIUEYO(=R+bPu)JiZU1{jTv?Ai&sq@)s^QpBrZjuI0F z0kmSp3erH8@B_Wn3Q+uROYrI_Mob6et`g6{QK%)48Ufi}o}kam6P}6X;pe=5{W=ZX zS0*;l%<>!k@IUt9haVC^mGO$+Ye=A~LKw~R+!|vG?ybeZbG0`MN$RlFnK(9bSSgg9G#8Qh5LF_K;|)WMDXL zku^WlG^7xKhym=0J!;snVLf7l7ZK%r4qS+YjkX)Xwe|3~02LLWt(!&cKw3ipVnxIXM~)onj__nF1)v=P zufX=uLzuF`ANG8-w8SBL{*~zfl;!4XG(gxF%~`s1Xb<^Kf(X!q{j zo$(W-vn)(005Myz#2t>kJGI0By7w>B5Ybc@LKP1p-g9YzZW$iJJtX@oDw?LyM{$B!Q;jT<+vN1*_*NyM>Z$2P+r$O|O{ z#BA)gE3jqQI&f_%HamOrg}TF7+VW+f%ghu4P-tjq^p{_L*@4)gb_SXmGqe1^U;v@j z!Ex45*2+*aK+LAE@r46}76=3Ad`Ab?L{*pB+d}5ygP_aK7S_tZ;lfAAL?dE@7(p{* zW|sdKxGY+>Y*`VXwXqw2MNI&a*&TcUOXmFsC%R7(R*_DyQV&GwKd%Dq^XEdxS|v%M zbMhg`L_L`e3p36#$DN4&QZ0NdkS81GPzylJM((@;$9qi?0+C-=dx##a?80K|tQpjW zFR{B5FI~FiOPh+!1|J~QLY47Z6VFEPR4YJa8|*g=3uj)~Ho1w@hf%KqiGH6nQ-b9i zm;>_m0zEppBqkEs(Vnws&z>MAWH!vX0L`5{w-P>UU@CsCQhk$%oRL;~tB;eD(=WtCQ2?rrV~iRAP*fq&FnI1$ z)%uN{%6RgXt@>u6IYagMP84u-beu{|7zEJFnKLO1tG0LV-hTLrI1N>00<>oAE+w4{ zQ~qfTeifQ(K)H(-FaCm<7&B%}#X({RZ3aFt4xCi4XWU?0p8f@D+8oc13)`J|G{*Q zHq(jJEt^4#i+hu|*HZ=-Of{g!jT=`cHqZ)P7FZLiiqE=FpFZ6T0IC3JjN>)jeZDgZ zfKnF=I1?`*1H%mj+}+&|5(_dD76t({c<^ANt+v0v|0n=Zg$7!<$W?LIyD0xw4!(fK zW6iK_+qPN60x@AIK;~b4^%V)}8uRAO>m^B266=X68KBjZ_Nf-2WCuIQI)04xu9RSU z0HD`lrj8gfVj!^~7ZJ@30*J&??b@}g!{w6jeaPyAc2nSL~%^s(0_+)tP`cbXk1<`R7$gJRLf8 zXfP>qRu57tK(khy6$21fl_&jr>Ho(@wqAy_!^5{_txt*0?|9SN2k#70& z<->^q3|~w@8@$Z%bSmkfnrJOCjNN6skWvBqZr>en?li_|fR4R44eSm*cd@NC)B3)=f$Wh;04vtwsa% zm?higV zY;U+8qFrk`Y8(1kV{GB-vkfy5mc4iOIE<|a)(g9*N@z+0L<%GRJA!h%; z1+aI}LfAHVHT*Q~FpR_0+}K@bk@sV`AC;jVP&mD>goFgD{Wy`Yu>)V0IOehbm~sIsd7JQC1Y8VD085yUbr`-wLt-g?X#!}S?{j#OEMTxpf$vGZfB(J?r%{KKk2`kkm|r{#0f?HfLrg-hc$;j}ghK5u#m^g9bw|EI- zHhp7yS}Fm^{_sQCcIhR=r?d7$sWDpg{)-%qMq^DrG$8X5z6+H@tKJe9K0CO&x~{`A zIFYRifR_3^fv_0i1MI4q_rbRi!uIXkw~+5-A66%y@^>HcyHG{6+BIs_Xob(t_>%hH zvCUB;S=qkeC?ViXc8UQ1$YjvzOdl3bzl&ZV7f3#neOH@&Y2fZ7UQpMqTbH)KHr={) z>x*rUS2Rx|8luzp6O009#IZ0qaU&KobEySWdf~x0Fwsei=uVwF4I-bR?-~me&PE98 z8Z4BpT)A>AruKT#dMG+Imo!VU#w>opF^i~FHMSqbpYy_L3O(MwGL3wNx~CQSPMB~u zB9v7^tAkbQwzMstIC0_^x)S6sTq3K~y8??A#uj)zf;+F$OFKBM)Baa@csQLh-A+D| zeN~5iCnj_W2xVyXt5>g1-&j|?(aIS|V13I)*@h1>BMQS+ zwe$S492@H$$F`jmV%ufMwr$(CZQHgz_`lTOsaH2nrfvrJR-M(`*Es#pf6%A6<(9na0X9wtD`8XH2P<^wY4pL^UXIuLj4I79oj;)BvePII@3j# z`3coDFKX~ibHr7QZdHsF5aI1tsfK8}exH*{rFyTq=9*iTYy?C+eTiT6-&n^A%4?7s8mBq_$Smy%R4Z-Hgl21k-3lu&q87y`U4Q=fAWBS@gtzGo z=o+5Oq}v{JY+YA}a6Jm1kqV+JC$VN}qFs8a-65O=`1$oa*@WcS3qNF05&9>;uv^n~7=PtYKvW04^-z00Tr!O&Sf7@jB;Y;3lcK-L~3cF2<86jNd0JIGk*(uC4||9pou=mFz9lsBl#1&qMtrMCW%^J#Ym1 zcI{y=jt(0d8rspRbz6wf=ZT+*m{{H$c?|;5Z1!jHi?q>q;KL6;{59EAJw#{`HcA|2 z(jZEQe0|`72Of&f(4Bt}8?4j(umDdC$;O;ylB8C5&`h5+X;KF?yylu~u71T8SKO@& zjs~#rb`k;7m<0|xfSF+(1t0gWyzJaF&X{ztqd`P-^gxV5HH^I6q;-2`sb)^yLXBoN$g3f{UXQ zbR!+jL|4|Co#kN;o*4Y1oOg+ITl3dYk=m6ye)~O#l?Tf|Hel4GXddFLH+I^XDa)a!KIW&;DK6CI!nvrf5nl!<|LMl>5u zuzhcYvZX|lWL+o~C-UTzPd@98JMMU!Y__hN0Yyk3VlWL?w&^$9^nreosBA$n7ro%` z?3GqpX>9|ii#gj|HCY!E64v==zEQ!^MKww+7K^ipJOU$0!3*!V-+p^5YI{)T?H5+k ztI7v>45nb_0M!2tn4be^I2i%k=!%?g82jFP@4Zh&Ma3e4ahM}>HE^n}gTg=ebrLoz zGgTK|2vs9#HX{Yo)-9QP?X}llSMT*a;kxUtdqO7g>2GBhD7@b2pt91t8I%0DjWx!3J=QM9&K#Ia3F8FdWE0>FJ&;gp(X?J^$FfzFKbxQ6 zbUGbai3RFyZ*N~tH?Hq=+;PX9sN>hv`M)f4_XP`!$s^(cSeO;S1*`n&V@Bcsa)6TW za=RDoqL&%1!x= zlC}V*z)Yopk}RdXVIiIOw;4|Go@`-@=;R6pm@U}L8U)81S&s=rYRrj5L{`uF?h7zCq( zUjZ=QhfbiBYxfHY2$Dq*DwC17fhw8%8fkW<>u;^}w$O%YH@%ntKG+B`gGs< z1T-TTOyj(0DLbb*Nx@7oJ^;%MQSQaBG;35xq{^RF`2S7`Kyh0|x1DoeDfc~Ylz=7y zR0LE=%FK{4OXMv1KaiXy>+hrkP$FPS05c{)6#-X7#ziFhITCGW%sqoSto+VsHI1DB O0000?hpbj?h-6mfK3P*Eck~k0Tzeg5-hkABxtZea0_k$f-mlF z0S@Qqtva`>x}TYzc}9LrO?P#qj+P1@HZ?W?0C;Muih9o&|G$cb@ocx1*PEUJ%~tM} z901hB;rx4#{@jOHs_MN00ADr$2n+#$yJuJ64gh!x0KlF(07#?(0ENrf7G3D`0EUHz zisCaq%dJ9dz%zhdRNuG*01nCjDhiPCl@b8xIMfv7^t~4jVRrSTGYyZUWqY@yW=)V_ z&3sUP1SK9v1f{4lDSN(agrKYULc;#EGDVeU*5b@#MOSY5JBn#QG8wqxQh+mdR638{mo5f>O zLUdZIPSjFk0~F26zDrM3y_#P^P91oWtLlPaZrhnM$NR%qsbHHK#?fN?cX?EvAhY1Sr9A(1;Kw4@87~|;2QP~ z(kKOGvCdB}qr4m#)1DwQFlh^NdBZvNLkld&yg%&GU`+boBMsoj5o?8tVuY^b0?4;E zsxoLxz8?S$y~a~x0{?dqk+6~Dd(EG7px_yH(X&NX&qEtHPUhu*JHD258=5$JS12rQ zcN+7p>R>tbFJ3NzEcRIpS98?}YEYxBIA8}1Y8zH9wq0c{hx+EXY&ZQ!-Hvy03X zLTMo4EZwtKfwb294-cY5XhQRxYJSybphcrNJWW2FY+b?|QB^?$5ZN=JlSs9Og(;8+ z*~-#CeeEOxt~F#aWn8wy-N_ilDDe_o+SwJD>4y?j5Lpj z2&!EX)RNxnadPBAa?fOj5D1C{l1E0X?&G3+ckcVfk`?%2FTsoUf4@~eaS#th=zq7v zMEJR@1T?Pi4;$xiPv`3)9rsrbVUH&b0e2{YTEG%;$GGzKUKEim;R6r>F@Q-}9JR-< zOPpQI>W0Vt6&7d?~$d&}chKTr_rELu} zWY;KTvtpJFr?P~ReHL4~2=ABn1`GN4Li%OI_1{mMRQi1Bf?+^Va?xdn4>h)Bq#ZRK zYo%R_h5etrv|!$1QF8fu80fN?1oXe(Jx#e6H^$+>C}N{*i$bNbELsXDA>cxlh|iFq zh~$yJ?1lTdcFd1Yv+Hr^PP!yupP!0H@Y6(wFcaVE+0?qjDJ1;*-Q8qL{NNPc{GAoi z_kBH`kw^(^7ShmzArk^A-!3_$W%!M-pGaZC=K`p-ch&iT%CV0>ofS74aPd7oT&cRr zXI30fVV6#PR*Z?c*orR0!$K6SUl9!H>hG+%`LdifNk`!Sw7Hon{Wn=|qV{a%v9nEq zAdBW*5kq6il=yA}x8cZQt^c+RBS|TRn;!?$ue?@jIV~0w1dt1FJRYI-K5>z-^01)R z)r}A&QXp^?-?}Uj`}ZPqB#}xO-?{0wrmi|eJOEjzdXbey4$rtKNHz)M*o?Ov+;S=K z-l~`)xV`%7Gvzy5wfvwqc0|80K29k0G~1nuBO+y-6)w11Kz2{>yD{HTt-uybe2pe? zUZK*Eij7TT4NwF1Jr@6R7gMuu^@qn#zPIgRtF?-SJL83LBDrh7k#{F^222EXPg}S0d4Lf0!|1 z|2k$^b~)^8$Z-yH{B-vo%7sVU@ZCvXN+Am)-fy$afZ_4HAUpK}j4p`UyXRel-+(VS z#K>-=-oA1pH+Lo$&|!lYB|M7Y&&bF##Oi@y_G3p1X$0I{jS1!NEdTz#x0`H`d*l%X z*8Y3>L*>j@ZQGOdPqwY(GzbA4nxqT(UAP<-tBf{_cb&Hn8hO5gEAotoV;tF6K4~wr2-M0v|2acQ!E@G*g$J z)~&_lvwN%WW>@U_taX5YX@a~pnG7A~jGwQwd4)QKk|^d_x9j+3JYmI5H`a)XMKwDt zk(nmso_I$Kc5m+8iVbIhY<4$34Oz!sg3oZF%UtS(sc6iq3?e8Z;P<{OFU9MACE6y( zeVprnhr!P;oc8pbE%A~S<+NGI2ZT@4A|o9bByQ0er$rYB3(c)7;=)^?$%a${0@70N zuiBVnAMd|qX7BE)8})+FAI&HM|BIb3e=e`b{Do8`J0jc$H>gl$zF26=haG31FDaep zd~i}CHSn$#8|WtE06vcA%1yxiy_TH|RmZ5>pI5*8pJZk0X54JDQQZgIf1Pp3*6hepV_cXe)L2iW$Ov=RZ4T)SP^a_8V} z+Nl?NJL7fAi<)Gt98U+LhE>x4W=bfo4F>5)qBx@^8&5-b>y*Wq19MyS(72ka8XFr2 zf*j(ExtQkjwN|4B?D z7+WzS*h6e_Po+Iqc-2n)gTz|de%FcTd_i9n+Y5*Vb=E{8xj&|h`CcUC*(yeCf~#Mf zzb-_ji&PNcctK6Xhe#gB0skjFFK5C4=k%tQQ}F|ZvEnPcH=#yH4n%z78?McMh!vek zVzwC0*OpmW2*-A6xz0=pE#WdXHMNxSJ*qGY(RoV9)|eu)HSSi_+|)IgT|!7HRx~ zjM$zp%LEBY)1AKKNI?~*>9DE3Y2t5p#jeqeq`1 zsjA-8eQKC*!$%k#=&jm+JG?UD(}M!tI{wD*3FQFt8jgv2xrRUJ}t}rWx2>XWz9ndH*cxl()ZC zoq?di!h6HY$fsglgay7|b6$cUG-f!U4blbj(rpP^1ZhHv@Oi~;BBvrv<+uC;%6QK!nyQ!bb3i3D~cvnpDAo3*3 zXRfZ@$J{FP?jf(NY7~-%Kem>jzZ2+LtbG!9I_fdJdD*;^T9gaiY>d+S$EdQrW9W62 z6w8M&v*8VWD_j)fmt?+bdavPn>oW8djd zRnQ}{XsIlwYWPp;GWLXvbSZ8#w25z1T}!<{_~(dcR_i1U?hyAe+lL*(Y6c;j2q7l! zMeN(nuA8Z9$#w2%ETSLjF{A#kE#WKus+%pal;-wx&tTsmFPOcbJtT?j&i(#-rB}l@ zXz|&%MXjD2YcYCZ3h4)?KnC*X$G%5N)1s!0!Ok!F9KLgV@wxMiFJIVH?E5JcwAnZF zU8ZPDJ_U_l81@&npI5WS7Y@_gf3vTXa;511h_(@{y1q-O{&bzJ z*8g>?c5=lUH6UfPj3=iuuHf4j?KJPq`x@en2Bp>#zIQjX5(C<9-X4X{a^S znWF1zJ=7rEUwQ&cZgyV4L12f&2^eIc^dGIJP@ToOgrU_Qe=T)utR;W$_2Vb7NiZ+d z$I0I>GFIutqOWiLmT~-Q<(?n5QaatHWj**>L8sxh1*pAkwG>siFMGEZYuZ)E!^Hfs zYBj`sbMQ5MR;6=1^0W*qO*Zthx-svsYqrUbJW)!vTGhWKGEu8c+=Yc%xi}Rncu3ph zTT1j_>={i3l#~$!rW!%ZtD9e6l6k-k8l{2w53!mmROAD^2yB^e)3f9_Qyf&C#zk`( z|5RL%r&}#t(;vF4nO&n}`iZpIL=p9tYtYv3%r@GzLWJ6%y_D(icSF^swYM`e8-n43iwo$C~>G<)dd0ze@5}n(!^YD zHf#OVbQ$Li@J}-qcOYn_iWF=_%)EXhrVuaYiai|B<1tXwNsow(m;XfL6^x~|Tr%L3~cs0@c) zDvOFU-AYn1!A;RBM0S}*EhYK49H$mBAxus)CB*KW(87#!#_C0wDr<0*dZ+GN&(3wR z6)cFLiDvOfs*-7Q75ekTAx)k!dtENUKHbP|2y4=tf*d_BeZ(9kR*m;dVzm&0fkKuD zVw5y9N>pz9C_wR+&Ql&&y{4@2M2?fWx~+>f|F%8E@fIfvSM$Dsk26(UL32oNvTR;M zE?F<7<;;jR4)ChzQaN((foV z)XqautTdMYtv<=oo-3W-t|gN7Q43N~%fnClny|NNcW9bIPPP5KK7_N8g!LB8{mK#! zH$74|$b4TAy@hAZ!;irT2?^B0kZ)7Dc?(7xawRUpO~AmA#}eX9A>+BA7{oDi)LA?F ze&CT`Cu_2=;8CWI)e~I_65cUmMPw5fqY1^6v))pc_TBArvAw_5Y8v0+fFFT`T zHP3&PYi2>CDO=a|@`asXnwe>W80%%<>JPo(DS}IQiBEBaNN0EF6HQ1L2i6GOPMOdN zjf3EMN!E(ceXhpd8~<6;6k<57OFRs;mpFM6VviPN>p3?NxrpNs0>K&nH_s ze)2#HhR9JHPAXf#viTkbc{-5C7U`N!`>J-$T!T6%=xo-)1_WO=+BG{J`iIk%tvxF39rJtK49Kj#ne;WG1JF1h7;~wauZ)nMvmBa2PPfrqREMKWX z@v}$0&+|nJrAAfRY-%?hS4+$B%DNMzBb_=Hl*i%euVLI5Ts~UsBVi(QHyKQ2LMXf` z0W+~Kz7$t#MuN|X2BJ(M=xZDRAyTLhPvC8i&9b=rS-T{k34X}|t+FMqf5gwQirD~N1!kK&^#+#8WvcfENOLA`Mcy@u~ zH10E=t+W=Q;gn}&;`R1D$n(8@Nd6f)9=F%l?A>?2w)H}O4avWOP@7IMVRjQ&aQDb) zzj{)MTY~Nk78>B!^EbpT{&h zy{wTABQlVVQG<4;UHY?;#Je#-E;cF3gVTx520^#XjvTlEX>+s{?KP#Rh@hM6R;~DE zaQY16$Axm5ycukte}4FtY-VZHc>=Ps8mJDLx3mwVvcF<^`Y6)v5tF`RMXhW1kE-;! z7~tpIQvz5a6~q-8@hTfF9`J;$QGQN%+VF#`>F4K3>h!tFU^L2jEagQ5Pk1U_I5&B> z+i<8EMFGFO$f7Z?pzI(jT0QkKnV)gw=j74h4*jfkk3UsUT5PemxD`pO^Y#~;P2Cte zzZ^pr>SQHC-576SI{p&FRy36<`&{Iej&&A&%>3-L{h(fUbGnb)*b&eaXj>i>gzllk zLXjw`pp#|yQIQ@;?mS=O-1Tj+ZLzy+aqr7%QwWl?j=*6dw5&4}>!wXqh&j%NuF{1q zzx$OXeWiAue+g#nkqQ#Uej@Zu;D+@z^VU*&HuNqqEm?V~(Z%7D`W5KSy^e|yF6kM7 z8Z9fEpcs^ElF9Vnolfs7^4b0fsNt+i?LwUX8Cv|iJeR|GOiFV!JyHdq+XQ&dER(KSqMxW{=M)lA?Exe&ZEB~6SmHg`zkcD7x#myq0h61+zhLr_NzEIjX zr~NGX_Uh~gdcrvjGI(&5K_zaEf}1t*)v3uT>~Gi$r^}R;H+0FEE5El{y;&DniH2@A z@!71_8mFHt1#V8MVsIYn={v&*0;3SWf4M$yLB^BdewOxz;Q=+gakk`S{_R_t!z2b| z+0d^C?G&7U6$_-W9@eR6SH%+qLx_Tf&Gu5%pn*mOGU0~kv~^K zhPeqYZMWWoA(Y+4GgQo9nNe6S#MZnyce_na@78ZnpwFenVafZC3N2lc5Jk-@V`{|l zhaF`zAL)+($xq8mFm{7fXtHru+DANoGz-A^1*@lTnE;1?03lz8kAnD{zQU=Pb^3f` zT5-g`z5|%qOa!WTBed-8`#AQ~wb9TrUZKU)H*O7!LtNnEd!r8!Oda)u!Gb5P`9(`b z`lMP6CLh4OzvXC#CR|@uo$EcHAyGr=)LB7)>=s3 zvU;aR#cN3<5&CLMFU@keW^R-Tqyf4fdkOnwI(H$x#@I1D6#dkUo@YW#7MU0@=NV-4 zEh2K?O@+2e{qW^7r?B~QTO)j}>hR$q9*n$8M(4+DOZ00WXFonLlk^;os8*zI>YG#? z9oq$CD~byz>;`--_NMy|iJRALZ#+qV8OXn=AmL^GL&|q1Qw-^*#~;WNNNbk(96Tnw zGjjscNyIyM2CYwiJ2l-}u_7mUGcvM+puPF^F89eIBx27&$|p_NG)fOaafGv|_b9G$;1LzZ-1aIE?*R6kHg}dy%~K(Q5S2O6086 z{lN&8;0>!pq^f*Jlh=J%Rmaoed<=uf@$iKl+bieC83IT!09J&IF)9H)C?d!eW1UQ}BQwxaqQY47DpOk@`zZ zo>#SM@oI^|nrWm~Ol7=r`!Bp9lQNbBCeHcfN&X$kjj0R(@?f$OHHt|fWe6jDrYg3(mdEd$8P2Yzjt9*EM zLE|cp-Tzsdyt(dvLhU8}_IX&I?B=|yoZ!&<`9&H5PtApt=VUIB4l0a1NH v0SQqt3DM`an1p};^>=lX|A*k@Y-MNT^ZzF}9G-1G696?OEyXH%^Pv9$0dR%J diff --git a/demos/ima/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/demos/ima/src/main/res/mipmap-xxhdpi/ic_launcher_round.png deleted file mode 100644 index 9bec2e623103ac9713b00cad8502a057c1efda61..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10056 zcmV-OC%4#%P)f{b8~La&ABzzjS$j|sySB+3lg7e=Ipr#6B0nslBeFh90 zSSvo;k;;{-H`UWrL#ckvHI)CYH~&mWOOQywast)FplM+W82a~aRKuwzQB9{>M-@hu zN|i@dN_B^-lB$~2Zq@v6clc-W_;w$o0*U~HsH7SRTub^rz-g7#hsU6Ec|iLuRk{&0*aR?Y!eR?l3@CnX($h`nZRl-$kvK*5?~ zZ16HwhzvM2O&AfiDtMnXb6O*rSV!{y6<#yBUtN{Gt}WTft+ja2;c=0? zpD8ihO(mmpSmuU{Nzy+v<@)e}D+u!UeW{|1td0{J)A5n$D)d=jxl+e{e+xpqud1qg zgZ{f*Vs&bqkXUwW5^Gfc%P+sYDc83TLcHVSv^vUIqsq!kU)rV3?(4Wnl4Z4`4c{$E z&7HB1eVH1|`tRPoyXVZAGp+B-R9^&o6%`d-__PYA%TmFm-Me=$Av-&}>wOhmi>u+z zojWKDW^s7#IR{>G-9yLHnCNstK|%lf!V-xF&_)fS?~9!9I1Hkq!otEKO&TI$LTO{3 zrSGrufX4}sgCL?7zvSGxb3>b?JCnFA%-Ol^?c0q!osAUQcX;~Q0G zCTOO97KOrVN=*Pmr_n5qT)K3L?1=RvOJc|CA=+~MD{`gea+7yu!gXD_c8RP{{69TB z{?T4!TZ}Jldy!HA=_ja_(oL(?KGi6KYNNO(O353e!UA2se3`@_k0vXlKG6fTG;Sh^ z$lAhOSyQ$`a8GDMSms*ly1exOE!9jW3CUX4b_D@qV}oN}ym&E=j#-NakB4||p&1>- z8A`=HQsL^P7YsRl`ZU=WwUz{EC+Q&yOqfj06`f*Mswr9_VPSJGX0QuFz_T!NEZGye znq+5Zv$iW8>tT!lEp=t{cs$gyL4#)Mzh6=+?vaZR(AWzXE|8?;V`Oc_cY1)JJ*hsV zwESAVU757zf@47#Fmn>0v!`AoTvusX3E7c6or2?~2WVB;m#nSSN~mRFSv+*@+BK4t zl=ORyVMIhk%Z74Y&8b;TP;*WXI-15;BsVvggvA^nOQYVab!G7rN%FZPsJL3y(Nb6d z1NIFUfgtwgtsA7`Mj0usxI(U$6_Mi7LYf8TGvPh{c8&fYK7-HVJNPd4A;7X0C~;vV z=7x};V#bn%F*<;L(o7^_+F;gJv>E$Wqfdn^qZei}9YYs~yE5Ur=t)df!*v-CItHt_ zxR|7;r<3iP#WbLvpoa*-=fx{|CSwI-Xy7&gKv_izxo|a?q!nmL)R`@;Jh1oVT(b4V zH*}w$l2wWCQ#bi86W*^){09j-@iqI*;jCr!JDW&azJ~7OEZZ0MiG5pwNyK)A#b?Q? zgumXqRnc$W{lbO>(@zUX6CmJb!EJg*{rCj=m|=4DR*7fYNxtr zY<_+|iBF6nD&8Cj9=SN8qIv2SpV zGti>gznImMxHrkNgty5$3fG~`0Fs<{h!kJDz>Z}MleF4gUQtdCo(#~#11$~zh_$Vt zpn#>@4oD8zY9cgHFAEM1ev(7f+)=SlbJ`iJ9W@t`@M*;0n&aa++we*Hd@&39DekS_p8| z0!XSQ6sFaQAJTJJN6#gjStXoX(Up9%>G(eltj~s{vq@@d3TvB#3#2TdzH;SCH4UWI z52(3`gZ0_d5R>6?1ygv*`Sa(AHZGC`XeLW)LlcPR)FzTsm_m-6T1nOAk4+|rPc0`o1*zm{`dVtK#?}I)d56TrN3k}cZH~T0BW`nKXJ?0^Hl&&x z6V``j2d{|<@eNfwxq9^~Id$q3*{xZ_1M0V!;G)*T;>1rd1V;uQr2vw%K2m_7g?I%> z3AiOQQ4%ty?!6bg~?7fU^uSElt^sOw@g7kk!*sbstOc zWE94-!k$&GtDf%55daAVCcMw4s9*pa5F%C=%FoX)U%h(u0F3#L9XnbmRdsGo2kwi8 zTB}FEbK}N!l5{piSI?1wr{S$n{QzR~e`4Pv$Ib?`HZ}xAI3C@qa0?|qK7KmJ{P^+X zE=t_IaX*-Pc&#t&apCoh5pcXmhsHHaCbR zV!<@#A%%p5jKtX66-;vz*5dZ<+kTFAU(%Q-A$Py+Zp#kqJ zM?wTQhDv@?Qql^HeZAe7a9>N8F6}^foayM`S=_ov%Zng^$KG!O@Yv_Rr1IB#kY#a` zNNS#@A?AKp1K2ZX&SX!XJh@A~-I#D+mo8m;P2#>B1`p~Y=PqTCbxEJt2961Mni@b* zVEkm(2j~k&LL_QJ`}XZ~ueTfHUusFs=p07|&tkS-N$C}`E%{s9z;O^f^><&E0TS>C zZ9e`la;@x&LmwbOsDkM;adB}0V8CX8B-vLh>Vsn(1&}^yrdde%sWp~iF$>R|7T{6W z`bYuN%{sI${xJp!I-0r4p+PkO!m%%3?PXIbHXQ%V0oF$jpt02b{)2>PuOabgcd@A@o06w-uq?YT zsTOMgLNfE?92pO>Y%DJ??*@&5hk*r~ii#rpqUqdQJpQS6lh+86-H2?0HhM|SmVB6{UUNUuwzTl1?LujZa14PU<*LdhQz6)xa6Wk zTp2GaR^xtSXlUq%V1WYE%GUVDh5A8%meXc^f4-Xo6T_!s<^ny%gRa(227~5 z>>4?mwUQ0296U-|AI$Z^v2aYebHO>r=H%oQO`JHf7r#T_+*pY!y}T9fc`y#P9T zdWG2m6WVohrpke{H`$do!>V&RbZUvs@GvVBuX`d_Z7W3g%>wBQ7cNw;UAy*oU}ELU zl`hr>&@J=x^Zz1Q$XV6Q3%)iYYqLS>ZH+`wyyxT`8laY#9k8pVm&xW6UnuChdDy)gS%gfpiT5>0P^aO$HNI1=1X#RwX4RU-S4! zRriIg;?k8uvN35YgTWeLjD<<-dBvG#2QBkL3|SukwyN-;))NpnfgUT??75t~oKBX} zbEzLd?$lC$LW*dgsrBTl00_1N=X><%(Yav4DuDQhT31w5ELA&z7Wcc3pFK(g<_TsB zewKw*y{=p?uveCMk35f=6g;%GdPj*XnCQa3v}EVPyUB zDK>*sUwDMpCjEmR`>5WXp(d1G7{xNi`UKAc9-*I4%wqdhIhd}3l}k)a#AN$+oDK8a z?|=V$e5l=>J9myDfL6Tn~!r$1r)(0LrfR@Mol@t`6RW+E#*kj+RbfZjkSwHz>D zKqpFemYM(w_myF^#R9T>tpSGuliaa=Ek&MB=O8a)`w~W1O_rPGIG0j z?~bK{TXIHB#y>6ihq}`NE>yDy1c2})W=Lv)O+Y+o@R$N?=(0xO$r_fKucoYBzc8r zRC_2<6ch9E@^1d{!w)Z54G?`DOyRksCO|BG&(W~?zYPhE>hP#!eV~O}Z<3T9u38)< z04gXbxI1&^%$LE2S%7${8u|V(3ePWU0VEcT(qwF5nTnDiCJMB zl@{!t5y$^SfG1W0mRKy z>kS(=459GcRudqsHnt;iPLqPCL0y*#fVL&fWPPb7K>7LkcfR@N8@RC6AAb0ui$#D| ztXT0Z-NAJ=vM~MX>{qUk4RQZ$WZ*O{c>Ji=#!h2>sYWJ-IuOsoZhY~@7cW{3(5zXr zo}^#Csun<~p5n2Qz}OEP5jYCDEj!_{6`*C&?S|U_Uzef@4fflP>TSGnTYSc z`|jhE=mNC>LfVOiw3o)d)2P8w3Ldqr540$HJbr~otyG=?bn4WpqLCv<4g?$gc7}O? zs2-(6pHkyih5!gFjQK~rNftzmB?~lTi67SjONy{8KOv2`74p(4qE-tc4F4@JPkCuP zY89b-oi8hQSFFJUhbTB>XV0!8XnCg3~ zAL!rp+QzjV^3dzwJGg!}mM8hoPOe=ZOw*y=y4M-vJ=Kgo678+k%zYB=hurm=B}4~s zHr31nZcMX+sSfBgJ7kQkW*v~z=sKEtU{qa&;P0c^>+I0cWbP3U)|V;)#MVxXjEux| zjxL-H^8nExsU3ZNm*%o5t~NukwgR%WS$%L!i=cuQFe2;n%-!M-y zFWiF(133>0ch~)m#WU6kv5dUN7{~_-=i+~xAE7Eh)u=IT-@bi5n6L$)PFk&Yyc(;q z)&VHmn`$iaj~Ywng?a0M*yqVyn_j^tbU;8tbq0=SOnU0fqb`t<(HScX>s))zLg-MUEkU zQSPb%gh}%c4mPH|0U;u@? zPIO=wSdbr+TU|v$V+=H3PEliMO0Sv)s^K-DyI+0v)t|w{-~RTuHWmTmd4Bs>UU{WA z4WP~|ory^S!X0(FMG5?PT%@-y%))rq(Hsdl0A&srtPHa>uq=9)s>UwGjK7fS$PYvJnZ+Md3;mX(zqvGbo=giQ0QpA=fIJKUQmSBR5g@HP07)`1Jlg!L9zA-r6Th=+X=^@i+_(<( zwd?uw=NBrSiCGH}gbYm%9y#kXSI+t{ad^xCgcwH$k7r$Y^ZClH#uxw(P1E*g#I9i;;tqI`Iu40xp0 z$5#RmQ@E#ICIQk1#dQHDg1CWgM@#Vp^JUjv*Ps4jwM)0sqE5f}FK$hYkHQ<4;4>bTn{1XuofhF#q01MUz z(E31n#E20c>1+2>r%w4a27n;k#GHG`3V0*{`5cjEVLEtB15_6t1ArnpJT?NP7CdSI zBnpUl+9N0^C=kiiOE10D$=U!~9|!&EPk%xt)^**wb#92rm8u8X1CSIVIe2P|gdTNk zKPIe?4j>PU0O{Xzcx2-r8GzJ;XMXf(H2`AupWNKss_(x0ZXy_bho z=wYfp)QzPnWrgeoNDt9rncEP&XsCzB2%x&w$FNXn3Lpb`%mHK+|0n~Gn@M=o00;w& z>9Ja^_B0)P{F?K_oCTW}8)rYT^6IOvK7u$XBO}9K9f1B~dSaFZ&8HB}IqYe=>TK5f zc<5zVX*Qg*gZosb0J7x1)PzSZfTZqg^XAQKF!nFM{4!RnZ)qz)(m3d`g$ozHPO~vZ zp3+bXAV^puDLlpi)xzV!WC|WBK;kB+tOc^*zD$Cn0z4`JRKp)-zDG0gH!=40iGTEQ z5N4ot?AY;9xUu5mVnrsHDG87sq9dkUmj}CRE(edC^)bFnZoB((EIdjB1nYzBD?B_L zt8w(_W8d1=_($r-T(}AAsnKY@!R$19*Nj#gARR=W92|F@01b!76hH!=+V}330g|cz z=x>ZF3Xhvr@GyX)l>tbs4UOXAvSrJBFy_OD4+lUl^>JT%H#TU{AVlDg(MWt)d3pII zdy9&OcjL$ECY{#@9HU9=3nBoGb?^viYTvutWqsHk^k~P!qXWoIDGS8LG$|?R%5Q%2 zo0l-=0|yT5SYP*L;KrVR{&}no(>paabq#-nwn|Ze6cQ@LzG3F!@d(T3Xt@_uqft8)MzCU%$@v&A#fm zF|3)`w{Krp`r0omD{G%UR!D7tAPlrIIQ4<24nR>lt78n00YLSF$2Pa6BtX(T?|b&_ z!Q}aVe5~8r>%I(vX&MV5nC>-e)-2EK*RNOBH>Ee2(kkc84EWu;m`nc=i zsbhVj&4Z&BJPKJLW_{Ar)2pUTnS#o5ucx1W+V0@l7$A_?u6OU=c(`mpN=nLZ{w#Kt zy#U$r$gi!ELS$>)BLEU}l>MS)020=x-tdgE3m$s`64r+;bg^T{A&e~_V=;M55r9N6 z-KtlwUa&$>eER99ua}gR+^UZiawI?kqWZY5`GCg=pgPtkN?EI8D?E^&eHMsWpA#oe z+@3UP(pZdb&z?PDeOlQYJe#sY?Voz;sh%KJtJSW>!)&%%Ax8sL3z2oMYhHxpi3oGn z#{xi(fX5zyg!RF~3>!9VK;}hrr2+U+mG(*n&$1~!C-jLI=~hrsa1keBOLe*-01^`w^0Y*ha^Tb#o_Y3JAokdDOiaw>VZ(-D@u(+y^ytx5iPYU}N)JLgsr|QZ z-TEz}cm9juHUoq;{u~96Nr)oc>%wCM(EO;n@W=t=Xn5wa_qGEhs?NE&xx~-U??;TK z+SbP)7Q!w5wr$%!PG6r+OG}I9uB_75#T6Dsz2Q)R7(`LEPl8$l4?wX5k6#191NldJ z+qAd>cU_gZ@b~ZEpGe2>89tT|s}cK{%*gum>C+uGgAYFVU`%0Q;cb5M)z&WWf_pA& zwf}SoG{(0V0ER_)B6Sb=&6fd432>Bv2U-(7&DP~z*cc@yCf*r8emnx_erjc2=ByBE z1f3{Eedz1JojZ5VMH$?h8?6E$tWXvlx0?7zd#MVGDM=wReuUT@JOUs`TOB!g@M!b? z_|>d0tpP~P_sPl0AxoAl`3Ymk$FLJ0)8-F3U=vn|ts~UAb7w4p|7=`bTo_hzuqG=* z4GEK$Qcs>B%QTD-4tYiin6PdghsD z{u^UP$F7GX0%uDBb!XwqX3UuJE)D3aEyY8^jTILcWBol69TQ2mg#JX9g#Ls47~)N4 zA9Pn#v-EP4SBM*#8SJKCBx+^|*MTuQ@qe58{>+duR%o=WW-yJC*8xLeVXL1Gd`vcl z`m;Vm-=Pn!a9`{>uhi7k>S@!aeS)!~aSyCdXGa9imRuQbx;@&fSFZsui(9sAnU5tw z_;0P&m|Ly>=FOXIfkl~jyf1Y(p zdU`sh72s-dN+R?L`UW86<>j$HL*H5By72k+>(}qc*zhrWtRY>ODOc99UAuNY_@f|$ z>D3Z};0_J21QBW&h>7rdfQPICSC><@LZ6^-&`0PixGiho!FPA;*bzg=1nWFM*|u$4 z+=}YhkgiM43N_~?@Q3Nv8$On5SZr);G745GT$%IH0wiP-=oqI=3w?yXvecjGb7Wk5 z_wGGO#{xgqG?0(Y!;;$-%^qqbn=~Hk;_B+!4^`>`0|vaDkdTmr9|N%jk!ZM6mSs() zxwNzti({Vc*RS8J7z;ioT^d8&V<{d&MYAgp)SekJV#I3{qI1F$srei954xoA96EF; z|HT(y{3FJIjs?Psu6%4-Hb!_1W-sypt((Zq08va#Otz(%$SM05g+g#mEl)0oM`T>x z_?WmfW_XNmb+E^QIQ`G|@85q!SXfvx=AUqgYMcYF+=7_sQ`{5VwQE;e-@bi+%i(#F zXIvc|d8@%|q&nlG`oV+xSyEC`)q({J z7Nbwmx4e&Cn>svl5Wx?3YtyDp-!5Ic45IIcOr1LQeXUkofC3q2$T?k_)h??VvE-2> zM=pHy(MKNx9`q^g+kQM??$DSDg-XUm?Rh%+MECC90nuR8DR%GP9gaCFD3Uo-ee)?g zUUADOC@3hhPoF-&Lmxi=_~Xx^PkG#q*9I zKYkO{Qv`*$(wx@FFi=JrBqk>2=Dd0H{LyFVJANTP&il08{Rod-u@Ti!tbW#`W55RrsJmBl&>gozJ43M7p_4WNvbaZqf(tVMsp)Vf_2hh#9d?_9Hc4%Qd5RWa{kO!0UX4D$;rugH*VZ`VC2Y=UNTmv zJMXKu_j|l!t2JuPYZu5QdbMud`l-hrdu#~OeRSf)i4!Mm-MaN44YY5;tRpT!VA&Mi zo77DqC5M~F&!8tICEeP*d2{Ia@#80PaE71{&==h5bme{2`a!ii)>@;^+`m5olTAAj zMY5sjR0NT$SFhd_6%};>)oe^CN34Kgn?F|6C}HB(riNP^Hb)snRNR63aVN@@S9Xob>KtRCC(9qDd)YQ~F$lhR?_`?VWKuMvpH-<8r z=vBiPnJ@qb))AHl(40JZ@(#`s=j!e4Jpt#=>p9F-af{Q3x3vpzduvI0?u17HkeEe6 zTtEZM!89|0Yh&&WccLdunDF+ZMT?g1*|R4$E-tPZH6_do22hAKB%2uMDv7nK77&Q{ za(@#Xitl1yVyA!!z#!m1bLI@eIqcoLHwNcKK0f{eO{1?+7_L#5Q85|rOzir#L5bVR(*VhO8#J*d$Z22-j*7N+>%+g4p>CeygSNz;N^R~2d zg5y|_TJVfSSf$Pqm~d~XFLezAX;Atc29LgqxXBo*UvmrbA_l)_&z`SQt1)u;@ZqCh zef3p02=DPX{2vEoINYV=`+8V-AUuR0^EsRY&V`?o6dK{CTzFfY;4}b8##TuR)1y57 z?ZK~j0QDr#<``5Ih+#;VCDux+VMa3ee{NNV@_jH^ux}iL1M>twwktmuDKy5`#tBX% zg{d7cygkf=({4Oa?a3`dZ$8+FMfzj#VKD##*Rx#Da5x5XK>G9V^yT|_obR(cKSmdR z%#QpVoX|8;m|E~bbK${hTV7M?z~d(Y)}!3DbmIZ7D~CZUSN?z9_-7xLfYOQYvpqjX zYktg@M()W8O%n%73Y7q>6(8_6eDK?Ht05=x|84kpT1h~W!r}zx0fEXGuI5IdNhS9g e?3S7wcyyOM-n-02&+!#EIxIce+u1iBJ_{ZA{D_@u zcDyU!&S*>E@jvsdP}X7OsVqtzrcRKo6p#%(5zG1PuE3<= zA`eXEIYz+nMzu5Nv5#1B0mMV61gJgU$3uY&2C4%-{OZX9z2jO;A z)(PKVr@6=i#BjkurqDkA{#R>AS4A$iGyhO_NMg^dSD)p|oK4QcspI_YP>r^*N z3b&fROcF~|Hj+o*^CBOj#tYu%aH(1amHBswiR^;-Nj#3)SOY8fIc&ArMN=|p@yu{r_2BZ?Or-rRVji`=Ib%+v< z;*cH7+w_z*3JRvf)}LMwM*}~eM;M_Dq#SlTzto*^p{WCIe?7{ryVBlwfdhxa)=7nW z_XK)_SH#hvC@&0Npp{A5&lhr3C^V}$*ZTu_3%_A(ENcDLj}1bEJQJZ5#Y>0scHaj_ z_d6p(=sXaZ0mWy_xYbfvVoAkHI7DT#)#i#U8Pcn}n;RJjd9GQSVCwRm!MJa^ zA_PnC<^*qF(!}xP@UZPIW~F14JyswE)=-79U^+8sr|mSvspCQxIQKTFal^^c(I|rd zLS!qoj60ah&k)=oai)?l>@A|J2i6WRaBvmc?RGn-9?VH&oQ%=-^t@ZU+M8IznIi~t z$7){SyR*!vehy#>y|DhFJ7dLR&~gsYy_p=XFjrRgiMOnhc)vW;ApF&MeLzN4!_d&5 zrc#ihks9`JzcI%=>TrPBjSoCgB}H^bA|nlm)cL0qFzE6%^wrJCbNHjmVT^dQR9=JR zo^(OyDr%iBpkDySsCZwm1J1>CAI}rI8*M>}&SC+O2l%`cd#|u;OjzMGa9NG#dYcKx z6xwe%IXSIG%*Wtj1zLw(TKkuF1X60QRyPA)MJi!}#^r@S1$(IEain=Bd@C50GM!ah zqYHEQVAHsr8}34>{2P`#0Riz}#Y9`JjdROjf2uD%m+;Vnmx-o9KL-W|h|LFL|Dk@M zEoXy(a!X4~8(YuC@Gt^><1Pa#qeDaiYX;>o@3o)MJ6yC7d59#DM=tVD|I!1Ab9}C?WzDw zLS4nH(`DjN9BYiRjQQ+!P;*j^(bRBsRZCnhiPvy>WyO6(&ohG!Rn1S&Af)|W6)Ri1 zWk`FwK7dpMlwNM}>-I@~5%10lc@$!Dg6p_2zb}jdx`EBG|_Tz63 z2I5G&5m+~~nMJV0Q-uPlFa8cMrT9eD-F^TKO;92W5BXX0>>(c@natK}&@z{fWW2kZ zV_}}F*u3Jyp+|Mik$sgSuF(e?Ee+HfS#+H!atf0aHhPPi>=O3(_tP7NKJEl&icI`I z(YyS`ElU{_Y@+pMh*e`>{gj=KWWA9Kl|tA{T&K$5L5Bts=_dvGcE3Jqp)=;*oeLOz z0{u7~EW)&iUmUZXWl}t{4LHw0UWMW>n*LP&%kn-^ogyAm0Gua-A^cjhI01ywVT1*VjLa66 z>lhL(>KsKlqW&1q>}JXk_4o+Di3l_;UNP;{zR1-oI-0LG{@3K7Z%7?>nG5cHd?QcU zBruWY!m|t|iw*Q6!-Bu_x_S z$xV$7>3UNYKSaYs%={Nh#3vtwroS&?_XleOy}#$FAd$2yLu9UhFJVym$PJOCRNN?(%^x z_PU_MyiW6uNjo2lqyVDVyv=@t?KF!5+V`4WFt$qI3n!o7BPkd=Xu`2;%R2Q_feRWH z;d*o;BPR;mm(Pf}DzB(`45=n3fwEQ47@&nk50NAX0)F)MkznH?L?$-2o`Uy|95Q%l;Z&Shi0u6kR_+k~uFOB;k4QiZ0&2ct9P?V98Wg23Q>f|l z&oR!fW*M-9YEKZImyr5`IN{~PpS;`V%R<9sB$tQ~fi0bO;*6*xdvqtUyNV-c~TfqOcq^pV9zyLtX^G~dI(l3PDHt84ESjUJ|G zfmANRK5Z!L`UFea-m2Tqgu}}vW&zj&d3C2UOIOS$yRCSaS6BL&N-Td6@NHic6BFBD zKu39%Kto(A|D}oTt9n4gCCc%>0-EYzAy1${EnjHA;{6~&Ped{|*s z3AmAkoKsf;GETmA2^kg~RuWwYr>dpo*`Dch3o_6`BEOqTG4ara>QXIfaK{E^6~~Sx zLz!y6T+!(YKYP2*=Zv;lB*6~j0Eusx^idN3aol!oDBJ1>XoFvTW5^Enb?PRr*+7xb zwo+KT zd1y*@>p6kNH`}jdux3%5*F~^~7cKwop->)63Vj|HjRAoo<&Xd6{Z%2e4pd8bOfe1M zoPN04^Ve_uqJ^nzIZA^DtA+-?Sff0+-W<+0?_A0`fg`i#W!cYT3;<&dWMX||#N$-a z_xuwo02~K0X76L7;WjU|Y7yWuzJHBbfAmBcdR-bAcdVKw*B%{r9NA(W?yr3Sp%yBn zh5Ph?o9Ts8?!yIwJk3j094gfW)?1iNX^TLoLq-5f2-z8SHnm~7 z^70a)hEb=9q{kKr0)a>_2;0i7LAf4|KR*DmpH@A}-iR2qh^-GB(FmS5{^#Tuf9AeB zrLK25pp*r=!MnKr|JZ@0qGe+LWkdp@^+U3X2rhK~6E;f}QY*!+DrfO;++z-j&=%W-A+-HxhRhgG$H^Emm! z4zf3w3?qQSo(_a>K5>KH`ZCW0{I>UDY8QzD2}ltG#`1i0npH51ODeO(=0x+{&lmA6 z1+?Razxk?rCy8-0ts)i%X6Zpa7n6KF$^#IY2+l4F0P_i=Rsu?{`yx!*$LqbtB_%<} zQ>5@NAkx)NM!*jae_Rj28vY9M`)h;(@v5ofwI4(RevRD5>0{zy!+(%4q6OR{nfjZp z$Gq!`RJ_HddGOjtXYowu4bZ@R63N&4zEl5zX=EB;4Iv06aJ(wxk^{A=#);`su56hGGU~CC z{<2g<(R9@D7rQUh;&3hS^_iSASAzyN3ciPbu=j|)?e@MZ&GCax<)c6Z*2ZKsRGq9< zCNJi_V=tw2qVJc(I0CnnGrByA*HsMmua_Jfbn4`t_K1TLRSX-zZHP@r6sa3qAuy_W zR*8KSr`3*DWX{pK$mBVO-e3T|7tVqB!zQaQt@u5aL$Fxf5PgyMp45c?aTkSN0a@N@M0$2U6K5YX^S{pmG>7cjL z5DN-9;gjuBmiuOxdBMv!)&SY&q{&*55{EU}j&5m1P-aR>3Kvc58zQ9iUo?OO(YLa< zqiW%vuax+xH6qvmJGovjPAgxYts8dO3V8vJ9WOW&pnab@Ca4m3LgQE zK;O#+4I;dH(}W^4s1?iZc%uvyL_*mN@cUI=)~@p}=6+H)pB>Rl^sR-+hbXt^{C zXQLurlDOIJ_V^zwOxy{?rmqCBJqzT570blD<=x?LE7AddKWDIhZE`#Y%r*rCKK$eZ zp?iD+Y1mh0OI0pA<%w7kpi~PoJOenm73sc82c2EzOzkFY}IOC-PPOJJmaX zs&UFkIy7maC5jn;t;Vu%N@l@{FJGA%0Dj^J4_SbOxi?LOAH?pqcJIPrNphD%I2G8C z$QmYA^WD*`HP8qGGA5fX7x4R>3$qdTHBN{aYd0>{W^<3^-d@x~ZwQ}fE;{P<{i19d z3Iq@O9v-Qb5X*oc8Fi_EFfj1;WCK4D!*aX=5fj<`8(WvKrRRaGn--G#Oa+8PUMDaT4 zqeo-1$udL1fL393kmadMT3t3k7~}SBw`nj13e~r1P+<0sXa*nemkd_$x&Po8plg)t zOLUZW3}7FDE{>RI*YzjrWUK3*9B!mgPxWQa3Iojwwz%|!o}A4^MzL+mfaTQ6@l!X} zQN09Up+xHleD&rj=oE{Hb6v`W*i&Ek6F^inyPlXsAptDj@$>VvP@+X>wS4?7*Q>ss>|Ec^n{SFZ9LCHSbsc zV|K1?EPf6S+&AG~>8`FIj;CDSK05M!7YEvQ81d* zd>8_7adG+CFP+(m4cNF?C=hzwhkWMrH(%zS5mfB&Bu(`r5n6)0K$BxQZ*r=wYSf+J1R^UDD;mQ4Up z2~qT(14Dz$)_~=I=)?|0Yqp!mtWQnr?d=VZY-GBXho~WF+a(h@?EoKmatWpVmly`U zgLEVK@Gta8+g`|Dj3{OvC>&D{J7Zae|FpKf4ZG8uy4)El1N0dHS+JD!WdJX70(>lw z_xBAcC{%CF{}BMK+Q*U*wCc$v7`+73U!P9hUhuF!JS8DNbQ~xk8(jm`F0C{$uDUeJ zRNo@J!>W`Y9J|2!H@8Me?d3}Ru^Z#u@a(yALmIF15H$vKw9#Vzg~ ztQ5|7d;amyJNs9_4MHfqx*GlRdwqR9b)zrh6&>9bwIQ|<%yhl#dsgJ4+NGGmG1f}x z{7EzTI1FyWzBCSFiDaqBH8ND0oY`Ryb6C6PN^U2}>l*N_`}O5%9%5PrKqU?BZx2G_ zwf+0Or9q>_N?rZ0S`@&_mr8shOU_2BUU|UhnoL-@JcckyP*ikc70+WG8Uz?{6ftR$ z2tj(w*JA<3_6@opJnXQZQcML`ePmG*xMj(&Wd`=#IT zWwwGIS{b^sd5QaDHmwj#3|2!^cR()W(O`L;BTZ2#2V!&1w9f!T{5O(532H3^bKgnX zhw!LmahG6mj&xQkq-#OG{s4dcD6N%`>l-IBM}sxV3}mncEz%dn6;4UbPiIQl{W(f8 zZIuNqq-t1=(P=0tts=LCu}=Lbw8bE|i2|(Bv>+6Up+!Z*aIDMPN~qGeZ3` zh$}(@r!tC%R#Qk_9?o9&`Dc&%8lEfH(lR1PJ@)v|e*mGEJ3r1c-k%m^qU+PlmPU5 zi_cbQjmQ^UV(21~WACWu;Q+pdmdL{sp7omp=NM7sCM_@JWvNw4ufAf0Ue%YBpe0ydjsS?@(l(7+~E=ve%BACh{G{7uJR?^o9&77Puh52 z(Gx7I{9Aw0faq?T*Udio4;4gopk}{i)8!;3N~`(&7xe7pLP7NiY?O^9x?pySPFx`= zeg|^XbxWV?SX`M~kLba49f)2oCx46dceLrs#ykb{Ee=(Y2C~9Zt)aoEiaYT;Lc+~{ z&*Oy)IgYvXA6slxSYZ?jk%lM_7sbFfSM1sZSAe_v6U?fj6v#m0cqbY;6VFR>Zww;#q zWqqp#eE$hAXCO_5u8uI(4WbO63zj3~Rr|e8B+<%!YtdXoTc|W__pr3wQF&v5E%$GC zEN4C#eN#yHHB}jyhYP@oo{~=r{Y_;oUpEZO&7W$zIVDQ zz3A+GK=qd(SZ_8amas!OQ~eTd2r_o`$9@viX!AHZL|5XoU#MyO`e1I#`vcSscLZW7 z@5b|y{sTS~nL8*6uJT1T8A)sl+G3@cQjfjbqCG#~5XINNpSMN&wEuv0lNAuToF4WA zBV;+DXsKFV^%{YDN$`9}B`}eSlatf;dVi`jsQ(d;q1%)M@4NBe_YbGBMp(39 z%JJmm2{7b=tU>@yagIkEe}f{uhX48o|17+UnT;@bytYPf0=zX1Na63Ep@y&#JI+-S z&nfeNt~HZU5k<7u#pOkrbd(YBep)TvZm=evi!FOa^r)-jcVOZi%3J#Y{SG=yxU|S5 z)6uQL@96E*M4pTfi( z>|9ush4g2bAA+OEazQX|6cbZ7J-m-XQl}cS=|xxU-j9R7e-~c5%KALcr*P_i?%d{& z80VBjJnfJOiaqIjq?Hq(5JFnUhW;e zYWg3fwR&p*x_{0e;ap_}pj3-khRRtQuwtwy^8;Uwo6zL=n z&_u%s&txIhLGP;D|B3JWL2>P zlf_221v+Qk{UpHTL;XT2S)R?v(JRJ5pPj(L?pPjhKZtAfM&oWt7o)A zNqM$bg{Ez|BRS8#irbku;Fklgxo*Ty)EDZd>dCNu{JyTj;A_(x2ruI+Kngeg3rPP^ zKvYXUvSbU55=NkGrY!#eDOI=WiL%jxtfe^&KlN^5r!5)y zSF<|C{#eKIL=~7y<;nLIiP$40s_X&0gIs*@MWKAccScHE8QKp35*4q{OJJA=ra{>c zfxgnp(o?y!{riip17RmoqRgZk4Mh(BnNNONiIQaQEg!?n^D-6&xex|Plg)lTBFJ$t zBp$f})?7y{FS{KVQfHCGd}QIm*n8{oTrmg%Dv$e2hA4{ywHiUNOSx{X*?qXtZjP1q zT8m4F5(xz))E!*q$9GnOSal5Ex<5$zH?k8!_FRVz0q>{3ct=kA{|Z zfeG55aLz*|0u zaGXr@h4 zA3?u6Q_(++HEkOTM>Pt)kM}E1j_pY`0Ej*HPabsM{$VWeLspup)T=Lgfx9L?xu*Fv?ncng8 zk)Yt7(`6x#6zSe~g^~TWCaRP3xm??^^As9_u8F5#4<9~5<3){L2UmFkddc9atHq|y ztwskP=PrFXSNs1t#`V%QuhyzIWfxyT`$h0@dqhW1&qs|d$HH$z6BU=PCKrI9Ylw)W&czkeTg3QJ%wQN_t_+Gqo%CCvMg)?x1p1=SuZl zsQdO%((@GE4n3(Abu$O2T0xsy@rbumhUZ$Be}rnp481!`<{Hs!d!9I&x$^UHY$?+S z>SD9g8bB(ks+L3Ch3xAQDCExDl$Viz%_cgctr)Xh@lX@Z$Q*X|E;fwj6AraxC^|Hj z1N*z;3+zFW!>Ge60y9cdnGar7C#X*L&Nsb{~f~ zWR=BhDoLO`j$N^2BfKDZo9g&?eGG}=@X83^(UDrp_gh9_NAHvRajC`!CGorf;aS^U zagW%oaz%l^AgK>gcr(BB?~KWEgrWv}YFU?!`2Vf9%lQYYsHh-6-V0X2-n*&Cv=zmt zR{dpJ=`ZFGb4VEx2)f3U(cq!_b(9IAuXnztskQtrpY+wl=>DrVH?uT%!)mCcNVq$L!6g-0xy4R2JK-Q5{`I# zQ~xEA5$ImRBQkK`oy(BcM%5-x)6Z-@SuatpUczEB?{WcW8OE3AB5AUS0ok-f#e2A| zK{n3oX^vyUPUWXoh`e*q#Q+8aAQ1T{7c&ePW@7k}ujjCcZ8IL8Ln*B!;n|zNW z3u#Mn=cCy7eRcu+t-*m0A9@)r!RGhC_kNpx%2r5?tr#n9jm-MPnRTV3%)TWdp$d#@ z7^|}??5cEQ0j&+%5sqsyj{9AT@*JM{lp#rAT2!cW|Zno zx~W;Cp)<&_W&!~Zuc^!$>BYJMk#>gU!r{!<%o)p4r0H?_XFr5Qv+6aaUyTU_`2~Jfkm-%@yuMhsg zMMiL)iMC!4tGFjp6{p4Z+k!tf6N@Nk6p(WCy>N75ZeYCt>k|5!)&|TiwItbA*wpLz z+UlIN`wxDc5N`i-B)(y9XE|-io15ZPB3wz(euy4 zM=AonmQ$bT{d6pPc_HBrObA?l!{OYxee=!%enQJ!YGT{mGTqE19NF5GbZM~=%UKj6 zs0D$?CVChe0n0pmkv<>Vo%5SmS3u1B@^|n@KO5CsbZXX?*cFP6v?=}9@q9l{!S_Cr zxl3^AdJR72npbVKe-_I4>I?^S7uh#N7HIOoY!phn!zjWBp)~EZ^0M+p=k`eW!{fhO zKKC4tYr(fLpc!5$!83OP1|kDM%=RC1fBQ!>MiyqtcBvtAPxPlp&8?kau(N$CYksQD-YlPbh4vhd%Z z64NGbW_#Fl6DMic`pu(Q%Zx3eHZeuz6(qI=0V2o-HCJcQ$VB&EQkY#Ik|XYZ*Ws40 zvcP?JOxooV`XjoTAtgz5J;?%*1PLRwMtsQtFX9}!@hlk#yBJ)Htq?fnC!OFE=odUr z6u7NCS&i>5+JN!eT8Y=!WW7ye0(#)#368n!w1VN}?S&w~1P z+1Iq{P>SeDFu>^r9j3?mWt;NF-u}(h2F6;@*|%8HdTC#bUApgVsY>6%{rmXisru_C z%Y3xuON9`)YN2$UR+Btn3``#OD?Gj+D+j>#41)hf@f2T8;uKMY*DX*s&%zA}vm0rv zGYb7Hp-?4O&A)5x^F!*_Sdr-V`@0BN73^m5YD*l3dwn1mwI z+gQL;V2|m%-X`JHq+P&LvKXBA(Yx*pYf&H>5Hy&(PuB7pH=4g~Bao)X{`KVQ>6!8R z+LXSXn4H?yoH(jspF+&h4tikg{u8)K+l4E|=GzXu`QCI5oz7{zW)M-RzFy`@>FRB? zq|CYJ4P+lwbct3=_w-S(AmmK+*(i$Hw3X;UvhCXE{~ip)|dX+qill&JfM zx|ObMJAB`+oVn4xSHE}KE^hS==E1~C1G2=U00BkhMm7QUSbk;jk2-}0FYQ(Hej>>M zuHU)C(y~;njtvB)Z=MaGsqPKFCfX^TQaAd{I8G^Qp{i8d`gAvaPq3xD{mhknAQFNT znvd;07tPHoi^0uDZAF^w5hst2@aCEPI`SZ~SY{lFmt}_SZ-}xNeaGpwwZvo1U33o~ zyo^pI0V+!7?f^G3*PFx!uqh%z2+I$mAI4D&pw_S3${MT^C#-qr6dNJeXHD24tSH9# zgo$eXE*KxR=sqT1_Pi#N8nFK936t|kIHx*JVVB-`Bh_Cdz$0*H>K_@hEjkr)EH)~M nT_xa0rZ2p8v=`VMwgSqw1s!`SXB7Rn?en9IvUHWCN$~#w$6JiI literal 10486 zcmai4byOU|lb&5k+^GN3bv-?^>(QkVinb zlU9`mfQEQnq$S4VGrg6fmMQ=QFarQQ0ss(?uiys&;LQU7M-~7engIZmZaH5x#UC3m z-zvYBd&I}<`b3rPHj1tDgVv1x| zQss$ELI?W?E(!7PKk$lm@;7PwPX3o43{Ccd9@_BUsL4kQzSMa&=g{>4wj9#)9wgYw;=H@gH9KK{s?Be8N1_8W< z1Rh%Lm&PAfyYb*rGB%E#3q+}riOBB~+@@X<`9mgIiAex!QP8vg-XT>=+N&y*jC-f< zGihyr7XAly+G)|_e)qA?rnKZGG(x?=lLM7nrPk&93@5eX#7I_$g8kMX`0h=}l`HH) z=bpOkBCx=z*-fyr{yp7A9F=%o*qm93t_#tB2lAM@O{fX9ju%X#0~)nRUMvrXClh9w ze8|a0|0}JJg(_@$2wItI?LUY{zF78o(P2BR7;aC^@(jOp{8RE%U3m>MV5%Lu*46b@ zw*c?Nweu!TULS~}*9mi!ejNfNa=`po1*!jiYK)osxi%b59(thEyUZ>#lX@uEXSb_x?3)0kvB?8*TAh)7}IbzSm}5Ia;_?10{}M; z7vq-OS;Ayk8%_c-gg1Ee0FsrRU5phNs#H9Lp!1t+hwyK~9W0bWCxuG$LM~wQuumEw z=fbBD@sQE%1^j z`T@`PZLRVyWjX@*tjc7r;w$H~aW&7vu?|war?84^sg!{J*RH|mhq?KTsCVQBC1~fR z>99jeR=g-Q2b=d;pKwzXwYjrG>?pd3tFSsHN4in{usYLdK;01X2BdRLFI`cuB9yI) zI_ZX?7_(bz`MX2@^mCknx7 z*f}KV@}TBBc}CXMR8T_5yInD3p`KrNROSA;HoJJtlNG3weri%utO$eeY0 z+w-NEn;(;UCBk=OM$f%=%ma24wV7$idelqyNWI>sz1>BlGwr_3UugqVjY+UYyi9P) zxCB?&rPUetoZN?|*D%=hOOJ_${JU3GRjppY%&8Ws^G6>iokr^Bmv1&*@#2#5mXu05 zhPVXaQ`qe5i0lP-1^XL45x`ertKU5d-8b_?*1+tSU!qCeqD9gZP_>ZLq9p)RKtV(B zOh&^x>gV^eqb&c~Oi0|HgGG|gjpbR`9aRdZhOimvS2Y3e?eCFiw+L#_mi9j z;nU}gih+zTn{nv_|L}IllD1Dr3~@yitI}+4C&+;SR+cEfelqJ?eUjZ%&Qz)W8S750 z+vG8Lvo}xXz2C}S-m|9*uE?NWQWT#W+p@$DkH8wVn#=gLKa13M!Yva9qsfE(5Z#0V`A0pN)Ok zP*Eq0(~e$~m@iej0#Av_z703y-7|W6`UuGDS8fpy2rUgINZs#`33@@0(S%~%XUO5G zscEp&x^dU`8syC67USOswNLq>Z_}q#gLh2x`zR)0wvor72-IW@oDpnT0x zWn%LZ_yvR*7geY6<}MC~SViD+4`S9XC|L}N0ANpsUU;50sAjL zb5h>&s<-wcdf2>}P91QgeAu~ZnB7;;FkfKJp^8ne8!-`jK0+O(^`s~#RE0@)=IWiQ z@(vh6D^4jN5ih;*c4J48FMC9MwoN(cXk1Wiq55Vi-^X#p8R_(!y81}YDdMefwdl2F zNA0n}-!P4!FaCe-jnf{^I#?5W=%9T1C|$ z`+tq*x!rEx)Bkv-eO9$mWML9_yId)A_OltKIH-X=0eJ`Opqqj&s^T;PLIZXJ!pEi!=3ZLHPGi*~?<(L&m6;{M(636VC<08tan>&c6fW z%KEuUN9x|i7Wc^-0l&Vf20kI~_XfD4hEac=&}5n&MoYL`Xsx=1po#V*6wUpwB@pu* z*@2n|zglL~zr$9&uOd9_%)GWk&0UN`<&GAm8=Ba-@MT&TH*`NHlt+CMi2Ag;LgGpm zm+ybGL-!1Z$kBYk66=39zAsErw1}|-l1npj-?3g1LE#PXU%%_{8kO=5!W!6pQ?z&i zc_MuV(xKMXSA0ga@IsiwYspm&d4|n@L_zji`zUWxsM}|=@R}BFfT2P!uJcrQf81WG z;7~y_$uMK=ih(2hrfqIGOzb(81e}^7h$dQ*w9&zG_k*kV{ml>Dkn2!p9tb_+Sa82P zf!TC+{4a(i^7UC$53;w?sleb~lFWqeCjv5msi}#JQ!wJtA>=k~`WL0M{^a9PG3%vT z6x=jB0{7wX7$gs%H}xJ&s+hHnzrl#L*=KB8OZd%sPoxKs(`;%|I$(^;nFYa4Cg|3D zmbQ)m6I_Y@t)A~{YBRo!2sYI^n!q)$tPp|m&n1BkYVmX22Z+nY#4N{Bb0!Ko=DOhh z8)8*=>e(W&-%LSWUN;u45Wex{{R747!a~45S>12$wNc{9N95&r%gU+b#-B7PcF%`_ zbDPAsmvpVBsQpf}s{igh23+1)`QSj71!|zjij@kvxgob&J{E97Lwu==Z)RY-lujF1 zts{7+jfS(K5+clZ(CY~%ks(F!=cb)YtqEu(dp_7=A?O!zz8KONrrma{eU-54%}Dm| zMb0!-=YUH?S7JzBX|TVr;=fB(8}a+Mcip|v&=pAeFMCaHj_Nkl!sWeZSb#k<%oczm z#`lGsgJHo7RywsRYYQs4O`J_C=fARQ$)B1peZk)|&ULCaa#RJ45lrml54sxO!CCv< zACe-^PSoZc!)x$#iZa*NuMlS%Jd!_x9|UdgLzlGyF0cI$EUFG4O;L+8*+s;KNL-ld z?R+O)guOt(>{+*e-+_A{1MBbRn&>53j=33ngVZ*A9^^??x8!ww@-m%DVVPmliJh;B zA?gVg!0|Rs7)?hBD^!lSxbI8;-8Q65B4DKw29-K9_w0glvBA&vz=a(hBCWqSnbKS0 zUg%$!iEY%1jOqivHBW;uSX*e&(J!Yr7cborEc&_4TQAAt(Hs@99pynWwVQc-PD)!b zEAfVEq-cX>10nj+=mUt(v;j?>9`bLJayfOcTYEOojVJwg!qg=XHGMAonnJPa; zUJ!+pYTulTHW%^S;&|h~V3suNSc{q3^zg~L0z(5QQ;Fz}<5*7QiE`G{EY!_Bq6Tf3 z#Y6<%5EL^6+vT44<%^2!TOb&Drb?#eUqR@vqcvAd=l_6n*oWcLU38eLio z&XA9a$>+}PoZ&n7&1;j$MfqAp&SK~ziPsl|%{|CWXWM9wxyVKXe0%lk}rDC8g z8X@%6X|;SG;muLTK4d!cPgVxqjvaX=-$(Q65p5S*rI%=0cH7U(J{e1RPLJ7=nOmA) zMlRB`!r37ZXhzV+&X?quSyu}sbAn^a+S992*Te=%QW1izNzH-(Fc!u`0^%jIwx-q{ zjJ$P>vDS90xVX3yM??JQE(8|%*Ent^LOWJSOM1DpOGR5rG_7xH(O_SiI zQPhe?AtaSr$aWQDFB=s4vG}6A7sKS9#`*O?Gvb$VpNFveZ{M$e6gN?k zBAf6x8lMv8irB7O2F*?SxjQ+G9(Zzcf(-v6B#Che%7km*jk@ z)2}#vcILe$u75B8OqP#aD^OyEpX+8%bA;T*9+xPtBOA56r>VBH?W|l@4D*s*oHF7b zKiEI(=9Q&zzKDNu(c_-(iYp|O=RX90e|T*1D)Vi}F|XXxwzlFY%vI5oyr@gp+zfor zE{L0=4=<&pTg$Vb2&yaL(=zg-A=-V)<6G@}QKeym;mw^FzryGI(YX6E{x5!pKKNFb zX2wUTC}&?H`qv0{Ouyp!O!9>BD+&bp+x5*hFxlEJ|Jlx!dC36CiNWcOOOUw5NPT2n zckQz+nHS7$v`1`e33@@emu_-PmpnE%>A~wldBhO+8|uKd(CXF1LguU>p-iuo+6+#A(zwt<~}iz8;e zi$`F>cJ*M;o0PM7dMP=uB26set3i}BC!lE@>Gk`4oZQIG&&(O{wh_khwAz^jz zLMdgg*JfCk1{LlNW)C?WLX_!#5OsEIb3ZPWV7*KBWoBhmt&{(fw|eI)9LZTDrF;Cm zrRI0DXcArT*)L<`{Gy!R-`j)ca2)6Ks~48Jcl^Qg{XgWYyo6RpJj`Aq>-T>){#|lR zRPY`?<2vJ#s7v8mNz1zwnz@<9ofov5TnYTqj(PJN^Hv0N1N6rZY2Q2ixJ9IY`5B)j z?o!|2DLA8bc-{QD-^}@UP_JB`BjVr};f3o#5P`$++U2>eVvNM%RKxPV7J0hzme%(z zR7M~;#x=}vL&%^k)1dkFp)ApEinI%CXma_IcfN1= zghNTqbv$mD$mXwAWysU;hUAFR0^jhAYjE}TV=j$O0>v_@{)|7er^HCFN$j4D(Rxa+ zr>@Me?gS|zVlda*cn+sM7^g8|~YJlBlxK`p<| zo$B!mr$%Z4An3pBbh@BK4Hi-E7l^3GMOiG?^~~z1Oxn$0PAR&}&*9D$O)(_>aB04e z*{ihG%K2UZE9c%O@J$1R+qtuhVW+Li7>Bw~LBLxQ_2GJ6dWmr`sMzGzRfiKQrm?9I zR~`S8uz0=lw5lTY3!?lQ|2LJNx(Ly%0Hkj_Q0C+f8>^@`ot4vM)#Bo9*u)9;#4lPQ zkD$dnQJ;T3;cR_9pRiRuc^MkgYiS>6*;09uV{z*IYw3#i;TH$m(R{*3w>BS-cM7T<{u?6<8}o91iDU^B)<6wJwL{eG{=U+MNz z>#f)F`15Bnp|A(04!41E4ixt89MvouKW88SEk-A`6{3;V9M)Ips3VNFol3u5WiBmL ze0Uor5Z+x~NDGz=5gd!i#D5L)gN!7;`5bPc*8~;4hQOzIJ_RM07TD_cA!r1XISg_x z%9r&%6tsJq$>~|UQ1|7AZe{Oeu!2V&rjYX=>T-qb@S?3(7FC=Z^XOYf24G=+FJR;^ z&+s!YCtoncOWkA~zS!&wfYTiV$WJeR&@pINr7!v$Vw3}H92S?Mj>$ckH9eSoqhxli^L9 zl6?;LH$mT|@_S}#35}P!_7@h%=&u7n2PH0zl8K6L4SX!;*Nkxnnt~qhgVoG_|@w$t9uwee?p`9loMG zr|Qqo!ws?ZaVp;+zT!zH^@xtf^zzvEF*EJK-3hdBe&e4hTya+V7cwy9k?-&u+1W$J9MsjiXQu0{sN!(0)p=yn;5R~ zm8G1M$wClU4oHZeWuEucT>8fj9@#M0kY>Zjx}{F%fX>qa5#{2}lM>g}Xnjo}l|ew8 zkXA5h=I9hvEufUW_wOT8b^(DlBKCuM+=VI>J`Ua;1OioQTVInOmu*pv>=0&M>MOS| z%x%82SVXH|##aK|&I9wXCi2Kuz8@~`}P*VwE0=zPr%s5aHvFP`FsjEx2cBo)6ex*A zWp5GPoq0Vy74R>2aPlQP>~oZKw3$U(jAdy#E}=(clqiqe%$7=zb#t-GOC`@<-LJz{!m%n21KVT2lg4>F^Qyl9E2SvvZNE^Kq<8~8z*~izg_2G$e)DWZ z&r)^t$fjc4=0*E2GgW8V@;;-uQTLpkoe4G&6_Gi{=*bj1demc_{W*z@M)N3w-y!I2 zxt>0g2bLTSCr87lvU@@?w=y0(8-&vH2iDYp1oVatM3hj{k zTI09~y|)(A+XuR&rxolH&~6OyHuw;ulgO_ zPuTLyiVw)P|B03nB7klGZ1SdadQT)(_wcJpUd5Dw*Tl^3%=>G;G`B&%wwFm(MjZi# zMzuQuU>R1Zq8as9MkmM~4%8aV4m60Cl4X`?$zw27Nx(x@)C3hiNs$loyeJV|;3R`m z=2BoxiLeZq;~pUpKfO}+8=>;xkRT&Wh?xRT*$vA=e1-1-a(LQ&8&RQ!R;p| z0{dFY6Iuv97U8}VgGV$6PB!6w5}-jehsz>M8R?2d0-?1=c9Ek)8Yhh)!3TZPk1>d^py>9{d~my1NBGJ)ypHC;!FbEqzyVi zu?k`sqbi!2$c8~?{{=5xCd5}QNx$~UD2(hV0{VWx-}##X2uo*=a!4(~o_<3lOh;=1 zGWy!R&!cXBeOPdKzslPq+FOzt2P)Y6SL*2}8s1q7(#-PEp*Wm`{7r`W-T4WD{gKfb zL=!WtyH86@TGc=5%hW+QVgF5lmp6`bUz|y3kvDq8cEX#Zcon0xK`W6icDQ>?Gb=4k zx9`mayKC`XvhQ;fwwljzxg#~7>oUV^PafLCvQ3GNmYh3%udW9gpP}zdP01_?V#F|} zu+6A+v$!2@w>!LQS}Htz#xrDTMCHF(viHn9B@`r*AN^Uh^K1dYX%OU(L;QO-NS7sm zB}n&5G=+cvZdostKMXC?^Pljs93+p|U_TbCD$_YFH_al)C6D--qOJJg^-4S{e(_Bh(hqonQpIAR3 zLn22yQovcP8^(~lYa;Iw1iN45bC1LAyPgyMn!Us#kC~Od)l{8iBF=vyb{%q5Uo|At z`GioU@7{~W>87(`5`y7oUan|z+y9y6kLnnMdpTsuWXtd+^OE@Rc1&DlS#6q{VJQ~^2R25csGlWAI6%1)G(k1hy(%a6 zP8;j(?t{iGcAAzn*N4^9x1BG`9YQD?lsKuJE}E(!LRb-C04hKL&@?*uDt+rmq#F+E zy;MAG%p~MH`3$_n9%+YIg%-3+vV)5OcqKaeQuCmrhtqvaxZ!JAr|$dSF%)+`Yvoou zOSNuZL?Y9b&gUmyj|pfc5HOzcO#wTn_4)qhXWH?-2h*_V$bXFzOAO}R;U0Utm6jK1 zARXYF88&Au<4|bU zjIqU6CietjeFXz>A`VLxAln~?Tc3Z$!7ZUwvHhxe6;yAIYyV5DChijA_*mxgWa1Hf zpMe^m_ zi=Br9$|jmRXy`ALU7%BL%h!;kp0u2jEG>Y(3_SumS4~Ap=R2K`FOb*E9xFaK2xw@q5)FC9ki5__UGG^ChH* zg8T@CWK(2ZAhn)tl(@xrQ|@?sJZYbg?wPRykjvXSzBgO!5l;~}n=Vx=*>!3~hpG!QO_vZ7nOf(H%X8Zyf5zQI9<;&VgO`J^g!d%ci*Gayzi9E zzV{ggWXFUOwfXv^Cu9g;LXloZZQq$>osapDJ&dlE+FA zOAq0EeuKAV6~J_=V4ai?3X&T(A2S-Y-bb`Ai`xZ-D`VrnQ>pAdiPR0)l-S!eWp};M zhdf*YpjTWa+F;wAvaF(x6TW7LroZ>f%xX1B>ku{kHy23f4Gr*{SyBzch&H417J0V$b=yDLEIl7<2;YbKQ&{=ZOVvMR0}AxP zsmR+tme$kQHP;7Yn9&3eFJljv567buHH|D~F|nOk<45BcE*rk)#MT#RvWplVxMlzpi*dmU?7Pzz{?ICX{O>V+&4<<0nM?7@q6?=qp|+- z^F2j+>w(o9IZ#i9MKt?we*u>AF^=)GwlEo-<8)ZNsl`DO9Ts^3mN?;` zpu-&&=Gn~8C2og^of_Emg!Z)!`}l6?zCnvZ2)$RRO7E_te3B9iY#R5%#LUxR2a$64 zRNuv={A!3W0>=Vd9-Gygqi!GqnO4Wu*hSIx$FOH*78(*CzB@93|C9L^)cR86oytQX zz(VBa;uz&eA4;0&+0T7h>1okMFU4QmpaK8N1A2wlN0S5ncCO%AcYgA${c!kFQ+TiA zSE{2T+HSjei*$%Ai4A}4W1S3}-mXNa1B^jTL+Biw<*SD;pmpz7SdmFu%Z231W zkED`=rBr|FkuV%mCW~b>XQTCw%K0Clxj&QGIm4o%6lpuc4OgwWW^N>I z$CiUaixkCEQf)R*DBF6P&%z|)%AGchvGhBH3v_5YPKL6o6gDG~@`ZoTScT$`HQPz7 zQiqtq$|yTKXN%7 zSaCG2Ucn>50Z`>XxJnz6%(tPlqY9dGm@zHtV2!nWMmS!~Ac!e66nI-(6fh>Qh>8n)+v%wQv>T#tc54h zB%~5--xs;qRhX+bIms&XJP;?K$K2_5H1EpFn-*GyZaD5sGDZ&n5P~FndmWj1xxfxb zSocm{R9OVmD?CfFE;Oebf@%V^7{ZETZUhZ?GM(@uT|gImuIH#AeMtxlE^*teXWH`b z$LnM8?Q_|vjv^u(kO-Y$cB1?ICmH@j5PY(q zaPxf3LgA{hO>D7{M2?XnUpAsX?0!P#eL3cHStcyY4^PB2N&Y`}U05UvjiREStj@u{ z|B)ET)+LPVvkvTJySZz%p9yT>L006*KQC84JeD?kCg^7-M*WGZz006}JRTO0P{npNd zG5qumV7)CN`i{&RgxVgioKN$1J|8zAKUGzbbc}RN6lZ;Ky0~oQ8NKB$i@Y%-vQlJ} zl`p?}r=`eoGKI1dl4@h-zxvPQ3w9zN|BbbX?`$6W7gEW+^STtfeERnAG~Ic)>6IMt zBl`dQWW!)8qf+#WBd6t^ig*+cQW9)cT$Dd%#c(vk`n|T@HT2MuhN(an9q^u~L{xOg zU1n*TG?)`zM?&_B=T|%_zfSk~74hq8Gu#*b3evyT_D-I*igRI*U8lV~b;}Vb5VC6* zN5E;X4OjRQ!JNdLy-WMcE{=v&^o^U|29wVS-Ai*G+?VeLGPYm%B?5ea`$ETmbLsMV zuiJFZNk})jLMuRt{=Zje`76#}#&Q3V26Dc8!}UHik>2-WLx2j8wjJtgf9=)R>8Fj` zFE*av-r!J0xiIKZ=FWHHmEwf_i<&;MI?)S0?HXsgeSf|Vdwciep&c%GwK}|@Gd1%C zPx_Dvy-tOWYC)cc%IxU5hWFRahFgTL`MW-E!fSGl4@u&*L&JnyUU@iw$)zbe=evjM zt%9xm6Y?gZ!w#c*4uAcV=SSq{@2c~b~PFc zrLk+YJ%voE`Km;35;%G)d%LORdN*Eq60==n7~OlR zeDy~0r+Q1hk8Yr?MxH*mAXicCi|m|AtCD8chU&|oBob+$`#`K>Z&%JO`Y%R7uDyRE zF5g9&e~dLD2ZIEeBG%T{e2<*tRN=!ovhEesu24}&nrdk1yHcs8dDLSfh#?!OG*Y`- zl)1>&QXhz7mtv_3w+Onw5moujv|FvvhWr@An6%|*_K+6y-Et^B2k5EJNa(4G6u+gZ#%FB$c>Z9t9-&I7gqC#_q%IHKMfPBUyrTeUAED`RyOHZ*lE3cF^YT^w=3_J}LVz_1$5uS^En^FgP{+ zwZh3iSKY!RJ$~CpQSq1M;=4*dXx_~juMzBpA``A*hPr_NET{O^Posj26|k4(rt zAHc=6#1`I^bRXZ6#FoV)T^cauCunE63*X{8+)QyR!F=o9Dh$t05}au@6(& z@P4%cYqyp7>VNlWtN+2Ii47Yf^_R^*o!eLUA@OZ@@tb#S1I2#JB@0elUXbp6r|42{ z>Up3u^Vvfrg^Il+stJvBXid@+&EVSOgR-g$BQby8*NSE(u*Tl&f2`!tbTR?=6uY^L zPmV1#CiH?yp9-)(yE+Z_^%o?|+{o#gn*KyKpZlws&guK|@#kd)uQ)L)!OY!Knx&P| zNp@L_L}5{}qGnN=&T5asB{T@XK=76W~DvO7em~fhn=gC4PSSYs4SoaDl z4SR_*-mpJaj#5&eNM^1s-C8E<%k98o<@`+7sc%qs*IIQqXIvO>K%p$Ngxw?&ke>v| zQcU2egr?SLxJr8NTG$4G?Ck6`0s>$-n!L!VquRp0WfWOX$)?iO$Ajpk z>7n<33vGN>qFeBio7xoe*0`-?PzmjX)HUP(Z8P<4deLYHj`)OsKl5>O`J@HzDTb{>)gRHJ*Y$4Gs??reV-nqI>o2 z(XleS1}kr_l4fnJdXlE(83<#vCA@UpZwSVI(iaMo<3Y( zhf!9!Wn^ckZ)}(o6Va(IMQB!vVxOu1rxZ7Rn3G9(3iJ)iX8e$aZ(di)O2MC<+B8nA zt6QMvIrA%RZ?}|{*_{Gw`j1S~Cw?}N$<0_Xt`_=MjXx`6AeLBGb5g|NCF>X)P-S}6 zSl7H@Q0njQ{*6l%c_D8^F+_7@;f8$aaG_JZNf^3CeT~BiV|W$E`tBMjBEK&7)0DkR z?z>hY-|gMqd9^Y3P&>pyQ~XmU@z*beD)dzp<>lo(Oj4w6nKcOkTJCP!ABl5Xv&?I_ zJ`cSkJ-$`pFA3ocK~Fx*R>Y$jr@`v(xq>dG?61*zt%i?D-~m)N?sNZb>o+|vyj z-P1A~|56bKm-o#W{_6P!q7YoBA?8Tah)qBGticj0=B(_p0}|mjGyRel%+YI>KwJ@n z^qRZ{oO<;bewX{$Tg(ztZtb2DUTkJ;Ry;NPRh5(23IsUxyxtqT+s;{WQv9+Mt@Qnn zwOx4AP_7(>wYZd6?ZAelWHhVc@(q>`FjOO!A^mLr>aOJ5g1s_}q}0vHBDLpFiR2;j zOAerCR@xs&%hW_H2B&Pxnz-P2VweWj@N#%B09O_hrLaqC2c=2;PHngFTyZxpNcoK< z#tIb^`g3OeZ)c)X8zmJX6PkwtK4|I2SVhV)tB4e~U?b0!Ptjea5!rx$zBKs7R9$^i zZQB%4^xSN0y;FX>r-#a?wlzGahK5R>o}S9uL)J|qXXyck4j60(CW@6y*ea5eCEKme zkd&$kva){zSj6%yjlOHkJU^XBUnND6@Z+g`p6E798cw4GM^A^H&~p+e`9?j!-{uP4#( zb2j-bBwJC$yC)}3BE{)hSxWa&b#RgYzr&HN}Y z7Ku~xdvis{1PCP~Z7|A9mtqU;tUl_D(q?ktNfV-~ud8FW=J0K}TuOYQ|1@)Dz$(m} z*-B&|oVY5BAvH_Dt)vnZ1jpFUAN(8xOed*0)^dv6r9`S*FlVyM)=V$kmGNY>C2v*9eaBUU8IB93V++|Aux;(T>}Q9T z%~-`gM2_p~%GaYUXQK z6PXG&_M+yM(zm%?ZkJOon=X)?uop!c=pM`cN8p1RvK;K_r7Y`6uEHZBcV7`a!ZXap zS|9d^O%X!cL4UbWzuLN2IL*2__5+%{NCa?ti5~o#UQ@%fB$8AG&1<9+uhwK^Wras` z4DsP7zU=JmoFB)QuLhKV7ryu^cPpdO`Qt|nE9-D-EtA*iNsccovR@v1^ktf4<(4-1 zmB@r8@llgA#O}<8w$)ciOBov1yWA=@;c&Y}EELbm{;OFebqSvNQwp1m>6V4Aw&`%D zaO*$u6mtCdm)lRIbkBFSgv4(il@~f$Y?&S8;FVc$Pmixi3&3vxL)zCEg}l4FuT*behEKMYV~DPF_4H!3MgyAO9k?H)N>5*- zuIwNe&4JxVO_$Jft`ze)-(CrKC?J>0XliQaR#!V?bR{DPvDb+uQvS_nf}QfCgv{_t z>Zzu^D;b;aVDRQi=_!HSp}uWPW$80+l7u;@WzcK%yizT(-y2`LPsI^>l8-Cakh{9I zuUf18fv_c#BTW-Om&f<t)e9l<2>wEz%eMmV3ayckm_V0v zKFd zE$!H$nT!BKw35QcH#@e(;PJv%ytPpk1rM4-V_jWOK}N>y`mfcPU+Ndb@UyEk&7r9u zU(9?8A__JTT`y>%W60>s+?FR2<~HbfJ71$FG2f0A@K9CdAfu+ffv&kGK|r`E&COlS zFBz&!|LpuN6rQXJ4}39Y4h{-yv3dLzV+j?!$@(B_Fw6cRXUc71(4?Y_}* zMdaZ%7=>5s!W%*^1pUU-IdheiHkRzvzZxe;oYIO zx9(9u&!D%#e4WMy6@El9pWaJKO6GgsSoA9W=$tA6J31b}t@=q_&i=m$7XC^2$JLHa z&P>oe&)aMwK$k!iNJ>egr8rFyfNyhA($Mhlb1n*;incWtZx>5x!V(0v`>DJ1L{ojQ zKYQdOBNWWNA zwRudxn3hl9E}7Rd?f8q2BCsf(0_ao`48#JMF(Y$V(qW5te)|I`Tj2eaf@_O*8cV`K zTo8ECnY7JySmSf9rK2K2#xks8>>_PYLV*GvI) znEV1m27uJ_JoyBH~+jV72 z-lkrB*eWrGGckj>1U%yw%Y@=JbY2nc@=)TK+^&%e5HtX+XfT%_brAb5+dswHh*MZv zZmD!r@7WyhQ7pl2Q9X(`-9yvH3qKHi<(yzMOMA5=yLMO3QBK;gV@I=l;}Xg0R*D+O z_bFwzTVrpe>K(M>d8>JRGbB`=G4yVi^!x#!FBufd#E#eeDevkHDD%N%!zBZ&U|w`q>1WzH$Uw$0>gV zACrR}e_6YXpy+Xl;xX-e7pb5U%OqLFA8k=yf~$C@YP_^~#9SHy0GHRCs-g(WErKK) zpQE`_;9*!-{@@g~!7GD+4JwZ|O)lWI4E2?Nyx@ntWmOHMcp9Vu8)^+!9rv1KCXx`Y zQbeE)fEz zd0RR4i2`G>k%~T$A@-;172D(;rocpUKna-J-TkunHk>RKfO84n*%fPg9ipvHVUVI1 z9k#VK@ly6~{FyNI-Yg!T`0X(auTwv`U;Qa-{GOy$AD~w9k?OwUxeum*)fu83(cIKD zj+p%-l(YpB{+`vt?0tM3n)#0`&$ESel1S`a(q{+JyB=*LOMYwC?t3*PUO~RH<2ZB z+j{q(;O9-%6uzYvH?_m=ip zu(NIOfP$xlJIdX{KKdAg+1?<1f;HZ?84C<&d&3s{ftnOasT~pDxYt(WNe@FbP3CEM zu1hUmmorNN6&?Kr6W@z3k0Zo-Fp3Go0T}$Py_CdC2iEOZ8Fr=uoo3&oNH@(9S}*vJ zsig1T7FF>>B0c}7N7&FDEmE>9acq70P&+#mEh00XcMUirmRM^!E?%h2taWZf6WR!A zZMf&x0^xoA9;Ctd(etb{vjgD7G&DLo3h>DBTJ=Uk3=#TM@IT;NKRc@E9AJ{u>=6 z6ciL{VhLufW?wY(43K@O-df3Ue8^`LP+45s{95*Gy%^t(Qlsap5@5#T+K_cA3It^F z1-c~w8oq1asxT}W;e%RETr)oX{rk5$;P&W?bcc)Kn+%+yI|6C=Y&@6Paw;-m>+5yA z-H>!}C$502{5`uoNL=xiO~;lpNQm49g z1`o34eh#gInycGeS|mPERe-Fl?93bi42|J{6RGdj7RTkaMOYIU9M@V zCOE3ss|p`^0gp|4ttdrhJb68wE@U~~c zD_%J-6yqLy*v=1~N_@#x@RK-iHed3^C-2j63N1r^d)ymxuz}oq^Y8!;O?&-`_)7M^ zch@9iCo8^}*w<#HP%^^j(0v{E1}PE}8+_8fME{$EMAYm~w09Z+c=kG-grCRzXPIc$ z{u1Pf_4VE6@Uf~6h_L@esnE43I}Bx_WF+ zWy`gP7thYl)Lx-8U<*L@l?zTYnoM+Z|H5GAdpUp&mV&>(*p-%zGT4rIC1B zl``%t4U1{S!D`Gax-le(Cj7J=P7w7UZ^*JGn2yByeAEB%8^{}T;!7Ez;qa+gpI^22 zN>d?deiX8?I_h2m=q@oI3*C#Xxuj(Sux?>tVSTp%LHB|E`$Q~CEdnNhU3<#7i{-kH zYTg-ux2a)f>-X%FZ1ID`slSR16>`um(2JnGjdw)$*b+R$%;>%_3;KAe<1I0pceoS9Ox-_z{7@g?+1$RiO_n^csRN`4c~@6f zid`rpS;^S}hg`1D`9!Z54UOKpHq$__IYh62Y5DoES-LG*QI8mzZR|A~(9ff_A=T}j zo>QwY4B*Voyt}0{Ta% z*an36!KOEnw*yiB45Kef9OLtOY38v4CbL@0;`%Rs{&8T3Oc41-6wkd)_q*5- z+ocoDn-o8hwSVkLcmLXzUhk_SGj^L8VYM{}o)|Er-@4q{-n03aI*@2RES2B2jeEhw2<-^hp=UfTIvwupO>zm2!zj+&6 zp5x<(J9su&`exW+=a?Wt1as<=W{}fl@`Hpf{R?s_r9A_cq67*s^_zeo;ufd^Rytv$ zsVpzsZx21y(zE4a=yr~rjRJ@)k~-d4aD_->HCI0WW5h}F*Bp548Q`sa`O|}hX>{j^Qo4VC>DcrN zgYi}|!8tEr$eDHf389(c{%_{7g^(jki|?ZREG<3#CX%I1kqG&H;62Z3-jPah=dc++ z=CzeV25~3f2j`MTeAG&Uag+#h!aX#5&&g|_&pGEDGGk*Q4rdj=Xz^u_#E^(-i9D8V zE_B*qm^I1%p>@=>rI+Cwqi{wTJ?4@XXqNK68M?dGZ%ZBNk6W5(r7t;&7WR(|+Vi(` z44yLg$*5Z%&Es(LKfzDyZLTYf?Gukzf5op3&2#twFd(JKhmoP7?g=!j<-|sB)D)pS zo`IMgu? zE4{$Id4GWZ+lXpXnti*!fpPR>JXEHE#)MG)HQ1a2C%Ma!P%eFwFn1-&sUd~E6K6Hh z2))}fX1QV53RlBC(Yi%~b?h=og*aj6Ml+}Xf4NIYV@pO(zG>3wxi8&sZDh2JZ;!LR zXk@8KcGNqSC;IwdRn_pOe@H$cODSm{IWt!*BcqvZZgqY}o+4Tde)<+jKy9N(I|t|- zHm91zxt&dc=AfI(%@bi6_gNldI5)@;;3VTD*cp@V_5*ALBb*wP&5(Y}Kwy8#G%Z6h zr>c$K*TW*5x5=#O$pt&cS!gL);uVpti5@JPxj@a z@J9(m$&T?v|B50s!MJ37!jXaHH*9Zje;WUT(ZBQZ{FEnwRY4ZALJ`w@&&kdGG`Bf} zk%DbyIqt&JT)9B3m|)91+b)=Ubis$C1lpNnQz+yJUD}M{@?L`Iy)>Gls(LUJGly(e}7nyrh*tZ%H&4#7g6WdgtD0C_wgxvK->Szk7_Z!LMQ9)?jHSbtC1Ag$!W zlZg9VUmCU%b2YEoehLQI2)^h%{E#b%QN#i$ko1M#&TAEx#d@SllI#p)%5aAuHF@7i9#nF6RBM`jXWOJr_tzOgF0>GwBzyRI|c z>O=XgR4}ZF*qecz)WFDyq4_iOhB4AYY@g8egc8`b)&f}&m9h3hh!fxn{r%?$Am!GS z`uSWDgn?a@#UI*7T?E>8tGDP`%hf|(d=qJ-CiYU)Sb&CxhI95GhA}fho;jseiuOa; zEJcVE6c5uXw5-5A7qFpD9Kr};Lw>6Y;x=W#zz%_egAS*^iHn9c=Xcdk@rIu0hgtaT zL{5)Z5HLu=@%LYN1NV_W*lBYCI$N*V*@pY+@5U_Mzb;`yHDX>Ed%s*yVD(M0BKeuf z0`3#w_>)LOZXT^(httov`E*i2e%ZtNA>LfF60t{8Uv`Izm+LLt&FHP-0P6k3hIH@v z0L_SnNU6P!cC7($%idO&!UUlx+_q`Z2DHV)htaGq{Q-?^0p8xXs|a}V?C;UmNXGb0 zfs(#TJ{tey@l!8CPsBKHWgRd@o{eK%xjy3mSY4|15{1U71u{X3IK}Q`gwha(l#W8) zJ7s)CV)`{egF7j(!3=auc-|%qzrhnnS>qj2fppNEtW-E;B`-7gA@RU0-I5- z7-8bMaC}05*=u@!zWMXj2t!v`wU)${!spmm_Y6Rbzs$qMpYvewkw~}?vWM-EXeL}2>BwE$1`kO{IS3*=->>#4khR&N=kJjl#_IF)X`B46b}#!iPW0)w&0sApO1H~z zqVJFAqgRV4EQ78bbG`RgJ?G5>v19~^9fE@BpdW<+J8XNR(y%;DkQZvmx8?2<9+qC- zF?Rwa<%d@+92{;c5tkLOZTrj3o-R|<7a@mm&JVcs5*-vS+D=XO?{dJNs4xr%>F8yBarda6AHdIz)i*J&QqO`4xF91VOGP*|E&v>2qTewcs^S6=UaaV05@$*`F6Q8crFJ( zOADo92CkU{Y>vI;*WwbJvjf#o;Bjkr)dv?9j;MTvPK zlvPz7KX->b-!p96APge`VR=hAa3>Gl8rzX1<)|lZ30-Y%!hT@rS_Ly;O1bFjmhlDt zx2}x?QC3#|GB3X>6u^-y^nsW%lW?2UK}5%3)4|6_qJV}?1-e>;PipbxO0Gs(lC9Q{ zk=EPYUn7!`4f$i&%m7U|_MBhuzpZMu-lQG4F{PCG?yVK=eF6KOg)3 z`(gI>c9Cp2?1&8_LKLF;PMs{8tR%Qt<^%T7)pw+&H90_F`sa6YYiVcb%kw}-WmjXs z5(lL5=#tEi`l{C2pIQxMh9#o_Ru6*0Ud9^xo;M5nl2|Pvc*)KJL3P7u!M?a9R9e( z3K2#tdYG&qZ{G}X=IN-Qcs5&0hr`%(?s*z97=kQ=}LX4&W5xI>uN~w^Yq4^ z;7~gaH$cLgFtJ1W3zJ!CsXozmCFicmPxf@_5;rgiL2{FX2&OO)jILzA-zxd8fPET1 zZsX!|HpLHt6X$)zJD@$SGJ<}I0h~Edc7qobj@{*vMyMWYtPR%XZu=CQ*t zA(u3yipVyJh$1dOn3JhU11FH*jk+_!0>!YPNSNZB{?X+G}4i65}5WFrlM2}AV zD=li$YS)FklOm?zmyaKOFB1GiqaD+()dKA8?RX;>kIGJe6=qNLB?V&Uol>%YbbHfc8c09$4Oj&MlQd{w@nVI!HlJ`PotRaXXAtSpxU8vNPM$6{>PJi%F z7B4Iv7xQvw7iWmh7n)Q;1%$GjBe{b2 z$%}GKgS3D5-yAJMD{1xHH>dEI_q!ifK~RAX{O@_wjuA>HfL z0+=B=r5OYDh$I20u?y%(Fua|>W{Qo949lLJ9A^bG2aR6$B^yVy(iBfIgTJ|2Yw5X! zz+p?kCqbY>FwU5?v zn=4^9reSg}$)CQL(>1d{bV@CzM@Qf5>FL=nC3!Lv^wn8*JO~O4XVT(4u$>}Tq(gyQ zvuABJqUlcH7!IzJREd%cXlFdyfKOrhgi=hy+?nLlf2kvBCpIl(#-sw{s0j;<8*j`(WaQ-G^Ec_YQx~+7?DFUE-Z4N1s-wVQq4T8-#_OF z#v~+k3n1{yOh481H;aI!?@&o>sS^{XjoNuc^=`D@JR;CAg^l0e2mB2YAJUNIZqI$} zW;q9|$HAc?g{7mGeq}$u_ie-4*1)2vx%(rOTQnGIaJZD5W$}!9>`NHDK~+UX<27-Oon6w18fKe+kBQJnt)-`z|=HuSis+1M~5gZa)2-v!q3UsHxIyS zHRQPlP=X9r=p9ZG++0H&kfDfwmg9)#HdQQ>p>c#q%K7hbB1S)vN2KQglgc9SYH4J} zModI@m_vYG(T0SUmNqU@we7R#5m~pXuqg#xvNSswi#b8BLwA<)PL#-{V52sh?&?b77cU)u5Il?AP}$^ zUdUw_3L-1~cj>3XYcCIJ9slC8X?fMA&dk)SD}Xj12)^*ejMW)xB*KTei`5IU=|e>^?TuPER-G_+iHHJAH>6ztc$yicfE(h-~G?i%F2ps+!leE z*69KzGRz{+=`AA|qw-9@UT%I92zvatJUh}8_%O`ejuf!3nO&g?>b!Ok2Zf`MAkh&Q zZsQ5%<7ZkUw1Q7KRW&_Vb=X}g5OO=+NlN!WKZSoHP}@wYJ3@kZ;b7al91!zZPO-dT zr>?|o5tFSptSwkY!0(I6Np+E)y12g1w2zZ3BO@c}KBr6PKugb=SJZY%*q-|r(bTOR zOk>U2POr~QVa3&mpa|XF`{O(7iUTz4L>Tj`qA))X&)IMo8ctR*!CZE?R^%b%bj)2D zm04i8&JyDF<%>1*<3XOg6b>F9ucC!ax~(w3cEi?4oHjx}Z`L~w?UiRJ;rFl9W9{aG zCbABfD6G{ZP9nVWb5NYfo*o!BU-%O6Z@b??Qmrfr9Xl3gjG3L5CfDY=PX4eP&!41F z=ySOl%xQ_Xp{095x=5c1S5jbPpIE^sk@ymjCUP?Gd`v_^;j2-@ZU96XQ3{rzKub6C zj_7Se6n)~xW&EcH>&<9Mzrszja!qHAET7#|xdx0q#uKJOLgvT4bS)`dOw7??Q|}t3 zq1&Gys8=LUwg$MgYyLi5U5%9oUkf1m<(VEC!AL5xA{Ms$@zE8Ud|&0kqg%FxuKIt1{dIFFYu(wY@L zVzD?ln|i7X-&{jnjeSg!uq8P+mx6K`J&`{W^YrJ!V3Dzz8GgJ}Oi`Pgr$hs$mF?mM zM(GPA8CNhu20#8E1m!qF*?G8}J460$se9}=^Q6rNW>I9UCHyne!`iGM^jm^Y2_>xnd9qlBcNr3$ws z7nGMLJ+8Z`bcndPLc;h1b@%<6bDdecnGSWaWuCX15gi+tq&T`pSlYba&veM+dVOfd|;{A6qI-MH;OVU%4_>fhegoxMiuwI*+=1s0rAE zjHn2)ozp4N&1&Az;zJKhE6_Kc^41k!!{f53ES7CzZf;KW>)8s?RIIf63SG;aHF8&; zD@4fptoL;9sr!7t?k`4zHprjxGqF+`7~?b$eeQP_uNnUQr%vK0qg@eo9Vs$BsD=S% z+LNzOMDn^TFgQkgo=q?6vMO*u#t9E1M}xUr z>e{hLG(;iw3Zm*NRSJ$Yj5GJ6stae8K4MWq#m-{!Msy&m0v7A+Y zRP2D$GA5b(?MY$il7$I`v01_A6glGWlG;l+6f>LrwAwGE10tq3N_!hlI@5joTdhv; zxDlZ(vLJ@OR3;+v@Y?UJ=O_$IN)$L*Fu!axdK1vGfa{-`#RhEm2HXObZ`0G#>Yz_g zg#*HqIRdsKJ?x?d3-5OS=0aPg$DE-9e;-6bAGx64j4}WCGe^UOmue)!Sd)oES6PAu zZZEgMs1@*@?ry{RIVRMyxTK`sIJ?y!x!X!~djuWN$?NPDcy5v{& z!LDd9Q_G>xXVD8dYv z85kIz-Y%CIXINf2C9g}WgxN~2t$M087;`7KU|B!Y?j!hA+tGo_Eg(jZy@4t15 z>-BN}4Gpj#@8fEzF`r%r-k(7^Rw~BQIlxNa(ht+v)Rx>3bi8!QRev}JNoC@=l6Qqv zcShO+EuHMRt*tHpF9bKG8)y*wfbeDR-yR-%9GY2KZNK5F;(?zdfMGJi7x;xiDjjrB z8-#I&`#ep-_6e-yX(1o!*V*H*pL`p9SJK1zId0F8?d2n51Ub4=B;UsCeMSN)P7d79G#XB(mxS>G zF0TaP3?K~11V!Gn#qN6H9EW%>&0$})XijA?@nMYD{-K06@p0g_^QjHvTDx{E_`x8t ztW?gKO2GS&yjb*MOjovn2ssPup~n*}nW1#B^>Dua@W5z~km(ENNMcO-wsr;onLMfo ziEw=ATF!d%BibpC0H+k*punkbRklp|*QyQZeDr6NuyqAm{*v!VU8F}c27KY3OI{ww z@QlC0pEsa66gSHd--B(AYo<1v1Rugf&!-T6MhGyTBpUr9}NwYYI zBY~zd6KSXg?eD_at<(P3Hu2Y*I(YNt->t<^u& - - #3F51B5 - #303F9F - #FF4081 - diff --git a/demos/ima/src/main/res/values/strings.xml b/demos/ima/src/main/res/values/strings.xml index 9bf928a6b3..67a7f06f8b 100644 --- a/demos/ima/src/main/res/values/strings.xml +++ b/demos/ima/src/main/res/values/strings.xml @@ -1,5 +1,24 @@ + + - Exo IMA Demo + + Exo IMA Demo + + + diff --git a/demos/ima/src/main/res/values/styles.xml b/demos/ima/src/main/res/values/styles.xml index 705be27764..1c78ad58df 100644 --- a/demos/ima/src/main/res/values/styles.xml +++ b/demos/ima/src/main/res/values/styles.xml @@ -1,11 +1,23 @@ - + + - diff --git a/demos/main/src/main/res/values/strings.xml b/demos/main/src/main/res/values/strings.xml index cc6357c574..b38ccf6e88 100644 --- a/demos/main/src/main/res/values/strings.xml +++ b/demos/main/src/main/res/values/strings.xml @@ -13,7 +13,6 @@ See the License for the specific language governing permissions and limitations under the License. --> - ExoPlayer diff --git a/demos/main/src/main/res/values/styles.xml b/demos/main/src/main/res/values/styles.xml index 751a224210..5616bb9869 100644 --- a/demos/main/src/main/res/values/styles.xml +++ b/demos/main/src/main/res/values/styles.xml @@ -13,7 +13,6 @@ See the License for the specific language governing permissions and limitations under the License. --> - - - diff --git a/library/ui/src/main/res/values/styles.xml b/library/ui/src/main/res/values/styles.xml index 4ef8971ccd..b57cbeaddf 100644 --- a/library/ui/src/main/res/values/styles.xml +++ b/library/ui/src/main/res/values/styles.xml @@ -16,7 +16,7 @@ diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java index f0e69dfc7e..4a5beb0501 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java @@ -180,7 +180,13 @@ public class FakeSimpleExoPlayer extends SimpleExoPlayer { @SuppressWarnings("ThreadJoinLoop") public void release() { stop(); - playbackThread.quitSafely(); + playbackHandler.post(new Runnable() { + @Override + public void run () { + playbackHandler.removeCallbacksAndMessages(null); + playbackThread.quit(); + } + }); while (playbackThread.isAlive()) { try { playbackThread.join(); From 3f6b4d18a992e3ef912b45fbb44f6a72ce5a09ca Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 20 Nov 2017 04:20:52 -0800 Subject: [PATCH 0724/2472] Move MockitoUtils to testutils and use it for all Mockito set-ups. In particular this allows to have the workaround for https://code.google.com/p/dexmaker/issues/detail?id=2 in one place only. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176340526 --- extensions/cronet/build.gradle | 1 + .../ByteArrayUploadDataProviderTest.java | 6 ++---- .../ext/cronet/CronetDataSourceTest.java | 6 ++---- .../drm/OfflineLicenseHelperTest.java | 14 ++------------ .../cache/CachedRegionTrackerTest.java | 14 ++------------ .../dash/offline/DashDownloaderTest.java | 2 +- .../exoplayer2/testutil}/MockitoUtil.java | 18 +++++++++++++++++- 7 files changed, 27 insertions(+), 34 deletions(-) rename {library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash => testutils/src/main/java/com/google/android/exoplayer2/testutil}/MockitoUtil.java (65%) diff --git a/extensions/cronet/build.gradle b/extensions/cronet/build.gradle index 197dec80a5..0b6f9a587c 100644 --- a/extensions/cronet/build.gradle +++ b/extensions/cronet/build.gradle @@ -40,6 +40,7 @@ dependencies { compile files('libs/cronet_impl_common_java.jar') compile files('libs/cronet_impl_native_java.jar') androidTestCompile project(modulePrefix + 'library') + androidTestCompile project(modulePrefix + 'testutils') androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion androidTestCompile 'org.mockito:mockito-core:' + mockitoVersion diff --git a/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/ByteArrayUploadDataProviderTest.java b/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/ByteArrayUploadDataProviderTest.java index a65bb0951b..bd81750fcb 100644 --- a/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/ByteArrayUploadDataProviderTest.java +++ b/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/ByteArrayUploadDataProviderTest.java @@ -19,10 +19,10 @@ import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import static org.mockito.MockitoAnnotations.initMocks; import android.support.test.InstrumentationRegistry; import android.support.test.runner.AndroidJUnit4; +import com.google.android.exoplayer2.testutil.MockitoUtil; import java.io.IOException; import java.nio.ByteBuffer; import java.util.Arrays; @@ -46,9 +46,7 @@ public final class ByteArrayUploadDataProviderTest { @Before public void setUp() { - System.setProperty("dexmaker.dexcache", - InstrumentationRegistry.getTargetContext().getCacheDir().getPath()); - initMocks(this); + MockitoUtil.setUpMockito(InstrumentationRegistry.getTargetContext(), this); byteBuffer = ByteBuffer.allocate(TEST_DATA.length); byteArrayUploadDataProvider = new ByteArrayUploadDataProvider(TEST_DATA); } diff --git a/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java b/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java index 4c6a42849f..f92574b7ab 100644 --- a/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java +++ b/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java @@ -31,13 +31,13 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import static org.mockito.MockitoAnnotations.initMocks; import android.net.Uri; import android.os.ConditionVariable; import android.support.test.InstrumentationRegistry; import android.support.test.runner.AndroidJUnit4; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.testutil.MockitoUtil; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.HttpDataSource.HttpDataSourceException; @@ -107,9 +107,7 @@ public final class CronetDataSourceTest { @Before public void setUp() throws Exception { - System.setProperty("dexmaker.dexcache", - InstrumentationRegistry.getTargetContext().getCacheDir().getPath()); - initMocks(this); + MockitoUtil.setUpMockito(InstrumentationRegistry.getTargetContext(), this); dataSourceUnderTest = spy( new CronetDataSource( mockCronetEngine, diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java index 22ae57932b..02b29a31b5 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java @@ -23,9 +23,9 @@ import android.test.MoreAsserts; import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; +import com.google.android.exoplayer2.testutil.MockitoUtil; import java.util.HashMap; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; /** * Tests {@link OfflineLicenseHelper}. @@ -38,7 +38,7 @@ public class OfflineLicenseHelperTest extends InstrumentationTestCase { @Override protected void setUp() throws Exception { - setUpMockito(this); + MockitoUtil.setUpMockito(this); when(mediaDrm.openSession()).thenReturn(new byte[] {1, 2, 3}); offlineLicenseHelper = new OfflineLicenseHelper<>(C.WIDEVINE_UUID, mediaDrm, mediaDrmCallback, null); @@ -156,14 +156,4 @@ public class OfflineLicenseHelperTest extends InstrumentationTestCase { new byte[] {1, 4, 7, 0, 3, 6})); } - /** - * Sets up Mockito for an instrumentation test. - */ - private static void setUpMockito(InstrumentationTestCase instrumentationTestCase) { - // Workaround for https://code.google.com/p/dexmaker/issues/detail?id=2. - System.setProperty("dexmaker.dexcache", - instrumentationTestCase.getInstrumentation().getTargetContext().getCacheDir().getPath()); - MockitoAnnotations.initMocks(instrumentationTestCase); - } - } diff --git a/library/core/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 index 472b5c724b..f40ae0bc7e 100644 --- a/library/core/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 @@ -17,11 +17,11 @@ package com.google.android.exoplayer2.upstream.cache; import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.extractor.ChunkIndex; +import com.google.android.exoplayer2.testutil.MockitoUtil; import com.google.android.exoplayer2.util.Util; import java.io.File; import java.io.IOException; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; /** * Tests for {@link CachedRegionTracker}. @@ -46,7 +46,7 @@ public final class CachedRegionTrackerTest extends InstrumentationTestCase { @Override protected void setUp() throws Exception { - setUpMockito(this); + MockitoUtil.setUpMockito(this); tracker = new CachedRegionTracker(cache, CACHE_KEY, CHUNK_INDEX); cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest"); @@ -123,14 +123,4 @@ public final class CachedRegionTrackerTest extends InstrumentationTestCase { return SimpleCacheSpanTest.createCacheSpan(index, cacheDir, CACHE_KEY, position, length, 0); } - /** - * Sets up Mockito for an instrumentation test. - */ - private static void setUpMockito(InstrumentationTestCase instrumentationTestCase) { - // Workaround for https://code.google.com/p/dexmaker/issues/detail?id=2. - System.setProperty("dexmaker.dexcache", - instrumentationTestCase.getInstrumentation().getTargetContext().getCacheDir().getPath()); - MockitoAnnotations.initMocks(instrumentationTestCase); - } - } diff --git a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java index 8532e65a68..ec0292514a 100644 --- a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java +++ b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java @@ -27,12 +27,12 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.offline.DownloadException; import com.google.android.exoplayer2.offline.Downloader.ProgressListener; import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; -import com.google.android.exoplayer2.source.dash.MockitoUtil; import com.google.android.exoplayer2.source.dash.manifest.DashManifest; import com.google.android.exoplayer2.source.dash.manifest.RepresentationKey; import com.google.android.exoplayer2.testutil.FakeDataSet; import com.google.android.exoplayer2.testutil.FakeDataSource; import com.google.android.exoplayer2.testutil.FakeDataSource.Factory; +import com.google.android.exoplayer2.testutil.MockitoUtil; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor; diff --git a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/MockitoUtil.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MockitoUtil.java similarity index 65% rename from library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/MockitoUtil.java rename to testutils/src/main/java/com/google/android/exoplayer2/testutil/MockitoUtil.java index e7cd9baf59..6bd1048bc0 100644 --- a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/MockitoUtil.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MockitoUtil.java @@ -13,8 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.source.dash; +package com.google.android.exoplayer2.testutil; +import android.content.Context; import android.test.InstrumentationTestCase; import org.mockito.MockitoAnnotations; @@ -25,6 +26,8 @@ public final class MockitoUtil { /** * Sets up Mockito for an instrumentation test. + * + * @param instrumentationTestCase The instrumentation test case class. */ public static void setUpMockito(InstrumentationTestCase instrumentationTestCase) { // Workaround for https://code.google.com/p/dexmaker/issues/detail?id=2. @@ -33,6 +36,19 @@ public final class MockitoUtil { MockitoAnnotations.initMocks(instrumentationTestCase); } + /** + * Sets up Mockito for a JUnit4 test. + * + * @param targetContext The target context. Usually obtained from + * {@code InstrumentationRegistry.getTargetContext()} + * @param testClass The JUnit4 test class. + */ + public static void setUpMockito(Context targetContext, Object testClass) { + // Workaround for https://code.google.com/p/dexmaker/issues/detail?id=2. + System.setProperty("dexmaker.dexcache", targetContext.getCacheDir().getPath()); + MockitoAnnotations.initMocks(testClass); + } + private MockitoUtil() {} } From 75acfc7957fd9ce2c9a7774266688c83de5c3a10 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 20 Nov 2017 04:22:12 -0800 Subject: [PATCH 0725/2472] Move media clock handling to its own class. This class implements MediaClock itself and handles the switching between renderer and standalone media clock. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176340615 --- .../android/exoplayer2/DefaultMediaClock.java | 188 ++++++++ .../exoplayer2/ExoPlayerImplInternal.java | 81 +--- .../exoplayer2/util/StandaloneMediaClock.java | 29 +- .../exoplayer2/DefaultMediaClockTest.java | 441 ++++++++++++++++++ 4 files changed, 672 insertions(+), 67 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/DefaultMediaClock.java create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/DefaultMediaClockTest.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultMediaClock.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultMediaClock.java new file mode 100644 index 0000000000..5f342bc722 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultMediaClock.java @@ -0,0 +1,188 @@ +/* + * 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.support.annotation.Nullable; +import com.google.android.exoplayer2.util.Clock; +import com.google.android.exoplayer2.util.MediaClock; +import com.google.android.exoplayer2.util.StandaloneMediaClock; + +/** + * Default {@link MediaClock} which uses a renderer media clock and falls back to a + * {@link StandaloneMediaClock} if necessary. + */ +/* package */ final class DefaultMediaClock implements MediaClock { + + /** + * Listener interface to be notified of changes to the active playback parameters. + */ + public interface PlaybackParameterListener { + + /** + * Called when the active playback parameters changed. + * + * @param newPlaybackParameters The newly active {@link PlaybackParameters}. + */ + void onPlaybackParametersChanged(PlaybackParameters newPlaybackParameters); + + } + + private final StandaloneMediaClock standaloneMediaClock; + private final PlaybackParameterListener listener; + + private @Nullable Renderer rendererClockSource; + private @Nullable MediaClock rendererClock; + + /** + * Creates a new instance with listener for playback parameter changes. + * + * @param listener A {@link PlaybackParameterListener} to listen for playback parameter + * changes. + */ + public DefaultMediaClock(PlaybackParameterListener listener) { + this(listener, Clock.DEFAULT); + } + + /** + * Creates a new instance with listener for playback parameter changes and a {@link Clock} to use + * for the standalone clock implementation. + * + * @param listener A {@link PlaybackParameterListener} to listen for playback parameter + * changes. + * @param clock A {@link Clock}. + */ + public DefaultMediaClock(PlaybackParameterListener listener, Clock clock) { + this.listener = listener; + this.standaloneMediaClock = new StandaloneMediaClock(clock); + } + + /** + * Starts the standalone fallback clock. + */ + public void start() { + standaloneMediaClock.start(); + } + + /** + * Stops the standalone fallback clock. + */ + public void stop() { + standaloneMediaClock.stop(); + } + + /** + * Resets the position of the standalone fallback clock. + * + * @param positionUs The position to set in microseconds. + */ + public void resetPosition(long positionUs) { + standaloneMediaClock.resetPosition(positionUs); + } + + /** + * Notifies the media clock that a renderer has been enabled. Starts using the media clock of the + * provided renderer if available. + * + * @param renderer The renderer which has been enabled. + * @throws ExoPlaybackException If the renderer provides a media clock and another renderer media + * clock is already provided. + */ + public void onRendererEnabled(Renderer renderer) throws ExoPlaybackException { + MediaClock rendererMediaClock = renderer.getMediaClock(); + if (rendererMediaClock != null && rendererMediaClock != rendererClock) { + if (rendererClock != null) { + throw ExoPlaybackException.createForUnexpected( + new IllegalStateException("Multiple renderer media clocks enabled.")); + } + this.rendererClock = rendererMediaClock; + this.rendererClockSource = renderer; + rendererClock.setPlaybackParameters(standaloneMediaClock.getPlaybackParameters()); + ensureSynced(); + } + } + + /** + * Notifies the media clock that a renderer has been disabled. Stops using the media clock of this + * renderer if used. + * + * @param renderer The renderer which has been disabled. + */ + public void onRendererDisabled(Renderer renderer) { + if (renderer == rendererClockSource) { + this.rendererClock = null; + this.rendererClockSource = null; + } + } + + /** + * Syncs internal clock if needed and returns current clock position in microseconds. + */ + public long syncAndGetPositionUs() { + if (isUsingRendererClock()) { + ensureSynced(); + return rendererClock.getPositionUs(); + } else { + return standaloneMediaClock.getPositionUs(); + } + } + + // MediaClock implementation. + + @Override + public long getPositionUs() { + if (isUsingRendererClock()) { + return rendererClock.getPositionUs(); + } else { + return standaloneMediaClock.getPositionUs(); + } + } + + @Override + public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) { + if (rendererClock != null) { + playbackParameters = rendererClock.setPlaybackParameters(playbackParameters); + } + standaloneMediaClock.setPlaybackParameters(playbackParameters); + listener.onPlaybackParametersChanged(playbackParameters); + return playbackParameters; + } + + @Override + public PlaybackParameters getPlaybackParameters() { + return rendererClock != null ? rendererClock.getPlaybackParameters() + : standaloneMediaClock.getPlaybackParameters(); + } + + private void ensureSynced() { + long rendererClockPositionUs = rendererClock.getPositionUs(); + standaloneMediaClock.resetPosition(rendererClockPositionUs); + PlaybackParameters playbackParameters = rendererClock.getPlaybackParameters(); + if (!playbackParameters.equals(standaloneMediaClock.getPlaybackParameters())) { + standaloneMediaClock.setPlaybackParameters(playbackParameters); + listener.onPlaybackParametersChanged(playbackParameters); + } + } + + private boolean isUsingRendererClock() { + // Use the renderer clock if the providing renderer has not ended or needs the next sample + // stream to reenter the ready state. The latter case uses the standalone clock to avoid getting + // stuck if tracks in the current period have uneven durations. + // See: https://github.com/google/ExoPlayer/issues/1874. + return rendererClockSource != null && !rendererClockSource.isEnded() + && (rendererClockSource.isReady() || !rendererClockSource.hasReadStreamToEnd()); + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 6acab54ba0..e4bb11c51f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -24,6 +24,7 @@ import android.os.SystemClock; import android.support.annotation.NonNull; import android.util.Log; import android.util.Pair; +import com.google.android.exoplayer2.DefaultMediaClock.PlaybackParameterListener; import com.google.android.exoplayer2.ExoPlayer.ExoPlayerMessage; import com.google.android.exoplayer2.MediaPeriodInfoSequence.MediaPeriodInfo; import com.google.android.exoplayer2.source.ClippingMediaPeriod; @@ -37,8 +38,6 @@ import com.google.android.exoplayer2.trackselection.TrackSelectionArray; 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.StandaloneMediaClock; import com.google.android.exoplayer2.util.TraceUtil; import java.io.IOException; @@ -46,7 +45,8 @@ import java.io.IOException; * Implements the internal behavior of {@link ExoPlayerImpl}. */ /* package */ final class ExoPlayerImplInternal implements Handler.Callback, - MediaPeriod.Callback, TrackSelector.InvalidationListener, MediaSource.Listener { + MediaPeriod.Callback, TrackSelector.InvalidationListener, MediaSource.Listener, + PlaybackParameterListener { private static final String TAG = "ExoPlayerImplInternal"; @@ -99,7 +99,6 @@ import java.io.IOException; private final RendererCapabilities[] rendererCapabilities; private final TrackSelector trackSelector; private final LoadControl loadControl; - private final StandaloneMediaClock standaloneMediaClock; private final Handler handler; private final HandlerThread internalPlaybackThread; private final Handler eventHandler; @@ -109,11 +108,9 @@ import java.io.IOException; private final MediaPeriodInfoSequence mediaPeriodInfoSequence; private final long backBufferDurationUs; private final boolean retainBackBufferFromKeyframe; + private final DefaultMediaClock mediaClock; private PlaybackInfo playbackInfo; - private PlaybackParameters playbackParameters; - private Renderer rendererMediaClockSource; - private MediaClock rendererMediaClock; private MediaSource mediaSource; private Renderer[] enabledRenderers; private boolean released; @@ -158,13 +155,12 @@ import java.io.IOException; renderers[i].setIndex(i); rendererCapabilities[i] = renderers[i].getCapabilities(); } - standaloneMediaClock = new StandaloneMediaClock(); + mediaClock = new DefaultMediaClock(this); enabledRenderers = new Renderer[0]; window = new Timeline.Window(); period = new Timeline.Period(); mediaPeriodInfoSequence = new MediaPeriodInfoSequence(); 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. @@ -284,6 +280,16 @@ import java.io.IOException; handler.sendEmptyMessage(MSG_TRACK_SELECTION_INVALIDATED); } + // DefaultMediaClock.PlaybackParameterListener implementation. + + @Override + public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { + // TODO(b/37237846): Make LoadControl, period transition position projection, adaptive track + // selection and potentially any time-related code in renderers take into account the playback + // speed. + eventHandler.obtainMessage(MSG_PLAYBACK_PARAMETERS_CHANGED, playbackParameters).sendToTarget(); + } + // Handler.Callback implementation. @SuppressWarnings("unchecked") @@ -486,14 +492,14 @@ import java.io.IOException; private void startRenderers() throws ExoPlaybackException { rebuffering = false; - standaloneMediaClock.start(); + mediaClock.start(); for (Renderer renderer : enabledRenderers) { renderer.start(); } } private void stopRenderers() throws ExoPlaybackException { - standaloneMediaClock.stop(); + mediaClock.stop(); for (Renderer renderer : enabledRenderers) { ensureStopped(renderer); } @@ -509,18 +515,7 @@ import java.io.IOException; if (periodPositionUs != C.TIME_UNSET) { resetRendererPosition(periodPositionUs); } else { - // Use the standalone clock if there's no renderer clock, or if the providing renderer has - // ended or needs the next sample stream to reenter the ready state. The latter case uses the - // standalone clock to avoid getting stuck if tracks in the current period have uneven - // durations. See: https://github.com/google/ExoPlayer/issues/1874. - if (rendererMediaClockSource == null || rendererMediaClockSource.isEnded() - || (!rendererMediaClockSource.isReady() - && rendererMediaClockSource.hasReadStreamToEnd())) { - rendererPositionUs = standaloneMediaClock.getPositionUs(); - } else { - rendererPositionUs = rendererMediaClock.getPositionUs(); - standaloneMediaClock.setPositionUs(rendererPositionUs); - } + rendererPositionUs = mediaClock.syncAndGetPositionUs(); periodPositionUs = playingPeriodHolder.toPeriodTime(rendererPositionUs); } playbackInfo.positionUs = periodPositionUs; @@ -573,19 +568,6 @@ 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.setPlaybackParameters(playbackParameters); - eventHandler.obtainMessage(MSG_PLAYBACK_PARAMETERS_CHANGED, playbackParameters) - .sendToTarget(); - } - } - long playingPeriodDurationUs = playingPeriodHolder.info.durationUs; if (allRenderersEnded && (playingPeriodDurationUs == C.TIME_UNSET @@ -771,19 +753,14 @@ import java.io.IOException; rendererPositionUs = playingPeriodHolder == null ? periodPositionUs + RENDERER_TIMESTAMP_OFFSET_US : playingPeriodHolder.toRendererTime(periodPositionUs); - standaloneMediaClock.setPositionUs(rendererPositionUs); + mediaClock.resetPosition(rendererPositionUs); for (Renderer renderer : enabledRenderers) { renderer.resetPosition(rendererPositionUs); } } private void setPlaybackParametersInternal(PlaybackParameters playbackParameters) { - if (rendererMediaClock != null) { - playbackParameters = rendererMediaClock.setPlaybackParameters(playbackParameters); - } - standaloneMediaClock.setPlaybackParameters(playbackParameters); - this.playbackParameters = playbackParameters; - eventHandler.obtainMessage(MSG_PLAYBACK_PARAMETERS_CHANGED, playbackParameters).sendToTarget(); + mediaClock.setPlaybackParameters(playbackParameters); } private void stopInternal() { @@ -806,7 +783,7 @@ import java.io.IOException; private void resetInternal(boolean releaseMediaSource) { handler.removeMessages(MSG_DO_SOME_WORK); rebuffering = false; - standaloneMediaClock.stop(); + mediaClock.stop(); rendererPositionUs = RENDERER_TIMESTAMP_OFFSET_US; for (Renderer renderer : enabledRenderers) { try { @@ -857,10 +834,7 @@ import java.io.IOException; } private void disableRenderer(Renderer renderer) throws ExoPlaybackException { - if (renderer == rendererMediaClockSource) { - rendererMediaClock = null; - rendererMediaClockSource = null; - } + mediaClock.onRendererDisabled(renderer); ensureStopped(renderer); renderer.disable(); } @@ -1498,16 +1472,7 @@ import java.io.IOException; renderer.enable(rendererConfiguration, formats, playingPeriodHolder.sampleStreams[rendererIndex], rendererPositionUs, joining, playingPeriodHolder.getRendererOffset()); - MediaClock mediaClock = renderer.getMediaClock(); - if (mediaClock != null) { - if (rendererMediaClock != null) { - throw ExoPlaybackException.createForUnexpected( - new IllegalStateException("Multiple renderer media clocks enabled.")); - } - rendererMediaClock = mediaClock; - rendererMediaClockSource = renderer; - rendererMediaClock.setPlaybackParameters(playbackParameters); - } + mediaClock.onRendererEnabled(renderer); // Start the renderer if playing. if (playing) { renderer.start(); 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 index 96203bb99a..fad3a00f10 100644 --- 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 @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.util; -import android.os.SystemClock; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.PlaybackParameters; @@ -25,6 +24,8 @@ import com.google.android.exoplayer2.PlaybackParameters; */ public final class StandaloneMediaClock implements MediaClock { + private final Clock clock; + private boolean started; private long baseUs; private long baseElapsedMs; @@ -34,7 +35,17 @@ public final class StandaloneMediaClock implements MediaClock { * Creates a new standalone media clock. */ public StandaloneMediaClock() { - playbackParameters = PlaybackParameters.DEFAULT; + this(Clock.DEFAULT); + } + + /** + * Creates a new standalone media clock using the given {@link Clock} implementation. + * + * @param clock A {@link Clock}. + */ + public StandaloneMediaClock(Clock clock) { + this.clock = clock; + this.playbackParameters = PlaybackParameters.DEFAULT; } /** @@ -42,7 +53,7 @@ public final class StandaloneMediaClock implements MediaClock { */ public void start() { if (!started) { - baseElapsedMs = SystemClock.elapsedRealtime(); + baseElapsedMs = clock.elapsedRealtime(); started = true; } } @@ -52,20 +63,20 @@ public final class StandaloneMediaClock implements MediaClock { */ public void stop() { if (started) { - setPositionUs(getPositionUs()); + resetPosition(getPositionUs()); started = false; } } /** - * Sets the clock's position. + * Resets the clock's position. * * @param positionUs The position to set in microseconds. */ - public void setPositionUs(long positionUs) { + public void resetPosition(long positionUs) { baseUs = positionUs; if (started) { - baseElapsedMs = SystemClock.elapsedRealtime(); + baseElapsedMs = clock.elapsedRealtime(); } } @@ -73,7 +84,7 @@ public final class StandaloneMediaClock implements MediaClock { public long getPositionUs() { long positionUs = baseUs; if (started) { - long elapsedSinceBaseMs = SystemClock.elapsedRealtime() - baseElapsedMs; + long elapsedSinceBaseMs = clock.elapsedRealtime() - baseElapsedMs; if (playbackParameters.speed == 1f) { positionUs += C.msToUs(elapsedSinceBaseMs); } else { @@ -87,7 +98,7 @@ public final class StandaloneMediaClock implements MediaClock { public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) { // Store the current position as the new base, in case the playback speed has changed. if (started) { - setPositionUs(getPositionUs()); + resetPosition(getPositionUs()); } this.playbackParameters = playbackParameters; return playbackParameters; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/DefaultMediaClockTest.java b/library/core/src/test/java/com/google/android/exoplayer2/DefaultMediaClockTest.java new file mode 100644 index 0000000000..9db4d57a65 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/DefaultMediaClockTest.java @@ -0,0 +1,441 @@ +/* + * 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 static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.MockitoAnnotations.initMocks; + +import com.google.android.exoplayer2.DefaultMediaClock.PlaybackParameterListener; +import com.google.android.exoplayer2.testutil.FakeClock; +import com.google.android.exoplayer2.testutil.FakeMediaClockRenderer; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Unit test for {@link DefaultMediaClock}. + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public class DefaultMediaClockTest { + + private static final long TEST_POSITION_US = 123456789012345678L; + private static final long SLEEP_TIME_MS = 1_000; + private static final PlaybackParameters TEST_PLAYBACK_PARAMETERS = + new PlaybackParameters(2.0f, 1.0f); + + @Mock private PlaybackParameterListener listener; + private FakeClock fakeClock; + private DefaultMediaClock mediaClock; + + @Before + public void initMediaClockWithFakeClock() { + initMocks(this); + fakeClock = new FakeClock(0); + mediaClock = new DefaultMediaClock(listener, fakeClock); + } + + @Test + public void standaloneResetPosition_getPositionShouldReturnSameValue() throws Exception { + mediaClock.resetPosition(TEST_POSITION_US); + assertThat(mediaClock.syncAndGetPositionUs()).isEqualTo(TEST_POSITION_US); + } + + @Test + public void standaloneGetAndResetPosition_shouldNotTriggerCallback() throws Exception { + mediaClock.resetPosition(TEST_POSITION_US); + mediaClock.syncAndGetPositionUs(); + verifyNoMoreInteractions(listener); + } + + @Test + public void standaloneClock_shouldNotAutoStart() throws Exception { + assertClockIsStopped(); + } + + @Test + public void standaloneResetPosition_shouldNotStartClock() throws Exception { + mediaClock.resetPosition(TEST_POSITION_US); + assertClockIsStopped(); + } + + @Test + public void standaloneStart_shouldStartClock() throws Exception { + mediaClock.start(); + assertClockIsRunning(); + } + + @Test + public void standaloneStop_shouldKeepClockStopped() throws Exception { + mediaClock.stop(); + assertClockIsStopped(); + } + + @Test + public void standaloneStartAndStop_shouldStopClock() throws Exception { + mediaClock.start(); + mediaClock.stop(); + assertClockIsStopped(); + } + + @Test + public void standaloneStartStopStart_shouldRestartClock() throws Exception { + mediaClock.start(); + mediaClock.stop(); + mediaClock.start(); + assertClockIsRunning(); + } + + @Test + public void standaloneStartAndStop_shouldNotTriggerCallback() throws Exception { + mediaClock.start(); + mediaClock.stop(); + verifyNoMoreInteractions(listener); + } + + @Test + public void standaloneGetPlaybackParameters_initializedWithDefaultPlaybackParameters() { + assertThat(mediaClock.getPlaybackParameters()).isEqualTo(PlaybackParameters.DEFAULT); + } + + @Test + public void standaloneSetPlaybackParameters_getPlaybackParametersShouldReturnSameValue() { + PlaybackParameters parameters = mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); + assertThat(parameters).isEqualTo(TEST_PLAYBACK_PARAMETERS); + assertThat(mediaClock.getPlaybackParameters()).isEqualTo(TEST_PLAYBACK_PARAMETERS); + } + + @Test + public void standaloneSetPlaybackParameters_shouldTriggerCallback() { + mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); + verify(listener).onPlaybackParametersChanged(TEST_PLAYBACK_PARAMETERS); + } + + @Test + public void standaloneSetPlaybackParameters_shouldApplyNewPlaybackSpeed() { + mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); + mediaClock.start(); + // Asserts that clock is running with speed declared in getPlaybackParameters(). + assertClockIsRunning(); + } + + @Test + public void standaloneSetOtherPlaybackParameters_getPlaybackParametersShouldReturnSameValue() { + mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); + PlaybackParameters parameters = mediaClock.setPlaybackParameters(PlaybackParameters.DEFAULT); + assertThat(parameters).isEqualTo(PlaybackParameters.DEFAULT); + assertThat(mediaClock.getPlaybackParameters()).isEqualTo(PlaybackParameters.DEFAULT); + } + + @Test + public void standaloneSetOtherPlaybackParameters_shouldTriggerCallbackAgain() { + mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); + mediaClock.setPlaybackParameters(PlaybackParameters.DEFAULT); + verify(listener).onPlaybackParametersChanged(PlaybackParameters.DEFAULT); + } + + @Test + public void standaloneSetSamePlaybackParametersAgain_shouldTriggerCallbackAgain() { + mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); + mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); + verify(listener, times(2)).onPlaybackParametersChanged(TEST_PLAYBACK_PARAMETERS); + } + + @Test + public void enableRendererMediaClock_shouldOverwriteRendererPlaybackParametersIfPossible() + throws ExoPlaybackException { + FakeMediaClockRenderer mediaClockRenderer = + new MediaClockRenderer(TEST_PLAYBACK_PARAMETERS, /* playbackParametersAreMutable= */ true); + mediaClock.onRendererEnabled(mediaClockRenderer); + assertThat(mediaClock.getPlaybackParameters()).isEqualTo(PlaybackParameters.DEFAULT); + verifyNoMoreInteractions(listener); + } + + @Test + public void enableRendererMediaClockWithFixedParameters_usesRendererPlaybackParameters() + throws ExoPlaybackException { + FakeMediaClockRenderer mediaClockRenderer = + new MediaClockRenderer(TEST_PLAYBACK_PARAMETERS, /* playbackParametersAreMutable= */ false); + mediaClock.onRendererEnabled(mediaClockRenderer); + assertThat(mediaClock.getPlaybackParameters()).isEqualTo(TEST_PLAYBACK_PARAMETERS); + } + + @Test + public void enableRendererMediaClockWithFixedParameters_shouldTriggerCallback() + throws ExoPlaybackException { + FakeMediaClockRenderer mediaClockRenderer = + new MediaClockRenderer(TEST_PLAYBACK_PARAMETERS, /* playbackParametersAreMutable= */ false); + mediaClock.onRendererEnabled(mediaClockRenderer); + verify(listener).onPlaybackParametersChanged(TEST_PLAYBACK_PARAMETERS); + } + + @Test + public void enableRendererMediaClockWithFixedButSamePlaybackParameters_shouldNotTriggerCallback() + throws ExoPlaybackException { + FakeMediaClockRenderer mediaClockRenderer = new MediaClockRenderer(PlaybackParameters.DEFAULT, + /* playbackParametersAreMutable= */ false); + mediaClock.onRendererEnabled(mediaClockRenderer); + verifyNoMoreInteractions(listener); + } + + @Test + public void disableRendererMediaClock_shouldKeepPlaybackParameters() + throws ExoPlaybackException { + FakeMediaClockRenderer mediaClockRenderer = + new MediaClockRenderer(TEST_PLAYBACK_PARAMETERS, /* playbackParametersAreMutable= */ false); + mediaClock.onRendererEnabled(mediaClockRenderer); + mediaClock.onRendererDisabled(mediaClockRenderer); + assertThat(mediaClock.getPlaybackParameters()).isEqualTo(TEST_PLAYBACK_PARAMETERS); + } + + @Test + public void rendererClockSetPlaybackParameters_getPlaybackParametersShouldReturnSameValue() + throws ExoPlaybackException { + FakeMediaClockRenderer mediaClockRenderer = new MediaClockRenderer(PlaybackParameters.DEFAULT, + /* playbackParametersAreMutable= */ true); + mediaClock.onRendererEnabled(mediaClockRenderer); + PlaybackParameters parameters = mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); + assertThat(parameters).isEqualTo(TEST_PLAYBACK_PARAMETERS); + assertThat(mediaClock.getPlaybackParameters()).isEqualTo(TEST_PLAYBACK_PARAMETERS); + } + + @Test + public void rendererClockSetPlaybackParameters_shouldTriggerCallback() + throws ExoPlaybackException { + FakeMediaClockRenderer mediaClockRenderer = new MediaClockRenderer(PlaybackParameters.DEFAULT, + /* playbackParametersAreMutable= */ true); + mediaClock.onRendererEnabled(mediaClockRenderer); + mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); + verify(listener).onPlaybackParametersChanged(TEST_PLAYBACK_PARAMETERS); + } + + @Test + public void rendererClockSetPlaybackParametersOverwrite_getParametersShouldReturnSameValue() + throws ExoPlaybackException { + FakeMediaClockRenderer mediaClockRenderer = new MediaClockRenderer(PlaybackParameters.DEFAULT, + /* playbackParametersAreMutable= */ false); + mediaClock.onRendererEnabled(mediaClockRenderer); + PlaybackParameters parameters = mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); + assertThat(parameters).isEqualTo(PlaybackParameters.DEFAULT); + assertThat(mediaClock.getPlaybackParameters()).isEqualTo(PlaybackParameters.DEFAULT); + } + + @Test + public void rendererClockSetPlaybackParametersOverwrite_shouldTriggerCallback() + throws ExoPlaybackException { + FakeMediaClockRenderer mediaClockRenderer = new MediaClockRenderer(PlaybackParameters.DEFAULT, + /* playbackParametersAreMutable= */ false); + mediaClock.onRendererEnabled(mediaClockRenderer); + mediaClock.setPlaybackParameters(TEST_PLAYBACK_PARAMETERS); + verify(listener).onPlaybackParametersChanged(PlaybackParameters.DEFAULT); + } + + @Test + public void enableRendererMediaClock_usesRendererClockPosition() throws ExoPlaybackException { + MediaClockRenderer mediaClockRenderer = new MediaClockRenderer(); + mediaClock.start(); + mediaClock.onRendererEnabled(mediaClockRenderer); + mediaClockRenderer.positionUs = TEST_POSITION_US; + assertThat(mediaClock.syncAndGetPositionUs()).isEqualTo(TEST_POSITION_US); + // We're not advancing the renderer media clock. Thus, the clock should appear to be stopped. + assertClockIsStopped(); + } + + @Test + public void resetPositionWhileUsingRendererMediaClock_shouldHaveNoEffect() + throws ExoPlaybackException { + MediaClockRenderer mediaClockRenderer = new MediaClockRenderer(); + mediaClock.start(); + mediaClock.onRendererEnabled(mediaClockRenderer); + mediaClockRenderer.positionUs = TEST_POSITION_US; + assertThat(mediaClock.syncAndGetPositionUs()).isEqualTo(TEST_POSITION_US); + mediaClock.resetPosition(0); + assertThat(mediaClock.syncAndGetPositionUs()).isEqualTo(TEST_POSITION_US); + } + + @Test + public void disableRendererMediaClock_standaloneShouldBeSynced() throws ExoPlaybackException { + MediaClockRenderer mediaClockRenderer = new MediaClockRenderer(); + mediaClock.start(); + mediaClock.onRendererEnabled(mediaClockRenderer); + mediaClockRenderer.positionUs = TEST_POSITION_US; + mediaClock.syncAndGetPositionUs(); + mediaClock.onRendererDisabled(mediaClockRenderer); + fakeClock.advanceTime(SLEEP_TIME_MS); + assertThat(mediaClock.syncAndGetPositionUs()) + .isEqualTo(TEST_POSITION_US + C.msToUs(SLEEP_TIME_MS)); + assertClockIsRunning(); + } + + @Test + public void getPositionWithPlaybackParameterChange_shouldTriggerCallback() + throws ExoPlaybackException { + MediaClockRenderer mediaClockRenderer = new MediaClockRenderer(PlaybackParameters.DEFAULT, + /* playbackParametersAreMutable= */ true); + mediaClock.onRendererEnabled(mediaClockRenderer); + // Silently change playback parameters of renderer clock. + mediaClockRenderer.playbackParameters = TEST_PLAYBACK_PARAMETERS; + mediaClock.syncAndGetPositionUs(); + verify(listener).onPlaybackParametersChanged(TEST_PLAYBACK_PARAMETERS); + } + + @Test + public void rendererNotReady_shouldStillUseRendererClock() throws ExoPlaybackException { + MediaClockRenderer mediaClockRenderer = new MediaClockRenderer(/* isReady= */ false, + /* isEnded= */ false, /* hasReadStreamToEnd= */ false); + mediaClock.start(); + mediaClock.onRendererEnabled(mediaClockRenderer); + // We're not advancing the renderer media clock. Thus, the clock should appear to be stopped. + assertClockIsStopped(); + } + + @Test + public void rendererNotReadyAndReadStreamToEnd_shouldFallbackToStandaloneClock() + throws ExoPlaybackException { + MediaClockRenderer mediaClockRenderer = new MediaClockRenderer(/* isReady= */ false, + /* isEnded= */ false, /* hasReadStreamToEnd= */ true); + mediaClock.start(); + mediaClock.onRendererEnabled(mediaClockRenderer); + assertClockIsRunning(); + } + + @Test + public void rendererEnded_shouldFallbackToStandaloneClock() + throws ExoPlaybackException { + MediaClockRenderer mediaClockRenderer = new MediaClockRenderer(/* isReady= */ true, + /* isEnded= */ true, /* hasReadStreamToEnd= */ true); + mediaClock.start(); + mediaClock.onRendererEnabled(mediaClockRenderer); + assertClockIsRunning(); + } + + @Test + public void staleDisableRendererClock_shouldNotThrow() + throws ExoPlaybackException { + MediaClockRenderer mediaClockRenderer = new MediaClockRenderer(); + mediaClockRenderer.positionUs = TEST_POSITION_US; + mediaClock.onRendererDisabled(mediaClockRenderer); + assertThat(mediaClock.syncAndGetPositionUs()).isEqualTo(C.msToUs(fakeClock.elapsedRealtime())); + } + + @Test + public void enableSameRendererClockTwice_shouldNotThrow() + throws ExoPlaybackException { + MediaClockRenderer mediaClockRenderer = new MediaClockRenderer(); + mediaClock.onRendererEnabled(mediaClockRenderer); + mediaClock.onRendererEnabled(mediaClockRenderer); + mediaClockRenderer.positionUs = TEST_POSITION_US; + assertThat(mediaClock.syncAndGetPositionUs()).isEqualTo(TEST_POSITION_US); + } + + @Test + public void enableOtherRendererClock_shouldThrow() + throws ExoPlaybackException { + MediaClockRenderer mediaClockRenderer1 = new MediaClockRenderer(); + MediaClockRenderer mediaClockRenderer2 = new MediaClockRenderer(); + mediaClockRenderer1.positionUs = TEST_POSITION_US; + mediaClock.onRendererEnabled(mediaClockRenderer1); + try { + mediaClock.onRendererEnabled(mediaClockRenderer2); + fail(); + } catch (ExoPlaybackException e) { + // Expected. + } + assertThat(mediaClock.syncAndGetPositionUs()).isEqualTo(TEST_POSITION_US); + } + + private void assertClockIsRunning() { + long clockStartUs = mediaClock.syncAndGetPositionUs(); + fakeClock.advanceTime(SLEEP_TIME_MS); + assertThat(mediaClock.syncAndGetPositionUs()).isEqualTo(clockStartUs + + mediaClock.getPlaybackParameters().getSpeedAdjustedDurationUs(SLEEP_TIME_MS)); + } + + private void assertClockIsStopped() { + long positionAtStartUs = mediaClock.syncAndGetPositionUs(); + fakeClock.advanceTime(SLEEP_TIME_MS); + assertThat(mediaClock.syncAndGetPositionUs()).isEqualTo(positionAtStartUs); + } + + private static class MediaClockRenderer extends FakeMediaClockRenderer { + + public long positionUs; + public PlaybackParameters playbackParameters; + + private final boolean playbackParametersAreMutable; + + public MediaClockRenderer() throws ExoPlaybackException { + this(PlaybackParameters.DEFAULT, false, true, false, false); + } + + public MediaClockRenderer(PlaybackParameters playbackParameters, + boolean playbackParametersAreMutable) + throws ExoPlaybackException { + this(playbackParameters, playbackParametersAreMutable, true, false, false); + } + + public MediaClockRenderer(boolean isReady, boolean isEnded, boolean hasReadStreamToEnd) + throws ExoPlaybackException { + this(PlaybackParameters.DEFAULT, false, isReady, isEnded, hasReadStreamToEnd); + } + + private MediaClockRenderer(PlaybackParameters playbackParameters, + boolean playbackParametersAreMutable, boolean isReady, boolean isEnded, + boolean hasReadStreamToEnd) + throws ExoPlaybackException { + this.positionUs = TEST_POSITION_US; + this.playbackParameters = playbackParameters; + this.playbackParametersAreMutable = playbackParametersAreMutable; + this.isReady = isReady; + this.isEnded = isEnded; + if (!hasReadStreamToEnd) { + resetPosition(0); + } + } + + @Override + public long getPositionUs() { + return positionUs; + } + + @Override + public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) { + if (playbackParametersAreMutable) { + this.playbackParameters = playbackParameters; + } + return this.playbackParameters; + } + + @Override + public PlaybackParameters getPlaybackParameters() { + return playbackParameters; + } + + @Override + public boolean isReady() { + return isReady; + } + + } + +} From c4fe0e648242bf2d2613f217605c83bf7cc91aed Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 20 Nov 2017 04:32:56 -0800 Subject: [PATCH 0726/2472] Add support for Dolby Atmos Issue: #2465 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176341309 --- RELEASENOTES.md | 2 + .../android/exoplayer2/audio/Ac3Util.java | 170 +++++++++++++++++- .../exoplayer2/extractor/ts/Ac3Reader.java | 2 +- .../exoplayer2/mediacodec/MediaCodecUtil.java | 60 +++++-- .../android/exoplayer2/util/MimeTypes.java | 4 + .../dash/manifest/DashManifestParser.java | 22 ++- 6 files changed, 237 insertions(+), 23 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index a43a8eb0bc..d9ed3e5d2a 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -12,6 +12,8 @@ * New Cast extension: Simplifies toggling between local and Cast playbacks. * Support 32-bit PCM float output from `DefaultAudioSink`, and add an option to use this with `FfmpegAudioRenderer`. +* Support extraction and decoding of Dolby Atmos + ([#2465](https://github.com/google/ExoPlayer/issues/2465)). ### 2.6.0 ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java index e1a70e2579..e9ffab7ace 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java @@ -15,6 +15,10 @@ */ package com.google.android.exoplayer2.audio; +import static com.google.android.exoplayer2.audio.Ac3Util.Ac3SyncFrameInfo.STREAM_TYPE_TYPE0; +import static com.google.android.exoplayer2.audio.Ac3Util.Ac3SyncFrameInfo.STREAM_TYPE_TYPE1; +import static com.google.android.exoplayer2.audio.Ac3Util.Ac3SyncFrameInfo.STREAM_TYPE_UNDEFINED; + import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.drm.DrmInitData; @@ -181,7 +185,14 @@ public final class Ac3Util { channelCount += 2; } } - return Format.createAudioSampleFormat(trackId, MimeTypes.AUDIO_E_AC3, null, Format.NO_VALUE, + String mimeType = MimeTypes.AUDIO_E_AC3; + if (data.bytesLeft() > 0) { + nextByte = data.readUnsignedByte(); + if ((nextByte & 0x01) != 0) { // flag_ec3_extension_type_a + mimeType = MimeTypes.AUDIO_ATMOS; + } + } + return Format.createAudioSampleFormat(trackId, mimeType, null, Format.NO_VALUE, Format.NO_VALUE, channelCount, sampleRate, null, drmInitData, 0, language); } @@ -198,29 +209,176 @@ public final class Ac3Util { boolean isEac3 = data.readBits(5) == 16; data.setPosition(initialPosition); String mimeType; - int streamType = Ac3SyncFrameInfo.STREAM_TYPE_UNDEFINED; + int streamType = STREAM_TYPE_UNDEFINED; int sampleRate; int acmod; int frameSize; int sampleCount; + boolean lfeon; + int channelCount; if (isEac3) { - mimeType = MimeTypes.AUDIO_E_AC3; + // Syntax from ETSI TS 102 366 V1.2.1 subsections E.1.2.1 and E.1.2.2. data.skipBits(16); // syncword streamType = data.readBits(2); data.skipBits(3); // substreamid frameSize = (data.readBits(11) + 1) * 2; int fscod = data.readBits(2); int audioBlocks; + int numblkscod; if (fscod == 3) { + numblkscod = 3; sampleRate = SAMPLE_RATE_BY_FSCOD2[data.readBits(2)]; audioBlocks = 6; } else { - int numblkscod = data.readBits(2); + 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); + lfeon = data.readBit(); + channelCount = CHANNEL_COUNT_BY_ACMOD[acmod] + (lfeon ? 1 : 0); + data.skipBits(5 + 5); // bsid, dialnorm + if (data.readBit()) { // compre + data.skipBits(8); // compr + } + if (acmod == 0) { + data.skipBits(5); // dialnorm2 + if (data.readBit()) { // compr2e + data.skipBits(8); // compr2 + } + } + if (streamType == STREAM_TYPE_TYPE1 && data.readBit()) { // chanmape + data.skipBits(16); // chanmap + } + if (data.readBit()) { // mixmdate + if (acmod > 2) { + data.skipBits(2); // dmixmod + } + if ((acmod & 0x01) != 0 && acmod > 2) { + data.skipBits(3 + 3); // ltrtcmixlev, lorocmixlev + } + if ((acmod & 0x04) != 0) { + data.skipBits(6); // ltrtsurmixlev, lorosurmixlev + } + if (lfeon && data.readBit()) { // lfemixlevcode + data.skipBits(5); // lfemixlevcod + } + if (streamType == STREAM_TYPE_TYPE0) { + if (data.readBit()) { // pgmscle + data.skipBits(6); //pgmscl + } + if (acmod == 0 && data.readBit()) { // pgmscl2e + data.skipBits(6); // pgmscl2 + } + if (data.readBit()) { // extpgmscle + data.skipBits(6); // extpgmscl + } + int mixdef = data.readBits(2); + if (mixdef == 1) { + data.skipBits(1 + 1 + 3); // premixcmpsel, drcsrc, premixcmpscl + } else if (mixdef == 2) { + data.skipBits(12); // mixdata + } else if (mixdef == 3) { + int mixdeflen = data.readBits(5); + if (data.readBit()) { // mixdata2e + data.skipBits(1 + 1 + 3); // premixcmpsel, drcsrc, premixcmpscl + if (data.readBit()) { // extpgmlscle + data.skipBits(4); // extpgmlscl + } + if (data.readBit()) { // extpgmcscle + data.skipBits(4); // extpgmcscl + } + if (data.readBit()) { // extpgmrscle + data.skipBits(4); // extpgmrscl + } + if (data.readBit()) { // extpgmlsscle + data.skipBits(4); // extpgmlsscl + } + if (data.readBit()) { // extpgmrsscle + data.skipBits(4); // extpgmrsscl + } + if (data.readBit()) { // extpgmlfescle + data.skipBits(4); // extpgmlfescl + } + if (data.readBit()) { // dmixscle + data.skipBits(4); // dmixscl + } + if (data.readBit()) { // addche + if (data.readBit()) { // extpgmaux1scle + data.skipBits(4); // extpgmaux1scl + } + if (data.readBit()) { // extpgmaux2scle + data.skipBits(4); // extpgmaux2scl + } + } + } + if (data.readBit()) { // mixdata3e + data.skipBits(5); // spchdat + if (data.readBit()) { // addspchdate + data.skipBits(5 + 2); // spchdat1, spchan1att + if (data.readBit()) { // addspdat1e + data.skipBits(5 + 3); // spchdat2, spchan2att + } + } + } + data.skipBits(8 * (mixdeflen + 2)); // mixdata + data.byteAlign(); // mixdatafill + } + if (acmod < 2) { + if (data.readBit()) { // paninfoe + data.skipBits(8 + 6); // panmean, paninfo + } + if (acmod == 0) { + if (data.readBit()) { // paninfo2e + data.skipBits(8 + 6); // panmean2, paninfo2 + } + } + } + if (data.readBit()) { // frmmixcfginfoe + if (numblkscod == 0) { + data.skipBits(5); // blkmixcfginfo[0] + } else { + for (int blk = 0; blk < audioBlocks; blk++) { + if (data.readBit()) { // blkmixcfginfoe + data.skipBits(5); // blkmixcfginfo[blk] + } + } + } + } + } + } + if (data.readBit()) { // infomdate + data.skipBits(3 + 1 + 1); // bsmod, copyrightb, origbs + if (acmod == 2) { + data.skipBits(2 + 2); // dsurmod, dheadphonmod + } + if (acmod >= 6) { + data.skipBits(2); // dsurexmod + } + if (data.readBit()) { // audioprodie + data.skipBits(5 + 2 + 1); // mixlevel, roomtyp, adconvtyp + } + if (acmod == 0 && data.readBit()) { // audioprodi2e + data.skipBits(5 + 2 + 1); // mixlevel2, roomtyp2, adconvtyp2 + } + if (fscod < 3) { + data.skipBit(); // sourcefscod + } + } + if (streamType == 0 && numblkscod != 3) { + data.skipBit(); // convsync + } + if (streamType == 2 && (numblkscod == 3 || data.readBit())) { // blkid + data.skipBits(6); // frmsizecod + } + mimeType = MimeTypes.AUDIO_E_AC3; + if (data.readBit()) { // addbsie + int addbsil = data.readBits(6); + if (addbsil == 1 && data.readBits(8) == 1) { // addbsi + mimeType = MimeTypes.AUDIO_ATMOS; + } + } } else /* is AC-3 */ { mimeType = MimeTypes.AUDIO_AC3; data.skipBits(16 + 16); // syncword, crc1 @@ -240,9 +398,9 @@ public final class Ac3Util { } sampleRate = SAMPLE_RATE_BY_FSCOD[fscod]; sampleCount = AC3_SYNCFRAME_AUDIO_SAMPLE_COUNT; + lfeon = data.readBit(); + channelCount = CHANNEL_COUNT_BY_ACMOD[acmod] + (lfeon ? 1 : 0); } - boolean lfeon = data.readBit(); - int channelCount = CHANNEL_COUNT_BY_ACMOD[acmod] + (lfeon ? 1 : 0); return new Ac3SyncFrameInfo(mimeType, streamType, channelCount, sampleRate, frameSize, sampleCount); } diff --git a/library/core/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 index 6a1c566faf..8383bfb8d2 100644 --- a/library/core/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 @@ -39,7 +39,7 @@ public final class Ac3Reader implements ElementaryStreamReader { private static final int STATE_READING_HEADER = 1; private static final int STATE_READING_SAMPLE = 2; - private static final int HEADER_SIZE = 8; + private static final int HEADER_SIZE = 128; private final ParsableBitArray headerScratchBits; private final ParsableByteArray headerScratchBytes; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java index f75ce5a9e5..7ae8eb3cd4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java @@ -20,6 +20,7 @@ import android.annotation.TargetApi; import android.media.MediaCodecInfo.CodecCapabilities; import android.media.MediaCodecInfo.CodecProfileLevel; import android.media.MediaCodecList; +import android.support.annotation.Nullable; import android.text.TextUtils; import android.util.Log; import android.util.Pair; @@ -120,7 +121,7 @@ public final class MediaCodecUtil { * exists. * @throws DecoderQueryException If there was an error querying the available decoders. */ - public static MediaCodecInfo getDecoderInfo(String mimeType, boolean secure) + public static @Nullable MediaCodecInfo getDecoderInfo(String mimeType, boolean secure) throws DecoderQueryException { List decoderInfos = getDecoderInfos(mimeType, secure); return decoderInfos.isEmpty() ? null : decoderInfos.get(0); @@ -140,27 +141,34 @@ public final class MediaCodecUtil { public static synchronized List getDecoderInfos(String mimeType, boolean secure) throws DecoderQueryException { CodecKey key = new CodecKey(mimeType, secure); - List decoderInfos = decoderInfosCache.get(key); - if (decoderInfos != null) { - return decoderInfos; + List cachedDecoderInfos = decoderInfosCache.get(key); + if (cachedDecoderInfos != null) { + return cachedDecoderInfos; } MediaCodecListCompat mediaCodecList = Util.SDK_INT >= 21 ? new MediaCodecListCompatV21(secure) : new MediaCodecListCompatV16(); - decoderInfos = getDecoderInfosInternal(key, mediaCodecList); + ArrayList decoderInfos = getDecoderInfosInternal(key, mediaCodecList, mimeType); if (secure && decoderInfos.isEmpty() && 21 <= Util.SDK_INT && Util.SDK_INT <= 23) { // Some devices don't list secure decoders on API level 21 [Internal: b/18678462]. Try the // legacy path. We also try this path on API levels 22 and 23 as a defensive measure. mediaCodecList = new MediaCodecListCompatV16(); - decoderInfos = getDecoderInfosInternal(key, mediaCodecList); + decoderInfos = getDecoderInfosInternal(key, mediaCodecList, mimeType); if (!decoderInfos.isEmpty()) { Log.w(TAG, "MediaCodecList API didn't list secure decoder for: " + mimeType + ". Assuming: " + decoderInfos.get(0).name); } } + if (MimeTypes.AUDIO_ATMOS.equals(mimeType)) { + // E-AC3 decoders can decode Atmos streams, but in 2-D rather than 3-D. + CodecKey eac3Key = new CodecKey(MimeTypes.AUDIO_E_AC3, key.secure); + ArrayList eac3DecoderInfos = + getDecoderInfosInternal(eac3Key, mediaCodecList, mimeType); + decoderInfos.addAll(eac3DecoderInfos); + } applyWorkarounds(decoderInfos); - decoderInfos = Collections.unmodifiableList(decoderInfos); - decoderInfosCache.put(key, decoderInfos); - return decoderInfos; + List unmodifiableDecoderInfos = Collections.unmodifiableList(decoderInfos); + decoderInfosCache.put(key, unmodifiableDecoderInfos); + return unmodifiableDecoderInfos; } /** @@ -212,10 +220,21 @@ public final class MediaCodecUtil { // Internal methods. - private static List getDecoderInfosInternal( - CodecKey key, MediaCodecListCompat mediaCodecList) throws DecoderQueryException { + /** + * Returns {@link MediaCodecInfo}s for the given codec {@code key} in the order given by + * {@code mediaCodecList}. + * + * @param key The codec key. + * @param mediaCodecList The codec list. + * @param requestedMimeType The originally requested MIME type, which may differ from the codec + * key MIME type if the codec key is being considered as a fallback. + * @return The codec information for usable codecs matching the specified key. + * @throws DecoderQueryException If there was an error querying the available decoders. + */ + private static ArrayList getDecoderInfosInternal(CodecKey key, + MediaCodecListCompat mediaCodecList, String requestedMimeType) throws DecoderQueryException { try { - List decoderInfos = new ArrayList<>(); + ArrayList decoderInfos = new ArrayList<>(); String mimeType = key.mimeType; int numberOfCodecs = mediaCodecList.getCodecCount(); boolean secureDecodersExplicit = mediaCodecList.secureDecodersExplicit(); @@ -223,7 +242,7 @@ public final class MediaCodecUtil { for (int i = 0; i < numberOfCodecs; i++) { android.media.MediaCodecInfo codecInfo = mediaCodecList.getCodecInfoAt(i); String codecName = codecInfo.getName(); - if (isCodecUsableDecoder(codecInfo, codecName, secureDecodersExplicit)) { + if (isCodecUsableDecoder(codecInfo, codecName, secureDecodersExplicit, requestedMimeType)) { for (String supportedType : codecInfo.getSupportedTypes()) { if (supportedType.equalsIgnoreCase(mimeType)) { try { @@ -265,9 +284,16 @@ public final class MediaCodecUtil { /** * Returns whether the specified codec is usable for decoding on the current device. + * + * @param info The codec information. + * @param name The name of the codec + * @param secureDecodersExplicit Whether secure decoders were explicitly listed, if present. + * @param requestedMimeType The originally requested MIME type, which may differ from the codec + * key MIME type if the codec key is being considered as a fallback. + * @return Whether the specified codec is usable for decoding on the current device. */ private static boolean isCodecUsableDecoder(android.media.MediaCodecInfo info, String name, - boolean secureDecodersExplicit) { + boolean secureDecodersExplicit, String requestedMimeType) { if (info.isEncoder() || (!secureDecodersExplicit && name.endsWith(".secure"))) { return false; } @@ -356,6 +382,12 @@ public final class MediaCodecUtil { return false; } + // MTK E-AC3 decoder doesn't support decoding Atmos streams in 2-D. See [Internal: b/69400041]. + if (MimeTypes.AUDIO_ATMOS.equals(requestedMimeType) + && "OMX.MTK.AUDIO.DECODER.DSPAC3".equals(name)) { + return false; + } + return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java b/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java index c29a4c3717..a68e0142d6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java @@ -51,6 +51,7 @@ public final class MimeTypes { public static final String AUDIO_MLAW = BASE_TYPE_AUDIO + "/g711-mlaw"; public static final String AUDIO_AC3 = BASE_TYPE_AUDIO + "/ac3"; public static final String AUDIO_E_AC3 = BASE_TYPE_AUDIO + "/eac3"; + public static final String AUDIO_ATMOS = BASE_TYPE_AUDIO + "/eac3-joc"; public static final String AUDIO_TRUEHD = BASE_TYPE_AUDIO + "/true-hd"; public static final String AUDIO_DTS = BASE_TYPE_AUDIO + "/vnd.dts"; public static final String AUDIO_DTS_HD = BASE_TYPE_AUDIO + "/vnd.dts.hd"; @@ -195,6 +196,8 @@ public final class MimeTypes { return MimeTypes.AUDIO_AC3; } else if (codec.startsWith("ec-3") || codec.startsWith("dec3")) { return MimeTypes.AUDIO_E_AC3; + } else if (codec.startsWith("ec+3")) { + return MimeTypes.AUDIO_ATMOS; } else if (codec.startsWith("dtsc") || codec.startsWith("dtse")) { return MimeTypes.AUDIO_DTS; } else if (codec.startsWith("dtsh") || codec.startsWith("dtsl")) { @@ -252,6 +255,7 @@ public final class MimeTypes { case MimeTypes.AUDIO_AC3: return C.ENCODING_AC3; case MimeTypes.AUDIO_E_AC3: + case MimeTypes.AUDIO_ATMOS: return C.ENCODING_E_AC3; case MimeTypes.AUDIO_DTS: return C.ENCODING_DTS; diff --git a/library/dash/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 index 1868a54d17..d3906acdf6 100644 --- a/library/dash/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 @@ -460,6 +460,7 @@ public class DashManifestParser extends DefaultHandler String drmSchemeType = null; ArrayList drmSchemeDatas = new ArrayList<>(); ArrayList inbandEventStreams = new ArrayList<>(); + ArrayList supplementalProperties = new ArrayList<>(); boolean seenFirstBaseUrl = false; do { @@ -487,12 +488,14 @@ public class DashManifestParser extends DefaultHandler } } else if (XmlPullParserUtil.isStartTag(xpp, "InbandEventStream")) { inbandEventStreams.add(parseDescriptor(xpp, "InbandEventStream")); + } else if (XmlPullParserUtil.isStartTag(xpp, "SupplementalProperty")) { + supplementalProperties.add(parseDescriptor(xpp, "SupplementalProperty")); } } while (!XmlPullParserUtil.isEndTag(xpp, "Representation")); Format format = buildFormat(id, mimeType, width, height, frameRate, audioChannels, audioSamplingRate, bandwidth, adaptationSetLanguage, adaptationSetSelectionFlags, - adaptationSetAccessibilityDescriptors, codecs); + adaptationSetAccessibilityDescriptors, codecs, supplementalProperties); segmentBase = segmentBase != null ? segmentBase : new SingleSegmentBase(); return new RepresentationInfo(format, baseUrl, segmentBase, drmSchemeType, drmSchemeDatas, @@ -502,9 +505,12 @@ public class DashManifestParser extends DefaultHandler protected Format buildFormat(String id, String containerMimeType, int width, int height, float frameRate, int audioChannels, int audioSamplingRate, int bitrate, String language, @C.SelectionFlags int selectionFlags, List accessibilityDescriptors, - String codecs) { + String codecs, List supplementalProperties) { String sampleMimeType = getSampleMimeType(containerMimeType, codecs); if (sampleMimeType != null) { + if (MimeTypes.AUDIO_E_AC3.equals(sampleMimeType)) { + sampleMimeType = parseEac3SupplementalProperties(supplementalProperties); + } if (MimeTypes.isVideo(sampleMimeType)) { return Format.createVideoContainerFormat(id, containerMimeType, sampleMimeType, codecs, bitrate, width, height, frameRate, null, selectionFlags); @@ -1045,6 +1051,18 @@ public class DashManifestParser extends DefaultHandler return Format.NO_VALUE; } + protected static String parseEac3SupplementalProperties(List supplementalProperties) { + for (int i = 0; i < supplementalProperties.size(); i++) { + Descriptor descriptor = supplementalProperties.get(i); + String schemeIdUri = descriptor.schemeIdUri; + if ("tag:dolby.com,2014:dash:DolbyDigitalPlusExtensionType:2014".equals(schemeIdUri) + && "ec+3".equals(descriptor.value)) { + return MimeTypes.AUDIO_ATMOS; + } + } + return MimeTypes.AUDIO_E_AC3; + } + protected static float parseFrameRate(XmlPullParser xpp, float defaultValue) { float frameRate = defaultValue; String frameRateAttribute = xpp.getAttributeValue(null, "frameRate"); From e469269f3c45e6c11737512447ccea6d95f505be Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 20 Nov 2017 03:36:50 -0800 Subject: [PATCH 0727/2472] Fix some lint issues. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176337058 --- extensions/ima/build.gradle | 9 +++---- .../exoplayer2/offline/SegmentDownloader.java | 2 +- .../google/android/exoplayer2/util/Util.java | 3 +++ .../dash/manifest/DashManifestParser.java | 2 +- .../source/dash/offline/DashDownloader.java | 4 ++-- .../smoothstreaming/offline/SsDownloader.java | 4 ++-- library/ui/src/main/res/values-v11/styles.xml | 24 ------------------- library/ui/src/main/res/values/styles.xml | 2 +- .../testutil/FakeSimpleExoPlayer.java | 8 ++++++- 9 files changed, 22 insertions(+), 36 deletions(-) delete mode 100644 library/ui/src/main/res/values-v11/styles.xml diff --git a/extensions/ima/build.gradle b/extensions/ima/build.gradle index 90c0a911d9..5038aaf5b9 100644 --- a/extensions/ima/build.gradle +++ b/extensions/ima/build.gradle @@ -28,10 +28,11 @@ android { dependencies { compile project(modulePrefix + 'library-core') // This dependency is necessary to force the supportLibraryVersion of - // com.android.support:support-v4 to be used. Else an older version (25.2.0) is included via: - // com.google.android.gms:play-services-ads:11.2.0 - // |-- com.google.android.gms:play-services-ads-lite:11.2.0 - // |-- com.google.android.gms:play-services-basement:11.2.0 + // com.android.support:support-v4 to be used. Else an older version (25.2.0) + // is included via: + // com.google.android.gms:play-services-ads:11.4.2 + // |-- com.google.android.gms:play-services-ads-lite:11.4.2 + // |-- com.google.android.gms:play-services-basement:11.4.2 // |-- com.android.support:support-v4:25.2.0 compile 'com.android.support:support-v4:' + supportLibraryVersion compile 'com.google.ads.interactivemedia.v3:interactivemedia:3.7.4' diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java index d81df90b81..3cb5db30ec 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java @@ -265,7 +265,7 @@ public abstract class SegmentDownloader implements Downloader { /** * Returns a list of all segments. * - * @see #getSegments(DataSource, M, Object[], boolean)}. + * @see #getSegments(DataSource, M, Object[], boolean) */ protected abstract List getAllSegments(DataSource dataSource, M manifest, boolean allowPartialIndex) throws InterruptedException, IOException; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java index f47caa046a..a79ed38755 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -32,6 +32,7 @@ import android.view.Display; import android.view.WindowManager; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; +import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.upstream.DataSource; import java.io.ByteArrayOutputStream; @@ -801,6 +802,8 @@ public final class Util { return channelCount * 3; case C.ENCODING_PCM_32BIT: return channelCount * 4; + case C.ENCODING_INVALID: + case Format.NO_VALUE: default: throw new IllegalArgumentException(); } diff --git a/library/dash/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 index 72df69f7e9..7ffb429784 100644 --- a/library/dash/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 @@ -352,7 +352,7 @@ public class DashManifestParser extends DefaultHandler String schemeIdUri = xpp.getAttributeValue(null, "schemeIdUri"); if (schemeIdUri != null) { - switch (schemeIdUri.toLowerCase()) { + switch (Util.toLowerInvariant(schemeIdUri)) { case "urn:mpeg:dash:mp4protection:2011": schemeType = xpp.getAttributeValue(null, "value"); String defaultKid = xpp.getAttributeValue(null, "cenc:default_KID"); diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloader.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloader.java index 558adca7bd..4c07e4874e 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloader.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloader.java @@ -39,8 +39,8 @@ import java.util.List; /** * Helper class to download DASH streams. * - *

          Except {@link #getTotalSegments()}, {@link #getDownloadedSegments()} and {@link - * #getDownloadedBytes()}, this class isn't thread safe. + *

          Except {@link #getTotalSegments()}, {@link #getDownloadedSegments()} and + * {@link #getDownloadedBytes()}, this class isn't thread safe. * *

          Example usage: * diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java index 21cacdc6f3..5e9ae9a164 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java @@ -33,8 +33,8 @@ import java.util.List; /** * Helper class to download SmoothStreaming streams. * - *

          Except {@link #getTotalSegments()}, {@link #getDownloadedSegments()} and {@link - * #getDownloadedBytes()}, this class isn't thread safe. + *

          Except {@link #getTotalSegments()}, {@link #getDownloadedSegments()} and + * {@link #getDownloadedBytes()}, this class isn't thread safe. * *

          Example usage: * diff --git a/library/ui/src/main/res/values-v11/styles.xml b/library/ui/src/main/res/values-v11/styles.xml deleted file mode 100644 index 6f77440287..0000000000 --- a/library/ui/src/main/res/values-v11/styles.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - diff --git a/library/ui/src/main/res/values/styles.xml b/library/ui/src/main/res/values/styles.xml index 4ef8971ccd..b57cbeaddf 100644 --- a/library/ui/src/main/res/values/styles.xml +++ b/library/ui/src/main/res/values/styles.xml @@ -16,7 +16,7 @@ diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java index 01f984b212..4d53a6c89d 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java @@ -215,7 +215,13 @@ public class FakeSimpleExoPlayer extends SimpleExoPlayer { @SuppressWarnings("ThreadJoinLoop") public void release() { stop(); - playbackThread.quitSafely(); + playbackHandler.post(new Runnable() { + @Override + public void run () { + playbackHandler.removeCallbacksAndMessages(null); + playbackThread.quit(); + } + }); while (playbackThread.isAlive()) { try { playbackThread.join(); From a9d91b3387262e7fd954db6346b44048616af76c Mon Sep 17 00:00:00 2001 From: Ian Bird Date: Tue, 21 Nov 2017 10:59:04 +0000 Subject: [PATCH 0728/2472] Fix initializationData check for SSA subtitles --- .../java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java index d2f5a67c27..12aa1e97d5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java @@ -59,7 +59,7 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { */ public SsaDecoder(List initializationData) { super("SsaDecoder"); - if (initializationData != null) { + if (initializationData != null && initializationData.size() > 0) { haveInitializationData = true; String formatLine = new String(initializationData.get(0)); Assertions.checkArgument(formatLine.startsWith(FORMAT_LINE_PREFIX)); From e45907193cb1ed7bb9af337aa7e941f8e834ae64 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Mon, 20 Nov 2017 06:22:54 -0800 Subject: [PATCH 0729/2472] Allow human readable strings as DRM intent extras. Issue:#3478 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176351086 --- .../android/exoplayer2/demo/DemoUtil.java | 31 ++++++++++++++++++- .../exoplayer2/demo/PlayerActivity.java | 12 ++++--- .../demo/SampleChooserActivity.java | 27 +++++----------- 3 files changed, 45 insertions(+), 25 deletions(-) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java index f9e9c34158..5ff7c5cb40 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java @@ -16,14 +16,43 @@ package com.google.android.exoplayer2.demo; import android.text.TextUtils; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.drm.UnsupportedDrmException; import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.Util; import java.util.Locale; +import java.util.UUID; /** * Utility methods for demo application. */ -/*package*/ final class DemoUtil { +/* package */ final class DemoUtil { + + /** + * Derives a DRM {@link UUID} from {@code drmScheme}. + * + * @param drmScheme A protection scheme UUID string; or {@code "widevine"}, {@code "playready"} or + * {@code "clearkey"}. + * @return The derived {@link UUID}. + * @throws UnsupportedDrmException If no {@link UUID} could be derived from {@code drmScheme}. + */ + public static UUID getDrmUuid(String drmScheme) throws UnsupportedDrmException { + switch (Util.toLowerInvariant(drmScheme)) { + case "widevine": + return C.WIDEVINE_UUID; + case "playready": + return C.PLAYREADY_UUID; + case "clearkey": + return C.CLEARKEY_UUID; + default: + try { + return UUID.fromString(drmScheme); + } catch (RuntimeException e) { + throw new UnsupportedDrmException(UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME); + } + } + } /** * Builds a track name for display. diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index ca253db809..efde775176 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -83,7 +83,7 @@ import java.util.UUID; public class PlayerActivity extends Activity implements OnClickListener, PlaybackControlView.VisibilityListener { - public static final String DRM_SCHEME_UUID_EXTRA = "drm_scheme_uuid"; + public static final String DRM_SCHEME_EXTRA = "drm_scheme"; public static final String DRM_LICENSE_URL = "drm_license_url"; public static final String DRM_KEY_REQUEST_PROPERTIES = "drm_key_request_properties"; public static final String DRM_MULTI_SESSION = "drm_multi_session"; @@ -98,6 +98,9 @@ public class PlayerActivity extends Activity implements OnClickListener, public static final String EXTENSION_LIST_EXTRA = "extension_list"; public static final String AD_TAG_URI_EXTRA = "ad_tag_uri"; + // For backwards compatibility. + private static final String DRM_SCHEME_UUID_EXTRA = "drm_scheme_uuid"; + private static final DefaultBandwidthMeter BANDWIDTH_METER = new DefaultBandwidthMeter(); private static final CookieManager DEFAULT_COOKIE_MANAGER; static { @@ -256,10 +259,8 @@ public class PlayerActivity extends Activity implements OnClickListener, lastSeenTrackGroupArray = null; eventLogger = new EventLogger(trackSelector); - UUID drmSchemeUuid = intent.hasExtra(DRM_SCHEME_UUID_EXTRA) - ? UUID.fromString(intent.getStringExtra(DRM_SCHEME_UUID_EXTRA)) : null; DrmSessionManager drmSessionManager = null; - if (drmSchemeUuid != null) { + if (intent.hasExtra(DRM_SCHEME_EXTRA) || intent.hasExtra(DRM_SCHEME_UUID_EXTRA)) { String drmLicenseUrl = intent.getStringExtra(DRM_LICENSE_URL); String[] keyRequestPropertiesArray = intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES); boolean multiSession = intent.getBooleanExtra(DRM_MULTI_SESSION, false); @@ -268,6 +269,9 @@ public class PlayerActivity extends Activity implements OnClickListener, errorStringId = R.string.error_drm_not_supported; } else { try { + String drmSchemeExtra = intent.hasExtra(DRM_SCHEME_EXTRA) ? DRM_SCHEME_EXTRA + : DRM_SCHEME_UUID_EXTRA; + UUID drmSchemeUuid = DemoUtil.getDrmUuid(intent.getStringExtra(drmSchemeExtra)); drmSessionManager = buildDrmSessionManagerV18(drmSchemeUuid, drmLicenseUrl, keyRequestPropertiesArray, multiSession); } catch (UnsupportedDrmException e) { diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java index 1f84b1f29c..308bab2a3b 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java @@ -32,8 +32,8 @@ import android.widget.ExpandableListView; import android.widget.ExpandableListView.OnChildClickListener; import android.widget.TextView; import android.widget.Toast; -import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.drm.UnsupportedDrmException; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSourceInputStream; import com.google.android.exoplayer2.upstream.DataSpec; @@ -202,7 +202,11 @@ public class SampleChooserActivity extends Activity { break; case "drm_scheme": Assertions.checkState(!insidePlaylist, "Invalid attribute on nested item: drm_scheme"); - drmUuid = getDrmUuid(reader.nextString()); + try { + drmUuid = DemoUtil.getDrmUuid(reader.nextString()); + } catch (UnsupportedDrmException e) { + throw new ParserException(e); + } break; case "drm_license_url": Assertions.checkState(!insidePlaylist, @@ -270,23 +274,6 @@ public class SampleChooserActivity extends Activity { return group; } - private UUID getDrmUuid(String typeString) throws ParserException { - switch (Util.toLowerInvariant(typeString)) { - case "widevine": - return C.WIDEVINE_UUID; - case "playready": - return C.PLAYREADY_UUID; - case "clearkey": - return C.CLEARKEY_UUID; - default: - try { - return UUID.fromString(typeString); - } catch (RuntimeException e) { - throw new ParserException("Unsupported drm type: " + typeString); - } - } - } - } private static final class SampleAdapter extends BaseExpandableListAdapter { @@ -393,7 +380,7 @@ public class SampleChooserActivity extends Activity { public void updateIntent(Intent intent) { Assertions.checkNotNull(intent); - intent.putExtra(PlayerActivity.DRM_SCHEME_UUID_EXTRA, drmSchemeUuid.toString()); + intent.putExtra(PlayerActivity.DRM_SCHEME_EXTRA, drmSchemeUuid.toString()); intent.putExtra(PlayerActivity.DRM_LICENSE_URL, drmLicenseUrl); intent.putExtra(PlayerActivity.DRM_KEY_REQUEST_PROPERTIES, drmKeyRequestProperties); intent.putExtra(PlayerActivity.DRM_MULTI_SESSION, drmMultiSession); From 0de57cbfae7165dd3bb829e323d089cd312b4b1b Mon Sep 17 00:00:00 2001 From: hoangtc Date: Mon, 20 Nov 2017 08:22:19 -0800 Subject: [PATCH 0730/2472] Allow more flexible loading strategy when loading multiple sub streams. Currently for a DASH ChunkSource that consists of multiple sub-streams, we always use a CompositeSequenceableLoader, which only allows the furthest behind loader or any loader that are behind current playback position to continue loading. This changes allow clients to have more flexibility when deciding the loading strategy: - They can construct a different kind of composite SequenceableLoader from the sub-loaders, and use it by injecting a different CompositeSequeableLoaderFactory accordingly. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176363870 --- RELEASENOTES.md | 8 +++- .../source/CompositeSequenceableLoader.java | 6 +-- .../CompositeSequenceableLoaderFactory.java | 31 +++++++++++++ ...ultCompositeSequenceableLoaderFactory.java | 29 ++++++++++++ .../exoplayer2/source/MergingMediaPeriod.java | 16 ++++--- .../exoplayer2/source/MergingMediaSource.java | 15 ++++++- .../source/dash/DashMediaPeriod.java | 23 ++++++---- .../source/dash/DashMediaSource.java | 42 ++++++++++++++--- .../exoplayer2/source/hls/HlsMediaPeriod.java | 20 ++++++--- .../exoplayer2/source/hls/HlsMediaSource.java | 42 ++++++++++++++++- .../source/smoothstreaming/SsMediaPeriod.java | 19 +++++--- .../source/smoothstreaming/SsMediaSource.java | 45 +++++++++++++++---- 12 files changed, 244 insertions(+), 52 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoaderFactory.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/source/DefaultCompositeSequenceableLoaderFactory.java diff --git a/RELEASENOTES.md b/RELEASENOTES.md index d9ed3e5d2a..41748fa10d 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,8 +2,12 @@ ### dev-v2 (not yet released) ### -* Add Builder to ExtractorMediaSource, HlsMediaSource, SsMediaSource, - DashMediaSource, SingleSampleMediaSource. +* Allow more flexible loading strategy when playing media containing multiple + sub-streams, by allowing injection of custom `CompositeSequenceableLoader` + factories through `DashMediaSource.Builder`, `HlsMediaSource.Builder`, + `SsMediaSource.Builder`, and `MergingMediaSource`. +* Add Builder to `ExtractorMediaSource`, `HlsMediaSource`, `SsMediaSource`, + `DashMediaSource`, `SingleSampleMediaSource`. * DASH: * Support in-MPD EventStream. * Allow a back-buffer of media to be retained behind the current playback diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java b/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java index a85d589762..e9a187a747 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java @@ -20,9 +20,9 @@ import com.google.android.exoplayer2.C; /** * A {@link SequenceableLoader} that encapsulates multiple other {@link SequenceableLoader}s. */ -public final class CompositeSequenceableLoader implements SequenceableLoader { +public class CompositeSequenceableLoader implements SequenceableLoader { - private final SequenceableLoader[] loaders; + protected final SequenceableLoader[] loaders; public CompositeSequenceableLoader(SequenceableLoader[] loaders) { this.loaders = loaders; @@ -53,7 +53,7 @@ public final class CompositeSequenceableLoader implements SequenceableLoader { } @Override - public final boolean continueLoading(long positionUs) { + public boolean continueLoading(long positionUs) { boolean madeProgress = false; boolean madeProgressThisIteration; do { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoaderFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoaderFactory.java new file mode 100644 index 0000000000..b4a266feef --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoaderFactory.java @@ -0,0 +1,31 @@ +/* + * 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; + +/** + * A factory to create composite {@link SequenceableLoader}s. + */ +public interface CompositeSequenceableLoaderFactory { + + /** + * Creates a composite {@link SequenceableLoader}. + * + * @param loaders The sub-loaders that make up the {@link SequenceableLoader} to be built. + * @return A composite {@link SequenceableLoader} that comprises the given loaders. + */ + SequenceableLoader createCompositeSequenceableLoader(SequenceableLoader... loaders); + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultCompositeSequenceableLoaderFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultCompositeSequenceableLoaderFactory.java new file mode 100644 index 0000000000..759b0824af --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultCompositeSequenceableLoaderFactory.java @@ -0,0 +1,29 @@ +/* + * 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; + +/** + * Default implementation of {@link CompositeSequenceableLoaderFactory}. + */ +public final class DefaultCompositeSequenceableLoaderFactory + implements CompositeSequenceableLoaderFactory { + + @Override + public SequenceableLoader createCompositeSequenceableLoader(SequenceableLoader... loaders) { + return new CompositeSequenceableLoader(loaders); + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java index 786a4693d0..bd37b5efec 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java @@ -30,15 +30,18 @@ import java.util.IdentityHashMap; public final MediaPeriod[] periods; private final IdentityHashMap streamPeriodIndices; + private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private Callback callback; private int pendingChildPrepareCount; private TrackGroupArray trackGroups; private MediaPeriod[] enabledPeriods; - private SequenceableLoader sequenceableLoader; + private SequenceableLoader compositeSequenceableLoader; - public MergingMediaPeriod(MediaPeriod... periods) { + public MergingMediaPeriod(CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, + MediaPeriod... periods) { + this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; this.periods = periods; streamPeriodIndices = new IdentityHashMap<>(); } @@ -124,7 +127,8 @@ import java.util.IdentityHashMap; // Update the local state. enabledPeriods = new MediaPeriod[enabledPeriodsList.size()]; enabledPeriodsList.toArray(enabledPeriods); - sequenceableLoader = new CompositeSequenceableLoader(enabledPeriods); + compositeSequenceableLoader = + compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(enabledPeriods); return positionUs; } @@ -137,12 +141,12 @@ import java.util.IdentityHashMap; @Override public boolean continueLoading(long positionUs) { - return sequenceableLoader.continueLoading(positionUs); + return compositeSequenceableLoader.continueLoading(positionUs); } @Override public long getNextLoadPositionUs() { - return sequenceableLoader.getNextLoadPositionUs(); + return compositeSequenceableLoader.getNextLoadPositionUs(); } @Override @@ -168,7 +172,7 @@ import java.util.IdentityHashMap; @Override public long getBufferedPositionUs() { - return sequenceableLoader.getBufferedPositionUs(); + return compositeSequenceableLoader.getBufferedPositionUs(); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java index 1550970e47..ea0274796f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java @@ -74,6 +74,7 @@ public final class MergingMediaSource implements MediaSource { private final MediaSource[] mediaSources; private final ArrayList pendingTimelineSources; private final Timeline.Window window; + private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private Listener listener; private Timeline primaryTimeline; @@ -85,7 +86,19 @@ public final class MergingMediaSource implements MediaSource { * @param mediaSources The {@link MediaSource}s to merge. */ public MergingMediaSource(MediaSource... mediaSources) { + this(new DefaultCompositeSequenceableLoaderFactory(), mediaSources); + } + + /** + * @param compositeSequenceableLoaderFactory A factory to create composite + * {@link SequenceableLoader}s for when this media source loads data from multiple streams + * (video, audio etc...). + * @param mediaSources The {@link MediaSource}s to merge. + */ + public MergingMediaSource(CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, + MediaSource... mediaSources) { this.mediaSources = mediaSources; + this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; pendingTimelineSources = new ArrayList<>(Arrays.asList(mediaSources)); window = new Timeline.Window(); periodCount = PERIOD_COUNT_UNSET; @@ -121,7 +134,7 @@ public final class MergingMediaSource implements MediaSource { for (int i = 0; i < periods.length; i++) { periods[i] = mediaSources[i].createPeriod(id, allocator); } - return new MergingMediaPeriod(periods); + return new MergingMediaPeriod(compositeSequenceableLoaderFactory, periods); } @Override diff --git a/library/dash/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 index 5a60ee46ae..70fba4dd00 100644 --- a/library/dash/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 @@ -21,7 +21,7 @@ import android.util.SparseIntArray; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; -import com.google.android.exoplayer2.source.CompositeSequenceableLoader; +import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.EmptySampleStream; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.SampleStream; @@ -64,19 +64,21 @@ import java.util.Map; private final Allocator allocator; private final TrackGroupArray trackGroups; private final TrackGroupInfo[] trackGroupInfos; + private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private Callback callback; private ChunkSampleStream[] sampleStreams; private EventSampleStream[] eventSampleStreams; - private CompositeSequenceableLoader sequenceableLoader; + private SequenceableLoader compositeSequenceableLoader; private DashManifest manifest; private int periodIndex; private List eventStreams; public DashMediaPeriod(int id, DashManifest manifest, int periodIndex, - DashChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, + DashChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, EventDispatcher eventDispatcher, long elapsedRealtimeOffset, - LoaderErrorThrower manifestLoaderErrorThrower, Allocator allocator) { + LoaderErrorThrower manifestLoaderErrorThrower, Allocator allocator, + CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory) { this.id = id; this.manifest = manifest; this.periodIndex = periodIndex; @@ -86,9 +88,11 @@ import java.util.Map; this.elapsedRealtimeOffset = elapsedRealtimeOffset; this.manifestLoaderErrorThrower = manifestLoaderErrorThrower; this.allocator = allocator; + this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; sampleStreams = newSampleStreamArray(0); eventSampleStreams = new EventSampleStream[0]; - sequenceableLoader = new CompositeSequenceableLoader(sampleStreams); + compositeSequenceableLoader = + compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(sampleStreams); Period period = manifest.getPeriod(periodIndex); eventStreams = period.eventStreams; Pair result = buildTrackGroups(period.adaptationSets, @@ -163,7 +167,8 @@ import java.util.Map; primarySampleStreams.values().toArray(sampleStreams); eventSampleStreams = new EventSampleStream[eventSampleStreamList.size()]; eventSampleStreamList.toArray(eventSampleStreams); - sequenceableLoader = new CompositeSequenceableLoader(sampleStreams); + compositeSequenceableLoader = + compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(sampleStreams); return positionUs; } @@ -267,12 +272,12 @@ import java.util.Map; @Override public boolean continueLoading(long positionUs) { - return sequenceableLoader.continueLoading(positionUs); + return compositeSequenceableLoader.continueLoading(positionUs); } @Override public long getNextLoadPositionUs() { - return sequenceableLoader.getNextLoadPositionUs(); + return compositeSequenceableLoader.getNextLoadPositionUs(); } @Override @@ -282,7 +287,7 @@ import java.util.Map; @Override public long getBufferedPositionUs() { - return sequenceableLoader.getBufferedPositionUs(); + return compositeSequenceableLoader.getBufferedPositionUs(); } @Override diff --git a/library/dash/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 index a82b5af583..68d39b5a18 100644 --- a/library/dash/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 @@ -28,8 +28,11 @@ import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener; import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; +import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; +import com.google.android.exoplayer2.source.DefaultCompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.SequenceableLoader; 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.UtcTimingElement; @@ -71,6 +74,7 @@ public final class DashMediaSource implements MediaSource { private ParsingLoadable.Parser manifestParser; private AdaptiveMediaSourceEventListener eventListener; private Handler eventHandler; + private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private int minLoadableRetryCount; private long livePresentationDelayMs; @@ -171,6 +175,22 @@ public final class DashMediaSource implements MediaSource { return this; } + /** + * Sets the factory to create composite {@link SequenceableLoader}s for when this media source + * loads data from multiple streams (video, audio etc...). The default is an instance of + * {@link DefaultCompositeSequenceableLoaderFactory}. + * + * @param compositeSequenceableLoaderFactory A factory to create composite + * {@link SequenceableLoader}s for when this media source loads data from multiple streams + * (video, audio etc...). + * @return This builder. + */ + public Builder setCompositeSequenceableLoaderFactory( + CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory) { + this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; + return this; + } + /** * Builds a new {@link DashMediaSource} using the current parameters. *

          @@ -186,9 +206,12 @@ public final class DashMediaSource implements MediaSource { if (loadableManifestUri && manifestParser == null) { manifestParser = new DashManifestParser(); } + if (compositeSequenceableLoaderFactory == null) { + compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory(); + } return new DashMediaSource(manifest, manifestUri, manifestDataSourceFactory, manifestParser, - chunkSourceFactory, minLoadableRetryCount, livePresentationDelayMs, eventHandler, - eventListener); + chunkSourceFactory, compositeSequenceableLoaderFactory, minLoadableRetryCount, + livePresentationDelayMs, eventHandler, eventListener); } } @@ -226,6 +249,7 @@ public final class DashMediaSource implements MediaSource { private final boolean sideloadedManifest; private final DataSource.Factory manifestDataSourceFactory; private final DashChunkSource.Factory chunkSourceFactory; + private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private final int minLoadableRetryCount; private final long livePresentationDelayMs; private final EventDispatcher eventDispatcher; @@ -280,7 +304,8 @@ public final class DashMediaSource implements MediaSource { public DashMediaSource(DashManifest manifest, DashChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { - this(manifest, null, null, null, chunkSourceFactory, minLoadableRetryCount, + this(manifest, null, null, null, chunkSourceFactory, + new DefaultCompositeSequenceableLoaderFactory(), minLoadableRetryCount, DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS, eventHandler, eventListener); } @@ -356,14 +381,16 @@ public final class DashMediaSource implements MediaSource { long livePresentationDelayMs, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { this(null, manifestUri, manifestDataSourceFactory, manifestParser, chunkSourceFactory, - minLoadableRetryCount, livePresentationDelayMs, eventHandler, eventListener); + new DefaultCompositeSequenceableLoaderFactory(), minLoadableRetryCount, + livePresentationDelayMs, eventHandler, eventListener); } private DashMediaSource(DashManifest manifest, Uri manifestUri, DataSource.Factory manifestDataSourceFactory, ParsingLoadable.Parser manifestParser, - DashChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, - long livePresentationDelayMs, Handler eventHandler, + DashChunkSource.Factory chunkSourceFactory, + CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, + int minLoadableRetryCount, long livePresentationDelayMs, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { this.manifest = manifest; this.manifestUri = manifestUri; @@ -372,6 +399,7 @@ public final class DashMediaSource implements MediaSource { this.chunkSourceFactory = chunkSourceFactory; this.minLoadableRetryCount = minLoadableRetryCount; this.livePresentationDelayMs = livePresentationDelayMs; + this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; sideloadedManifest = manifest != null; eventDispatcher = new EventDispatcher(eventHandler, eventListener); manifestUriLock = new Object(); @@ -438,7 +466,7 @@ public final class DashMediaSource implements MediaSource { manifest.getPeriod(periodIndex).startMs); DashMediaPeriod mediaPeriod = new DashMediaPeriod(firstPeriodId + periodIndex, manifest, periodIndex, chunkSourceFactory, minLoadableRetryCount, periodEventDispatcher, - elapsedRealtimeOffsetMs, loaderErrorThrower, allocator); + elapsedRealtimeOffsetMs, loaderErrorThrower, allocator, compositeSequenceableLoaderFactory); periodsById.put(mediaPeriod.id, mediaPeriod); return mediaPeriod; } diff --git a/library/hls/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 index bc2b92cfe8..b6c74d61bb 100644 --- a/library/hls/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 @@ -20,9 +20,10 @@ import android.text.TextUtils; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; -import com.google.android.exoplayer2.source.CompositeSequenceableLoader; +import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.SampleStream; +import com.google.android.exoplayer2.source.SequenceableLoader; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist; @@ -53,23 +54,26 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper private final IdentityHashMap streamWrapperIndices; private final TimestampAdjusterProvider timestampAdjusterProvider; private final Handler continueLoadingHandler; + private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private Callback callback; private int pendingPrepareCount; private TrackGroupArray trackGroups; private HlsSampleStreamWrapper[] sampleStreamWrappers; private HlsSampleStreamWrapper[] enabledSampleStreamWrappers; - private CompositeSequenceableLoader sequenceableLoader; + private SequenceableLoader compositeSequenceableLoader; public HlsMediaPeriod(HlsExtractorFactory extractorFactory, HlsPlaylistTracker playlistTracker, HlsDataSourceFactory dataSourceFactory, int minLoadableRetryCount, - EventDispatcher eventDispatcher, Allocator allocator) { + EventDispatcher eventDispatcher, Allocator allocator, + CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory) { this.extractorFactory = extractorFactory; this.playlistTracker = playlistTracker; this.dataSourceFactory = dataSourceFactory; this.minLoadableRetryCount = minLoadableRetryCount; this.eventDispatcher = eventDispatcher; this.allocator = allocator; + this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; streamWrapperIndices = new IdentityHashMap<>(); timestampAdjusterProvider = new TimestampAdjusterProvider(); continueLoadingHandler = new Handler(); @@ -178,7 +182,9 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper // Update the local state. enabledSampleStreamWrappers = Arrays.copyOf(newEnabledSampleStreamWrappers, newEnabledSampleStreamWrapperCount); - sequenceableLoader = new CompositeSequenceableLoader(enabledSampleStreamWrappers); + compositeSequenceableLoader = + compositeSequenceableLoaderFactory.createCompositeSequenceableLoader( + enabledSampleStreamWrappers); return positionUs; } @@ -191,12 +197,12 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper @Override public boolean continueLoading(long positionUs) { - return sequenceableLoader.continueLoading(positionUs); + return compositeSequenceableLoader.continueLoading(positionUs); } @Override public long getNextLoadPositionUs() { - return sequenceableLoader.getNextLoadPositionUs(); + return compositeSequenceableLoader.getNextLoadPositionUs(); } @Override @@ -206,7 +212,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper @Override public long getBufferedPositionUs() { - return sequenceableLoader.getBufferedPositionUs(); + return compositeSequenceableLoader.getBufferedPositionUs(); } @Override diff --git a/library/hls/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 index 3f28981f0e..a412b8c3e9 100644 --- a/library/hls/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 @@ -23,8 +23,11 @@ import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener; import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; +import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; +import com.google.android.exoplayer2.source.DefaultCompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.SequenceableLoader; import com.google.android.exoplayer2.source.SinglePeriodTimeline; import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylist; @@ -59,6 +62,8 @@ public final class HlsMediaSource implements MediaSource, private ParsingLoadable.Parser playlistParser; private AdaptiveMediaSourceEventListener eventListener; private Handler eventHandler; + private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; + private int minLoadableRetryCount; private boolean isBuildCalled; @@ -150,6 +155,22 @@ public final class HlsMediaSource implements MediaSource, return this; } + /** + * Sets the factory to create composite {@link SequenceableLoader}s for when this media source + * loads data from multiple streams (video, audio etc...). The default is an instance of + * {@link DefaultCompositeSequenceableLoaderFactory}. + * + * @param compositeSequenceableLoaderFactory A factory to create composite + * {@link SequenceableLoader}s for when this media source loads data from multiple streams + * (video, audio etc...). + * @return This builder. + */ + public Builder setCompositeSequenceableLoaderFactory( + CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory) { + this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; + return this; + } + /** * Builds a new {@link HlsMediaSource} using the current parameters. *

          @@ -167,8 +188,12 @@ public final class HlsMediaSource implements MediaSource, if (playlistParser == null) { playlistParser = new HlsPlaylistParser(); } + if (compositeSequenceableLoaderFactory == null) { + compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory(); + } return new HlsMediaSource(manifestUri, hlsDataSourceFactory, extractorFactory, - minLoadableRetryCount, eventHandler, eventListener, playlistParser); + compositeSequenceableLoaderFactory, minLoadableRetryCount, eventHandler, eventListener, + playlistParser); } } @@ -181,6 +206,7 @@ public final class HlsMediaSource implements MediaSource, private final HlsExtractorFactory extractorFactory; private final Uri manifestUri; private final HlsDataSourceFactory dataSourceFactory; + private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private final int minLoadableRetryCount; private final EventDispatcher eventDispatcher; private final ParsingLoadable.Parser playlistParser; @@ -242,11 +268,23 @@ public final class HlsMediaSource implements MediaSource, HlsExtractorFactory extractorFactory, int minLoadableRetryCount, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener, ParsingLoadable.Parser playlistParser) { + this(manifestUri, dataSourceFactory, extractorFactory, + new DefaultCompositeSequenceableLoaderFactory(), minLoadableRetryCount, eventHandler, + eventListener, playlistParser); + } + + private HlsMediaSource(Uri manifestUri, HlsDataSourceFactory dataSourceFactory, + HlsExtractorFactory extractorFactory, + CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, + int minLoadableRetryCount, Handler eventHandler, + AdaptiveMediaSourceEventListener eventListener, + ParsingLoadable.Parser playlistParser) { this.manifestUri = manifestUri; this.dataSourceFactory = dataSourceFactory; this.extractorFactory = extractorFactory; this.minLoadableRetryCount = minLoadableRetryCount; this.playlistParser = playlistParser; + this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; eventDispatcher = new EventDispatcher(eventHandler, eventListener); } @@ -268,7 +306,7 @@ public final class HlsMediaSource implements MediaSource, public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { Assertions.checkArgument(id.periodIndex == 0); return new HlsMediaPeriod(extractorFactory, playlistTracker, dataSourceFactory, - minLoadableRetryCount, eventDispatcher, allocator); + minLoadableRetryCount, eventDispatcher, allocator, compositeSequenceableLoaderFactory); } @Override diff --git a/library/smoothstreaming/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 index 3c51abcd49..c079a36d62 100644 --- a/library/smoothstreaming/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 @@ -19,7 +19,7 @@ import android.util.Base64; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.mp4.TrackEncryptionBox; import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; -import com.google.android.exoplayer2.source.CompositeSequenceableLoader; +import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.source.SequenceableLoader; @@ -49,13 +49,15 @@ import java.util.ArrayList; private final Allocator allocator; private final TrackGroupArray trackGroups; private final TrackEncryptionBox[] trackEncryptionBoxes; + private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private Callback callback; private SsManifest manifest; private ChunkSampleStream[] sampleStreams; - private CompositeSequenceableLoader sequenceableLoader; + private SequenceableLoader compositeSequenceableLoader; public SsMediaPeriod(SsManifest manifest, SsChunkSource.Factory chunkSourceFactory, + CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, int minLoadableRetryCount, EventDispatcher eventDispatcher, LoaderErrorThrower manifestLoaderErrorThrower, Allocator allocator) { this.chunkSourceFactory = chunkSourceFactory; @@ -63,6 +65,7 @@ import java.util.ArrayList; this.minLoadableRetryCount = minLoadableRetryCount; this.eventDispatcher = eventDispatcher; this.allocator = allocator; + this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; trackGroups = buildTrackGroups(manifest); ProtectionElement protectionElement = manifest.protectionElement; @@ -76,7 +79,8 @@ import java.util.ArrayList; } this.manifest = manifest; sampleStreams = newSampleStreamArray(0); - sequenceableLoader = new CompositeSequenceableLoader(sampleStreams); + compositeSequenceableLoader = + compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(sampleStreams); } public void updateManifest(SsManifest manifest) { @@ -133,7 +137,8 @@ import java.util.ArrayList; } sampleStreams = newSampleStreamArray(sampleStreamsList.size()); sampleStreamsList.toArray(sampleStreams); - sequenceableLoader = new CompositeSequenceableLoader(sampleStreams); + compositeSequenceableLoader = + compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(sampleStreams); return positionUs; } @@ -146,12 +151,12 @@ import java.util.ArrayList; @Override public boolean continueLoading(long positionUs) { - return sequenceableLoader.continueLoading(positionUs); + return compositeSequenceableLoader.continueLoading(positionUs); } @Override public long getNextLoadPositionUs() { - return sequenceableLoader.getNextLoadPositionUs(); + return compositeSequenceableLoader.getNextLoadPositionUs(); } @Override @@ -161,7 +166,7 @@ import java.util.ArrayList; @Override public long getBufferedPositionUs() { - return sequenceableLoader.getBufferedPositionUs(); + return compositeSequenceableLoader.getBufferedPositionUs(); } @Override diff --git a/library/smoothstreaming/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 index 5a93847428..a4b601aafe 100644 --- a/library/smoothstreaming/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 @@ -26,8 +26,11 @@ import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener; import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; +import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; +import com.google.android.exoplayer2.source.DefaultCompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.SequenceableLoader; import com.google.android.exoplayer2.source.SinglePeriodTimeline; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest.StreamElement; @@ -65,6 +68,7 @@ public final class SsMediaSource implements MediaSource, private ParsingLoadable.Parser manifestParser; private AdaptiveMediaSourceEventListener eventListener; private Handler eventHandler; + private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private int minLoadableRetryCount; private long livePresentationDelayMs; @@ -162,6 +166,22 @@ public final class SsMediaSource implements MediaSource, return this; } + /** + * Sets the factory to create composite {@link SequenceableLoader}s for when this media source + * loads data from multiple streams (video, audio etc...). The default is an instance of + * {@link DefaultCompositeSequenceableLoaderFactory}. + * + * @param compositeSequenceableLoaderFactory A factory to create composite + * {@link SequenceableLoader}s for when this media source loads data from multiple streams + * (video, audio etc...). + * @return This builder. + */ + public Builder setCompositeSequenceableLoaderFactory( + CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory) { + this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; + return this; + } + /** * Builds a new {@link SsMediaSource} using the current parameters. *

          @@ -177,9 +197,12 @@ public final class SsMediaSource implements MediaSource, if (loadableManifestUri && manifestParser == null) { manifestParser = new SsManifestParser(); } + if (compositeSequenceableLoaderFactory == null) { + compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory(); + } return new SsMediaSource(manifest, manifestUri, manifestDataSourceFactory, manifestParser, - chunkSourceFactory, minLoadableRetryCount, livePresentationDelayMs, eventHandler, - eventListener); + chunkSourceFactory, compositeSequenceableLoaderFactory, minLoadableRetryCount, + livePresentationDelayMs, eventHandler, eventListener); } } @@ -206,6 +229,7 @@ public final class SsMediaSource implements MediaSource, private final Uri manifestUri; private final DataSource.Factory manifestDataSourceFactory; private final SsChunkSource.Factory chunkSourceFactory; + private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private final int minLoadableRetryCount; private final long livePresentationDelayMs; private final EventDispatcher eventDispatcher; @@ -252,7 +276,8 @@ public final class SsMediaSource implements MediaSource, public SsMediaSource(SsManifest manifest, SsChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { - this(manifest, null, null, null, chunkSourceFactory, minLoadableRetryCount, + this(manifest, null, null, null, chunkSourceFactory, + new DefaultCompositeSequenceableLoaderFactory(), minLoadableRetryCount, DEFAULT_LIVE_PRESENTATION_DELAY_MS, eventHandler, eventListener); } @@ -324,14 +349,16 @@ public final class SsMediaSource implements MediaSource, long livePresentationDelayMs, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { this(null, manifestUri, manifestDataSourceFactory, manifestParser, chunkSourceFactory, - minLoadableRetryCount, livePresentationDelayMs, eventHandler, eventListener); + new DefaultCompositeSequenceableLoaderFactory(), minLoadableRetryCount, + livePresentationDelayMs, eventHandler, eventListener); } private SsMediaSource(SsManifest manifest, Uri manifestUri, DataSource.Factory manifestDataSourceFactory, ParsingLoadable.Parser manifestParser, - SsChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, - long livePresentationDelayMs, Handler eventHandler, + SsChunkSource.Factory chunkSourceFactory, + CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, + int minLoadableRetryCount, long livePresentationDelayMs, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { Assertions.checkState(manifest == null || !manifest.isLive); this.manifest = manifest; @@ -341,6 +368,7 @@ public final class SsMediaSource implements MediaSource, this.manifestDataSourceFactory = manifestDataSourceFactory; this.manifestParser = manifestParser; this.chunkSourceFactory = chunkSourceFactory; + this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; this.minLoadableRetryCount = minLoadableRetryCount; this.livePresentationDelayMs = livePresentationDelayMs; this.eventDispatcher = new EventDispatcher(eventHandler, eventListener); @@ -372,8 +400,9 @@ public final class SsMediaSource implements MediaSource, @Override public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { Assertions.checkArgument(id.periodIndex == 0); - SsMediaPeriod period = new SsMediaPeriod(manifest, chunkSourceFactory, minLoadableRetryCount, - eventDispatcher, manifestLoaderErrorThrower, allocator); + SsMediaPeriod period = new SsMediaPeriod(manifest, chunkSourceFactory, + compositeSequenceableLoaderFactory, minLoadableRetryCount, eventDispatcher, + manifestLoaderErrorThrower, allocator); mediaPeriods.add(period); return period; } From 2a685da4eb6dec725f737803d223ad9e5d27fa75 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 20 Nov 2017 08:42:34 -0800 Subject: [PATCH 0731/2472] Improve robustness of ImaAdsLoader Remove an assertion that there was a call to pause content between two content -> ad transitions. Also, only use the player position for resuming an ad on reattaching if the player is currently playing an ad, in case IMA pauses content before the player actually transitions to an ad. Issue: #3430 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176365842 --- RELEASENOTES.md | 3 +++ .../com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java | 3 +-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 41748fa10d..81a45c9c24 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -72,6 +72,9 @@ ([#3303](https://github.com/google/ExoPlayer/issues/3303)). * Ignore seeks if an ad is playing ([#3309](https://github.com/google/ExoPlayer/issues/3309)). + * Improve robustness of `ImaAdsLoader` in case content is not paused between + content to ad transitions + ([#3430](https://github.com/google/ExoPlayer/issues/3430)). * UI: * Allow specifying a `Drawable` for the `TimeBar` scrubber ([#3337](https://github.com/google/ExoPlayer/issues/3337)). diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index 0b11a97f84..5b61db0264 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -260,7 +260,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A @Override public void detachPlayer() { if (adsManager != null && imaPausedContent) { - adPlaybackState.setAdResumePositionUs(C.msToUs(player.getCurrentPosition())); + adPlaybackState.setAdResumePositionUs(playingAd ? C.msToUs(player.getCurrentPosition()) : 0); adsManager.pause(); } lastAdProgress = getAdProgress(); @@ -628,7 +628,6 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A if (!wasPlayingAd && playingAd) { int adGroupIndex = player.getCurrentAdGroupIndex(); // IMA hasn't sent CONTENT_PAUSE_REQUESTED yet, so fake the content position. - Assertions.checkState(fakeContentProgressElapsedRealtimeMs == C.TIME_UNSET); fakeContentProgressElapsedRealtimeMs = SystemClock.elapsedRealtime(); fakeContentProgressOffsetMs = C.usToMs(adPlaybackState.adGroupTimesUs[adGroupIndex]); if (fakeContentProgressOffsetMs == C.TIME_END_OF_SOURCE) { From 3656230cb1b93092fb5ee698fe260ff4dfeb976f Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 20 Nov 2017 08:48:10 -0800 Subject: [PATCH 0732/2472] Use MediaSourceTestRunner in additional source tests ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176366471 --- .../source/ClippingMediaSourceTest.java | 13 +++++--- .../source/ConcatenatingMediaSourceTest.java | 12 ++++--- .../DynamicConcatenatingMediaSourceTest.java | 14 ++++----- .../source/LoopingMediaSourceTest.java | 14 ++++++--- .../testutil/MediaSourceTestRunner.java | 30 ++++++++++++------ .../android/exoplayer2/testutil/TestUtil.java | 31 ------------------- 6 files changed, 52 insertions(+), 62 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java index 5e615dbc7f..3c870f06f4 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java @@ -24,7 +24,7 @@ import com.google.android.exoplayer2.Timeline.Window; import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; -import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.testutil.MediaSourceTestRunner; import com.google.android.exoplayer2.testutil.TimelineAsserts; /** @@ -123,9 +123,14 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase { * Wraps the specified timeline in a {@link ClippingMediaSource} and returns the clipped timeline. */ private static Timeline getClippedTimeline(Timeline timeline, long startMs, long endMs) { - MediaSource mediaSource = new FakeMediaSource(timeline, null); - return TestUtil.extractTimelineFromMediaSource( - new ClippingMediaSource(mediaSource, startMs, endMs)); + FakeMediaSource fakeMediaSource = new FakeMediaSource(timeline, null); + ClippingMediaSource mediaSource = new ClippingMediaSource(fakeMediaSource, startMs, endMs); + MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null); + try { + return testRunner.prepareSource(); + } finally { + testRunner.release(); + } } } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java index 429325defc..1ca32be46d 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java @@ -24,7 +24,6 @@ import com.google.android.exoplayer2.testutil.FakeShuffleOrder; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.testutil.MediaSourceTestRunner; -import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.testutil.TimelineAsserts; import junit.framework.TestCase; @@ -33,8 +32,6 @@ import junit.framework.TestCase; */ public final class ConcatenatingMediaSourceTest extends TestCase { - private static final int TIMEOUT_MS = 10000; - public void testEmptyConcatenation() { for (boolean atomic : new boolean[] {false, true}) { Timeline timeline = getConcatenatedTimeline(atomic); @@ -211,7 +208,7 @@ public final class ConcatenatingMediaSourceTest extends TestCase { ConcatenatingMediaSource mediaSource = new ConcatenatingMediaSource(mediaSourceContentOnly, mediaSourceWithAds); - MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null, TIMEOUT_MS); + MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null); try { Timeline timeline = testRunner.prepareSource(); TimelineAsserts.assertAdGroupCounts(timeline, 0, 0, 1, 1); @@ -241,7 +238,12 @@ public final class ConcatenatingMediaSourceTest extends TestCase { } ConcatenatingMediaSource mediaSource = new ConcatenatingMediaSource(isRepeatOneAtomic, new FakeShuffleOrder(mediaSources.length), mediaSources); - return TestUtil.extractTimelineFromMediaSource(mediaSource); + MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null); + try { + return testRunner.prepareSource(); + } finally { + testRunner.release(); + } } private static FakeTimeline createFakeTimeline(int periodCount, int windowId) { diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java index 536180fafc..16c9e1a17c 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java @@ -39,19 +39,19 @@ import org.mockito.Mockito; */ public final class DynamicConcatenatingMediaSourceTest extends TestCase { - private static final int TIMEOUT_MS = 10000; - private DynamicConcatenatingMediaSource mediaSource; private MediaSourceTestRunner testRunner; @Override - public void setUp() { + public void setUp() throws Exception { + super.setUp(); mediaSource = new DynamicConcatenatingMediaSource(new FakeShuffleOrder(0)); - testRunner = new MediaSourceTestRunner(mediaSource, null, TIMEOUT_MS); + testRunner = new MediaSourceTestRunner(mediaSource, null); } @Override - public void tearDown() { + public void tearDown() throws Exception { + super.tearDown(); testRunner.release(); } @@ -623,7 +623,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { finishedCondition.open(); } }); - assertTrue(finishedCondition.block(TIMEOUT_MS)); + assertTrue(finishedCondition.block(MediaSourceTestRunner.TIMEOUT_MS)); } public void release() { @@ -656,7 +656,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { } public Timeline assertTimelineChangeBlocking() { - assertTrue(finishedCondition.block(TIMEOUT_MS)); + assertTrue(finishedCondition.block(MediaSourceTestRunner.TIMEOUT_MS)); if (error != null) { throw error; } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java index 79f646b5c4..6f69923ea2 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java @@ -21,7 +21,7 @@ import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; -import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.testutil.MediaSourceTestRunner; import com.google.android.exoplayer2.testutil.TimelineAsserts; import junit.framework.TestCase; @@ -110,10 +110,14 @@ public class LoopingMediaSourceTest extends TestCase { * the looping timeline. */ private static Timeline getLoopingTimeline(Timeline timeline, int loopCount) { - MediaSource mediaSource = new FakeMediaSource(timeline, null); - return TestUtil.extractTimelineFromMediaSource( - new LoopingMediaSource(mediaSource, loopCount)); + FakeMediaSource fakeMediaSource = new FakeMediaSource(timeline, null); + LoopingMediaSource mediaSource = new LoopingMediaSource(fakeMediaSource, loopCount); + MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null); + try { + return testRunner.prepareSource(); + } finally { + testRunner.release(); + } } } - diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java index df1282c7e1..235c04bef5 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java @@ -31,6 +31,8 @@ import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; + import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.TimeUnit; @@ -39,7 +41,8 @@ import java.util.concurrent.TimeUnit; */ public class MediaSourceTestRunner { - private final long timeoutMs; + public static final int TIMEOUT_MS = 10000; + private final StubExoPlayer player; private final MediaSource mediaSource; private final MediaSourceListener mediaSourceListener; @@ -53,12 +56,10 @@ public class MediaSourceTestRunner { /** * @param mediaSource The source under test. * @param allocator The allocator to use during the test run. - * @param timeoutMs The timeout for operations in milliseconds. */ - public MediaSourceTestRunner(MediaSource mediaSource, Allocator allocator, long timeoutMs) { + public MediaSourceTestRunner(MediaSource mediaSource, Allocator allocator) { this.mediaSource = mediaSource; this.allocator = allocator; - this.timeoutMs = timeoutMs; playbackThread = new HandlerThread("PlaybackThread"); playbackThread.start(); Looper playbackLooper = playbackThread.getLooper(); @@ -74,15 +75,24 @@ public class MediaSourceTestRunner { * @param runnable The {@link Runnable} to run. */ public void runOnPlaybackThread(final Runnable runnable) { + final Throwable[] throwable = new Throwable[1]; final ConditionVariable finishedCondition = new ConditionVariable(); playbackHandler.post(new Runnable() { @Override public void run() { - runnable.run(); - finishedCondition.open(); + try { + runnable.run(); + } catch (Throwable e) { + throwable[0] = e; + } finally { + finishedCondition.open(); + } } }); - assertTrue(finishedCondition.block(timeoutMs)); + assertTrue(finishedCondition.block(TIMEOUT_MS)); + if (throwable[0] != null) { + Util.sneakyThrow(throwable[0]); + } } /** @@ -200,7 +210,7 @@ public class MediaSourceTestRunner { */ public Timeline assertTimelineChangeBlocking() { try { - timeline = timelines.poll(timeoutMs, TimeUnit.MILLISECONDS); + timeline = timelines.poll(TIMEOUT_MS, TimeUnit.MILLISECONDS); assertNotNull(timeline); // Null indicates the poll timed out. assertNoTimelineChange(); return timeline; @@ -231,12 +241,12 @@ public class MediaSourceTestRunner { private void assertPrepareAndReleasePeriod(MediaPeriodId mediaPeriodId) { MediaPeriod mediaPeriod = createPeriod(mediaPeriodId); ConditionVariable preparedCondition = preparePeriod(mediaPeriod, 0); - assertTrue(preparedCondition.block(timeoutMs)); + assertTrue(preparedCondition.block(TIMEOUT_MS)); // MediaSource is supposed to support multiple calls to createPeriod with the same id without an // intervening call to releasePeriod. MediaPeriod secondMediaPeriod = createPeriod(mediaPeriodId); ConditionVariable secondPreparedCondition = preparePeriod(secondMediaPeriod, 0); - assertTrue(secondPreparedCondition.block(timeoutMs)); + assertTrue(secondPreparedCondition.block(TIMEOUT_MS)); // Release the periods. releasePeriod(mediaPeriod); releasePeriod(secondMediaPeriod); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java index 9ee181024c..d10b8a8269 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java @@ -19,10 +19,7 @@ import android.app.Instrumentation; import android.content.Context; import android.test.MoreAsserts; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.extractor.Extractor; -import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.MediaSource.Listener; import com.google.android.exoplayer2.testutil.FakeExtractorInput.SimulatedIOException; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; @@ -143,34 +140,6 @@ public class TestUtil { return new String(getByteArray(instrumentation, fileName)); } - /** - * Extracts the timeline from a media source. - */ - // TODO: Remove this method and transition callers over to MediaSourceTestRunner. - public static Timeline extractTimelineFromMediaSource(MediaSource mediaSource) { - class TimelineListener implements Listener { - private Timeline timeline; - @Override - public synchronized void onSourceInfoRefreshed(MediaSource source, Timeline timeline, - Object manifest) { - this.timeline = timeline; - this.notify(); - } - } - TimelineListener listener = new TimelineListener(); - mediaSource.prepareSource(null, true, listener); - synchronized (listener) { - while (listener.timeline == null) { - try { - listener.wait(); - } catch (InterruptedException e) { - Assert.fail(e.getMessage()); - } - } - } - return listener.timeline; - } - /** * Asserts that data read from a {@link DataSource} matches {@code expected}. * From c06fe73b66a9664d68adce4a951bfe0e4258fcbd Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 20 Nov 2017 08:50:09 -0800 Subject: [PATCH 0733/2472] Bump target API level to 27 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176366693 --- constants.gradle | 4 ++-- demos/cast/src/main/AndroidManifest.xml | 2 +- demos/ima/src/main/AndroidManifest.xml | 2 +- demos/main/src/main/AndroidManifest.xml | 2 +- extensions/cronet/src/androidTest/AndroidManifest.xml | 2 +- extensions/flac/src/androidTest/AndroidManifest.xml | 2 +- extensions/opus/src/androidTest/AndroidManifest.xml | 2 +- extensions/vp9/src/androidTest/AndroidManifest.xml | 2 +- library/core/src/androidTest/AndroidManifest.xml | 2 +- library/dash/src/androidTest/AndroidManifest.xml | 2 +- library/hls/src/androidTest/AndroidManifest.xml | 2 +- library/smoothstreaming/src/androidTest/AndroidManifest.xml | 2 +- playbacktests/src/androidTest/AndroidManifest.xml | 2 +- 13 files changed, 14 insertions(+), 14 deletions(-) diff --git a/constants.gradle b/constants.gradle index 2a7754d65c..bad69389a5 100644 --- a/constants.gradle +++ b/constants.gradle @@ -17,8 +17,8 @@ project.ext { // However, please note that the core media playback functionality provided // by the library requires API level 16 or greater. minSdkVersion = 14 - compileSdkVersion = 26 - targetSdkVersion = 26 + compileSdkVersion = 27 + targetSdkVersion = 27 buildToolsVersion = '26.0.2' testSupportLibraryVersion = '0.5' supportLibraryVersion = '27.0.0' diff --git a/demos/cast/src/main/AndroidManifest.xml b/demos/cast/src/main/AndroidManifest.xml index 11f8e39b53..8aaef5f8ce 100644 --- a/demos/cast/src/main/AndroidManifest.xml +++ b/demos/cast/src/main/AndroidManifest.xml @@ -19,7 +19,7 @@ android:versionName="2.6.0"> - + diff --git a/demos/ima/src/main/AndroidManifest.xml b/demos/ima/src/main/AndroidManifest.xml index 5252d2feeb..f14feeda74 100644 --- a/demos/ima/src/main/AndroidManifest.xml +++ b/demos/ima/src/main/AndroidManifest.xml @@ -19,7 +19,7 @@ android:versionName="2.6.0"> - + diff --git a/demos/main/src/main/AndroidManifest.xml b/demos/main/src/main/AndroidManifest.xml index d041e24d80..ec8016e8a3 100644 --- a/demos/main/src/main/AndroidManifest.xml +++ b/demos/main/src/main/AndroidManifest.xml @@ -23,7 +23,7 @@ - + - + - + - + - + - + - + - + - + - + Date: Mon, 20 Nov 2017 08:56:23 -0800 Subject: [PATCH 0734/2472] Report additional position discontinuities - Properly report internal discontinuities - Add DISCONTINUITY_REASON_SEEK_ADJUSTMENT to distinguish seek adjustments from other internal discontinuity events ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176367365 --- RELEASENOTES.md | 4 + .../android/exoplayer2/demo/EventLogger.java | 2 + .../android/exoplayer2/ExoPlayerTest.java | 89 +++++++++++++++++-- .../android/exoplayer2/ExoPlayerImpl.java | 15 ++-- .../exoplayer2/ExoPlayerImplInternal.java | 12 ++- .../com/google/android/exoplayer2/Player.java | 9 +- .../testutil/ExoPlayerTestRunner.java | 34 ++++--- 7 files changed, 139 insertions(+), 26 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 81a45c9c24..6438cbdd68 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,6 +2,10 @@ ### dev-v2 (not yet released) ### +* Fix reporting of internal position discontinuities via + `Player.onPositionDiscontinuity`. `DISCONTINUITY_REASON_SEEK_ADJUSTMENT` is + added to disambiguate position adjustments during seeks from other types of + internal position discontinuity. * Allow more flexible loading strategy when playing media containing multiple sub-streams, by allowing injection of custom `CompositeSequenceableLoader` factories through `DashMediaSource.Builder`, `HlsMediaSource.Builder`, diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java index d72f747940..9233b016f5 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java @@ -499,6 +499,8 @@ import java.util.Locale; return "PERIOD_TRANSITION"; case Player.DISCONTINUITY_REASON_SEEK: return "SEEK"; + case Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT: + return "SEEK_ADJUSTMENT"; case Player.DISCONTINUITY_REASON_INTERNAL: return "INTERNAL"; default: diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index 95d5d96163..f0f1c23c2b 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -23,12 +23,14 @@ import com.google.android.exoplayer2.testutil.ActionSchedule; import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner; import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner.Builder; import com.google.android.exoplayer2.testutil.FakeMediaClockRenderer; +import com.google.android.exoplayer2.testutil.FakeMediaPeriod; import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeRenderer; import com.google.android.exoplayer2.testutil.FakeShuffleOrder; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTrackSelection; import com.google.android.exoplayer2.testutil.FakeTrackSelector; +import com.google.android.exoplayer2.upstream.Allocator; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; @@ -56,7 +58,7 @@ public final class ExoPlayerTest extends TestCase { ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() .setTimeline(timeline).setRenderers(renderer) .build().start().blockUntilEnded(TIMEOUT_MS); - testRunner.assertPositionDiscontinuityCount(0); + testRunner.assertNoPositionDiscontinuities(); testRunner.assertTimelinesEqual(); assertEquals(0, renderer.formatReadCount); assertEquals(0, renderer.bufferReadCount); @@ -73,7 +75,7 @@ public final class ExoPlayerTest extends TestCase { ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() .setTimeline(timeline).setManifest(manifest).setRenderers(renderer) .build().start().blockUntilEnded(TIMEOUT_MS); - testRunner.assertPositionDiscontinuityCount(0); + testRunner.assertNoPositionDiscontinuities(); testRunner.assertTimelinesEqual(timeline); testRunner.assertManifestsEqual(manifest); testRunner.assertTrackGroupsEqual(new TrackGroupArray(new TrackGroup(Builder.VIDEO_FORMAT))); @@ -91,7 +93,9 @@ public final class ExoPlayerTest extends TestCase { ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() .setTimeline(timeline).setRenderers(renderer) .build().start().blockUntilEnded(TIMEOUT_MS); - testRunner.assertPositionDiscontinuityCount(2); + testRunner.assertPositionDiscontinuityReasonsEqual( + Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, + Player.DISCONTINUITY_REASON_PERIOD_TRANSITION); testRunner.assertTimelinesEqual(timeline); assertEquals(3, renderer.formatReadCount); assertEquals(1, renderer.bufferReadCount); @@ -136,7 +140,9 @@ public final class ExoPlayerTest extends TestCase { .setTimeline(timeline).setRenderers(videoRenderer, audioRenderer) .setSupportedFormats(Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT) .build().start().blockUntilEnded(TIMEOUT_MS); - testRunner.assertPositionDiscontinuityCount(2); + testRunner.assertPositionDiscontinuityReasonsEqual( + Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, + Player.DISCONTINUITY_REASON_PERIOD_TRANSITION); testRunner.assertTimelinesEqual(timeline); assertEquals(1, audioRenderer.positionResetCount); assertTrue(videoRenderer.isEnded); @@ -198,7 +204,7 @@ public final class ExoPlayerTest extends TestCase { ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() .setMediaSource(firstSource).setRenderers(renderer).setActionSchedule(actionSchedule) .build().start().blockUntilEnded(TIMEOUT_MS); - testRunner.assertPositionDiscontinuityCount(0); + testRunner.assertNoPositionDiscontinuities(); // 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. @@ -226,6 +232,16 @@ public final class ExoPlayerTest extends TestCase { .setTimeline(timeline).setRenderers(renderer).setActionSchedule(actionSchedule) .build().start().blockUntilEnded(TIMEOUT_MS); testRunner.assertPlayedPeriodIndices(0, 1, 1, 2, 2, 0, 0, 0, 1, 2); + testRunner.assertPositionDiscontinuityReasonsEqual( + Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, + Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, + Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, + Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, + Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, + Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, + Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, + Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, + Player.DISCONTINUITY_REASON_PERIOD_TRANSITION); testRunner.assertTimelinesEqual(timeline); assertTrue(renderer.isEnded); } @@ -250,6 +266,12 @@ public final class ExoPlayerTest extends TestCase { .setMediaSource(mediaSource).setRenderers(renderer).setActionSchedule(actionSchedule) .build().start().blockUntilEnded(TIMEOUT_MS); testRunner.assertPlayedPeriodIndices(0, 1, 0, 2, 1, 2); + testRunner.assertPositionDiscontinuityReasonsEqual( + Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, + Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, + Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, + Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, + Player.DISCONTINUITY_REASON_PERIOD_TRANSITION); assertTrue(renderer.isEnded); } @@ -300,6 +322,63 @@ public final class ExoPlayerTest extends TestCase { assertEquals(Player.STATE_BUFFERING, (int) playbackStatesWhenSeekProcessed.get(2)); } + public void testSeekDiscontinuity() throws Exception { + FakeTimeline timeline = new FakeTimeline(1); + ActionSchedule actionSchedule = new ActionSchedule.Builder("testSeekDiscontinuity") + .seek(10).build(); + ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder().setTimeline(timeline) + .setActionSchedule(actionSchedule).build().start().blockUntilEnded(TIMEOUT_MS); + testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_SEEK); + } + + public void testSeekDiscontinuityWithAdjustment() throws Exception { + FakeTimeline timeline = new FakeTimeline(1); + FakeMediaSource mediaSource = new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT) { + @Override + protected FakeMediaPeriod createFakeMediaPeriod(MediaPeriodId id, + TrackGroupArray trackGroupArray, Allocator allocator) { + return new FakeMediaPeriod(trackGroupArray) { + @Override + public long seekToUs(long positionUs) { + return positionUs + 10; // Adjusts the requested seek position. + } + }; + } + }; + ActionSchedule actionSchedule = new ActionSchedule.Builder("testSeekDiscontinuityAdjust") + .waitForPlaybackState(Player.STATE_READY).seek(10).build(); + ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder().setMediaSource(mediaSource) + .setActionSchedule(actionSchedule).build().start().blockUntilEnded(TIMEOUT_MS); + testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_SEEK, + Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT); + } + + public void testInternalDiscontinuity() throws Exception { + FakeTimeline timeline = new FakeTimeline(1); + FakeMediaSource mediaSource = new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT) { + @Override + protected FakeMediaPeriod createFakeMediaPeriod(MediaPeriodId id, + TrackGroupArray trackGroupArray, Allocator allocator) { + return new FakeMediaPeriod(trackGroupArray) { + boolean discontinuityRead; + @Override + public long readDiscontinuity() { + if (!discontinuityRead) { + discontinuityRead = true; + return 10; // Return a discontinuity. + } + return C.TIME_UNSET; + } + }; + } + }; + ActionSchedule actionSchedule = new ActionSchedule.Builder("testInternalDiscontinuity") + .waitForPlaybackState(Player.STATE_READY).build(); + ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder().setMediaSource(mediaSource) + .setActionSchedule(actionSchedule).build().start().blockUntilEnded(TIMEOUT_MS); + testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_INTERNAL); + } + public void testAllActivatedTrackSelectionAreReleasedForSinglePeriod() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 1); MediaSource mediaSource = diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index d28f72e739..ff00f9de91 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -467,7 +467,8 @@ import java.util.concurrent.CopyOnWriteArraySet; case ExoPlayerImplInternal.MSG_SOURCE_INFO_REFRESHED: { int prepareAcks = msg.arg1; int seekAcks = msg.arg2; - handlePlaybackInfo((PlaybackInfo) msg.obj, prepareAcks, seekAcks, false); + handlePlaybackInfo((PlaybackInfo) msg.obj, prepareAcks, seekAcks, false, + /* ignored */ DISCONTINUITY_REASON_INTERNAL); break; } case ExoPlayerImplInternal.MSG_TRACKS_CHANGED: { @@ -485,11 +486,13 @@ import java.util.concurrent.CopyOnWriteArraySet; } case ExoPlayerImplInternal.MSG_SEEK_ACK: { boolean seekPositionAdjusted = msg.arg1 != 0; - handlePlaybackInfo((PlaybackInfo) msg.obj, 0, 1, seekPositionAdjusted); + handlePlaybackInfo((PlaybackInfo) msg.obj, 0, 1, seekPositionAdjusted, + DISCONTINUITY_REASON_SEEK_ADJUSTMENT); break; } case ExoPlayerImplInternal.MSG_POSITION_DISCONTINUITY: { - handlePlaybackInfo((PlaybackInfo) msg.obj, 0, 0, true); + @DiscontinuityReason int discontinuityReason = msg.arg1; + handlePlaybackInfo((PlaybackInfo) msg.obj, 0, 0, true, discontinuityReason); break; } case ExoPlayerImplInternal.MSG_PLAYBACK_PARAMETERS_CHANGED: { @@ -515,7 +518,7 @@ import java.util.concurrent.CopyOnWriteArraySet; } private void handlePlaybackInfo(PlaybackInfo playbackInfo, int prepareAcks, int seekAcks, - boolean positionDiscontinuity) { + boolean positionDiscontinuity, @DiscontinuityReason int positionDiscontinuityReason) { Assertions.checkNotNull(playbackInfo.timeline); pendingPrepareAcks -= prepareAcks; pendingSeekAcks -= seekAcks; @@ -536,9 +539,7 @@ import java.util.concurrent.CopyOnWriteArraySet; } if (positionDiscontinuity) { for (Player.EventListener listener : listeners) { - listener.onPositionDiscontinuity( - seekAcks > 0 ? DISCONTINUITY_REASON_INTERNAL : DISCONTINUITY_REASON_PERIOD_TRANSITION - ); + listener.onPositionDiscontinuity(positionDiscontinuityReason); } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index e4bb11c51f..d7b2b4cbf4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -514,6 +514,10 @@ import java.io.IOException; long periodPositionUs = playingPeriodHolder.mediaPeriod.readDiscontinuity(); if (periodPositionUs != C.TIME_UNSET) { resetRendererPosition(periodPositionUs); + playbackInfo = playbackInfo.fromNewPosition(playbackInfo.periodId, periodPositionUs, + playbackInfo.contentPositionUs); + eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, Player.DISCONTINUITY_REASON_INTERNAL, + 0, playbackInfo).sendToTarget(); } else { rendererPositionUs = mediaClock.syncAndGetPositionUs(); periodPositionUs = playingPeriodHolder.toPeriodTime(rendererPositionUs); @@ -875,7 +879,10 @@ import java.io.IOException; long periodPositionUs = playingPeriodHolder.updatePeriodTrackSelection( playbackInfo.positionUs, recreateStreams, streamResetFlags); if (periodPositionUs != playbackInfo.positionUs) { - playbackInfo.positionUs = periodPositionUs; + playbackInfo = playbackInfo.fromNewPosition(playbackInfo.periodId, periodPositionUs, + playbackInfo.contentPositionUs); + eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, Player.DISCONTINUITY_REASON_INTERNAL, + 0, playbackInfo).sendToTarget(); resetRendererPosition(periodPositionUs); } @@ -1262,7 +1269,8 @@ import java.io.IOException; playbackInfo = playbackInfo.fromNewPosition(playingPeriodHolder.info.id, playingPeriodHolder.info.startPositionUs, playingPeriodHolder.info.contentPositionUs); updatePlaybackPositions(); - eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, playbackInfo).sendToTarget(); + eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, + Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, 0, playbackInfo).sendToTarget(); } if (readingPeriodHolder.info.isFinal) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Player.java b/library/core/src/main/java/com/google/android/exoplayer2/Player.java index af653ec2bd..dc703f924a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Player.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Player.java @@ -243,7 +243,7 @@ public interface Player { */ @Retention(RetentionPolicy.SOURCE) @IntDef({DISCONTINUITY_REASON_PERIOD_TRANSITION, DISCONTINUITY_REASON_SEEK, - DISCONTINUITY_REASON_INTERNAL}) + DISCONTINUITY_REASON_SEEK_ADJUSTMENT, DISCONTINUITY_REASON_INTERNAL}) public @interface DiscontinuityReason {} /** * Automatic playback transition from one period in the timeline to the next. The period index may @@ -254,10 +254,15 @@ public interface Player { * Seek within the current period or to another period. */ int DISCONTINUITY_REASON_SEEK = 1; + /** + * Seek adjustment due to being unable to seek to the requested position or because the seek was + * permitted to be inexact. + */ + int DISCONTINUITY_REASON_SEEK_ADJUSTMENT = 2; /** * Discontinuity introduced internally by the source. */ - int DISCONTINUITY_REASON_INTERNAL = 2; + int DISCONTINUITY_REASON_INTERNAL = 3; /** * Register a listener to receive events from the player. The listener's methods will be called on diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java index 591e63dc5b..6730bf1c7f 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java @@ -23,6 +23,7 @@ import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.LoadControl; import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Player.DiscontinuityReason; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.SimpleExoPlayer; @@ -38,6 +39,7 @@ import com.google.android.exoplayer2.trackselection.MappingTrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.video.VideoRendererEventListener; +import java.util.ArrayList; import java.util.LinkedList; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -261,14 +263,14 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { */ public ExoPlayerTestRunner build() { if (supportedFormats == null) { - supportedFormats = new Format[] { VIDEO_FORMAT }; + supportedFormats = new Format[] {VIDEO_FORMAT}; } if (trackSelector == null) { trackSelector = new DefaultTrackSelector(); } if (renderersFactory == null) { if (renderers == null) { - renderers = new Renderer[] { new FakeRenderer(supportedFormats) }; + renderers = new Renderer[] {new FakeRenderer(supportedFormats)}; } renderersFactory = new RenderersFactory() { @Override @@ -317,11 +319,11 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { private final LinkedList timelines; private final LinkedList manifests; private final LinkedList periodIndices; + private final ArrayList discontinuityReasons; private SimpleExoPlayer player; private Exception exception; private TrackGroupArray trackGroups; - private int positionDiscontinuityCount; private boolean playerWasPrepared; private ExoPlayerTestRunner(PlayerFactory playerFactory, MediaSource mediaSource, @@ -337,6 +339,7 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { this.timelines = new LinkedList<>(); this.manifests = new LinkedList<>(); this.periodIndices = new LinkedList<>(); + this.discontinuityReasons = new ArrayList<>(); this.endedCountDownLatch = new CountDownLatch(1); this.playerThread = new HandlerThread("ExoPlayerTest thread"); playerThread.start(); @@ -439,13 +442,24 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { } /** - * Asserts that the number of reported discontinuities by - * {@link Player.EventListener#onPositionDiscontinuity(int)} is equal to the provided number. - * - * @param expectedCount The expected number of position discontinuities. + * Asserts that {@link Player.EventListener#onPositionDiscontinuity(int)} was not called. */ - public void assertPositionDiscontinuityCount(int expectedCount) { - Assert.assertEquals(expectedCount, positionDiscontinuityCount); + public void assertNoPositionDiscontinuities() { + Assert.assertTrue(discontinuityReasons.isEmpty()); + } + + /** + * Asserts that the discontinuity reasons reported by + * {@link Player.EventListener#onPositionDiscontinuity(int)} are equal to the provided values. + * + * @param discontinuityReasons The expected discontinuity reasons. + */ + public void assertPositionDiscontinuityReasonsEqual( + @DiscontinuityReason int... discontinuityReasons) { + Assert.assertEquals(discontinuityReasons.length, this.discontinuityReasons.size()); + for (int i = 0; i < discontinuityReasons.length; i++) { + Assert.assertEquals(discontinuityReasons[i], (int) this.discontinuityReasons.get(i)); + } } /** @@ -522,7 +536,7 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { @Override public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { - positionDiscontinuityCount++; + discontinuityReasons.add(reason); periodIndices.add(player.getCurrentPeriodIndex()); } From 13b595ed3908588810dce32811830e601bc2548e Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 20 Nov 2017 09:35:01 -0800 Subject: [PATCH 0735/2472] Don't do work after track selection when in ended state This causes the player to report that it's started loading when in the ended state. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176371892 --- .../android/exoplayer2/ExoPlayerImplInternal.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index d7b2b4cbf4..1732026540 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -878,7 +878,7 @@ import java.io.IOException; boolean[] streamResetFlags = new boolean[renderers.length]; long periodPositionUs = playingPeriodHolder.updatePeriodTrackSelection( playbackInfo.positionUs, recreateStreams, streamResetFlags); - if (periodPositionUs != playbackInfo.positionUs) { + if (state != Player.STATE_ENDED && periodPositionUs != playbackInfo.positionUs) { playbackInfo = playbackInfo.fromNewPosition(playbackInfo.periodId, periodPositionUs, playbackInfo.contentPositionUs); eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, Player.DISCONTINUITY_REASON_INTERNAL, @@ -923,9 +923,11 @@ import java.io.IOException; loadingPeriodHolder.updatePeriodTrackSelection(loadingPeriodPositionUs, false); } } - maybeContinueLoading(); - updatePlaybackPositions(); - handler.sendEmptyMessage(MSG_DO_SOME_WORK); + if (state != Player.STATE_ENDED) { + maybeContinueLoading(); + updatePlaybackPositions(); + handler.sendEmptyMessage(MSG_DO_SOME_WORK); + } } private boolean isTimelineReady(long playingPeriodDurationUs) { From aac53cac56143e96bebcc0243f74cebe3e52b817 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 21 Nov 2017 01:33:48 -0800 Subject: [PATCH 0736/2472] Add reason to onTimelineChanged. Currently onTimelineChanged doesn't allow to distinguish easily between the different reasons why it's being called. Especially, finding out whether a new media source has been prepared or the current source refreshed dynamically was impossible without tightly coupling the player operations with the listener. The new reasons provide this disdinction by either indicating a newly initialized media source, a dynamic update to an existing timeline or manifest, or a reset of the player (which usually results in an empty timeline). The original onTimelineChanged method without reason is kept in the DefaultEventListener as deprecated to prevent the need to update all existing listeners in one go. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176478701 --- RELEASENOTES.md | 2 + .../android/exoplayer2/demo/EventLogger.java | 20 ++++++++- .../exoplayer2/ext/cast/CastPlayer.java | 7 +++- .../exoplayer2/ext/ima/ImaAdsLoader.java | 7 ++-- .../ext/leanback/LeanbackPlayerAdapter.java | 4 +- .../mediasession/MediaSessionConnector.java | 3 +- .../android/exoplayer2/ExoPlayerTest.java | 30 +++++++++++++- .../android/exoplayer2/ExoPlayerImpl.java | 12 ++++-- .../com/google/android/exoplayer2/Player.java | 41 +++++++++++++++++-- .../exoplayer2/ui/PlaybackControlView.java | 5 ++- .../android/exoplayer2/testutil/Action.java | 5 ++- .../testutil/ExoPlayerTestRunner.java | 18 +++++++- .../testutil/FakeSimpleExoPlayer.java | 3 +- 13 files changed, 136 insertions(+), 21 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 6438cbdd68..c01f2c29ee 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -22,6 +22,8 @@ use this with `FfmpegAudioRenderer`. * Support extraction and decoding of Dolby Atmos ([#2465](https://github.com/google/ExoPlayer/issues/2465)). +* Added a reason to `EventListener.onTimelineChanged` to distinguish between + initial preparation, reset and dynamic updates. ### 2.6.0 ### diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java index 9233b016f5..473a0d3441 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java @@ -116,10 +116,12 @@ import java.util.Locale; } @Override - public void onTimelineChanged(Timeline timeline, Object manifest) { + public void onTimelineChanged(Timeline timeline, Object manifest, + @Player.TimelineChangeReason int reason) { int periodCount = timeline.getPeriodCount(); int windowCount = timeline.getWindowCount(); - Log.d(TAG, "sourceInfo [periodCount=" + periodCount + ", windowCount=" + windowCount); + Log.d(TAG, "timelineChanged [periodCount=" + periodCount + ", windowCount=" + windowCount + + ", reason=" + getTimelineChangeReasonString(reason)); for (int i = 0; i < Math.min(periodCount, MAX_TIMELINE_ITEM_LINES); i++) { timeline.getPeriod(i, period); Log.d(TAG, " " + "period [" + getTimeString(period.getDurationMs()) + "]"); @@ -507,4 +509,18 @@ import java.util.Locale; return "?"; } } + + private static String getTimelineChangeReasonString(@Player.TimelineChangeReason int reason) { + switch (reason) { + case Player.TIMELINE_CHANGE_REASON_PREPARED: + return "PREPARED"; + case Player.TIMELINE_CHANGE_REASON_RESET: + return "RESET"; + case Player.TIMELINE_CHANGE_REASON_DYNAMIC: + return "DYNAMIC"; + default: + return "?"; + } + } + } diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java index 9a8986409a..32e064e834 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java @@ -116,6 +116,7 @@ public final class CastPlayer implements Player { private int pendingSeekCount; private int pendingSeekWindowIndex; private long pendingSeekPositionMs; + private boolean waitingForInitialTimeline; /** * @param castContext The context from which the cast session is obtained. @@ -170,6 +171,7 @@ public final class CastPlayer implements Player { public PendingResult loadItems(MediaQueueItem[] items, int startIndex, long positionMs, @RepeatMode int repeatMode) { if (remoteMediaClient != null) { + waitingForInitialTimeline = true; return remoteMediaClient.queueLoad(items, startIndex, getCastRepeatMode(repeatMode), positionMs, null); } @@ -556,8 +558,11 @@ public final class CastPlayer implements Player { private void maybeUpdateTimelineAndNotify() { if (updateTimeline()) { + @Player.TimelineChangeReason int reason = waitingForInitialTimeline + ? Player.TIMELINE_CHANGE_REASON_PREPARED : Player.TIMELINE_CHANGE_REASON_DYNAMIC; + waitingForInitialTimeline = false; for (EventListener listener : listeners) { - listener.onTimelineChanged(currentTimeline, null); + listener.onTimelineChanged(currentTimeline, null, reason); } } } diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index 5b61db0264..fe6a6d6196 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -523,9 +523,10 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A // Player.EventListener implementation. @Override - public void onTimelineChanged(Timeline timeline, Object manifest) { - if (timeline.isEmpty()) { - // The player is being re-prepared and this source will be released. + public void onTimelineChanged(Timeline timeline, Object manifest, + @Player.TimelineChangeReason int reason) { + if (reason == Player.TIMELINE_CHANGE_REASON_RESET) { + // The player is being reset and this source will be released. return; } Assertions.checkArgument(timeline.getPeriodCount() == 1); diff --git a/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java b/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java index 510ed9cf4f..c9ed54398e 100644 --- a/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java +++ b/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java @@ -32,6 +32,7 @@ import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player.DiscontinuityReason; +import com.google.android.exoplayer2.Player.TimelineChangeReason; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.util.ErrorMessageProvider; @@ -258,7 +259,8 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { } @Override - public void onTimelineChanged(Timeline timeline, Object manifest) { + public void onTimelineChanged(Timeline timeline, Object manifest, + @TimelineChangeReason int reason) { Callback callback = getCallback(); callback.onDurationChanged(LeanbackPlayerAdapter.this); callback.onCurrentPositionChanged(LeanbackPlayerAdapter.this); diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java index aa007ea1d6..d80487f2bd 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java @@ -628,7 +628,8 @@ public final class MediaSessionConnector { private int currentWindowCount; @Override - public void onTimelineChanged(Timeline timeline, Object manifest) { + public void onTimelineChanged(Timeline timeline, Object manifest, + @Player.TimelineChangeReason int reason) { int windowCount = player.getCurrentTimeline().getWindowCount(); int windowIndex = player.getCurrentWindowIndex(); if (queueNavigator != null) { diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index f0f1c23c2b..59a58a4912 100644 --- a/library/core/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 com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSource.Listener; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.testutil.ActionSchedule; @@ -28,6 +29,7 @@ import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeRenderer; import com.google.android.exoplayer2.testutil.FakeShuffleOrder; import com.google.android.exoplayer2.testutil.FakeTimeline; +import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.testutil.FakeTrackSelection; import com.google.android.exoplayer2.testutil.FakeTrackSelector; import com.google.android.exoplayer2.upstream.Allocator; @@ -59,7 +61,7 @@ public final class ExoPlayerTest extends TestCase { .setTimeline(timeline).setRenderers(renderer) .build().start().blockUntilEnded(TIMEOUT_MS); testRunner.assertNoPositionDiscontinuities(); - testRunner.assertTimelinesEqual(); + testRunner.assertTimelinesEqual(timeline); assertEquals(0, renderer.formatReadCount); assertEquals(0, renderer.bufferReadCount); assertFalse(renderer.isEnded); @@ -78,6 +80,7 @@ public final class ExoPlayerTest extends TestCase { testRunner.assertNoPositionDiscontinuities(); testRunner.assertTimelinesEqual(timeline); testRunner.assertManifestsEqual(manifest); + testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); testRunner.assertTrackGroupsEqual(new TrackGroupArray(new TrackGroup(Builder.VIDEO_FORMAT))); assertEquals(1, renderer.formatReadCount); assertEquals(1, renderer.bufferReadCount); @@ -97,6 +100,7 @@ public final class ExoPlayerTest extends TestCase { Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, Player.DISCONTINUITY_REASON_PERIOD_TRANSITION); testRunner.assertTimelinesEqual(timeline); + testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); assertEquals(3, renderer.formatReadCount); assertEquals(1, renderer.bufferReadCount); assertTrue(renderer.isEnded); @@ -210,6 +214,8 @@ public final class ExoPlayerTest extends TestCase { // info refresh from the second source was suppressed as we re-prepared with the third source. testRunner.assertTimelinesEqual(timeline, Timeline.EMPTY, timeline); testRunner.assertManifestsEqual(firstSourceManifest, null, thirdSourceManifest); + testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED, + Player.TIMELINE_CHANGE_REASON_RESET, Player.TIMELINE_CHANGE_REASON_PREPARED); testRunner.assertTrackGroupsEqual(new TrackGroupArray(new TrackGroup(Builder.VIDEO_FORMAT))); assertEquals(1, renderer.formatReadCount); assertEquals(1, renderer.bufferReadCount); @@ -243,6 +249,7 @@ public final class ExoPlayerTest extends TestCase { Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, Player.DISCONTINUITY_REASON_PERIOD_TRANSITION); testRunner.assertTimelinesEqual(timeline); + testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); assertTrue(renderer.isEnded); } @@ -513,4 +520,25 @@ public final class ExoPlayerTest extends TestCase { assertEquals(3, numSelectionsEnabled); } + public void testDynamicTimelineChangeReason() throws Exception { + Timeline timeline1 = new FakeTimeline(new TimelineWindowDefinition(false, false, 100000)); + final Timeline timeline2 = new FakeTimeline(new TimelineWindowDefinition(false, false, 20000)); + final FakeMediaSource mediaSource = new FakeMediaSource(timeline1, null, Builder.VIDEO_FORMAT); + ActionSchedule actionSchedule = new ActionSchedule.Builder("testDynamicTimelineChangeReason") + .waitForTimelineChanged(timeline1) + .executeRunnable(new Runnable() { + @Override + public void run() { + mediaSource.setNewSourceInfo(timeline2, null); + } + }) + .build(); + ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() + .setMediaSource(mediaSource).setActionSchedule(actionSchedule) + .build().start().blockUntilEnded(TIMEOUT_MS); + testRunner.assertTimelinesEqual(timeline1, timeline2); + testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED, + Player.TIMELINE_CHANGE_REASON_DYNAMIC); + } + } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index ff00f9de91..77131f5ded 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -56,6 +56,7 @@ import java.util.concurrent.CopyOnWriteArraySet; private int playbackState; private int pendingSeekAcks; private int pendingPrepareAcks; + private boolean waitingForInitialTimeline; private boolean isLoading; private TrackGroupArray trackGroups; private TrackSelectionArray trackSelections; @@ -146,7 +147,8 @@ import java.util.concurrent.CopyOnWriteArraySet; if (!playbackInfo.timeline.isEmpty() || playbackInfo.manifest != null) { playbackInfo = playbackInfo.copyWithTimeline(Timeline.EMPTY, null); for (Player.EventListener listener : listeners) { - listener.onTimelineChanged(playbackInfo.timeline, playbackInfo.manifest); + listener.onTimelineChanged(playbackInfo.timeline, playbackInfo.manifest, + Player.TIMELINE_CHANGE_REASON_RESET); } } if (tracksSelected) { @@ -159,6 +161,7 @@ import java.util.concurrent.CopyOnWriteArraySet; } } } + waitingForInitialTimeline = true; pendingPrepareAcks++; internalPlayer.prepare(mediaSource, resetPosition); } @@ -532,9 +535,12 @@ import java.util.concurrent.CopyOnWriteArraySet; maskingWindowIndex = 0; maskingWindowPositionMs = 0; } - if (timelineOrManifestChanged) { + if (timelineOrManifestChanged || waitingForInitialTimeline) { + @Player.TimelineChangeReason int reason = waitingForInitialTimeline + ? Player.TIMELINE_CHANGE_REASON_PREPARED : Player.TIMELINE_CHANGE_REASON_DYNAMIC; + waitingForInitialTimeline = false; for (Player.EventListener listener : listeners) { - listener.onTimelineChanged(playbackInfo.timeline, playbackInfo.manifest); + listener.onTimelineChanged(playbackInfo.timeline, playbackInfo.manifest, reason); } } if (positionDiscontinuity) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Player.java b/library/core/src/main/java/com/google/android/exoplayer2/Player.java index dc703f924a..77fced0832 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Player.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Player.java @@ -59,8 +59,9 @@ public interface Player { * * @param timeline The latest timeline. Never null, but may be empty. * @param manifest The latest manifest. May be null. + * @param reason The {@link TimelineChangeReason} responsible for this timeline change. */ - void onTimelineChanged(Timeline timeline, Object manifest); + void onTimelineChanged(Timeline timeline, Object manifest, @TimelineChangeReason int reason); /** * Called when the available or selected tracks change. @@ -118,7 +119,8 @@ public interface Player { * when the source introduces a discontinuity internally). *

          * When a position discontinuity occurs as a result of a change to the timeline this method is - * not called. {@link #onTimelineChanged(Timeline, Object)} is called in this case. + * not called. {@link #onTimelineChanged(Timeline, Object, int)} is called in this + * case. * * @param reason The {@link DiscontinuityReason} responsible for the discontinuity. */ @@ -149,8 +151,10 @@ public interface Player { abstract class DefaultEventListener implements EventListener { @Override - public void onTimelineChanged(Timeline timeline, Object manifest) { - // Do nothing. + public void onTimelineChanged(Timeline timeline, Object manifest, + @TimelineChangeReason int reason) { + // Call deprecated version. Otherwise, do nothing. + onTimelineChanged(timeline, manifest); } @Override @@ -198,6 +202,15 @@ public interface Player { // Do nothing. } + /** + * @deprecated Use {@link DefaultEventListener#onTimelineChanged(Timeline, Object, int)} + * instead. + */ + @Deprecated + public void onTimelineChanged(Timeline timeline, Object manifest) { + // Do nothing. + } + } /** @@ -264,6 +277,26 @@ public interface Player { */ int DISCONTINUITY_REASON_INTERNAL = 3; + /** + * Reasons for timeline and/or manifest changes. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({TIMELINE_CHANGE_REASON_PREPARED, TIMELINE_CHANGE_REASON_RESET, + TIMELINE_CHANGE_REASON_DYNAMIC}) + public @interface TimelineChangeReason {} + /** + * Timeline and manifest changed as a result of a player initialization with new media. + */ + int TIMELINE_CHANGE_REASON_PREPARED = 0; + /** + * Timeline and manifest changed as a result of a player reset. + */ + int TIMELINE_CHANGE_REASON_RESET = 1; + /** + * Timeline or manifest changed as a result of an dynamic update introduced by the played media. + */ + int TIMELINE_CHANGE_REASON_DYNAMIC = 2; + /** * Register a listener to receive events from the player. The listener's methods will be called on * the thread that was used to construct the player. However, if the thread used to construct the diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java index a96ed3a622..751a6c81a9 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java @@ -699,6 +699,8 @@ public class PlaybackControlView extends FrameLayout { repeatToggleButton.setImageDrawable(repeatAllButtonDrawable); repeatToggleButton.setContentDescription(repeatAllButtonContentDescription); break; + default: + // Never happens. } repeatToggleButton.setVisibility(View.VISIBLE); } @@ -1098,7 +1100,8 @@ public class PlaybackControlView extends FrameLayout { } @Override - public void onTimelineChanged(Timeline timeline, Object manifest) { + public void onTimelineChanged(Timeline timeline, Object manifest, + @Player.TimelineChangeReason int reason) { updateNavigation(); updateTimeBarMode(); updateProgress(); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java index 2abe521883..357d69df38 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java @@ -304,7 +304,7 @@ public abstract class Action { } /** - * Waits for {@link Player.EventListener#onTimelineChanged(Timeline, Object)}. + * Waits for {@link Player.EventListener#onTimelineChanged(Timeline, Object, int)}. */ public static final class WaitForTimelineChanged extends Action { @@ -327,7 +327,8 @@ public abstract class Action { } Player.EventListener listener = new Player.DefaultEventListener() { @Override - public void onTimelineChanged(Timeline timeline, Object manifest) { + public void onTimelineChanged(Timeline timeline, Object manifest, + @Player.TimelineChangeReason int reason) { if (timeline.equals(expectedTimeline)) { player.removeListener(this); nextAction.schedule(player, trackSelector, surface, handler); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java index 6730bf1c7f..638ad9e12d 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java @@ -318,6 +318,7 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { private final CountDownLatch endedCountDownLatch; private final LinkedList timelines; private final LinkedList manifests; + private final ArrayList timelineChangeReasons; private final LinkedList periodIndices; private final ArrayList discontinuityReasons; @@ -338,6 +339,7 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { this.eventListener = eventListener; this.timelines = new LinkedList<>(); this.manifests = new LinkedList<>(); + this.timelineChangeReasons = new ArrayList<>(); this.periodIndices = new LinkedList<>(); this.discontinuityReasons = new ArrayList<>(); this.endedCountDownLatch = new CountDownLatch(1); @@ -430,6 +432,18 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { } } + /** + * Asserts that the timeline change reasons reported by + * {@link Player.EventListener#onTimelineChanged(Timeline, Object, int)} are equal to the provided + * timeline change reasons. + */ + public void assertTimelineChangeReasonsEqual(@Player.TimelineChangeReason int... reasons) { + Assert.assertEquals(reasons.length, timelineChangeReasons.size()); + for (int i = 0; i < reasons.length; i++) { + Assert.assertEquals(reasons[i], (int) timelineChangeReasons.get(i)); + } + } + /** * Asserts that the last track group array reported by * {@link Player.EventListener#onTracksChanged(TrackGroupArray, TrackSelectionArray)} is equal to @@ -507,9 +521,11 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { // Player.EventListener @Override - public void onTimelineChanged(Timeline timeline, Object manifest) { + public void onTimelineChanged(Timeline timeline, Object manifest, + @Player.TimelineChangeReason int reason) { timelines.add(timeline); manifests.add(manifest); + timelineChangeReasons.add(reason); } @Override diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java index 4a5beb0501..6dc9cf7fd8 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java @@ -340,7 +340,8 @@ public class FakeSimpleExoPlayer extends SimpleExoPlayer { FakeExoPlayer.this.durationUs = timeline.getPeriod(0, new Period()).durationUs; FakeExoPlayer.this.timeline = timeline; FakeExoPlayer.this.manifest = manifest; - eventListener.onTimelineChanged(timeline, manifest); + eventListener.onTimelineChanged(timeline, manifest, + Player.TIMELINE_CHANGE_REASON_PREPARED); waitForNotification.open(); } } From fdb53ac8d1c9a5f13ce36c0286ef69c070cf3560 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 21 Nov 2017 01:44:18 -0800 Subject: [PATCH 0737/2472] Correct period index counting in ExoPlayerTestRunner. The initial period index was counted in onPlayerStateChanged. However, we actually want to save the period index in the newly introduced onPositionDiscontinuity after preparation. While being here, also updated deprecated LinkedList to ArrayList. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176479509 --- .../testutil/ExoPlayerTestRunner.java | 39 +++++++++++-------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java index 638ad9e12d..cafc50f0b4 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java @@ -40,7 +40,6 @@ import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.video.VideoRendererEventListener; import java.util.ArrayList; -import java.util.LinkedList; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -316,10 +315,10 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { private final HandlerThread playerThread; private final Handler handler; private final CountDownLatch endedCountDownLatch; - private final LinkedList timelines; - private final LinkedList manifests; + private final ArrayList timelines; + private final ArrayList manifests; private final ArrayList timelineChangeReasons; - private final LinkedList periodIndices; + private final ArrayList periodIndices; private final ArrayList discontinuityReasons; private SimpleExoPlayer player; @@ -337,10 +336,10 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { this.loadControl = loadControl; this.actionSchedule = actionSchedule; this.eventListener = eventListener; - this.timelines = new LinkedList<>(); - this.manifests = new LinkedList<>(); + this.timelines = new ArrayList<>(); + this.manifests = new ArrayList<>(); this.timelineChangeReasons = new ArrayList<>(); - this.periodIndices = new LinkedList<>(); + this.periodIndices = new ArrayList<>(); this.discontinuityReasons = new ArrayList<>(); this.endedCountDownLatch = new CountDownLatch(1); this.playerThread = new HandlerThread("ExoPlayerTest thread"); @@ -413,8 +412,8 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { */ public void assertTimelinesEqual(Timeline... timelines) { Assert.assertEquals(timelines.length, this.timelines.size()); - for (Timeline timeline : timelines) { - Assert.assertEquals(timeline, this.timelines.remove()); + for (int i = 0; i < timelines.length; i++) { + Assert.assertEquals(timelines[i], this.timelines.get(i)); } } @@ -427,8 +426,8 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { */ public void assertManifestsEqual(Object... manifests) { Assert.assertEquals(manifests.length, this.manifests.size()); - for (Object manifest : manifests) { - Assert.assertEquals(manifest, this.manifests.remove()); + for (int i = 0; i < manifests.length; i++) { + Assert.assertEquals(manifests[i], this.manifests.get(i)); } } @@ -486,8 +485,8 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { */ public void assertPlayedPeriodIndices(int... periodIndices) { Assert.assertEquals(periodIndices.length, this.periodIndices.size()); - for (int periodIndex : periodIndices) { - Assert.assertEquals(periodIndex, (int) this.periodIndices.remove()); + for (int i = 0; i < periodIndices.length; i++) { + Assert.assertEquals(periodIndices[i], (int) this.periodIndices.get(i)); } } @@ -526,6 +525,9 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { timelines.add(timeline); manifests.add(manifest); timelineChangeReasons.add(reason); + if (reason == Player.TIMELINE_CHANGE_REASON_PREPARED) { + periodIndices.add(player.getCurrentPeriodIndex()); + } } @Override @@ -535,9 +537,6 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { @Override public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { - if (periodIndices.isEmpty() && playbackState == Player.STATE_READY) { - periodIndices.add(player.getCurrentPeriodIndex()); - } playerWasPrepared |= playbackState != Player.STATE_IDLE; if (playbackState == Player.STATE_ENDED || (playbackState == Player.STATE_IDLE && playerWasPrepared)) { @@ -553,7 +552,13 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { @Override public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { discontinuityReasons.add(reason); - periodIndices.add(player.getCurrentPeriodIndex()); + int currentIndex = player.getCurrentPeriodIndex(); + if (reason == Player.DISCONTINUITY_REASON_PERIOD_TRANSITION + || periodIndices.isEmpty() + || periodIndices.get(periodIndices.size() - 1) != currentIndex) { + // Ignore seek or internal discontinuities within a period. + periodIndices.add(currentIndex); + } } } From e1d960db68b59590190a0f20ca707cb9a0f745f8 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 21 Nov 2017 02:13:04 -0800 Subject: [PATCH 0738/2472] Unify internal reset method to support state and position resets. The ExoPlayerImplInternal.reset method now takes the same set of options as the ExoPlayer.prepare method. This also allows to - Remove some code duplication within ExoPlayerImplInternal - Fix calls to prepare(sameSource, resetPosition=true, resetState=false) with enabled shuffle mode where the position was not correctly reset to the first period index. - Keep the current timeline when calling stop (in line with ExoPlayerImpl). ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176481878 --- .../android/exoplayer2/ExoPlayerTest.java | 27 ++++++++- .../exoplayer2/ExoPlayerImplInternal.java | 59 +++++++++---------- 2 files changed, 55 insertions(+), 31 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index 59a58a4912..2392c32e0a 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2; import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.MediaSource.Listener; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.testutil.ActionSchedule; @@ -541,4 +540,30 @@ public final class ExoPlayerTest extends TestCase { Player.TIMELINE_CHANGE_REASON_DYNAMIC); } + public void testRepreparationWithPositionResetAndShufflingUsesFirstPeriod() throws Exception { + Timeline fakeTimeline = new FakeTimeline(new TimelineWindowDefinition(/* isSeekable= */ true, + /* isDynamic= */ false, /* durationUs= */ 100000)); + ConcatenatingMediaSource firstMediaSource = new ConcatenatingMediaSource(/* isAtomic= */ false, + new FakeShuffleOrder(/* length= */ 2), + new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT), + new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT) + ); + ConcatenatingMediaSource secondMediaSource = new ConcatenatingMediaSource(/* isAtomic= */ false, + new FakeShuffleOrder(/* length= */ 2), + new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT), + new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT) + ); + ActionSchedule actionSchedule = new ActionSchedule.Builder("testRepreparationWithShuffle") + // Wait for first preparation and enable shuffling. Plays period 0. + .waitForPlaybackState(Player.STATE_READY).setShuffleModeEnabled(true) + // Reprepare with second media source (keeping state, but with position reset). + // Plays period 1 and 0 because of the reversed fake shuffle order. + .prepareSource(secondMediaSource, /* resetPosition= */ true, /* resetState= */ false) + .build(); + ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() + .setMediaSource(firstMediaSource).setActionSchedule(actionSchedule) + .build().start().blockUntilEnded(TIMEOUT_MS); + testRunner.assertPlayedPeriodIndices(0, 1, 0); + } + } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 1732026540..668d52425e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -393,17 +393,10 @@ import java.io.IOException; private void prepareInternal(MediaSource mediaSource, boolean resetPosition) { pendingPrepareCount++; - resetInternal(true); + resetInternal(/* releaseMediaSource= */ true, resetPosition); loadControl.onPrepared(); - if (resetPosition) { - playbackInfo = new PlaybackInfo(null, null, 0, C.TIME_UNSET); - } else { - // The new start position is the current playback position. - playbackInfo = new PlaybackInfo(null, null, playbackInfo.periodId, playbackInfo.positionUs, - playbackInfo.contentPositionUs); - } this.mediaSource = mediaSource; - mediaSource.prepareSource(player, true, this); + mediaSource.prepareSource(player, /* isTopLevelSource= */ true, /* listener = */ this); setState(Player.STATE_BUFFERING); handler.sendEmptyMessage(MSG_DO_SOME_WORK); } @@ -638,18 +631,16 @@ import java.io.IOException; Pair periodPosition = resolveSeekPosition(seekPosition); if (periodPosition == null) { - int firstPeriodIndex = timeline.isEmpty() ? 0 : timeline.getWindow( - timeline.getFirstWindowIndex(shuffleModeEnabled), window).firstPeriodIndex; // The seek position was valid for the timeline that it was performed into, but the // timeline has changed and a suitable seek position could not be resolved in the new one. - // Set the internal position to (firstPeriodIndex,TIME_UNSET) so that a subsequent seek to - // (firstPeriodIndex,0) isn't ignored. - playbackInfo = playbackInfo.fromNewPosition(firstPeriodIndex, C.TIME_UNSET, C.TIME_UNSET); setState(Player.STATE_ENDED); - eventHandler.obtainMessage(MSG_SEEK_ACK, 1, 0, - playbackInfo.fromNewPosition(firstPeriodIndex, 0, C.TIME_UNSET)).sendToTarget(); // Reset, but retain the source so that it can still be used should a seek occur. - resetInternal(false); + resetInternal(false, true); + // Set the playback position to 0 for notifying the eventHandler (instead of C.TIME_UNSET). + eventHandler.obtainMessage(MSG_SEEK_ACK, /* seekAdjusted = */ 1, 0, + playbackInfo.fromNewPosition(playbackInfo.periodId.periodIndex, /* startPositionUs = */ 0, + /* contentPositionUs= */ C.TIME_UNSET)) + .sendToTarget(); return; } @@ -768,13 +759,13 @@ import java.io.IOException; } private void stopInternal() { - resetInternal(true); + resetInternal(/* releaseMediaSource= */ false, /* resetPosition= */ false); loadControl.onStopped(); setState(Player.STATE_IDLE); } private void releaseInternal() { - resetInternal(true); + resetInternal(/* releaseMediaSource= */ true, /* resetPosition= */ true); loadControl.onReleased(); setState(Player.STATE_IDLE); internalPlaybackThread.quit(); @@ -784,7 +775,7 @@ import java.io.IOException; } } - private void resetInternal(boolean releaseMediaSource) { + private void resetInternal(boolean releaseMediaSource, boolean resetPosition) { handler.removeMessages(MSG_DO_SOME_WORK); rebuffering = false; mediaClock.stop(); @@ -804,6 +795,20 @@ import java.io.IOException; readingPeriodHolder = null; playingPeriodHolder = null; setIsLoading(false); + if (resetPosition) { + // Set the internal position to (firstPeriodIndex,TIME_UNSET) so that a subsequent seek to + // (firstPeriodIndex,0) isn't ignored. + Timeline timeline = playbackInfo.timeline; + int firstPeriodIndex = timeline == null || timeline.isEmpty() + ? 0 + : timeline.getWindow(timeline.getFirstWindowIndex(shuffleModeEnabled), window) + .firstPeriodIndex; + playbackInfo = playbackInfo.fromNewPosition(firstPeriodIndex, C.TIME_UNSET, C.TIME_UNSET); + } else { + // The new start position is the current playback position. + playbackInfo = playbackInfo.fromNewPosition(playbackInfo.periodId, playbackInfo.positionUs, + playbackInfo.contentPositionUs); + } if (releaseMediaSource) { if (mediaSource != null) { mediaSource.releaseSource(); @@ -1129,18 +1134,12 @@ import java.io.IOException; } private void handleSourceInfoRefreshEndedPlayback(int prepareAcks, int seekAcks) { - Timeline timeline = playbackInfo.timeline; - int firstPeriodIndex = timeline.isEmpty() ? 0 : timeline.getWindow( - timeline.getFirstWindowIndex(shuffleModeEnabled), window).firstPeriodIndex; - // Set the internal position to (firstPeriodIndex,TIME_UNSET) so that a subsequent seek to - // (firstPeriodIndex,0) isn't ignored. - playbackInfo = playbackInfo.fromNewPosition(firstPeriodIndex, C.TIME_UNSET, C.TIME_UNSET); setState(Player.STATE_ENDED); - // Set the playback position to (firstPeriodIndex,0) for notifying the eventHandler. - notifySourceInfoRefresh(prepareAcks, seekAcks, - playbackInfo.fromNewPosition(firstPeriodIndex, 0, C.TIME_UNSET)); // Reset, but retain the source so that it can still be used should a seek occur. - resetInternal(false); + resetInternal(false, true); + // Set the playback position to 0 for notifying the eventHandler (instead of C.TIME_UNSET). + notifySourceInfoRefresh(prepareAcks, seekAcks, + playbackInfo.fromNewPosition(playbackInfo.periodId.periodIndex, 0, C.TIME_UNSET)); } private void notifySourceInfoRefresh() { From a8d867be37ef813a60d64779e2cc9693d3364238 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 21 Nov 2017 02:44:09 -0800 Subject: [PATCH 0739/2472] Support multiple transitions to STATE_ENDED in ExoPlayerTestRunner. Currently testRunner.blockUntilEnded waits for the first transition to STATE_ENDED or _IDLE before returning. In order to support tests with player repreparations after one playback finished, this change adds an option to specifiy the number of expected transitions to ended. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176484047 --- .../testutil/ExoPlayerTestRunner.java | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java index cafc50f0b4..5ada65ef1e 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java @@ -100,6 +100,7 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { private RenderersFactory renderersFactory; private ActionSchedule actionSchedule; private Player.EventListener eventListener; + private Integer expectedPlayerEndedCount; /** * Sets a {@link Timeline} to be used by a {@link FakeMediaSource} in the test runner. The @@ -255,6 +256,20 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { return this; } + /** + * Sets the number of times the test runner is expected to reach the {@link Player#STATE_ENDED} + * or {@link Player#STATE_IDLE}. The default is 1. This affects how long + * {@link ExoPlayerTestRunner#blockUntilEnded(long)} waits. + * + * @param expectedPlayerEndedCount The number of times the player is expected to reach the ended + * or idle state. + * @return This builder. + */ + public Builder setExpectedPlayerEndedCount(int expectedPlayerEndedCount) { + this.expectedPlayerEndedCount = expectedPlayerEndedCount; + return this; + } + /** * Builds an {@link ExoPlayerTestRunner} using the provided values or their defaults. * @@ -299,8 +314,11 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { } mediaSource = new FakeMediaSource(timeline, manifest, supportedFormats); } + if (expectedPlayerEndedCount == null) { + expectedPlayerEndedCount = 1; + } return new ExoPlayerTestRunner(playerFactory, mediaSource, renderersFactory, trackSelector, - loadControl, actionSchedule, eventListener); + loadControl, actionSchedule, eventListener, expectedPlayerEndedCount); } } @@ -328,7 +346,8 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { private ExoPlayerTestRunner(PlayerFactory playerFactory, MediaSource mediaSource, RenderersFactory renderersFactory, MappingTrackSelector trackSelector, - LoadControl loadControl, ActionSchedule actionSchedule, Player.EventListener eventListener) { + LoadControl loadControl, ActionSchedule actionSchedule, Player.EventListener eventListener, + int expectedPlayerEndedCount) { this.playerFactory = playerFactory; this.mediaSource = mediaSource; this.renderersFactory = renderersFactory; @@ -341,7 +360,7 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { this.timelineChangeReasons = new ArrayList<>(); this.periodIndices = new ArrayList<>(); this.discontinuityReasons = new ArrayList<>(); - this.endedCountDownLatch = new CountDownLatch(1); + this.endedCountDownLatch = new CountDownLatch(expectedPlayerEndedCount); this.playerThread = new HandlerThread("ExoPlayerTest thread"); playerThread.start(); this.handler = new Handler(playerThread.getLooper()); @@ -514,7 +533,9 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { if (this.exception == null) { this.exception = exception; } - endedCountDownLatch.countDown(); + while (endedCountDownLatch.getCount() > 0) { + endedCountDownLatch.countDown(); + } } // Player.EventListener From 31e2cfce9e21c0d594d6b10cf726d498b07a7492 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 21 Nov 2017 02:48:44 -0800 Subject: [PATCH 0740/2472] Pass playback speed to LoadControl and TrackSelection This allows implementations of those classes to take into account the playback speed for adaptive track selection and controlling when to resume the player. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176484361 --- .../exoplayer2/DefaultLoadControl.java | 5 +- .../exoplayer2/ExoPlayerImplInternal.java | 49 +++++++++++++------ .../android/exoplayer2/LoadControl.java | 6 ++- .../exoplayer2/PlaybackParameters.java | 14 ++++-- .../exoplayer2/audio/DefaultAudioSink.java | 3 +- .../trackselection/BaseTrackSelection.java | 5 ++ .../trackselection/TrackSelection.java | 8 +++ .../exoplayer2/util/StandaloneMediaClock.java | 2 +- .../google/android/exoplayer2/util/Util.java | 14 ++++++ .../exoplayer2/DefaultMediaClockTest.java | 2 +- .../testutil/FakeSimpleExoPlayer.java | 5 +- .../testutil/FakeTrackSelection.java | 5 ++ 12 files changed, 89 insertions(+), 29 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java index 4cbcc00886..56bc633c9b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java @@ -174,13 +174,14 @@ public class DefaultLoadControl implements LoadControl { } @Override - public boolean shouldStartPlayback(long bufferedDurationUs, boolean rebuffering) { + public boolean shouldStartPlayback(long bufferedDurationUs, float playbackSpeed, + boolean rebuffering) { long minBufferDurationUs = rebuffering ? bufferForPlaybackAfterRebufferUs : bufferForPlaybackUs; return minBufferDurationUs <= 0 || bufferedDurationUs >= minBufferDurationUs; } @Override - public boolean shouldContinueLoading(long bufferedDurationUs) { + public boolean shouldContinueLoading(long bufferedDurationUs, float playbackSpeed) { int bufferTimeState = getBufferTimeState(bufferedDurationUs); boolean targetBufferSizeReached = allocator.getTotalBytesAllocated() >= targetBufferSize; boolean wasBuffering = isBuffering; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 668d52425e..71da7043be 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -284,10 +284,8 @@ import java.io.IOException; @Override public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { - // TODO(b/37237846): Make LoadControl, period transition position projection, adaptive track - // selection and potentially any time-related code in renderers take into account the playback - // speed. eventHandler.obtainMessage(MSG_PLAYBACK_PARAMETERS_CHANGED, playbackParameters).sendToTarget(); + updateTrackSelectionPlaybackSpeed(playbackParameters.speed); } // Handler.Callback implementation. @@ -573,9 +571,10 @@ import java.io.IOException; setState(Player.STATE_ENDED); stopRenderers(); } else if (state == Player.STATE_BUFFERING) { + float playbackSpeed = mediaClock.getPlaybackParameters().speed; boolean isNewlyReady = enabledRenderers.length > 0 - ? (allRenderersReadyOrEnded - && loadingPeriodHolder.haveSufficientBuffer(rebuffering, rendererPositionUs)) + ? (allRenderersReadyOrEnded && loadingPeriodHolder.haveSufficientBuffer( + rendererPositionUs, playbackSpeed, rebuffering)) : isTimelineReady(playingPeriodDurationUs); if (isNewlyReady) { setState(Player.STATE_READY); @@ -853,6 +852,7 @@ import java.io.IOException; // We don't have tracks yet, so we don't care. return; } + float playbackSpeed = mediaClock.getPlaybackParameters().speed; // Reselect tracks on each period in turn, until the selection changes. MediaPeriodHolder periodHolder = playingPeriodHolder; boolean selectionsChangedForReadPeriod = true; @@ -861,7 +861,7 @@ import java.io.IOException; // The reselection did not change any prepared periods. return; } - if (periodHolder.selectTracks()) { + if (periodHolder.selectTracks(playbackSpeed)) { // Selected tracks have changed for this period. break; } @@ -935,6 +935,18 @@ import java.io.IOException; } } + private void updateTrackSelectionPlaybackSpeed(float playbackSpeed) { + MediaPeriodHolder periodHolder = + playingPeriodHolder != null ? playingPeriodHolder : loadingPeriodHolder; + while (periodHolder != null) { + TrackSelection[] trackSelections = periodHolder.trackSelectorResult.selections.getAll(); + for (TrackSelection trackSelection : trackSelections) { + trackSelection.onPlaybackSpeed(playbackSpeed); + } + periodHolder = periodHolder.next; + } + } + private boolean isTimelineReady(long playingPeriodDurationUs) { return playingPeriodDurationUs == C.TIME_UNSET || playbackInfo.positionUs < playingPeriodDurationUs @@ -1391,7 +1403,7 @@ import java.io.IOException; // Stale event. return; } - loadingPeriodHolder.handlePrepared(); + loadingPeriodHolder.handlePrepared(mediaClock.getPlaybackParameters().speed); if (playingPeriodHolder == null) { // This is the first prepared period, so start playing it. readingPeriodHolder = loadingPeriodHolder; @@ -1410,7 +1422,8 @@ import java.io.IOException; } private void maybeContinueLoading() { - boolean continueLoading = loadingPeriodHolder.shouldContinueLoading(rendererPositionUs); + boolean continueLoading = loadingPeriodHolder.shouldContinueLoading( + rendererPositionUs, mediaClock.getPlaybackParameters().speed); setIsLoading(continueLoading); if (continueLoading) { loadingPeriodHolder.continueLoading(rendererPositionUs); @@ -1572,7 +1585,8 @@ import java.io.IOException; && (!hasEnabledTracks || mediaPeriod.getBufferedPositionUs() == C.TIME_END_OF_SOURCE); } - public boolean haveSufficientBuffer(boolean rebuffering, long rendererPositionUs) { + public boolean haveSufficientBuffer(long rendererPositionUs, float playbackSpeed, + boolean rebuffering) { long bufferedPositionUs = !prepared ? info.startPositionUs : mediaPeriod.getBufferedPositionUs(); if (bufferedPositionUs == C.TIME_END_OF_SOURCE) { @@ -1582,24 +1596,24 @@ import java.io.IOException; bufferedPositionUs = info.durationUs; } return loadControl.shouldStartPlayback(bufferedPositionUs - toPeriodTime(rendererPositionUs), - rebuffering); + playbackSpeed, rebuffering); } - public void handlePrepared() throws ExoPlaybackException { + public void handlePrepared(float playbackSpeed) throws ExoPlaybackException { prepared = true; - selectTracks(); + selectTracks(playbackSpeed); long newStartPositionUs = updatePeriodTrackSelection(info.startPositionUs, false); info = info.copyWithStartPositionUs(newStartPositionUs); } - public boolean shouldContinueLoading(long rendererPositionUs) { + public boolean shouldContinueLoading(long rendererPositionUs, float playbackSpeed) { long nextLoadPositionUs = !prepared ? 0 : mediaPeriod.getNextLoadPositionUs(); if (nextLoadPositionUs == C.TIME_END_OF_SOURCE) { return false; } else { long loadingPeriodPositionUs = toPeriodTime(rendererPositionUs); long bufferedDurationUs = nextLoadPositionUs - loadingPeriodPositionUs; - return loadControl.shouldContinueLoading(bufferedDurationUs); + return loadControl.shouldContinueLoading(bufferedDurationUs, playbackSpeed); } } @@ -1608,13 +1622,18 @@ import java.io.IOException; mediaPeriod.continueLoading(loadingPeriodPositionUs); } - public boolean selectTracks() throws ExoPlaybackException { + public boolean selectTracks(float playbackSpeed) throws ExoPlaybackException { TrackSelectorResult selectorResult = trackSelector.selectTracks(rendererCapabilities, mediaPeriod.getTrackGroups()); if (selectorResult.isEquivalent(periodTrackSelectorResult)) { return false; } trackSelectorResult = selectorResult; + for (TrackSelection trackSelection : trackSelectorResult.selections.getAll()) { + if (trackSelection != null) { + trackSelection.onPlaybackSpeed(playbackSpeed); + } + } return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/LoadControl.java b/library/core/src/main/java/com/google/android/exoplayer2/LoadControl.java index 44b16b0cf6..ee4775d048 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/LoadControl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/LoadControl.java @@ -92,19 +92,21 @@ public interface LoadControl { * started or resumed. * * @param bufferedDurationUs The duration of media that's currently buffered. + * @param playbackSpeed The current playback speed. * @param rebuffering Whether the player is rebuffering. A rebuffer is defined to be caused by * buffer depletion rather than a user action. Hence this parameter is false during initial * buffering and when buffering as a result of a seek operation. * @return Whether playback should be allowed to start or resume. */ - boolean shouldStartPlayback(long bufferedDurationUs, boolean rebuffering); + boolean shouldStartPlayback(long bufferedDurationUs, float playbackSpeed, boolean rebuffering); /** * Called by the player to determine whether it should continue to load the source. * * @param bufferedDurationUs The duration of media that's currently buffered. + * @param playbackSpeed The current playback speed. * @return Whether the loading should continue. */ - boolean shouldContinueLoading(long bufferedDurationUs); + boolean shouldContinueLoading(long bufferedDurationUs, float playbackSpeed); } 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 index 90aded7660..47d5bc88b9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackParameters.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackParameters.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2; +import com.google.android.exoplayer2.util.Assertions; + /** * The parameters that apply to playback. */ @@ -40,23 +42,25 @@ public final class PlaybackParameters { /** * 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. + * @param speed The factor by which playback will be sped up. Must be greater than zero. + * @param pitch The factor by which the audio pitch will be scaled. Must be greater than zero. */ public PlaybackParameters(float speed, float pitch) { + Assertions.checkArgument(speed > 0); + Assertions.checkArgument(pitch > 0); 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. + * Returns the media time in microseconds that will elapse in {@code timeMs} milliseconds of + * wallclock time. * * @param timeMs The time to scale, in milliseconds. * @return The scaled time, in microseconds. */ - public long getSpeedAdjustedDurationUs(long timeMs) { + public long getMediaTimeUsForPlayoutTimeMs(long timeMs) { return timeMs * scaledUsPerMs; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java index ba62ac126e..3b14b69916 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java @@ -1012,7 +1012,8 @@ public final class DefaultAudioSink implements AudioSink { } // We are playing data at a previous playback speed, so fall back to multiplying by the speed. return playbackParametersOffsetUs - + (long) ((double) playbackParameters.speed * (positionUs - playbackParametersPositionUs)); + + Util.getMediaDurationForPlayoutDuration( + positionUs - playbackParametersPositionUs, playbackParameters.speed); } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java index 6bc6afb88b..9a58ac07aa 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java @@ -138,6 +138,11 @@ public abstract class BaseTrackSelection implements TrackSelection { return tracks[getSelectedIndex()]; } + @Override + public void onPlaybackSpeed(float playbackSpeed) { + // Do nothing. + } + @Override public int evaluateQueueSize(long playbackPositionUs, List queue) { return queue.size(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java index 027b2abde9..55e6050622 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java @@ -136,6 +136,14 @@ public interface TrackSelection { // Adaptation. + /** + * Called to notify the selection of the current playback speed. The playback speed may affect + * adaptive track selection. + * + * @param speed The playback speed. + */ + void onPlaybackSpeed(float speed); + /** * Updates the selected track. *

          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 index fad3a00f10..3c0ec2a854 100644 --- 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 @@ -88,7 +88,7 @@ public final class StandaloneMediaClock implements MediaClock { if (playbackParameters.speed == 1f) { positionUs += C.msToUs(elapsedSinceBaseMs); } else { - positionUs += playbackParameters.getSpeedAdjustedDurationUs(elapsedSinceBaseMs); + positionUs += playbackParameters.getMediaTimeUsForPlayoutTimeMs(elapsedSinceBaseMs); } } return positionUs; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java index 3b402ec59d..4582ab7c86 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -672,6 +672,20 @@ public final class Util { } } + /** + * Returns the duration of media that will elapse in {@code playoutDuration}. + * + * @param playoutDuration The duration to scale. + * @param speed The playback speed. + * @return The scaled duration, in the same units as {@code playoutDuration}. + */ + public static long getMediaDurationForPlayoutDuration(long playoutDuration, float speed) { + if (speed == 1f) { + return playoutDuration; + } + return Math.round((double) playoutDuration * speed); + } + /** * Converts a list of integers to a primitive array. * diff --git a/library/core/src/test/java/com/google/android/exoplayer2/DefaultMediaClockTest.java b/library/core/src/test/java/com/google/android/exoplayer2/DefaultMediaClockTest.java index 9db4d57a65..9edb84eaa9 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/DefaultMediaClockTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/DefaultMediaClockTest.java @@ -368,7 +368,7 @@ public class DefaultMediaClockTest { long clockStartUs = mediaClock.syncAndGetPositionUs(); fakeClock.advanceTime(SLEEP_TIME_MS); assertThat(mediaClock.syncAndGetPositionUs()).isEqualTo(clockStartUs - + mediaClock.getPlaybackParameters().getSpeedAdjustedDurationUs(SLEEP_TIME_MS)); + + mediaClock.getPlaybackParameters().getMediaTimeUsForPlayoutTimeMs(SLEEP_TIME_MS)); } private void assertClockIsStopped() { diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java index 6dc9cf7fd8..094aaa5273 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java @@ -465,7 +465,7 @@ public class FakeSimpleExoPlayer extends SimpleExoPlayer { long nextLoadPositionUs = mediaPeriod.getNextLoadPositionUs(); if (nextLoadPositionUs != C.TIME_END_OF_SOURCE) { long bufferedDurationUs = nextLoadPositionUs - rendererPositionUs; - if (loadControl.shouldContinueLoading(bufferedDurationUs)) { + if (loadControl.shouldContinueLoading(bufferedDurationUs, 1f)) { newIsLoading = true; mediaPeriod.continueLoading(rendererPositionUs); } @@ -488,7 +488,8 @@ public class FakeSimpleExoPlayer extends SimpleExoPlayer { if (bufferedPositionUs == C.TIME_END_OF_SOURCE) { return true; } - return loadControl.shouldStartPlayback(bufferedPositionUs - rendererPositionUs, rebuffering); + return + loadControl.shouldStartPlayback(bufferedPositionUs - rendererPositionUs, 1f, rebuffering); } private void handlePlayerError(final ExoPlaybackException e) { diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelection.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelection.java index 20346a0355..717dcda7b1 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelection.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelection.java @@ -111,6 +111,11 @@ public final class FakeTrackSelection implements TrackSelection { return null; } + @Override + public void onPlaybackSpeed(float speed) { + // Do nothing. + } + @Override public void updateSelectedTrack(long playbackPositionUs, long bufferedDurationUs, long availableDurationUs) { From c49ae5369993059151813ba58ddfc7009928917d Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 21 Nov 2017 03:44:06 -0800 Subject: [PATCH 0741/2472] Remove unnecessary dependency ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176487991 --- extensions/mediasession/build.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/extensions/mediasession/build.gradle b/extensions/mediasession/build.gradle index 85a8ac46e2..651bd952f8 100644 --- a/extensions/mediasession/build.gradle +++ b/extensions/mediasession/build.gradle @@ -27,7 +27,6 @@ android { dependencies { compile project(modulePrefix + 'library-core') compile 'com.android.support:support-media-compat:' + supportLibraryVersion - compile 'com.android.support:appcompat-v7:' + supportLibraryVersion } ext { From 856c2f8d3ee59e6b8b0fba23532d8cd98d2616b3 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 21 Nov 2017 04:46:52 -0800 Subject: [PATCH 0742/2472] Make ExtractorMediaSource timeline dynamic until duration is set We (eventually - albeit possibly infinitely far in the future) expect a timeline update with a window of known duration. This also stops live radio stream playbacks transitioning to ended state when their tracks are disabled. As part of this fix, I found an issue where getPeriodPosition could return null even when defaultPositionProjectionUs is 0, which is not as documented. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176492024 --- .../source/ClippingMediaSourceTest.java | 10 +-- .../source/ExtractorMediaSource.java | 7 +- .../source/SinglePeriodTimeline.java | 23 +++--- .../source/SingleSampleMediaSource.java | 2 +- .../source/SinglePeriodTimelineTest.java | 77 +++++++++++++++++++ 5 files changed, 102 insertions(+), 17 deletions(-) create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/source/SinglePeriodTimelineTest.java diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java index 3c870f06f4..c72188ad2c 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java @@ -45,7 +45,7 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase { } public void testNoClipping() { - Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true); + Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true, false); Timeline clippedTimeline = getClippedTimeline(timeline, 0, TEST_PERIOD_DURATION_US); @@ -56,7 +56,7 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase { } public void testClippingUnseekableWindowThrows() { - Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), false); + Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), false, false); // If the unseekable window isn't clipped, clipping succeeds. getClippedTimeline(timeline, 0, TEST_PERIOD_DURATION_US); @@ -70,7 +70,7 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase { } public void testClippingStart() { - Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true); + Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true, false); Timeline clippedTimeline = getClippedTimeline(timeline, TEST_CLIP_AMOUNT_US, TEST_PERIOD_DURATION_US); @@ -81,7 +81,7 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase { } public void testClippingEnd() { - Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true); + Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true, false); Timeline clippedTimeline = getClippedTimeline(timeline, 0, TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US); @@ -92,7 +92,7 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase { } public void testClippingStartAndEnd() { - Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true); + Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true, false); Timeline clippedTimeline = getClippedTimeline(timeline, TEST_CLIP_AMOUNT_US, TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US * 2); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java index 066953b998..0839d06fdd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java @@ -327,8 +327,11 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe private void notifySourceInfoRefreshed(long durationUs, boolean isSeekable) { timelineDurationUs = durationUs; timelineIsSeekable = isSeekable; - sourceListener.onSourceInfoRefreshed( - this, new SinglePeriodTimeline(timelineDurationUs, timelineIsSeekable), null); + // If the duration is currently unset, we expect to be able to update the window when its + // duration eventually becomes known. + boolean isDynamic = timelineDurationUs == C.TIME_UNSET; + sourceListener.onSourceInfoRefreshed(this, + new SinglePeriodTimeline(timelineDurationUs, timelineIsSeekable, isDynamic), null); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java index 6f35438444..9cce67f68c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java @@ -36,14 +36,14 @@ public final class SinglePeriodTimeline extends Timeline { private final boolean isDynamic; /** - * Creates a timeline of one period of known duration, and a static window starting at zero and - * extending to that duration. + * Creates a timeline containing a single period and a window that spans it. * * @param durationUs The duration of the period, in microseconds. * @param isSeekable Whether seeking is supported within the period. + * @param isDynamic Whether the window may change when the timeline is updated. */ - public SinglePeriodTimeline(long durationUs, boolean isSeekable) { - this(durationUs, durationUs, 0, 0, isSeekable, false); + public SinglePeriodTimeline(long durationUs, boolean isSeekable, boolean isDynamic) { + this(durationUs, durationUs, 0, 0, isSeekable, isDynamic); } /** @@ -63,7 +63,7 @@ public final class SinglePeriodTimeline extends Timeline { long windowPositionInPeriodUs, long windowDefaultStartPositionUs, boolean isSeekable, boolean isDynamic) { this(C.TIME_UNSET, C.TIME_UNSET, periodDurationUs, windowDurationUs, windowPositionInPeriodUs, - windowDefaultStartPositionUs, isSeekable, isDynamic); + windowDefaultStartPositionUs, isSeekable, isDynamic); } /** @@ -106,11 +106,16 @@ public final class SinglePeriodTimeline extends Timeline { Assertions.checkIndex(windowIndex, 0, 1); Object id = setIds ? ID : null; long windowDefaultStartPositionUs = this.windowDefaultStartPositionUs; - if (isDynamic) { - windowDefaultStartPositionUs += defaultPositionProjectionUs; - if (windowDefaultStartPositionUs > windowDurationUs) { - // The projection takes us beyond the end of the live window. + if (isDynamic && defaultPositionProjectionUs != 0) { + if (windowDurationUs == C.TIME_UNSET) { + // Don't allow projection into a window that has an unknown duration. windowDefaultStartPositionUs = C.TIME_UNSET; + } else { + windowDefaultStartPositionUs += defaultPositionProjectionUs; + if (windowDefaultStartPositionUs > windowDurationUs) { + // The projection takes us beyond the end of the window. + windowDefaultStartPositionUs = C.TIME_UNSET; + } } } return window.set(id, presentationStartTimeMs, windowStartTimeMs, isSeekable, isDynamic, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java index 2aa8ccc712..51afb8eee9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java @@ -217,7 +217,7 @@ public final class SingleSampleMediaSource implements MediaSource { this.eventListener = eventListener; this.eventSourceId = eventSourceId; this.treatLoadErrorsAsEndOfStream = treatLoadErrorsAsEndOfStream; - timeline = new SinglePeriodTimeline(durationUs, true); + timeline = new SinglePeriodTimeline(durationUs, true, false); } // MediaSource implementation. diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/SinglePeriodTimelineTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/SinglePeriodTimelineTest.java new file mode 100644 index 0000000000..94ca8b03f0 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/SinglePeriodTimelineTest.java @@ -0,0 +1,77 @@ +/* + * 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; + +import static com.google.common.truth.Truth.assertThat; + +import android.util.Pair; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Timeline.Period; +import com.google.android.exoplayer2.Timeline.Window; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Unit test for {@link SinglePeriodTimeline}. + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class SinglePeriodTimelineTest { + + private Window window; + private Period period; + + @Before + public void setUp() throws Exception { + window = new Window(); + period = new Period(); + } + + @Test + public void testGetPeriodPositionDynamicWindowUnknownDuration() { + SinglePeriodTimeline timeline = new SinglePeriodTimeline(C.TIME_UNSET, false, true); + // Should return null with any positive position projection. + Pair position = timeline.getPeriodPosition(window, period, 0, C.TIME_UNSET, 1); + assertThat(position).isNull(); + // Should return (0, 0) without a position projection. + position = timeline.getPeriodPosition(window, period, 0, C.TIME_UNSET, 0); + assertThat(position.first).isEqualTo(0); + assertThat(position.second).isEqualTo(0); + } + + @Test + public void testGetPeriodPositionDynamicWindowKnownDuration() { + long windowDurationUs = 1000; + SinglePeriodTimeline timeline = new SinglePeriodTimeline(windowDurationUs, windowDurationUs, 0, + 0, false, true); + // Should return null with a positive position projection beyond window duration. + Pair position = timeline.getPeriodPosition(window, period, 0, C.TIME_UNSET, + windowDurationUs + 1); + assertThat(position).isNull(); + // Should return (0, duration) with a projection equal to window duration. + position = timeline.getPeriodPosition(window, period, 0, C.TIME_UNSET, windowDurationUs); + assertThat(position.first).isEqualTo(0); + assertThat(position.second).isEqualTo(windowDurationUs); + // Should return (0, 0) without a position projection. + position = timeline.getPeriodPosition(window, period, 0, C.TIME_UNSET, 0); + assertThat(position.first).isEqualTo(0); + assertThat(position.second).isEqualTo(0); + } + +} From 3998ed49ae0d7dd42fab9e1c1e72e17d27c5b988 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 21 Nov 2017 13:42:12 +0000 Subject: [PATCH 0743/2472] Mini cleanup --- .../google/android/exoplayer2/text/ssa/SsaDecoder.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java index 12aa1e97d5..eec4a1269c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java @@ -53,13 +53,14 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { } /** - * @param initializationData Optional initialization data for the decoder. If not null, the - * initialization data must consist of two byte arrays. The first must contain an SSA format - * line. The second must contain an SSA header that will be assumed common to all samples. + * @param initializationData Optional initialization data for the decoder. If not null or empty, + * the initialization data must consist of two byte arrays. The first must contain an SSA + * format line. The second must contain an SSA header that will be assumed common to all + * samples. */ public SsaDecoder(List initializationData) { super("SsaDecoder"); - if (initializationData != null && initializationData.size() > 0) { + if (initializationData != null && !initializationData.isEmpty()) { haveInitializationData = true; String formatLine = new String(initializationData.get(0)); Assertions.checkArgument(formatLine.startsWith(FORMAT_LINE_PREFIX)); From 494237548ada77c3dc56e98fc25dd2c479c7f7d5 Mon Sep 17 00:00:00 2001 From: Ian Bird Date: Tue, 21 Nov 2017 10:59:04 +0000 Subject: [PATCH 0744/2472] Fix initializationData check for SSA subtitles --- .../java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java index d2f5a67c27..12aa1e97d5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java @@ -59,7 +59,7 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { */ public SsaDecoder(List initializationData) { super("SsaDecoder"); - if (initializationData != null) { + if (initializationData != null && initializationData.size() > 0) { haveInitializationData = true; String formatLine = new String(initializationData.get(0)); Assertions.checkArgument(formatLine.startsWith(FORMAT_LINE_PREFIX)); From 1439b4a3ef5a31f15553db30071177c115ea12f5 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 21 Nov 2017 13:42:12 +0000 Subject: [PATCH 0745/2472] Mini cleanup --- .../google/android/exoplayer2/text/ssa/SsaDecoder.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java index 12aa1e97d5..eec4a1269c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java @@ -53,13 +53,14 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { } /** - * @param initializationData Optional initialization data for the decoder. If not null, the - * initialization data must consist of two byte arrays. The first must contain an SSA format - * line. The second must contain an SSA header that will be assumed common to all samples. + * @param initializationData Optional initialization data for the decoder. If not null or empty, + * the initialization data must consist of two byte arrays. The first must contain an SSA + * format line. The second must contain an SSA header that will be assumed common to all + * samples. */ public SsaDecoder(List initializationData) { super("SsaDecoder"); - if (initializationData != null && initializationData.size() > 0) { + if (initializationData != null && !initializationData.isEmpty()) { haveInitializationData = true; String formatLine = new String(initializationData.get(0)); Assertions.checkArgument(formatLine.startsWith(FORMAT_LINE_PREFIX)); From 60555e2c4b6d74f1276ae30b6524174ac8ee2365 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 20 Nov 2017 08:42:34 -0800 Subject: [PATCH 0746/2472] Improve robustness of ImaAdsLoader Remove an assertion that there was a call to pause content between two content -> ad transitions. Also, only use the player position for resuming an ad on reattaching if the player is currently playing an ad, in case IMA pauses content before the player actually transitions to an ad. Issue: #3430 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176365842 --- RELEASENOTES.md | 3 +++ .../com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java | 3 +-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index b645cc81a0..13218f073f 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -53,6 +53,9 @@ ([#3303](https://github.com/google/ExoPlayer/issues/3303)). * Ignore seeks if an ad is playing ([#3309](https://github.com/google/ExoPlayer/issues/3309)). + * Improve robustness of `ImaAdsLoader` in case content is not paused between + content to ad transitions + ([#3430](https://github.com/google/ExoPlayer/issues/3430)). * UI: * Allow specifying a `Drawable` for the `TimeBar` scrubber ([#3337](https://github.com/google/ExoPlayer/issues/3337)). diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index 0b11a97f84..5b61db0264 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -260,7 +260,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A @Override public void detachPlayer() { if (adsManager != null && imaPausedContent) { - adPlaybackState.setAdResumePositionUs(C.msToUs(player.getCurrentPosition())); + adPlaybackState.setAdResumePositionUs(playingAd ? C.msToUs(player.getCurrentPosition()) : 0); adsManager.pause(); } lastAdProgress = getAdProgress(); @@ -628,7 +628,6 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A if (!wasPlayingAd && playingAd) { int adGroupIndex = player.getCurrentAdGroupIndex(); // IMA hasn't sent CONTENT_PAUSE_REQUESTED yet, so fake the content position. - Assertions.checkState(fakeContentProgressElapsedRealtimeMs == C.TIME_UNSET); fakeContentProgressElapsedRealtimeMs = SystemClock.elapsedRealtime(); fakeContentProgressOffsetMs = C.usToMs(adPlaybackState.adGroupTimesUs[adGroupIndex]); if (fakeContentProgressOffsetMs == C.TIME_END_OF_SOURCE) { From d51944f4b3fe9586c542e4aec4d3d5c7d81d83a2 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 21 Nov 2017 03:44:06 -0800 Subject: [PATCH 0747/2472] Remove unnecessary dependency ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176487991 --- extensions/mediasession/build.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/extensions/mediasession/build.gradle b/extensions/mediasession/build.gradle index 85a8ac46e2..651bd952f8 100644 --- a/extensions/mediasession/build.gradle +++ b/extensions/mediasession/build.gradle @@ -27,7 +27,6 @@ android { dependencies { compile project(modulePrefix + 'library-core') compile 'com.android.support:support-media-compat:' + supportLibraryVersion - compile 'com.android.support:appcompat-v7:' + supportLibraryVersion } ext { From b688a562508e74721841aaaa770e9dc47bd378dd Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 21 Nov 2017 04:46:52 -0800 Subject: [PATCH 0748/2472] Make ExtractorMediaSource timeline dynamic until duration is set We (eventually - albeit possibly infinitely far in the future) expect a timeline update with a window of known duration. This also stops live radio stream playbacks transitioning to ended state when their tracks are disabled. As part of this fix, I found an issue where getPeriodPosition could return null even when defaultPositionProjectionUs is 0, which is not as documented. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176492024 --- .../source/ClippingMediaSourceTest.java | 10 +-- .../source/ExtractorMediaSource.java | 7 +- .../source/SinglePeriodTimeline.java | 23 +++--- .../source/SingleSampleMediaSource.java | 2 +- .../source/SinglePeriodTimelineTest.java | 77 +++++++++++++++++++ 5 files changed, 102 insertions(+), 17 deletions(-) create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/source/SinglePeriodTimelineTest.java diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java index 5e615dbc7f..0fefff86a2 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java @@ -45,7 +45,7 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase { } public void testNoClipping() { - Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true); + Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true, false); Timeline clippedTimeline = getClippedTimeline(timeline, 0, TEST_PERIOD_DURATION_US); @@ -56,7 +56,7 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase { } public void testClippingUnseekableWindowThrows() { - Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), false); + Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), false, false); // If the unseekable window isn't clipped, clipping succeeds. getClippedTimeline(timeline, 0, TEST_PERIOD_DURATION_US); @@ -70,7 +70,7 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase { } public void testClippingStart() { - Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true); + Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true, false); Timeline clippedTimeline = getClippedTimeline(timeline, TEST_CLIP_AMOUNT_US, TEST_PERIOD_DURATION_US); @@ -81,7 +81,7 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase { } public void testClippingEnd() { - Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true); + Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true, false); Timeline clippedTimeline = getClippedTimeline(timeline, 0, TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US); @@ -92,7 +92,7 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase { } public void testClippingStartAndEnd() { - Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true); + Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true, false); Timeline clippedTimeline = getClippedTimeline(timeline, TEST_CLIP_AMOUNT_US, TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US * 2); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java index 1b3f6cb95c..17f2fc2dad 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java @@ -204,8 +204,11 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe private void notifySourceInfoRefreshed(long durationUs, boolean isSeekable) { timelineDurationUs = durationUs; timelineIsSeekable = isSeekable; - sourceListener.onSourceInfoRefreshed( - this, new SinglePeriodTimeline(timelineDurationUs, timelineIsSeekable), null); + // If the duration is currently unset, we expect to be able to update the window when its + // duration eventually becomes known. + boolean isDynamic = timelineDurationUs == C.TIME_UNSET; + sourceListener.onSourceInfoRefreshed(this, + new SinglePeriodTimeline(timelineDurationUs, timelineIsSeekable, isDynamic), null); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java index 6f35438444..9cce67f68c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java @@ -36,14 +36,14 @@ public final class SinglePeriodTimeline extends Timeline { private final boolean isDynamic; /** - * Creates a timeline of one period of known duration, and a static window starting at zero and - * extending to that duration. + * Creates a timeline containing a single period and a window that spans it. * * @param durationUs The duration of the period, in microseconds. * @param isSeekable Whether seeking is supported within the period. + * @param isDynamic Whether the window may change when the timeline is updated. */ - public SinglePeriodTimeline(long durationUs, boolean isSeekable) { - this(durationUs, durationUs, 0, 0, isSeekable, false); + public SinglePeriodTimeline(long durationUs, boolean isSeekable, boolean isDynamic) { + this(durationUs, durationUs, 0, 0, isSeekable, isDynamic); } /** @@ -63,7 +63,7 @@ public final class SinglePeriodTimeline extends Timeline { long windowPositionInPeriodUs, long windowDefaultStartPositionUs, boolean isSeekable, boolean isDynamic) { this(C.TIME_UNSET, C.TIME_UNSET, periodDurationUs, windowDurationUs, windowPositionInPeriodUs, - windowDefaultStartPositionUs, isSeekable, isDynamic); + windowDefaultStartPositionUs, isSeekable, isDynamic); } /** @@ -106,11 +106,16 @@ public final class SinglePeriodTimeline extends Timeline { Assertions.checkIndex(windowIndex, 0, 1); Object id = setIds ? ID : null; long windowDefaultStartPositionUs = this.windowDefaultStartPositionUs; - if (isDynamic) { - windowDefaultStartPositionUs += defaultPositionProjectionUs; - if (windowDefaultStartPositionUs > windowDurationUs) { - // The projection takes us beyond the end of the live window. + if (isDynamic && defaultPositionProjectionUs != 0) { + if (windowDurationUs == C.TIME_UNSET) { + // Don't allow projection into a window that has an unknown duration. windowDefaultStartPositionUs = C.TIME_UNSET; + } else { + windowDefaultStartPositionUs += defaultPositionProjectionUs; + if (windowDefaultStartPositionUs > windowDurationUs) { + // The projection takes us beyond the end of the window. + windowDefaultStartPositionUs = C.TIME_UNSET; + } } } return window.set(id, presentationStartTimeMs, windowStartTimeMs, isSeekable, isDynamic, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java index dd901958fd..ede2c7ccf9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java @@ -110,7 +110,7 @@ public final class SingleSampleMediaSource implements MediaSource { this.eventListener = eventListener; this.eventSourceId = eventSourceId; this.treatLoadErrorsAsEndOfStream = treatLoadErrorsAsEndOfStream; - timeline = new SinglePeriodTimeline(durationUs, true); + timeline = new SinglePeriodTimeline(durationUs, true, false); } // MediaSource implementation. diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/SinglePeriodTimelineTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/SinglePeriodTimelineTest.java new file mode 100644 index 0000000000..94ca8b03f0 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/SinglePeriodTimelineTest.java @@ -0,0 +1,77 @@ +/* + * 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; + +import static com.google.common.truth.Truth.assertThat; + +import android.util.Pair; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Timeline.Period; +import com.google.android.exoplayer2.Timeline.Window; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** + * Unit test for {@link SinglePeriodTimeline}. + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class SinglePeriodTimelineTest { + + private Window window; + private Period period; + + @Before + public void setUp() throws Exception { + window = new Window(); + period = new Period(); + } + + @Test + public void testGetPeriodPositionDynamicWindowUnknownDuration() { + SinglePeriodTimeline timeline = new SinglePeriodTimeline(C.TIME_UNSET, false, true); + // Should return null with any positive position projection. + Pair position = timeline.getPeriodPosition(window, period, 0, C.TIME_UNSET, 1); + assertThat(position).isNull(); + // Should return (0, 0) without a position projection. + position = timeline.getPeriodPosition(window, period, 0, C.TIME_UNSET, 0); + assertThat(position.first).isEqualTo(0); + assertThat(position.second).isEqualTo(0); + } + + @Test + public void testGetPeriodPositionDynamicWindowKnownDuration() { + long windowDurationUs = 1000; + SinglePeriodTimeline timeline = new SinglePeriodTimeline(windowDurationUs, windowDurationUs, 0, + 0, false, true); + // Should return null with a positive position projection beyond window duration. + Pair position = timeline.getPeriodPosition(window, period, 0, C.TIME_UNSET, + windowDurationUs + 1); + assertThat(position).isNull(); + // Should return (0, duration) with a projection equal to window duration. + position = timeline.getPeriodPosition(window, period, 0, C.TIME_UNSET, windowDurationUs); + assertThat(position.first).isEqualTo(0); + assertThat(position.second).isEqualTo(windowDurationUs); + // Should return (0, 0) without a position projection. + position = timeline.getPeriodPosition(window, period, 0, C.TIME_UNSET, 0); + assertThat(position.first).isEqualTo(0); + assertThat(position.second).isEqualTo(0); + } + +} From fa3052d36b78455c80ce7b23e6526edc092b0561 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 20 Nov 2017 08:56:23 -0800 Subject: [PATCH 0749/2472] Report additional position discontinuities - Properly report internal discontinuities - Add DISCONTINUITY_REASON_SEEK_ADJUSTMENT to distinguish seek adjustments from other internal discontinuity events ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176367365 --- RELEASENOTES.md | 4 ++++ .../android/exoplayer2/demo/EventLogger.java | 2 ++ .../google/android/exoplayer2/ExoPlayerImpl.java | 15 ++++++++------- .../android/exoplayer2/ExoPlayerImplInternal.java | 12 ++++++++++-- .../com/google/android/exoplayer2/Player.java | 9 +++++++-- 5 files changed, 31 insertions(+), 11 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 13218f073f..579c2a92ac 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -11,6 +11,10 @@ * SimpleExoPlayer: Support for multiple video, text and metadata outputs. * Support for `Renderer`s that don't consume any media ([#3212](https://github.com/google/ExoPlayer/issues/3212)). +* Fix reporting of internal position discontinuities via + `Player.onPositionDiscontinuity`. `DISCONTINUITY_REASON_SEEK_ADJUSTMENT` is + added to disambiguate position adjustments during seeks from other types of + internal position discontinuity. * Fix potential `IndexOutOfBoundsException` when calling `ExoPlayer.getDuration` ([#3362](https://github.com/google/ExoPlayer/issues/3362)). * Fix playbacks involving looping, concatenation and ads getting stuck when diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java index 68f7ddfd21..27a5c68e28 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java @@ -496,6 +496,8 @@ import java.util.Locale; return "PERIOD_TRANSITION"; case Player.DISCONTINUITY_REASON_SEEK: return "SEEK"; + case Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT: + return "SEEK_ADJUSTMENT"; case Player.DISCONTINUITY_REASON_INTERNAL: return "INTERNAL"; default: diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index 8ee8af5980..349751eb59 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -473,7 +473,8 @@ import java.util.concurrent.CopyOnWriteArraySet; case ExoPlayerImplInternal.MSG_SOURCE_INFO_REFRESHED: { int prepareAcks = msg.arg1; int seekAcks = msg.arg2; - handlePlaybackInfo((PlaybackInfo) msg.obj, prepareAcks, seekAcks, false); + handlePlaybackInfo((PlaybackInfo) msg.obj, prepareAcks, seekAcks, false, + /* ignored */ DISCONTINUITY_REASON_INTERNAL); break; } case ExoPlayerImplInternal.MSG_TRACKS_CHANGED: { @@ -491,11 +492,13 @@ import java.util.concurrent.CopyOnWriteArraySet; } case ExoPlayerImplInternal.MSG_SEEK_ACK: { boolean seekPositionAdjusted = msg.arg1 != 0; - handlePlaybackInfo((PlaybackInfo) msg.obj, 0, 1, seekPositionAdjusted); + handlePlaybackInfo((PlaybackInfo) msg.obj, 0, 1, seekPositionAdjusted, + DISCONTINUITY_REASON_SEEK_ADJUSTMENT); break; } case ExoPlayerImplInternal.MSG_POSITION_DISCONTINUITY: { - handlePlaybackInfo((PlaybackInfo) msg.obj, 0, 0, true); + @DiscontinuityReason int discontinuityReason = msg.arg1; + handlePlaybackInfo((PlaybackInfo) msg.obj, 0, 0, true, discontinuityReason); break; } case ExoPlayerImplInternal.MSG_PLAYBACK_PARAMETERS_CHANGED: { @@ -521,7 +524,7 @@ import java.util.concurrent.CopyOnWriteArraySet; } private void handlePlaybackInfo(PlaybackInfo playbackInfo, int prepareAcks, int seekAcks, - boolean positionDiscontinuity) { + boolean positionDiscontinuity, @DiscontinuityReason int positionDiscontinuityReason) { Assertions.checkNotNull(playbackInfo.timeline); pendingPrepareAcks -= prepareAcks; pendingSeekAcks -= seekAcks; @@ -542,9 +545,7 @@ import java.util.concurrent.CopyOnWriteArraySet; } if (positionDiscontinuity) { for (Player.EventListener listener : listeners) { - listener.onPositionDiscontinuity( - seekAcks > 0 ? DISCONTINUITY_REASON_INTERNAL : DISCONTINUITY_REASON_PERIOD_TRANSITION - ); + listener.onPositionDiscontinuity(positionDiscontinuityReason); } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index f00a5ce02d..56e16343ed 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -503,6 +503,10 @@ import java.io.IOException; long periodPositionUs = playingPeriodHolder.mediaPeriod.readDiscontinuity(); if (periodPositionUs != C.TIME_UNSET) { resetRendererPosition(periodPositionUs); + playbackInfo = playbackInfo.fromNewPosition(playbackInfo.periodId, periodPositionUs, + playbackInfo.contentPositionUs); + eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, Player.DISCONTINUITY_REASON_INTERNAL, + 0, playbackInfo).sendToTarget(); } else { // Use the standalone clock if there's no renderer clock, or if the providing renderer has // ended or needs the next sample stream to reenter the ready state. The latter case uses the @@ -893,7 +897,10 @@ import java.io.IOException; long periodPositionUs = playingPeriodHolder.updatePeriodTrackSelection( playbackInfo.positionUs, recreateStreams, streamResetFlags); if (periodPositionUs != playbackInfo.positionUs) { - playbackInfo.positionUs = periodPositionUs; + playbackInfo = playbackInfo.fromNewPosition(playbackInfo.periodId, periodPositionUs, + playbackInfo.contentPositionUs); + eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, Player.DISCONTINUITY_REASON_INTERNAL, + 0, playbackInfo).sendToTarget(); resetRendererPosition(periodPositionUs); } @@ -1280,7 +1287,8 @@ import java.io.IOException; playbackInfo = playbackInfo.fromNewPosition(playingPeriodHolder.info.id, playingPeriodHolder.info.startPositionUs, playingPeriodHolder.info.contentPositionUs); updatePlaybackPositions(); - eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, playbackInfo).sendToTarget(); + eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, + Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, 0, playbackInfo).sendToTarget(); } if (readingPeriodHolder.info.isFinal) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Player.java b/library/core/src/main/java/com/google/android/exoplayer2/Player.java index af653ec2bd..dc703f924a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Player.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Player.java @@ -243,7 +243,7 @@ public interface Player { */ @Retention(RetentionPolicy.SOURCE) @IntDef({DISCONTINUITY_REASON_PERIOD_TRANSITION, DISCONTINUITY_REASON_SEEK, - DISCONTINUITY_REASON_INTERNAL}) + DISCONTINUITY_REASON_SEEK_ADJUSTMENT, DISCONTINUITY_REASON_INTERNAL}) public @interface DiscontinuityReason {} /** * Automatic playback transition from one period in the timeline to the next. The period index may @@ -254,10 +254,15 @@ public interface Player { * Seek within the current period or to another period. */ int DISCONTINUITY_REASON_SEEK = 1; + /** + * Seek adjustment due to being unable to seek to the requested position or because the seek was + * permitted to be inexact. + */ + int DISCONTINUITY_REASON_SEEK_ADJUSTMENT = 2; /** * Discontinuity introduced internally by the source. */ - int DISCONTINUITY_REASON_INTERNAL = 2; + int DISCONTINUITY_REASON_INTERNAL = 3; /** * Register a listener to receive events from the player. The listener's methods will be called on From 74569bba450cc91b21771361f5e323b12ed1227b Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 20 Nov 2017 09:35:01 -0800 Subject: [PATCH 0750/2472] Don't do work after track selection when in ended state This causes the player to report that it's started loading when in the ended state. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176371892 --- .../android/exoplayer2/ExoPlayerImplInternal.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 56e16343ed..998779858b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -896,7 +896,7 @@ import java.io.IOException; boolean[] streamResetFlags = new boolean[renderers.length]; long periodPositionUs = playingPeriodHolder.updatePeriodTrackSelection( playbackInfo.positionUs, recreateStreams, streamResetFlags); - if (periodPositionUs != playbackInfo.positionUs) { + if (state != Player.STATE_ENDED && periodPositionUs != playbackInfo.positionUs) { playbackInfo = playbackInfo.fromNewPosition(playbackInfo.periodId, periodPositionUs, playbackInfo.contentPositionUs); eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, Player.DISCONTINUITY_REASON_INTERNAL, @@ -941,9 +941,11 @@ import java.io.IOException; loadingPeriodHolder.updatePeriodTrackSelection(loadingPeriodPositionUs, false); } } - maybeContinueLoading(); - updatePlaybackPositions(); - handler.sendEmptyMessage(MSG_DO_SOME_WORK); + if (state != Player.STATE_ENDED) { + maybeContinueLoading(); + updatePlaybackPositions(); + handler.sendEmptyMessage(MSG_DO_SOME_WORK); + } } private boolean isTimelineReady(long playingPeriodDurationUs) { From 56c1c3f6a724b62ccb624148e2c892a4ef9519bf Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 22 Nov 2017 17:59:36 +0000 Subject: [PATCH 0751/2472] Revert "Make ExtractorMediaSource timeline dynamic until duration is set" This reverts commit b688a562508e74721841aaaa770e9dc47bd378dd. --- .../source/ClippingMediaSourceTest.java | 10 +-- .../source/ExtractorMediaSource.java | 7 +- .../source/SinglePeriodTimeline.java | 23 +++--- .../source/SingleSampleMediaSource.java | 2 +- .../source/SinglePeriodTimelineTest.java | 77 ------------------- 5 files changed, 17 insertions(+), 102 deletions(-) delete mode 100644 library/core/src/test/java/com/google/android/exoplayer2/source/SinglePeriodTimelineTest.java diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java index 0fefff86a2..5e615dbc7f 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java @@ -45,7 +45,7 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase { } public void testNoClipping() { - Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true, false); + Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true); Timeline clippedTimeline = getClippedTimeline(timeline, 0, TEST_PERIOD_DURATION_US); @@ -56,7 +56,7 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase { } public void testClippingUnseekableWindowThrows() { - Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), false, false); + Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), false); // If the unseekable window isn't clipped, clipping succeeds. getClippedTimeline(timeline, 0, TEST_PERIOD_DURATION_US); @@ -70,7 +70,7 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase { } public void testClippingStart() { - Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true, false); + Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true); Timeline clippedTimeline = getClippedTimeline(timeline, TEST_CLIP_AMOUNT_US, TEST_PERIOD_DURATION_US); @@ -81,7 +81,7 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase { } public void testClippingEnd() { - Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true, false); + Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true); Timeline clippedTimeline = getClippedTimeline(timeline, 0, TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US); @@ -92,7 +92,7 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase { } public void testClippingStartAndEnd() { - Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true, false); + Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true); Timeline clippedTimeline = getClippedTimeline(timeline, TEST_CLIP_AMOUNT_US, TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US * 2); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java index 17f2fc2dad..1b3f6cb95c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java @@ -204,11 +204,8 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe private void notifySourceInfoRefreshed(long durationUs, boolean isSeekable) { timelineDurationUs = durationUs; timelineIsSeekable = isSeekable; - // If the duration is currently unset, we expect to be able to update the window when its - // duration eventually becomes known. - boolean isDynamic = timelineDurationUs == C.TIME_UNSET; - sourceListener.onSourceInfoRefreshed(this, - new SinglePeriodTimeline(timelineDurationUs, timelineIsSeekable, isDynamic), null); + sourceListener.onSourceInfoRefreshed( + this, new SinglePeriodTimeline(timelineDurationUs, timelineIsSeekable), null); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java index 9cce67f68c..6f35438444 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java @@ -36,14 +36,14 @@ public final class SinglePeriodTimeline extends Timeline { private final boolean isDynamic; /** - * Creates a timeline containing a single period and a window that spans it. + * Creates a timeline of one period of known duration, and a static window starting at zero and + * extending to that duration. * * @param durationUs The duration of the period, in microseconds. * @param isSeekable Whether seeking is supported within the period. - * @param isDynamic Whether the window may change when the timeline is updated. */ - public SinglePeriodTimeline(long durationUs, boolean isSeekable, boolean isDynamic) { - this(durationUs, durationUs, 0, 0, isSeekable, isDynamic); + public SinglePeriodTimeline(long durationUs, boolean isSeekable) { + this(durationUs, durationUs, 0, 0, isSeekable, false); } /** @@ -63,7 +63,7 @@ public final class SinglePeriodTimeline extends Timeline { long windowPositionInPeriodUs, long windowDefaultStartPositionUs, boolean isSeekable, boolean isDynamic) { this(C.TIME_UNSET, C.TIME_UNSET, periodDurationUs, windowDurationUs, windowPositionInPeriodUs, - windowDefaultStartPositionUs, isSeekable, isDynamic); + windowDefaultStartPositionUs, isSeekable, isDynamic); } /** @@ -106,16 +106,11 @@ public final class SinglePeriodTimeline extends Timeline { Assertions.checkIndex(windowIndex, 0, 1); Object id = setIds ? ID : null; long windowDefaultStartPositionUs = this.windowDefaultStartPositionUs; - if (isDynamic && defaultPositionProjectionUs != 0) { - if (windowDurationUs == C.TIME_UNSET) { - // Don't allow projection into a window that has an unknown duration. + if (isDynamic) { + windowDefaultStartPositionUs += defaultPositionProjectionUs; + if (windowDefaultStartPositionUs > windowDurationUs) { + // The projection takes us beyond the end of the live window. windowDefaultStartPositionUs = C.TIME_UNSET; - } else { - windowDefaultStartPositionUs += defaultPositionProjectionUs; - if (windowDefaultStartPositionUs > windowDurationUs) { - // The projection takes us beyond the end of the window. - windowDefaultStartPositionUs = C.TIME_UNSET; - } } } return window.set(id, presentationStartTimeMs, windowStartTimeMs, isSeekable, isDynamic, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java index ede2c7ccf9..dd901958fd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java @@ -110,7 +110,7 @@ public final class SingleSampleMediaSource implements MediaSource { this.eventListener = eventListener; this.eventSourceId = eventSourceId; this.treatLoadErrorsAsEndOfStream = treatLoadErrorsAsEndOfStream; - timeline = new SinglePeriodTimeline(durationUs, true, false); + timeline = new SinglePeriodTimeline(durationUs, true); } // MediaSource implementation. diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/SinglePeriodTimelineTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/SinglePeriodTimelineTest.java deleted file mode 100644 index 94ca8b03f0..0000000000 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/SinglePeriodTimelineTest.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * 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; - -import static com.google.common.truth.Truth.assertThat; - -import android.util.Pair; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Timeline.Period; -import com.google.android.exoplayer2.Timeline.Window; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; - -/** - * Unit test for {@link SinglePeriodTimeline}. - */ -@RunWith(RobolectricTestRunner.class) -@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) -public final class SinglePeriodTimelineTest { - - private Window window; - private Period period; - - @Before - public void setUp() throws Exception { - window = new Window(); - period = new Period(); - } - - @Test - public void testGetPeriodPositionDynamicWindowUnknownDuration() { - SinglePeriodTimeline timeline = new SinglePeriodTimeline(C.TIME_UNSET, false, true); - // Should return null with any positive position projection. - Pair position = timeline.getPeriodPosition(window, period, 0, C.TIME_UNSET, 1); - assertThat(position).isNull(); - // Should return (0, 0) without a position projection. - position = timeline.getPeriodPosition(window, period, 0, C.TIME_UNSET, 0); - assertThat(position.first).isEqualTo(0); - assertThat(position.second).isEqualTo(0); - } - - @Test - public void testGetPeriodPositionDynamicWindowKnownDuration() { - long windowDurationUs = 1000; - SinglePeriodTimeline timeline = new SinglePeriodTimeline(windowDurationUs, windowDurationUs, 0, - 0, false, true); - // Should return null with a positive position projection beyond window duration. - Pair position = timeline.getPeriodPosition(window, period, 0, C.TIME_UNSET, - windowDurationUs + 1); - assertThat(position).isNull(); - // Should return (0, duration) with a projection equal to window duration. - position = timeline.getPeriodPosition(window, period, 0, C.TIME_UNSET, windowDurationUs); - assertThat(position.first).isEqualTo(0); - assertThat(position.second).isEqualTo(windowDurationUs); - // Should return (0, 0) without a position projection. - position = timeline.getPeriodPosition(window, period, 0, C.TIME_UNSET, 0); - assertThat(position.first).isEqualTo(0); - assertThat(position.second).isEqualTo(0); - } - -} From 75b90625839a548124d2f90788b790b3bcd21579 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 22 Nov 2017 18:06:14 +0000 Subject: [PATCH 0752/2472] Send discontinuity at adjustments after shuffle/repeat mode changes. --- .../google/android/exoplayer2/ExoPlayerImplInternal.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 998779858b..4d1767b64c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -474,8 +474,12 @@ import java.io.IOException; // position of the playing period to make sure none of the removed period is played. MediaPeriodId periodId = playingPeriodHolder.info.id; long newPositionUs = seekToPeriodPosition(periodId, playbackInfo.positionUs); - playbackInfo = playbackInfo.fromNewPosition(periodId, newPositionUs, - playbackInfo.contentPositionUs); + if (newPositionUs != playbackInfo.positionUs) { + playbackInfo = playbackInfo.fromNewPosition(periodId, newPositionUs, + playbackInfo.contentPositionUs); + eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, Player.DISCONTINUITY_REASON_INTERNAL, + 0, playbackInfo).sendToTarget(); + } } } From e619079a0d7ef598d3b30c2a34b3dd86f1eac844 Mon Sep 17 00:00:00 2001 From: hoangtc Date: Tue, 21 Nov 2017 07:03:55 -0800 Subject: [PATCH 0753/2472] Let EventMessage encloses its presentationTimeMs. Currently EventMessage's presentationTimeMs is kept separately in EventSampleStream. However, EventMessage's presentationTimeMs maybe used in other places besides EventSampleStream, such as when handling `emsg' messages targeting the player. This CL let EventMessage object to holds its presentationTimeMs for such use cases. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176502938 --- .../metadata/emsg/EventMessage.java | 21 ++++++++++---- .../metadata/emsg/EventMessageDecoder.java | 9 ++++-- .../metadata/emsg/EventMessageEncoder.java | 7 ++--- .../emsg/EventMessageDecoderTest.java | 3 +- .../emsg/EventMessageEncoderTest.java | 22 +++++++------- .../metadata/emsg/EventMessageTest.java | 2 +- .../dash/manifest/DashManifestParserTest.java | 6 ++-- .../source/dash/EventSampleStream.java | 2 +- .../dash/manifest/DashManifestParser.java | 29 +++++++++---------- 9 files changed, 57 insertions(+), 44 deletions(-) diff --git a/library/core/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 index fbe3184c0d..57e7f0bfd6 100644 --- a/library/core/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 @@ -41,6 +41,13 @@ public final class EventMessage implements Metadata.Entry { */ public final long durationMs; + /** + * The presentation time value of this event message in microseconds. + *

          + * Except in special cases, application code should not use this field. + */ + public final long presentationTimeUs; + /** * The instance identifier. */ @@ -55,25 +62,27 @@ public final class EventMessage implements Metadata.Entry { private int hashCode; /** - * * @param schemeIdUri The message scheme. * @param value The value for the event. * @param durationMs The duration of the event in milliseconds. * @param id The instance identifier. * @param messageData The body of the message. + * @param presentationTimeUs The presentation time value of this event message in microseconds. */ public EventMessage(String schemeIdUri, String value, long durationMs, long id, - byte[] messageData) { + byte[] messageData, long presentationTimeUs) { this.schemeIdUri = schemeIdUri; this.value = value; this.durationMs = durationMs; this.id = id; this.messageData = messageData; + this.presentationTimeUs = presentationTimeUs; } /* package */ EventMessage(Parcel in) { schemeIdUri = in.readString(); value = in.readString(); + presentationTimeUs = in.readLong(); durationMs = in.readLong(); id = in.readLong(); messageData = in.createByteArray(); @@ -85,6 +94,7 @@ public final class EventMessage implements Metadata.Entry { int result = 17; result = 31 * result + (schemeIdUri != null ? schemeIdUri.hashCode() : 0); result = 31 * result + (value != null ? value.hashCode() : 0); + result = 31 * result + (int) (presentationTimeUs ^ (presentationTimeUs >>> 32)); result = 31 * result + (int) (durationMs ^ (durationMs >>> 32)); result = 31 * result + (int) (id ^ (id >>> 32)); result = 31 * result + Arrays.hashCode(messageData); @@ -102,9 +112,9 @@ public final class EventMessage implements Metadata.Entry { return false; } EventMessage other = (EventMessage) obj; - return durationMs == other.durationMs && id == other.id - && Util.areEqual(schemeIdUri, other.schemeIdUri) && Util.areEqual(value, other.value) - && Arrays.equals(messageData, other.messageData); + return presentationTimeUs == other.presentationTimeUs && durationMs == other.durationMs + && id == other.id && Util.areEqual(schemeIdUri, other.schemeIdUri) + && Util.areEqual(value, other.value) && Arrays.equals(messageData, other.messageData); } // Parcelable implementation. @@ -118,6 +128,7 @@ public final class EventMessage implements Metadata.Entry { public void writeToParcel(Parcel dest, int flags) { dest.writeString(schemeIdUri); dest.writeString(value); + dest.writeLong(presentationTimeUs); dest.writeLong(durationMs); dest.writeLong(id); dest.writeByteArray(messageData); diff --git a/library/core/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 index 266988246d..7e5125e71c 100644 --- a/library/core/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 @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.metadata.emsg; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataDecoder; import com.google.android.exoplayer2.metadata.MetadataInputBuffer; @@ -24,7 +25,7 @@ import java.nio.ByteBuffer; import java.util.Arrays; /** - * Decodes Event Message (emsg) atoms, as defined in ISO 23009-1. + * Decodes Event Message (emsg) atoms, as defined in ISO/IEC 23009-1:2014, Section 5.10.3.3. *

          * Atom data should be provided to the decoder without the full atom header (i.e. starting from the * first byte of the scheme_id_uri field). @@ -40,11 +41,13 @@ public final class EventMessageDecoder implements MetadataDecoder { String schemeIdUri = emsgData.readNullTerminatedString(); String value = emsgData.readNullTerminatedString(); long timescale = emsgData.readUnsignedInt(); - emsgData.skipBytes(4); // presentation_time_delta + long presentationTimeUs = Util.scaleLargeTimestamp(emsgData.readUnsignedInt(), + C.MICROS_PER_SECOND, timescale); long durationMs = Util.scaleLargeTimestamp(emsgData.readUnsignedInt(), 1000, timescale); long id = emsgData.readUnsignedInt(); byte[] messageData = Arrays.copyOfRange(data, emsgData.getPosition(), size); - return new Metadata(new EventMessage(schemeIdUri, value, durationMs, id, messageData)); + return new Metadata(new EventMessage(schemeIdUri, value, durationMs, id, messageData, + presentationTimeUs)); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoder.java b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoder.java index 2bd54367e1..eca498a6df 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoder.java @@ -42,11 +42,10 @@ public final class EventMessageEncoder { * * @param eventMessage The event message to be encoded. * @param timescale Timescale of the event message, in units per second. - * @param presentationTimeUs The presentation time of the event message in microseconds. * @return The serialized byte array. */ @Nullable - public byte[] encode(EventMessage eventMessage, long timescale, long presentationTimeUs) { + public byte[] encode(EventMessage eventMessage, long timescale) { Assertions.checkArgument(timescale >= 0); byteArrayOutputStream.reset(); try { @@ -54,8 +53,8 @@ public final class EventMessageEncoder { String nonNullValue = eventMessage.value != null ? eventMessage.value : ""; writeNullTerminatedString(dataOutputStream, nonNullValue); writeUnsignedInt(dataOutputStream, timescale); - long presentationTime = Util.scaleLargeTimestamp(presentationTimeUs, timescale, - C.MICROS_PER_SECOND); + long presentationTime = Util.scaleLargeTimestamp(eventMessage.presentationTimeUs, + timescale, C.MICROS_PER_SECOND); writeUnsignedInt(dataOutputStream, presentationTime); long duration = Util.scaleLargeTimestamp(eventMessage.durationMs, timescale, 1000); writeUnsignedInt(dataOutputStream, duration); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoderTest.java index 1ce0ccb93d..3a6e96b3e8 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageDecoderTest.java @@ -38,7 +38,7 @@ public final class EventMessageDecoderTest { 117, 114, 110, 58, 116, 101, 115, 116, 0, // scheme_id_uri = "urn:test" 49, 50, 51, 0, // value = "123" 0, 0, -69, -128, // timescale = 48000 - 0, 0, 0, 0, // presentation_time_delta (ignored) = 0 + 0, 0, -69, -128, // presentation_time_delta = 48000 0, 2, 50, -128, // event_duration = 144000 0, 15, 67, -45, // id = 1000403 0, 1, 2, 3, 4}; // message_data = {0, 1, 2, 3, 4} @@ -53,6 +53,7 @@ public final class EventMessageDecoderTest { assertThat(eventMessage.durationMs).isEqualTo(3000); assertThat(eventMessage.id).isEqualTo(1000403); assertThat(eventMessage.messageData).isEqualTo(new byte[]{0, 1, 2, 3, 4}); + assertThat(eventMessage.presentationTimeUs).isEqualTo(1000000); } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoderTest.java index f526fc3451..f0a6d3e19b 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageEncoderTest.java @@ -36,24 +36,24 @@ public final class EventMessageEncoderTest { @Test public void testEncodeEventStream() throws IOException { EventMessage eventMessage = new EventMessage("urn:test", "123", 3000, 1000403, - new byte[] {0, 1, 2, 3, 4}); + new byte[] {0, 1, 2, 3, 4}, 1000000); byte[] expectedEmsgBody = new byte[] { 117, 114, 110, 58, 116, 101, 115, 116, 0, // scheme_id_uri = "urn:test" 49, 50, 51, 0, // value = "123" 0, 0, -69, -128, // timescale = 48000 - 0, 0, -69, -128, // presentation_time_delta = 48 + 0, 0, -69, -128, // presentation_time_delta = 48000 0, 2, 50, -128, // event_duration = 144000 0, 15, 67, -45, // id = 1000403 0, 1, 2, 3, 4}; // message_data = {0, 1, 2, 3, 4} - byte[] encodedByteArray = new EventMessageEncoder().encode(eventMessage, 48000, 1000000); + byte[] encodedByteArray = new EventMessageEncoder().encode(eventMessage, 48000); assertThat(encodedByteArray).isEqualTo(expectedEmsgBody); } @Test public void testEncodeDecodeEventStream() throws IOException { EventMessage expectedEmsg = new EventMessage("urn:test", "123", 3000, 1000403, - new byte[] {0, 1, 2, 3, 4}); - byte[] encodedByteArray = new EventMessageEncoder().encode(expectedEmsg, 48000, 1); + new byte[] {0, 1, 2, 3, 4}, 1000000); + byte[] encodedByteArray = new EventMessageEncoder().encode(expectedEmsg, 48000); MetadataInputBuffer buffer = new MetadataInputBuffer(); buffer.data = ByteBuffer.allocate(encodedByteArray.length).put(encodedByteArray); @@ -66,29 +66,29 @@ public final class EventMessageEncoderTest { @Test public void testEncodeEventStreamMultipleTimesWorkingCorrectly() throws IOException { EventMessage eventMessage = new EventMessage("urn:test", "123", 3000, 1000403, - new byte[] {0, 1, 2, 3, 4}); + new byte[] {0, 1, 2, 3, 4}, 1000000); byte[] expectedEmsgBody = new byte[] { 117, 114, 110, 58, 116, 101, 115, 116, 0, // scheme_id_uri = "urn:test" 49, 50, 51, 0, // value = "123" 0, 0, -69, -128, // timescale = 48000 - 0, 0, -69, -128, // presentation_time_delta = 48 + 0, 0, -69, -128, // presentation_time_delta = 48000 0, 2, 50, -128, // event_duration = 144000 0, 15, 67, -45, // id = 1000403 0, 1, 2, 3, 4}; // message_data = {0, 1, 2, 3, 4} EventMessage eventMessage1 = new EventMessage("urn:test", "123", 3000, 1000402, - new byte[] {4, 3, 2, 1, 0}); + new byte[] {4, 3, 2, 1, 0}, 1000000); byte[] expectedEmsgBody1 = new byte[] { 117, 114, 110, 58, 116, 101, 115, 116, 0, // scheme_id_uri = "urn:test" 49, 50, 51, 0, // value = "123" 0, 0, -69, -128, // timescale = 48000 - 0, 0, -69, -128, // presentation_time_delta = 48 + 0, 0, -69, -128, // presentation_time_delta = 48000 0, 2, 50, -128, // event_duration = 144000 0, 15, 67, -46, // id = 1000402 4, 3, 2, 1, 0}; // message_data = {4, 3, 2, 1, 0} EventMessageEncoder eventMessageEncoder = new EventMessageEncoder(); - byte[] encodedByteArray = eventMessageEncoder.encode(eventMessage, 48000, 1000000); + byte[] encodedByteArray = eventMessageEncoder.encode(eventMessage, 48000); assertThat(encodedByteArray).isEqualTo(expectedEmsgBody); - byte[] encodedByteArray1 = eventMessageEncoder.encode(eventMessage1, 48000, 1000000); + byte[] encodedByteArray1 = eventMessageEncoder.encode(eventMessage1, 48000); assertThat(encodedByteArray1).isEqualTo(expectedEmsgBody1); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageTest.java b/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageTest.java index b48a071d0d..58f2b9f55d 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/metadata/emsg/EventMessageTest.java @@ -33,7 +33,7 @@ public final class EventMessageTest { @Test public void testEventMessageParcelable() { EventMessage eventMessage = new EventMessage("urn:test", "123", 3000, 1000403, - new byte[] {0, 1, 2, 3, 4}); + new byte[] {0, 1, 2, 3, 4}, 1000); // Write to parcel. Parcel parcel = Parcel.obtain(); eventMessage.writeToParcel(parcel, 0); diff --git a/library/dash/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 index 37dc6a748e..5c54a7884b 100644 --- a/library/dash/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 @@ -84,7 +84,7 @@ public class DashManifestParserTest extends InstrumentationTestCase { EventStream eventStream1 = period.eventStreams.get(0); assertEquals(1, eventStream1.events.length); EventMessage expectedEvent1 = new EventMessage("urn:uuid:XYZY", "call", 10000, 0, - "+ 1 800 10101010".getBytes()); + "+ 1 800 10101010".getBytes(), 0); assertEquals(expectedEvent1, eventStream1.events[0]); // assert CData-structured event stream @@ -102,7 +102,7 @@ public class DashManifestParserTest extends InstrumentationTestCase { + " GB\n" + " \n" + " \n" - + " ]]>").getBytes()), + + " ]]>").getBytes(), 300000000), eventStream2.events[0]); // assert xml-structured event stream @@ -114,7 +114,7 @@ public class DashManifestParserTest extends InstrumentationTestCase { + " \n" + " /DAIAAAAAAAAAAAQAAZ/I0VniQAQAgBDVUVJQAAAAH+cAAAAAA==\n" + " \n" - + " ").getBytes()), + + " ").getBytes(), 1000000000), eventStream3.events[0]); } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/EventSampleStream.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/EventSampleStream.java index 549bfdef7b..694f9f843e 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/EventSampleStream.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/EventSampleStream.java @@ -95,7 +95,7 @@ import java.io.IOException; } int sampleIndex = currentIndex++; byte[] serializedEvent = eventMessageEncoder.encode(eventStream.events[sampleIndex], - eventStream.timescale, eventTimesUs[sampleIndex]); + eventStream.timescale); if (serializedEvent != null) { buffer.ensureSpaceForWrite(serializedEvent.length); buffer.setFlags(C.BUFFER_FLAG_KEY_FRAME); diff --git a/library/dash/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 index d3906acdf6..07f9660755 100644 --- a/library/dash/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 @@ -688,23 +688,23 @@ public class DashManifestParser extends DefaultHandler String schemeIdUri = parseString(xpp, "schemeIdUri", ""); String value = parseString(xpp, "value", ""); long timescale = parseLong(xpp, "timescale", 1); - List> timedEvents = new ArrayList<>(); + List eventMessages = new ArrayList<>(); ByteArrayOutputStream scratchOutputStream = new ByteArrayOutputStream(512); do { xpp.next(); if (XmlPullParserUtil.isStartTag(xpp, "Event")) { - Pair timedEvent = parseEvent(xpp, schemeIdUri, value, timescale, + EventMessage event = parseEvent(xpp, schemeIdUri, value, timescale, scratchOutputStream); - timedEvents.add(timedEvent); + eventMessages.add(event); } } while (!XmlPullParserUtil.isEndTag(xpp, "EventStream")); - long[] presentationTimesUs = new long[timedEvents.size()]; - EventMessage[] events = new EventMessage[timedEvents.size()]; - for (int i = 0; i < timedEvents.size(); i++) { - Pair timedEvent = timedEvents.get(i); - presentationTimesUs[i] = timedEvent.first; - events[i] = timedEvent.second; + long[] presentationTimesUs = new long[eventMessages.size()]; + EventMessage[] events = new EventMessage[eventMessages.size()]; + for (int i = 0; i < eventMessages.size(); i++) { + EventMessage event = eventMessages.get(i); + presentationTimesUs[i] = event.presentationTimeUs; + events[i] = event; } return buildEventStream(schemeIdUri, value, timescale, presentationTimesUs, events); } @@ -723,11 +723,11 @@ public class DashManifestParser extends DefaultHandler * @param timescale The timescale of the parent EventStream. * @param scratchOutputStream A {@link ByteArrayOutputStream} that is used to write serialize data * in between and tags into. - * @return The {@link EventStream} parsed from this EventStream node. + * @return The {@link EventMessage} parsed from this EventStream node. * @throws XmlPullParserException If there is any error parsing this node. * @throws IOException If there is any error reading from the underlying input stream. */ - protected Pair parseEvent(XmlPullParser xpp, String schemeIdUri, String value, + protected EventMessage parseEvent(XmlPullParser xpp, String schemeIdUri, String value, long timescale, ByteArrayOutputStream scratchOutputStream) throws IOException, XmlPullParserException { long id = parseLong(xpp, "id", 0); @@ -737,8 +737,7 @@ public class DashManifestParser extends DefaultHandler long presentationTimesUs = Util.scaleLargeTimestamp(presentationTime, C.MICROS_PER_SECOND, timescale); byte[] eventObject = parseEventObject(xpp, scratchOutputStream); - return new Pair<>(presentationTimesUs, buildEvent(schemeIdUri, value, id, durationMs, - eventObject)); + return buildEvent(schemeIdUri, value, id, durationMs, eventObject, presentationTimesUs); } /** @@ -807,8 +806,8 @@ public class DashManifestParser extends DefaultHandler } protected EventMessage buildEvent(String schemeIdUri, String value, long id, - long durationMs, byte[] messageData) { - return new EventMessage(schemeIdUri, value, durationMs, id, messageData); + long durationMs, byte[] messageData, long presentationTimeUs) { + return new EventMessage(schemeIdUri, value, durationMs, id, messageData, presentationTimeUs); } protected List parseSegmentTimeline(XmlPullParser xpp) From 15a1f9a55263ac5a571ea89b409125d91676584f Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 21 Nov 2017 08:58:02 -0800 Subject: [PATCH 0754/2472] Remove DefaultLoadControl buffer time state ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176515168 --- .../android/exoplayer2/DefaultLoadControl.java | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java index 56bc633c9b..bfafd409f3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java @@ -51,10 +51,6 @@ public class DefaultLoadControl implements LoadControl { */ public static final int DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = 5000; - private static final int ABOVE_HIGH_WATERMARK = 0; - private static final int BETWEEN_WATERMARKS = 1; - private static final int BELOW_LOW_WATERMARK = 2; - private final DefaultAllocator allocator; private final long minBufferUs; @@ -182,11 +178,11 @@ public class DefaultLoadControl implements LoadControl { @Override public boolean shouldContinueLoading(long bufferedDurationUs, float playbackSpeed) { - int bufferTimeState = getBufferTimeState(bufferedDurationUs); boolean targetBufferSizeReached = allocator.getTotalBytesAllocated() >= targetBufferSize; boolean wasBuffering = isBuffering; - isBuffering = bufferTimeState == BELOW_LOW_WATERMARK - || (bufferTimeState == BETWEEN_WATERMARKS && isBuffering && !targetBufferSizeReached); + isBuffering = bufferedDurationUs < minBufferUs // below low watermark + || (bufferedDurationUs <= maxBufferUs // between watermarks + && isBuffering && !targetBufferSizeReached); if (priorityTaskManager != null && isBuffering != wasBuffering) { if (isBuffering) { priorityTaskManager.add(C.PRIORITY_PLAYBACK); @@ -197,11 +193,6 @@ public class DefaultLoadControl implements LoadControl { return isBuffering; } - private int getBufferTimeState(long bufferedDurationUs) { - return bufferedDurationUs > maxBufferUs ? ABOVE_HIGH_WATERMARK - : (bufferedDurationUs < minBufferUs ? BELOW_LOW_WATERMARK : BETWEEN_WATERMARKS); - } - private void reset(boolean resetAllocator) { targetBufferSize = 0; if (priorityTaskManager != null && isBuffering) { From 6607f49be609b692efbe210832544d375afc494c Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 21 Nov 2017 09:28:10 -0800 Subject: [PATCH 0755/2472] Fix reporting of format changes in ChunkSampleStream. Until recently, changing primary track formats were reported when the corresponding media chunk was discarded which always happened immediately after the sample has been read. Now, media chunks may be discarded later on or in batches, leaving the current reporting mechanism broken because changes may never be reported. This fix separates the discarding from the reporting such that format changes can be reported when the media chunk is first read from, while the discarding operation only discards without reporting format changes. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176519071 --- .../source/chunk/ChunkSampleStream.java | 54 +++++++++++++++---- 1 file changed, 43 insertions(+), 11 deletions(-) diff --git a/library/core/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 index 53742238ef..cfed38dd4a 100644 --- a/library/core/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 @@ -258,8 +258,12 @@ public class ChunkSampleStream implements SampleStream, S if (isPendingReset()) { return C.RESULT_NOTHING_READ; } - return primarySampleQueue.read(formatHolder, buffer, formatRequired, loadingFinished, + int result = primarySampleQueue.read(formatHolder, buffer, formatRequired, loadingFinished, lastSeekPositionUs); + if (result == C.RESULT_BUFFER_READ) { + maybeNotifyPrimaryTrackFormatChanged(primarySampleQueue.getReadIndex(), 1); + } + return result; } @Override @@ -274,6 +278,9 @@ public class ChunkSampleStream implements SampleStream, S skipCount = 0; } } + if (skipCount > 0) { + maybeNotifyPrimaryTrackFormatChanged(primarySampleQueue.getReadIndex(), skipCount); + } return skipCount; } @@ -434,23 +441,48 @@ public class ChunkSampleStream implements SampleStream, S return pendingResetPositionUs != C.TIME_UNSET; } - private void discardDownstreamMediaChunks(int primaryStreamReadIndex) { + private void discardDownstreamMediaChunks(int discardToPrimaryStreamIndex) { if (!mediaChunks.isEmpty()) { while (mediaChunks.size() > 1 - && mediaChunks.get(1).getFirstSampleIndex(0) <= primaryStreamReadIndex) { + && mediaChunks.get(1).getFirstSampleIndex(0) <= discardToPrimaryStreamIndex) { mediaChunks.removeFirst(); } - BaseMediaChunk currentChunk = mediaChunks.getFirst(); - Format trackFormat = currentChunk.trackFormat; - if (!trackFormat.equals(primaryDownstreamTrackFormat)) { - eventDispatcher.downstreamFormatChanged(primaryTrackType, trackFormat, - currentChunk.trackSelectionReason, currentChunk.trackSelectionData, - currentChunk.startTimeUs); - } - primaryDownstreamTrackFormat = trackFormat; } } + private void maybeNotifyPrimaryTrackFormatChanged(int toPrimaryStreamReadIndex, int readCount) { + if (!mediaChunks.isEmpty()) { + int fromPrimaryStreamReadIndex = toPrimaryStreamReadIndex - readCount; + int fromChunkIndex = 0; + while (fromChunkIndex < mediaChunks.size() - 1 + && mediaChunks.get(fromChunkIndex + 1).getFirstSampleIndex(0) + <= fromPrimaryStreamReadIndex) { + fromChunkIndex++; + } + int toChunkIndex = fromChunkIndex + 1; + if (readCount > 1) { + while (toChunkIndex < mediaChunks.size() + && mediaChunks.get(toChunkIndex).getFirstSampleIndex(0) < toPrimaryStreamReadIndex) { + toChunkIndex++; + } + } + for (int i = fromChunkIndex; i < toChunkIndex; i++) { + maybeNotifyPrimaryTrackFormatChanged(i); + } + } + } + + private void maybeNotifyPrimaryTrackFormatChanged(int mediaChunkReadIndex) { + BaseMediaChunk currentChunk = mediaChunks.get(mediaChunkReadIndex); + Format trackFormat = currentChunk.trackFormat; + if (!trackFormat.equals(primaryDownstreamTrackFormat)) { + eventDispatcher.downstreamFormatChanged(primaryTrackType, trackFormat, + currentChunk.trackSelectionReason, currentChunk.trackSelectionData, + currentChunk.startTimeUs); + } + primaryDownstreamTrackFormat = trackFormat; + } + /** * Discard upstream media chunks until the queue length is equal to the length specified. * From e575af3ac38e06e78d92d3b97ddc77d2b5bb9b8f Mon Sep 17 00:00:00 2001 From: hoangtc Date: Tue, 21 Nov 2017 10:16:50 -0800 Subject: [PATCH 0756/2472] Parse DASH manifest's publish time. Parse DASH manifest's publishTime node as defined by ISO/IEC 23009-1:2014, section 5.3.1.2. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176525922 --- .../source/dash/manifest/DashManifestTest.java | 3 ++- .../source/dash/manifest/DashManifest.java | 15 +++++++++++---- .../source/dash/manifest/DashManifestParser.java | 13 +++++++------ 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/library/dash/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 index dfcb9e72a5..882b0eb374 100644 --- a/library/dash/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 @@ -136,6 +136,7 @@ public class DashManifestTest extends TestCase { assertEquals(expected.minUpdatePeriodMs, actual.minUpdatePeriodMs); assertEquals(expected.timeShiftBufferDepthMs, actual.timeShiftBufferDepthMs); assertEquals(expected.suggestedPresentationDelayMs, actual.suggestedPresentationDelayMs); + assertEquals(expected.publishTimeMs, actual.publishTimeMs); assertEquals(expected.utcTiming, actual.utcTiming); assertEquals(expected.location, actual.location); assertEquals(expected.getPeriodCount(), actual.getPeriodCount()); @@ -179,7 +180,7 @@ public class DashManifestTest extends TestCase { } private static DashManifest newDashManifest(int duration, Period... periods) { - return new DashManifest(0, duration, 1, false, 2, 3, 4, DUMMY_UTC_TIMING, Uri.EMPTY, + return new DashManifest(0, duration, 1, false, 2, 3, 4, 12345, DUMMY_UTC_TIMING, Uri.EMPTY, Arrays.asList(periods)); } diff --git a/library/dash/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 index cd24526d7c..95fe938fa4 100644 --- a/library/dash/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 @@ -67,6 +67,12 @@ public class DashManifest { */ public final long suggestedPresentationDelayMs; + /** + * The {@code publishTime} value in milliseconds since epoch, or {@link C#TIME_UNSET} if + * not present. + */ + public final long publishTimeMs; + /** * The {@link UtcTimingElement}, or null if not present. Defined in DVB A168:7/2016, Section * 4.7.2. @@ -82,8 +88,8 @@ public class DashManifest { public DashManifest(long availabilityStartTimeMs, long durationMs, long minBufferTimeMs, boolean dynamic, long minUpdatePeriodMs, long timeShiftBufferDepthMs, - long suggestedPresentationDelayMs, UtcTimingElement utcTiming, Uri location, - List periods) { + long suggestedPresentationDelayMs, long publishTimeMs, UtcTimingElement utcTiming, + Uri location, List periods) { this.availabilityStartTimeMs = availabilityStartTimeMs; this.durationMs = durationMs; this.minBufferTimeMs = minBufferTimeMs; @@ -91,6 +97,7 @@ public class DashManifest { this.minUpdatePeriodMs = minUpdatePeriodMs; this.timeShiftBufferDepthMs = timeShiftBufferDepthMs; this.suggestedPresentationDelayMs = suggestedPresentationDelayMs; + this.publishTimeMs = publishTimeMs; this.utcTiming = utcTiming; this.location = location; this.periods = periods == null ? Collections.emptyList() : periods; @@ -147,8 +154,8 @@ public class DashManifest { } long newDuration = durationMs != C.TIME_UNSET ? durationMs - shiftMs : C.TIME_UNSET; return new DashManifest(availabilityStartTimeMs, newDuration, minBufferTimeMs, dynamic, - minUpdatePeriodMs, timeShiftBufferDepthMs, suggestedPresentationDelayMs, utcTiming, - location, copyPeriods); + minUpdatePeriodMs, timeShiftBufferDepthMs, suggestedPresentationDelayMs, publishTimeMs, + utcTiming, location, copyPeriods); } private static ArrayList copyAdaptationSets( diff --git a/library/dash/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 index 07f9660755..73d234fa72 100644 --- a/library/dash/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 @@ -119,6 +119,7 @@ public class DashManifestParser extends DefaultHandler ? parseDuration(xpp, "timeShiftBufferDepth", C.TIME_UNSET) : C.TIME_UNSET; long suggestedPresentationDelayMs = dynamic ? parseDuration(xpp, "suggestedPresentationDelay", C.TIME_UNSET) : C.TIME_UNSET; + long publishTimeMs = parseDateTime(xpp, "publishTime", C.TIME_UNSET); UtcTimingElement utcTiming = null; Uri location = null; @@ -171,17 +172,17 @@ public class DashManifestParser extends DefaultHandler } return buildMediaPresentationDescription(availabilityStartTime, durationMs, minBufferTimeMs, - dynamic, minUpdateTimeMs, timeShiftBufferDepthMs, suggestedPresentationDelayMs, utcTiming, - location, periods); + dynamic, minUpdateTimeMs, timeShiftBufferDepthMs, suggestedPresentationDelayMs, + publishTimeMs, utcTiming, location, periods); } protected DashManifest buildMediaPresentationDescription(long availabilityStartTime, long durationMs, long minBufferTimeMs, boolean dynamic, long minUpdateTimeMs, - long timeShiftBufferDepthMs, long suggestedPresentationDelayMs, UtcTimingElement utcTiming, - Uri location, List periods) { + long timeShiftBufferDepthMs, long suggestedPresentationDelayMs, long publishTimeMs, + UtcTimingElement utcTiming, Uri location, List periods) { return new DashManifest(availabilityStartTime, durationMs, minBufferTimeMs, - dynamic, minUpdateTimeMs, timeShiftBufferDepthMs, suggestedPresentationDelayMs, utcTiming, - location, periods); + dynamic, minUpdateTimeMs, timeShiftBufferDepthMs, suggestedPresentationDelayMs, + publishTimeMs, utcTiming, location, periods); } protected UtcTimingElement parseUtcTiming(XmlPullParser xpp) { From 4193a1f705b68627e0090d4bb66f44bf6302dd5a Mon Sep 17 00:00:00 2001 From: jrochest Date: Wed, 22 Nov 2017 01:49:47 -0800 Subject: [PATCH 0757/2472] Guard against null TrackSelections in updateTrackSelectionPlaybackSpeed. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176629070 --- .../com/google/android/exoplayer2/ExoPlayerImplInternal.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 71da7043be..63ae3c630e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -941,7 +941,9 @@ import java.io.IOException; while (periodHolder != null) { TrackSelection[] trackSelections = periodHolder.trackSelectorResult.selections.getAll(); for (TrackSelection trackSelection : trackSelections) { - trackSelection.onPlaybackSpeed(playbackSpeed); + if (trackSelection != null) { + trackSelection.onPlaybackSpeed(playbackSpeed); + } } periodHolder = periodHolder.next; } From b5480e0e97cce89ce6867a2b2b61620e48f41881 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 22 Nov 2017 05:10:43 -0800 Subject: [PATCH 0758/2472] Relax requirement that ClippingMediaSource children are not dynamic Tests to follow (want to fix breakages first). ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176642610 --- .../android/exoplayer2/source/ClippingMediaSource.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java index c6924e844a..0b2ff33f30 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java @@ -25,8 +25,7 @@ import java.util.ArrayList; /** * {@link MediaSource} that wraps a source and clips its timeline based on specified start/end - * positions. The wrapped source may only have a single period/window and it must not be dynamic - * (live). + * positions. The wrapped source may only have a single period/window. */ public final class ClippingMediaSource implements MediaSource, MediaSource.Listener { @@ -41,7 +40,7 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste /** * Creates a new clipping source that wraps the specified source. * - * @param mediaSource The single-period, non-dynamic source to wrap. + * @param mediaSource The single-period source to wrap. * @param startPositionUs The start position within {@code mediaSource}'s timeline at which to * start providing samples, in microseconds. * @param endPositionUs The end position within {@code mediaSource}'s timeline at which to stop @@ -61,7 +60,7 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste * {@code enableInitialPositionDiscontinuity} to suppress an initial discontinuity when a period * is first read from. * - * @param mediaSource The single-period, non-dynamic source to wrap. + * @param mediaSource The single-period source to wrap. * @param startPositionUs The start position within {@code mediaSource}'s timeline at which to * start providing samples, in microseconds. * @param endPositionUs The end position within {@code mediaSource}'s timeline at which to stop @@ -145,7 +144,6 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste Assertions.checkArgument(timeline.getWindowCount() == 1); Assertions.checkArgument(timeline.getPeriodCount() == 1); Window window = timeline.getWindow(0, new Window(), false); - Assertions.checkArgument(!window.isDynamic); long resolvedEndUs = endUs == C.TIME_END_OF_SOURCE ? window.durationUs : endUs; if (window.durationUs != C.TIME_UNSET) { if (resolvedEndUs > window.durationUs) { From d909dc1863b5bda2fcfc1edd1e2e00a3a199e3a0 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 22 Nov 2017 05:36:20 -0800 Subject: [PATCH 0759/2472] Report correct discontinuity from ClippingMediaPeriod It currently always reports 0, but it should report the position passed through selectTracks. Reporting should also be disabled if there's a seekToUs call. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176644228 --- .../source/ClippingMediaPeriod.java | 105 ++++++++---------- 1 file changed, 48 insertions(+), 57 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java index 7742444323..36e8e51ffb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java @@ -36,10 +36,10 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb public final MediaPeriod mediaPeriod; private MediaPeriod.Callback callback; - private long startUs; - private long endUs; private ClippingSampleStream[] sampleStreams; - private boolean pendingInitialDiscontinuity; + private long pendingInitialDiscontinuityPositionUs; + /* package */ long startUs; + /* package */ long endUs; /** * Creates a new clipping media period that provides a clipped view of the specified @@ -57,10 +57,10 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb */ public ClippingMediaPeriod(MediaPeriod mediaPeriod, boolean enableInitialDiscontinuity) { this.mediaPeriod = mediaPeriod; + sampleStreams = new ClippingSampleStream[0]; + pendingInitialDiscontinuityPositionUs = enableInitialDiscontinuity ? 0 : C.TIME_UNSET; startUs = C.TIME_UNSET; endUs = C.TIME_UNSET; - sampleStreams = new ClippingSampleStream[0]; - pendingInitialDiscontinuity = enableInitialDiscontinuity; } /** @@ -95,29 +95,27 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { sampleStreams = new ClippingSampleStream[streams.length]; - SampleStream[] internalStreams = new SampleStream[streams.length]; + SampleStream[] childStreams = new SampleStream[streams.length]; for (int i = 0; i < streams.length; i++) { sampleStreams[i] = (ClippingSampleStream) streams[i]; - internalStreams[i] = sampleStreams[i] != null ? sampleStreams[i].stream : null; + childStreams[i] = sampleStreams[i] != null ? sampleStreams[i].childStream : null; } long enablePositionUs = mediaPeriod.selectTracks(selections, mayRetainStreamFlags, - internalStreams, streamResetFlags, positionUs + startUs); - if (pendingInitialDiscontinuity) { - pendingInitialDiscontinuity = startUs != 0 && shouldKeepInitialDiscontinuity(selections); - } - Assertions.checkState(enablePositionUs == positionUs + startUs - || (enablePositionUs >= startUs - && (endUs == C.TIME_END_OF_SOURCE || enablePositionUs <= endUs))); + childStreams, streamResetFlags, positionUs + startUs) - startUs; + pendingInitialDiscontinuityPositionUs = isPendingInitialDiscontinuity() && positionUs == 0 + && shouldKeepInitialDiscontinuity(startUs, selections) ? enablePositionUs : C.TIME_UNSET; + Assertions.checkState(enablePositionUs == positionUs + || (enablePositionUs >= 0 + && (endUs == C.TIME_END_OF_SOURCE || startUs + enablePositionUs <= endUs))); for (int i = 0; i < streams.length; i++) { - if (internalStreams[i] == null) { + if (childStreams[i] == null) { sampleStreams[i] = null; - } else if (streams[i] == null || sampleStreams[i].stream != internalStreams[i]) { - sampleStreams[i] = new ClippingSampleStream(this, internalStreams[i], startUs, endUs, - pendingInitialDiscontinuity); + } else if (streams[i] == null || sampleStreams[i].childStream != childStreams[i]) { + sampleStreams[i] = new ClippingSampleStream(childStreams[i]); } streams[i] = sampleStreams[i]; } - return enablePositionUs - startUs; + return enablePositionUs; } @Override @@ -127,16 +125,12 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb @Override public long readDiscontinuity() { - if (pendingInitialDiscontinuity) { - for (ClippingSampleStream sampleStream : sampleStreams) { - if (sampleStream != null) { - sampleStream.clearPendingDiscontinuity(); - } - } - pendingInitialDiscontinuity = false; - // Always read an initial discontinuity, using mediaPeriod's discontinuity if set. - long discontinuityUs = readDiscontinuity(); - return discontinuityUs != C.TIME_UNSET ? discontinuityUs : 0; + if (isPendingInitialDiscontinuity()) { + long initialDiscontinuityUs = pendingInitialDiscontinuityPositionUs; + pendingInitialDiscontinuityPositionUs = C.TIME_UNSET; + // Always read an initial discontinuity from the child, and use it if set. + long childDiscontinuityUs = readDiscontinuity(); + return childDiscontinuityUs != C.TIME_UNSET ? childDiscontinuityUs : initialDiscontinuityUs; } long discontinuityUs = mediaPeriod.readDiscontinuity(); if (discontinuityUs == C.TIME_UNSET) { @@ -159,6 +153,7 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb @Override public long seekToUs(long positionUs) { + pendingInitialDiscontinuityPositionUs = C.TIME_UNSET; for (ClippingSampleStream sampleStream : sampleStreams) { if (sampleStream != null) { sampleStream.clearSentEos(); @@ -198,7 +193,11 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb callback.onContinueLoadingRequested(this); } - private static boolean shouldKeepInitialDiscontinuity(TrackSelection[] selections) { + /* package */ boolean isPendingInitialDiscontinuity() { + return pendingInitialDiscontinuityPositionUs != C.TIME_UNSET; + } + + private static boolean shouldKeepInitialDiscontinuity(long startUs, TrackSelection[] selections) { // If the clipping start position is non-zero, the clipping sample streams will adjust // timestamps on buffers they read from the unclipped sample streams. These adjusted buffer // timestamps can be negative, because sample streams provide buffers starting at a key-frame, @@ -208,11 +207,13 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb // discontinuity which resets the renderers before they read the clipping sample stream. // However, for audio-only track selections we assume to have random access seek behaviour and // do not need an initial discontinuity to reset the renderer. - for (TrackSelection trackSelection : selections) { - if (trackSelection != null) { - Format selectedFormat = trackSelection.getSelectedFormat(); - if (!MimeTypes.isAudio(selectedFormat.sampleMimeType)) { - return true; + if (startUs != 0) { + for (TrackSelection trackSelection : selections) { + if (trackSelection != null) { + Format selectedFormat = trackSelection.getSelectedFormat(); + if (!MimeTypes.isAudio(selectedFormat.sampleMimeType)) { + return true; + } } } } @@ -222,27 +223,14 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb /** * Wraps a {@link SampleStream} and clips its samples. */ - private static final class ClippingSampleStream implements SampleStream { + private final class ClippingSampleStream implements SampleStream { - private final MediaPeriod mediaPeriod; - private final SampleStream stream; - private final long startUs; - private final long endUs; + public final SampleStream childStream; - private boolean pendingDiscontinuity; private boolean sentEos; - public ClippingSampleStream(MediaPeriod mediaPeriod, SampleStream stream, long startUs, - long endUs, boolean pendingDiscontinuity) { - this.mediaPeriod = mediaPeriod; - this.stream = stream; - this.startUs = startUs; - this.endUs = endUs; - this.pendingDiscontinuity = pendingDiscontinuity; - } - - public void clearPendingDiscontinuity() { - pendingDiscontinuity = false; + public ClippingSampleStream(SampleStream childStream) { + this.childStream = childStream; } public void clearSentEos() { @@ -251,25 +239,25 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb @Override public boolean isReady() { - return stream.isReady(); + return !isPendingInitialDiscontinuity() && childStream.isReady(); } @Override public void maybeThrowError() throws IOException { - stream.maybeThrowError(); + childStream.maybeThrowError(); } @Override public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean requireFormat) { - if (pendingDiscontinuity) { + if (isPendingInitialDiscontinuity()) { return C.RESULT_NOTHING_READ; } if (sentEos) { buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); return C.RESULT_BUFFER_READ; } - int result = stream.readData(formatHolder, buffer, requireFormat); + int result = childStream.readData(formatHolder, buffer, requireFormat); if (result == C.RESULT_FORMAT_READ) { // Clear gapless playback metadata if the start/end points don't match the media. Format format = formatHolder.format; @@ -294,7 +282,10 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb @Override public int skipData(long positionUs) { - return stream.skipData(startUs + positionUs); + if (isPendingInitialDiscontinuity()) { + return C.RESULT_NOTHING_READ; + } + return childStream.skipData(startUs + positionUs); } } From 494a41c8b2eb84178b07acbe1877d84c32952fb9 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 22 Nov 2017 08:30:45 -0800 Subject: [PATCH 0760/2472] Improve ClippingMediaSource "cannot clip" behavior This brings ClippingMediaSource clip failures in line with what MergingMediaSource does when it cannot merge. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176660123 --- .../source/ClippingMediaSourceTest.java | 22 +++-- .../source/ConcatenatingMediaSourceTest.java | 15 ++-- .../DynamicConcatenatingMediaSourceTest.java | 23 ++--- .../source/LoopingMediaSourceTest.java | 11 +-- .../source/ClippingMediaSource.java | 89 ++++++++++++++++--- .../exoplayer2/source/MergingMediaSource.java | 4 +- .../testutil/MediaSourceTestRunner.java | 16 +++- 7 files changed, 134 insertions(+), 46 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java index c72188ad2c..6b17bf1e40 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java @@ -21,11 +21,13 @@ import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline.Period; import com.google.android.exoplayer2.Timeline.Window; +import com.google.android.exoplayer2.source.ClippingMediaSource.IllegalClippingException; import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.testutil.MediaSourceTestRunner; import com.google.android.exoplayer2.testutil.TimelineAsserts; +import java.io.IOException; /** * Unit tests for {@link ClippingMediaSource}. @@ -40,11 +42,12 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase { @Override protected void setUp() throws Exception { + super.setUp(); window = new Timeline.Window(); period = new Timeline.Period(); } - public void testNoClipping() { + public void testNoClipping() throws IOException { Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true, false); Timeline clippedTimeline = getClippedTimeline(timeline, 0, TEST_PERIOD_DURATION_US); @@ -55,7 +58,7 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase { assertEquals(TEST_PERIOD_DURATION_US, clippedTimeline.getPeriod(0, period).getDurationUs()); } - public void testClippingUnseekableWindowThrows() { + public void testClippingUnseekableWindowThrows() throws IOException { Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), false, false); // If the unseekable window isn't clipped, clipping succeeds. @@ -64,12 +67,12 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase { // If the unseekable window is clipped, clipping fails. getClippedTimeline(timeline, 1, TEST_PERIOD_DURATION_US); fail("Expected clipping to fail."); - } catch (IllegalArgumentException e) { - // Expected. + } catch (IllegalClippingException e) { + assertEquals(IllegalClippingException.REASON_NOT_SEEKABLE_TO_START, e.reason); } } - public void testClippingStart() { + public void testClippingStart() throws IOException { Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true, false); Timeline clippedTimeline = getClippedTimeline(timeline, TEST_CLIP_AMOUNT_US, @@ -80,7 +83,7 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase { clippedTimeline.getPeriod(0, period).getDurationUs()); } - public void testClippingEnd() { + public void testClippingEnd() throws IOException { Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true, false); Timeline clippedTimeline = getClippedTimeline(timeline, 0, @@ -91,7 +94,7 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase { clippedTimeline.getPeriod(0, period).getDurationUs()); } - public void testClippingStartAndEnd() { + public void testClippingStartAndEnd() throws IOException { Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true, false); Timeline clippedTimeline = getClippedTimeline(timeline, TEST_CLIP_AMOUNT_US, @@ -102,7 +105,7 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase { clippedTimeline.getPeriod(0, period).getDurationUs()); } - public void testWindowAndPeriodIndices() { + public void testWindowAndPeriodIndices() throws IOException { Timeline timeline = new FakeTimeline( new TimelineWindowDefinition(1, 111, true, false, TEST_PERIOD_DURATION_US)); Timeline clippedTimeline = getClippedTimeline(timeline, TEST_CLIP_AMOUNT_US, @@ -122,7 +125,8 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase { /** * Wraps the specified timeline in a {@link ClippingMediaSource} and returns the clipped timeline. */ - private static Timeline getClippedTimeline(Timeline timeline, long startMs, long endMs) { + private static Timeline getClippedTimeline(Timeline timeline, long startMs, long endMs) + throws IOException { FakeMediaSource fakeMediaSource = new FakeMediaSource(timeline, null); ClippingMediaSource mediaSource = new ClippingMediaSource(fakeMediaSource, startMs, endMs); MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null); diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java index 1ca32be46d..71c4b71023 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java @@ -25,6 +25,7 @@ import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.testutil.MediaSourceTestRunner; import com.google.android.exoplayer2.testutil.TimelineAsserts; +import java.io.IOException; import junit.framework.TestCase; /** @@ -32,7 +33,7 @@ import junit.framework.TestCase; */ public final class ConcatenatingMediaSourceTest extends TestCase { - public void testEmptyConcatenation() { + public void testEmptyConcatenation() throws IOException { for (boolean atomic : new boolean[] {false, true}) { Timeline timeline = getConcatenatedTimeline(atomic); TimelineAsserts.assertEmpty(timeline); @@ -45,7 +46,7 @@ public final class ConcatenatingMediaSourceTest extends TestCase { } } - public void testSingleMediaSource() { + public void testSingleMediaSource() throws IOException { Timeline timeline = getConcatenatedTimeline(false, createFakeTimeline(3, 111)); TimelineAsserts.assertWindowIds(timeline, 111); TimelineAsserts.assertPeriodCounts(timeline, 3); @@ -75,7 +76,7 @@ public final class ConcatenatingMediaSourceTest extends TestCase { } } - public void testMultipleMediaSources() { + public void testMultipleMediaSources() throws IOException { Timeline[] timelines = { createFakeTimeline(3, 111), createFakeTimeline(1, 222), createFakeTimeline(3, 333) }; Timeline timeline = getConcatenatedTimeline(false, timelines); @@ -121,7 +122,7 @@ public final class ConcatenatingMediaSourceTest extends TestCase { } } - public void testNestedMediaSources() { + public void testNestedMediaSources() throws IOException { Timeline timeline = getConcatenatedTimeline(false, getConcatenatedTimeline(false, createFakeTimeline(1, 111), createFakeTimeline(1, 222)), getConcatenatedTimeline(true, createFakeTimeline(1, 333), createFakeTimeline(1, 444))); @@ -149,7 +150,7 @@ public final class ConcatenatingMediaSourceTest extends TestCase { TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_ALL, true, 2, 0, 3, 1); } - public void testEmptyTimelineMediaSources() { + public void testEmptyTimelineMediaSources() throws IOException { // Empty timelines in the front, back, and the middle (single and multiple in a row). Timeline[] timelines = { Timeline.EMPTY, createFakeTimeline(1, 111), Timeline.EMPTY, Timeline.EMPTY, createFakeTimeline(2, 222), Timeline.EMPTY, createFakeTimeline(3, 333), @@ -197,7 +198,7 @@ public final class ConcatenatingMediaSourceTest extends TestCase { } } - public void testPeriodCreationWithAds() throws InterruptedException { + public void testPeriodCreationWithAds() throws IOException, InterruptedException { // Create media source with ad child source. Timeline timelineContentOnly = new FakeTimeline( new TimelineWindowDefinition(2, 111, true, false, 10 * C.MICROS_PER_SECOND)); @@ -231,7 +232,7 @@ public final class ConcatenatingMediaSourceTest extends TestCase { * the concatenated timeline. */ private static Timeline getConcatenatedTimeline(boolean isRepeatOneAtomic, - Timeline... timelines) { + Timeline... timelines) throws IOException { MediaSource[] mediaSources = new MediaSource[timelines.length]; for (int i = 0; i < timelines.length; i++) { mediaSources[i] = new FakeMediaSource(timelines[i], null); diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java index 16c9e1a17c..5fa158725d 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java @@ -30,6 +30,7 @@ import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.testutil.MediaSourceTestRunner; import com.google.android.exoplayer2.testutil.TimelineAsserts; +import java.io.IOException; import java.util.Arrays; import junit.framework.TestCase; import org.mockito.Mockito; @@ -55,7 +56,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { testRunner.release(); } - public void testPlaylistChangesAfterPreparation() { + public void testPlaylistChangesAfterPreparation() throws IOException { Timeline timeline = testRunner.prepareSource(); TimelineAsserts.assertEmpty(timeline); @@ -171,7 +172,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { childSources[3].assertReleased(); } - public void testPlaylistChangesBeforePreparation() { + public void testPlaylistChangesBeforePreparation() throws IOException { FakeMediaSource[] childSources = createMediaSources(4); mediaSource.addMediaSource(childSources[0]); mediaSource.addMediaSource(childSources[1]); @@ -201,7 +202,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { } } - public void testPlaylistWithLazyMediaSource() { + public void testPlaylistWithLazyMediaSource() throws IOException { // Create some normal (immediately preparing) sources and some lazy sources whose timeline // updates need to be triggered. FakeMediaSource[] fastSources = createMediaSources(2); @@ -290,7 +291,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { } } - public void testEmptyTimelineMediaSource() { + public void testEmptyTimelineMediaSource() throws IOException { Timeline timeline = testRunner.prepareSource(); TimelineAsserts.assertEmpty(timeline); @@ -426,7 +427,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { verify(runnable).run(); } - public void testCustomCallbackAfterPreparationAddSingle() { + public void testCustomCallbackAfterPreparationAddSingle() throws IOException { DummyMainThread dummyMainThread = new DummyMainThread(); try { testRunner.prepareSource(); @@ -444,7 +445,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { } } - public void testCustomCallbackAfterPreparationAddMultiple() { + public void testCustomCallbackAfterPreparationAddMultiple() throws IOException { DummyMainThread dummyMainThread = new DummyMainThread(); try { testRunner.prepareSource(); @@ -464,7 +465,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { } } - public void testCustomCallbackAfterPreparationAddSingleWithIndex() { + public void testCustomCallbackAfterPreparationAddSingleWithIndex() throws IOException { DummyMainThread dummyMainThread = new DummyMainThread(); try { testRunner.prepareSource(); @@ -482,7 +483,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { } } - public void testCustomCallbackAfterPreparationAddMultipleWithIndex() { + public void testCustomCallbackAfterPreparationAddMultipleWithIndex() throws IOException { DummyMainThread dummyMainThread = new DummyMainThread(); try { testRunner.prepareSource(); @@ -502,7 +503,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { } } - public void testCustomCallbackAfterPreparationRemove() { + public void testCustomCallbackAfterPreparationRemove() throws IOException { DummyMainThread dummyMainThread = new DummyMainThread(); try { testRunner.prepareSource(); @@ -528,7 +529,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { } } - public void testCustomCallbackAfterPreparationMove() { + public void testCustomCallbackAfterPreparationMove() throws IOException { DummyMainThread dummyMainThread = new DummyMainThread(); try { testRunner.prepareSource(); @@ -556,7 +557,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { } } - public void testPeriodCreationWithAds() throws InterruptedException { + public void testPeriodCreationWithAds() throws IOException, InterruptedException { // Create dynamic media source with ad child source. Timeline timelineContentOnly = new FakeTimeline( new TimelineWindowDefinition(2, 111, true, false, 10 * C.MICROS_PER_SECOND)); diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java index 6f69923ea2..7648af195c 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java @@ -23,6 +23,7 @@ import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.testutil.MediaSourceTestRunner; import com.google.android.exoplayer2.testutil.TimelineAsserts; +import java.io.IOException; import junit.framework.TestCase; /** @@ -39,7 +40,7 @@ public class LoopingMediaSourceTest extends TestCase { new TimelineWindowDefinition(1, 222), new TimelineWindowDefinition(1, 333)); } - public void testSingleLoop() { + public void testSingleLoop() throws IOException { Timeline timeline = getLoopingTimeline(multiWindowTimeline, 1); TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1); @@ -57,7 +58,7 @@ public class LoopingMediaSourceTest extends TestCase { } } - public void testMultiLoop() { + public void testMultiLoop() throws IOException { Timeline timeline = getLoopingTimeline(multiWindowTimeline, 3); TimelineAsserts.assertWindowIds(timeline, 111, 222, 333, 111, 222, 333, 111, 222, 333); TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1, 1, 1, 1, 1, 1, 1); @@ -77,7 +78,7 @@ public class LoopingMediaSourceTest extends TestCase { } } - public void testInfiniteLoop() { + public void testInfiniteLoop() throws IOException { Timeline timeline = getLoopingTimeline(multiWindowTimeline, Integer.MAX_VALUE); TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 1); @@ -94,7 +95,7 @@ public class LoopingMediaSourceTest extends TestCase { } } - public void testEmptyTimelineLoop() { + public void testEmptyTimelineLoop() throws IOException { Timeline timeline = getLoopingTimeline(Timeline.EMPTY, 1); TimelineAsserts.assertEmpty(timeline); @@ -109,7 +110,7 @@ public class LoopingMediaSourceTest extends TestCase { * Wraps the specified timeline in a {@link LoopingMediaSource} and returns * the looping timeline. */ - private static Timeline getLoopingTimeline(Timeline timeline, int loopCount) { + private static Timeline getLoopingTimeline(Timeline timeline, int loopCount) throws IOException { FakeMediaSource fakeMediaSource = new FakeMediaSource(timeline, null); LoopingMediaSource mediaSource = new LoopingMediaSource(fakeMediaSource, loopCount); MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java index 0b2ff33f30..721950f6b9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java @@ -15,20 +15,68 @@ */ package com.google.android.exoplayer2.source; +import android.support.annotation.IntDef; 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 java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; /** * {@link MediaSource} that wraps a source and clips its timeline based on specified start/end - * positions. The wrapped source may only have a single period/window. + * positions. The wrapped source must consist of a single period that starts at the beginning of the + * corresponding window. */ public final class ClippingMediaSource implements MediaSource, MediaSource.Listener { + /** + * Thrown when a {@link ClippingMediaSource} cannot clip its wrapped source. + */ + public static final class IllegalClippingException extends IOException { + + /** + * The reason the clipping failed. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({REASON_INVALID_PERIOD_COUNT, REASON_PERIOD_OFFSET_IN_WINDOW, + REASON_NOT_SEEKABLE_TO_START, REASON_START_EXCEEDS_END}) + public @interface Reason {} + /** + * The wrapped source doesn't consist of a single period. + */ + public static final int REASON_INVALID_PERIOD_COUNT = 0; + /** + * The wrapped source period doesn't start at the beginning of the corresponding window. + */ + public static final int REASON_PERIOD_OFFSET_IN_WINDOW = 1; + /** + * The wrapped source is not seekable and a non-zero clipping start position was specified. + */ + public static final int REASON_NOT_SEEKABLE_TO_START = 2; + /** + * The wrapped source ends before the specified clipping start position. + */ + public static final int REASON_START_EXCEEDS_END = 3; + + /** + * The reason clipping failed. + */ + @Reason + public final int reason; + + /** + * @param reason The reason clipping failed. + */ + public IllegalClippingException(@Reason int reason) { + this.reason = reason; + } + + } + private final MediaSource mediaSource; private final long startUs; private final long endUs; @@ -36,6 +84,7 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste private final ArrayList mediaPeriods; private MediaSource.Listener sourceListener; + private IllegalClippingException clippingError; /** * Creates a new clipping source that wraps the specified source. @@ -88,6 +137,9 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste @Override public void maybeThrowSourceInfoRefreshError() throws IOException { + if (clippingError != null) { + throw clippingError; + } mediaSource.maybeThrowSourceInfoRefreshError(); } @@ -115,8 +167,17 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste @Override public void onSourceInfoRefreshed(MediaSource source, Timeline timeline, Object manifest) { - sourceListener.onSourceInfoRefreshed(this, new ClippingTimeline(timeline, startUs, endUs), - manifest); + if (clippingError != null) { + return; + } + ClippingTimeline clippingTimeline; + try { + clippingTimeline = new ClippingTimeline(timeline, startUs, endUs); + } catch (IllegalClippingException e) { + clippingError = e; + return; + } + sourceListener.onSourceInfoRefreshed(this, clippingTimeline, manifest); int count = mediaPeriods.size(); for (int i = 0; i < count; i++) { mediaPeriods.get(i).setClipping(startUs, endUs); @@ -138,22 +199,30 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste * @param startUs The number of microseconds to clip from the start of {@code timeline}. * @param endUs The end position in microseconds for the clipped timeline relative to the start * of {@code timeline}, or {@link C#TIME_END_OF_SOURCE} to clip no samples from the end. + * @throws IllegalClippingException If the timeline could not be clipped. */ - public ClippingTimeline(Timeline timeline, long startUs, long endUs) { + public ClippingTimeline(Timeline timeline, long startUs, long endUs) + throws IllegalClippingException { super(timeline); - Assertions.checkArgument(timeline.getWindowCount() == 1); - Assertions.checkArgument(timeline.getPeriodCount() == 1); + if (timeline.getPeriodCount() != 1) { + throw new IllegalClippingException(IllegalClippingException.REASON_INVALID_PERIOD_COUNT); + } + if (timeline.getPeriod(0, new Period()).getPositionInWindowUs() != 0) { + throw new IllegalClippingException(IllegalClippingException.REASON_PERIOD_OFFSET_IN_WINDOW); + } Window window = timeline.getWindow(0, new Window(), false); long resolvedEndUs = endUs == C.TIME_END_OF_SOURCE ? window.durationUs : endUs; if (window.durationUs != C.TIME_UNSET) { if (resolvedEndUs > window.durationUs) { resolvedEndUs = window.durationUs; } - Assertions.checkArgument(startUs == 0 || window.isSeekable); - Assertions.checkArgument(startUs <= resolvedEndUs); + if (startUs != 0 && !window.isSeekable) { + throw new IllegalClippingException(IllegalClippingException.REASON_NOT_SEEKABLE_TO_START); + } + if (startUs > resolvedEndUs) { + throw new IllegalClippingException(IllegalClippingException.REASON_START_EXCEEDS_END); + } } - Period period = timeline.getPeriod(0, new Period()); - Assertions.checkArgument(period.getPositionInWindowUs() == 0); this.startUs = startUs; this.endUs = resolvedEndUs; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java index ea0274796f..3b468d8709 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java @@ -45,11 +45,11 @@ public final class MergingMediaSource implements MediaSource { @IntDef({REASON_WINDOWS_ARE_DYNAMIC, REASON_PERIOD_COUNT_MISMATCH}) public @interface Reason {} /** - * The merge failed because one of the sources being merged has a dynamic window. + * One of the sources being merged has a dynamic window. */ public static final int REASON_WINDOWS_ARE_DYNAMIC = 0; /** - * The merge failed because the sources have different period counts. + * The sources have different period counts. */ public static final int REASON_PERIOD_COUNT_MISMATCH = 1; diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java index 235c04bef5..4f31a8b027 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java @@ -32,7 +32,7 @@ import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; 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.concurrent.LinkedBlockingDeque; import java.util.concurrent.TimeUnit; @@ -100,13 +100,25 @@ public class MediaSourceTestRunner { * * @return The initial {@link Timeline}. */ - public Timeline prepareSource() { + public Timeline prepareSource() throws IOException { + final IOException[] prepareError = new IOException[1]; runOnPlaybackThread(new Runnable() { @Override public void run() { mediaSource.prepareSource(player, true, mediaSourceListener); + try { + // TODO: This only catches errors that are set synchronously in prepareSource. To capture + // async errors we'll need to poll maybeThrowSourceInfoRefreshError until the first call + // to onSourceInfoRefreshed. + mediaSource.maybeThrowSourceInfoRefreshError(); + } catch (IOException e) { + prepareError[0] = e; + } } }); + if (prepareError[0] != null) { + throw prepareError[0]; + } return assertTimelineChangeBlocking(); } From e04bdcea50c3242f91537554e01dbada73f1e6a3 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 22 Nov 2017 09:14:00 -0800 Subject: [PATCH 0761/2472] Relax requirement that MergingMediaSource children are not dynamic ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176664332 --- .../source/MergingMediaSourceTest.java | 80 +++++++++++++++++++ .../exoplayer2/source/MergingMediaSource.java | 25 ++---- 2 files changed, 85 insertions(+), 20 deletions(-) create mode 100644 library/core/src/androidTest/java/com/google/android/exoplayer2/source/MergingMediaSourceTest.java diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/MergingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/MergingMediaSourceTest.java new file mode 100644 index 0000000000..ba37385c75 --- /dev/null +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/MergingMediaSourceTest.java @@ -0,0 +1,80 @@ +/* + * 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; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.MergingMediaSource.IllegalMergeException; +import com.google.android.exoplayer2.testutil.FakeMediaSource; +import com.google.android.exoplayer2.testutil.FakeTimeline; +import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; +import com.google.android.exoplayer2.testutil.MediaSourceTestRunner; +import java.io.IOException; +import junit.framework.TestCase; + +/** + * Unit tests for {@link MergingMediaSource}. + */ +public class MergingMediaSourceTest extends TestCase { + + public void testMergingDynamicTimelines() throws IOException { + FakeTimeline firstTimeline = new FakeTimeline( + new TimelineWindowDefinition(true, true, C.TIME_UNSET)); + FakeTimeline secondTimeline = new FakeTimeline( + new TimelineWindowDefinition(true, true, C.TIME_UNSET)); + testMergingMediaSourcePrepare(firstTimeline, secondTimeline); + } + + public void testMergingStaticTimelines() throws IOException { + FakeTimeline firstTimeline = new FakeTimeline( + new TimelineWindowDefinition(true, false, 20)); + FakeTimeline secondTimeline = new FakeTimeline( + new TimelineWindowDefinition(true, false, 10)); + testMergingMediaSourcePrepare(firstTimeline, secondTimeline); + } + + public void testMergingTimelinesWithDifferentPeriodCounts() throws IOException { + FakeTimeline firstTimeline = new FakeTimeline(new TimelineWindowDefinition(1, null)); + FakeTimeline secondTimeline = new FakeTimeline(new TimelineWindowDefinition(2, null)); + try { + testMergingMediaSourcePrepare(firstTimeline, secondTimeline); + fail("Expected merging to fail."); + } catch (IllegalMergeException e) { + assertEquals(IllegalMergeException.REASON_PERIOD_COUNT_MISMATCH, e.reason); + } + } + + /** + * Wraps the specified timelines in a {@link MergingMediaSource}, prepares it and checks that it + * forwards the first of the wrapped timelines. + */ + private static void testMergingMediaSourcePrepare(Timeline... timelines) throws IOException { + MediaSource[] mediaSources = new MediaSource[timelines.length]; + for (int i = 0; i < timelines.length; i++) { + mediaSources[i] = new FakeMediaSource(timelines[i], null); + } + MergingMediaSource mediaSource = new MergingMediaSource(mediaSources); + MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null); + try { + Timeline timeline = testRunner.prepareSource(); + // The merged timeline should always be the one from the first child. + assertEquals(timelines[0], timeline); + } finally { + testRunner.release(); + } + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java index 3b468d8709..79ed864e25 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java @@ -28,8 +28,7 @@ import java.util.Arrays; /** * Merges multiple {@link MediaSource}s. *

          - * The {@link Timeline}s of the sources being merged must have the same number of periods, and must - * not have any dynamic windows. + * The {@link Timeline}s of the sources being merged must have the same number of periods. */ public final class MergingMediaSource implements MediaSource { @@ -42,26 +41,20 @@ public final class MergingMediaSource implements MediaSource { * The reason the merge failed. */ @Retention(RetentionPolicy.SOURCE) - @IntDef({REASON_WINDOWS_ARE_DYNAMIC, REASON_PERIOD_COUNT_MISMATCH}) + @IntDef({REASON_PERIOD_COUNT_MISMATCH}) public @interface Reason {} - /** - * One of the sources being merged has a dynamic window. - */ - public static final int REASON_WINDOWS_ARE_DYNAMIC = 0; /** * The sources have different period counts. */ - public static final int REASON_PERIOD_COUNT_MISMATCH = 1; + public static final int REASON_PERIOD_COUNT_MISMATCH = 0; /** - * The reason the merge failed. One of {@link #REASON_WINDOWS_ARE_DYNAMIC} and - * {@link #REASON_PERIOD_COUNT_MISMATCH}. + * The reason the merge failed. */ @Reason public final int reason; /** - * @param reason The reason the merge failed. One of {@link #REASON_WINDOWS_ARE_DYNAMIC} and - * {@link #REASON_PERIOD_COUNT_MISMATCH}. + * @param reason The reason the merge failed. */ public IllegalMergeException(@Reason int reason) { this.reason = reason; @@ -73,7 +66,6 @@ public final class MergingMediaSource implements MediaSource { private final MediaSource[] mediaSources; private final ArrayList pendingTimelineSources; - private final Timeline.Window window; private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private Listener listener; @@ -100,7 +92,6 @@ public final class MergingMediaSource implements MediaSource { this.mediaSources = mediaSources; this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; pendingTimelineSources = new ArrayList<>(Arrays.asList(mediaSources)); - window = new Timeline.Window(); periodCount = PERIOD_COUNT_UNSET; } @@ -170,12 +161,6 @@ public final class MergingMediaSource implements MediaSource { } private IllegalMergeException checkTimelineMerges(Timeline timeline) { - int windowCount = timeline.getWindowCount(); - for (int i = 0; i < windowCount; i++) { - if (timeline.getWindow(i, window, false).isDynamic) { - return new IllegalMergeException(IllegalMergeException.REASON_WINDOWS_ARE_DYNAMIC); - } - } if (periodCount == PERIOD_COUNT_UNSET) { periodCount = timeline.getPeriodCount(); } else if (timeline.getPeriodCount() != periodCount) { From d537c21888ef0dcc7f4dae0fbb2fc8030877fab3 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 22 Nov 2017 09:33:10 -0800 Subject: [PATCH 0762/2472] Test ClippingMediaSource handles initial dynamic timelines ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176666247 --- .../source/ClippingMediaSourceTest.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java index 6b17bf1e40..07e807ef9e 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java @@ -94,6 +94,21 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase { clippedTimeline.getPeriod(0, period).getDurationUs()); } + public void testClippingStartAndEndInitial() throws IOException { + // Timeline that's dynamic and not seekable. A child source might report such a timeline prior + // to it having loaded sufficient data to establish its duration and seekability. Such timelines + // should not result in clipping failure. + Timeline timeline = new SinglePeriodTimeline(C.TIME_UNSET, /* isSeekable= */ false, + /* isDynamic= */true); + + Timeline clippedTimeline = getClippedTimeline(timeline, TEST_CLIP_AMOUNT_US, + TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US * 2); + assertEquals(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US * 3, + clippedTimeline.getWindow(0, window).getDurationUs()); + assertEquals(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US * 3, + clippedTimeline.getPeriod(0, period).getDurationUs()); + } + public void testClippingStartAndEnd() throws IOException { Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true, false); From 3562fe1c69c8a085ff5c92449a6d4b2ff95a133e Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 22 Nov 2017 20:38:53 +0000 Subject: [PATCH 0763/2472] SampleStream fixes --- .../source/ExtractorMediaPeriod.java | 11 ++++- .../source/chunk/ChunkSampleStream.java | 8 +++- .../source/hls/HlsSampleStreamWrapper.java | 41 +++++++++++++------ 3 files changed, 43 insertions(+), 17 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java index c418c427f7..1228061cde 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -361,7 +361,7 @@ import java.util.Arrays; // SampleStream methods. /* package */ boolean isReady(int track) { - return loadingFinished || (!isPendingReset() && sampleQueues[track].hasNextSample()); + return !suppressRead() && (loadingFinished || sampleQueues[track].hasNextSample()); } /* package */ void maybeThrowError() throws IOException { @@ -370,7 +370,7 @@ import java.util.Arrays; /* package */ int readData(int track, FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired) { - if (notifyDiscontinuity || isPendingReset()) { + if (suppressRead()) { return C.RESULT_NOTHING_READ; } return sampleQueues[track].read(formatHolder, buffer, formatRequired, loadingFinished, @@ -378,6 +378,9 @@ import java.util.Arrays; } /* package */ int skipData(int track, long positionUs) { + if (suppressRead()) { + return 0; + } SampleQueue sampleQueue = sampleQueues[track]; if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) { return sampleQueue.advanceToEnd(); @@ -387,6 +390,10 @@ import java.util.Arrays; } } + private boolean suppressRead() { + return notifyDiscontinuity || isPendingReset(); + } + // Loader.Callback implementation. @Override diff --git a/library/core/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 index 8a9be92d75..bb51ae074e 100644 --- a/library/core/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 @@ -272,9 +272,11 @@ public class ChunkSampleStream implements SampleStream, S @Override public int skipData(long positionUs) { + if (isPendingReset()) { + return 0; + } int skipCount; if (loadingFinished && positionUs > primarySampleQueue.getLargestQueuedTimestampUs()) { - primarySampleQueue.advanceToEnd(); skipCount = primarySampleQueue.advanceToEnd(); } else { skipCount = primarySampleQueue.advanceTo(positionUs, true, true); @@ -282,7 +284,9 @@ public class ChunkSampleStream implements SampleStream, S skipCount = 0; } } - primarySampleQueue.discardToRead(); + if (skipCount > 0) { + primarySampleQueue.discardToRead(); + } return skipCount; } diff --git a/library/hls/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 index 3eae83624b..ddd6689fa6 100644 --- a/library/hls/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 @@ -385,7 +385,35 @@ import java.util.LinkedList; if (isPendingReset()) { return C.RESULT_NOTHING_READ; } + int result = sampleQueues[trackGroupIndex].read(formatHolder, buffer, requireFormat, + loadingFinished, lastSeekPositionUs); + if (result == C.RESULT_BUFFER_READ) { + discardToRead(); + } + return result; + } + public int skipData(int trackGroupIndex, long positionUs) { + if (isPendingReset()) { + return 0; + } + int skipCount; + SampleQueue sampleQueue = sampleQueues[trackGroupIndex]; + if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) { + skipCount = sampleQueue.advanceToEnd(); + } else { + skipCount = sampleQueue.advanceTo(positionUs, true, true); + if (skipCount == SampleQueue.ADVANCE_FAILED) { + skipCount = 0; + } + } + if (skipCount > 0) { + discardToRead(); + } + return skipCount; + } + + private void discardToRead() { if (!mediaChunks.isEmpty()) { while (mediaChunks.size() > 1 && finishedReadingChunk(mediaChunks.getFirst())) { mediaChunks.removeFirst(); @@ -399,19 +427,6 @@ import java.util.LinkedList; } downstreamTrackFormat = trackFormat; } - - return sampleQueues[trackGroupIndex].read(formatHolder, buffer, requireFormat, loadingFinished, - lastSeekPositionUs); - } - - public int skipData(int trackGroupIndex, long positionUs) { - SampleQueue sampleQueue = sampleQueues[trackGroupIndex]; - if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) { - return sampleQueue.advanceToEnd(); - } else { - int skipCount = sampleQueue.advanceTo(positionUs, true, true); - return skipCount == SampleQueue.ADVANCE_FAILED ? 0 : skipCount; - } } private boolean finishedReadingChunk(HlsMediaChunk chunk) { From 1442c047cffc17d9307d4579a805c1def64b4f22 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 22 Nov 2017 13:22:41 -0800 Subject: [PATCH 0764/2472] Update gradle wrapper ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176693785 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 2623db66fc..9f9081a945 100644 --- a/build.gradle +++ b/build.gradle @@ -17,7 +17,7 @@ buildscript { google() } dependencies { - classpath 'com.android.tools.build:gradle:3.0.0' + classpath 'com.android.tools.build:gradle:3.0.1' classpath 'com.novoda:bintray-release:0.5.0' } // Workaround for the following test coverage issue. Remove when fixed: From ba5f35995f21c9c2a86842ac9dce1d13009a6378 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 23 Nov 2017 02:00:10 -0800 Subject: [PATCH 0765/2472] Send discontinuity at adjustments after shuffle/repeat mode changes. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176749136 --- .../google/android/exoplayer2/ExoPlayerImplInternal.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 63ae3c630e..316735da77 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -476,8 +476,12 @@ import java.io.IOException; // position of the playing period to make sure none of the removed period is played. MediaPeriodId periodId = playingPeriodHolder.info.id; long newPositionUs = seekToPeriodPosition(periodId, playbackInfo.positionUs); - playbackInfo = playbackInfo.fromNewPosition(periodId, newPositionUs, + if (newPositionUs != playbackInfo.positionUs) { + playbackInfo = playbackInfo.fromNewPosition(periodId, newPositionUs, playbackInfo.contentPositionUs); + eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, Player.DISCONTINUITY_REASON_INTERNAL, + 0, playbackInfo).sendToTarget(); + } } } From 2086c129fcea32590a3b43e0919137c21d61cf1b Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 23 Nov 2017 04:26:05 -0800 Subject: [PATCH 0766/2472] Suppress discontinuities that don't change the position This is mostly useful for suppressing the initial position discontinuity reported by ClippingMediaPeriod. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176758972 --- .../android/exoplayer2/ExoPlayerTest.java | 46 ++++++++++--------- .../exoplayer2/ExoPlayerImplInternal.java | 12 +++-- .../exoplayer2/testutil/FakeMediaPeriod.java | 28 ++++++++++- 3 files changed, 59 insertions(+), 27 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index 2392c32e0a..e911778992 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -343,12 +343,9 @@ public final class ExoPlayerTest extends TestCase { @Override protected FakeMediaPeriod createFakeMediaPeriod(MediaPeriodId id, TrackGroupArray trackGroupArray, Allocator allocator) { - return new FakeMediaPeriod(trackGroupArray) { - @Override - public long seekToUs(long positionUs) { - return positionUs + 10; // Adjusts the requested seek position. - } - }; + FakeMediaPeriod mediaPeriod = new FakeMediaPeriod(trackGroupArray); + mediaPeriod.setSeekToUsOffset(10); + return mediaPeriod; } }; ActionSchedule actionSchedule = new ActionSchedule.Builder("testSeekDiscontinuityAdjust") @@ -359,32 +356,39 @@ public final class ExoPlayerTest extends TestCase { Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT); } - public void testInternalDiscontinuity() throws Exception { + public void testInternalDiscontinuityAtNewPosition() throws Exception { FakeTimeline timeline = new FakeTimeline(1); FakeMediaSource mediaSource = new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT) { @Override protected FakeMediaPeriod createFakeMediaPeriod(MediaPeriodId id, TrackGroupArray trackGroupArray, Allocator allocator) { - return new FakeMediaPeriod(trackGroupArray) { - boolean discontinuityRead; - @Override - public long readDiscontinuity() { - if (!discontinuityRead) { - discontinuityRead = true; - return 10; // Return a discontinuity. - } - return C.TIME_UNSET; - } - }; + FakeMediaPeriod mediaPeriod = new FakeMediaPeriod(trackGroupArray); + mediaPeriod.setDiscontinuityPositionUs(10); + return mediaPeriod; } }; - ActionSchedule actionSchedule = new ActionSchedule.Builder("testInternalDiscontinuity") - .waitForPlaybackState(Player.STATE_READY).build(); ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder().setMediaSource(mediaSource) - .setActionSchedule(actionSchedule).build().start().blockUntilEnded(TIMEOUT_MS); + .build().start().blockUntilEnded(TIMEOUT_MS); testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_INTERNAL); } + public void testInternalDiscontinuityAtInitialPosition() throws Exception { + FakeTimeline timeline = new FakeTimeline(1); + FakeMediaSource mediaSource = new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT) { + @Override + protected FakeMediaPeriod createFakeMediaPeriod(MediaPeriodId id, + TrackGroupArray trackGroupArray, Allocator allocator) { + FakeMediaPeriod mediaPeriod = new FakeMediaPeriod(trackGroupArray); + mediaPeriod.setDiscontinuityPositionUs(0); + return mediaPeriod; + } + }; + ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder().setMediaSource(mediaSource) + .build().start().blockUntilEnded(TIMEOUT_MS); + // If the position is unchanged we do not expect the discontinuity to be reported externally. + testRunner.assertNoPositionDiscontinuities(); + } + public void testAllActivatedTrackSelectionAreReleasedForSinglePeriod() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 1); MediaSource mediaSource = diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 316735da77..69da4b1f5b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -509,10 +509,14 @@ import java.io.IOException; long periodPositionUs = playingPeriodHolder.mediaPeriod.readDiscontinuity(); if (periodPositionUs != C.TIME_UNSET) { resetRendererPosition(periodPositionUs); - playbackInfo = playbackInfo.fromNewPosition(playbackInfo.periodId, periodPositionUs, - playbackInfo.contentPositionUs); - eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, Player.DISCONTINUITY_REASON_INTERNAL, - 0, playbackInfo).sendToTarget(); + // A MediaPeriod may report a discontinuity at the current playback position to ensure the + // renderers are flushed. Only report the discontinuity externally if the position changed. + if (periodPositionUs != playbackInfo.positionUs) { + playbackInfo = playbackInfo.fromNewPosition(playbackInfo.periodId, periodPositionUs, + playbackInfo.contentPositionUs); + eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, Player.DISCONTINUITY_REASON_INTERNAL, + 0, playbackInfo).sendToTarget(); + } } else { rendererPositionUs = mediaClock.syncAndGetPositionUs(); periodPositionUs = playingPeriodHolder.toPeriodTime(rendererPositionUs); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java index 153a427bbd..0b409f5348 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java @@ -33,9 +33,31 @@ public class FakeMediaPeriod implements MediaPeriod { private final TrackGroupArray trackGroupArray; private boolean preparedPeriod; + private long seekOffsetUs; + private long discontinuityPositionUs; public FakeMediaPeriod(TrackGroupArray trackGroupArray) { this.trackGroupArray = trackGroupArray; + discontinuityPositionUs = C.TIME_UNSET; + } + + /** + * Sets a discontinuity position to be returned from the next call to + * {@link #readDiscontinuity()}. + * + * @param discontinuityPositionUs The position to be returned, in microseconds. + */ + public void setDiscontinuityPositionUs(long discontinuityPositionUs) { + this.discontinuityPositionUs = discontinuityPositionUs; + } + + /** + * Sets an offset to be applied to positions returned by {@link #seekToUs(long)}. + * + * @param seekOffsetUs The offset to be applied, in microseconds. + */ + public void setSeekToUsOffset(long seekOffsetUs) { + this.seekOffsetUs = seekOffsetUs; } public void release() { @@ -92,7 +114,9 @@ public class FakeMediaPeriod implements MediaPeriod { @Override public long readDiscontinuity() { Assert.assertTrue(preparedPeriod); - return C.TIME_UNSET; + long positionDiscontinuityUs = this.discontinuityPositionUs; + this.discontinuityPositionUs = C.TIME_UNSET; + return positionDiscontinuityUs; } @Override @@ -104,7 +128,7 @@ public class FakeMediaPeriod implements MediaPeriod { @Override public long seekToUs(long positionUs) { Assert.assertTrue(preparedPeriod); - return positionUs; + return positionUs + seekOffsetUs; } @Override From 7eb0af7c0ef801c1a5a3497b91d129ab82abf942 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 23 Nov 2017 04:27:34 -0800 Subject: [PATCH 0767/2472] Replace LinkedList with ArrayList in ChunkSampleStream. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176759080 --- .../source/chunk/ChunkSampleStream.java | 89 ++++++++++--------- .../google/android/exoplayer2/util/Util.java | 11 +++ 2 files changed, 59 insertions(+), 41 deletions(-) diff --git a/library/core/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 index cfed38dd4a..e352ba551e 100644 --- a/library/core/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 @@ -28,9 +28,10 @@ import com.google.android.exoplayer2.source.SequenceableLoader; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.Loader; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; import java.io.IOException; +import java.util.ArrayList; import java.util.Collections; -import java.util.LinkedList; import java.util.List; /** @@ -51,7 +52,7 @@ public class ChunkSampleStream implements SampleStream, S private final int minLoadableRetryCount; private final Loader loader; private final ChunkHolder nextChunkHolder; - private final LinkedList mediaChunks; + private final ArrayList mediaChunks; private final List readOnlyMediaChunks; private final SampleQueue primarySampleQueue; private final SampleQueue[] embeddedSampleQueues; @@ -85,7 +86,7 @@ public class ChunkSampleStream implements SampleStream, S this.minLoadableRetryCount = minLoadableRetryCount; loader = new Loader("Loader:ChunkSampleStream"); nextChunkHolder = new ChunkHolder(); - mediaChunks = new LinkedList<>(); + mediaChunks = new ArrayList<>(); readOnlyMediaChunks = Collections.unmodifiableList(mediaChunks); int embeddedTrackCount = embeddedTrackTypes == null ? 0 : embeddedTrackTypes.length; @@ -170,7 +171,7 @@ public class ChunkSampleStream implements SampleStream, S return pendingResetPositionUs; } else { long bufferedPositionUs = lastSeekPositionUs; - BaseMediaChunk lastMediaChunk = mediaChunks.getLast(); + BaseMediaChunk lastMediaChunk = getLastMediaChunk(); BaseMediaChunk lastCompletedMediaChunk = lastMediaChunk.isLoadCompleted() ? lastMediaChunk : mediaChunks.size() > 1 ? mediaChunks.get(mediaChunks.size() - 2) : null; if (lastCompletedMediaChunk != null) { @@ -268,9 +269,11 @@ public class ChunkSampleStream implements SampleStream, S @Override public int skipData(long positionUs) { + if (isPendingReset()) { + return 0; + } int skipCount; if (loadingFinished && positionUs > primarySampleQueue.getLargestQueuedTimestampUs()) { - primarySampleQueue.advanceToEnd(); skipCount = primarySampleQueue.advanceToEnd(); } else { skipCount = primarySampleQueue.advanceTo(positionUs, true, true); @@ -325,7 +328,7 @@ public class ChunkSampleStream implements SampleStream, S } else { canceled = true; if (isMediaChunk) { - BaseMediaChunk removed = mediaChunks.removeLast(); + BaseMediaChunk removed = mediaChunks.remove(mediaChunks.size() - 1); Assertions.checkState(removed == loadable); primarySampleQueue.discardUpstreamSamples(removed.getFirstSampleIndex(0)); for (int i = 0; i < embeddedSampleQueues.length; i++) { @@ -363,7 +366,7 @@ public class ChunkSampleStream implements SampleStream, S previousChunk = null; loadPositionUs = pendingResetPositionUs; } else { - previousChunk = mediaChunks.getLast(); + previousChunk = getLastMediaChunk(); loadPositionUs = previousChunk.endTimeUs; } chunkSource.getNextChunk(previousChunk, positionUs, loadPositionUs, nextChunkHolder); @@ -399,7 +402,7 @@ public class ChunkSampleStream implements SampleStream, S if (isPendingReset()) { return pendingResetPositionUs; } else { - return loadingFinished ? C.TIME_END_OF_SOURCE : mediaChunks.getLast().endTimeUs; + return loadingFinished ? C.TIME_END_OF_SOURCE : getLastMediaChunk().endTimeUs; } } @@ -412,6 +415,7 @@ public class ChunkSampleStream implements SampleStream, S * * @param positionUs The current playback position in microseconds. */ + @SuppressWarnings("unused") private void maybeDiscardUpstream(long positionUs) { int queueSize = chunkSource.getPreferredQueueSize(positionUs, readOnlyMediaChunks); discardUpstreamMediaChunks(Math.max(1, queueSize)); @@ -425,7 +429,7 @@ public class ChunkSampleStream implements SampleStream, S * Returns whether samples have been read from {@code mediaChunks.getLast()}. */ private boolean haveReadFromLastMediaChunk() { - BaseMediaChunk lastChunk = mediaChunks.getLast(); + BaseMediaChunk lastChunk = getLastMediaChunk(); if (primarySampleQueue.getReadIndex() > lastChunk.getFirstSampleIndex(0)) { return true; } @@ -442,33 +446,21 @@ public class ChunkSampleStream implements SampleStream, S } private void discardDownstreamMediaChunks(int discardToPrimaryStreamIndex) { - if (!mediaChunks.isEmpty()) { - while (mediaChunks.size() > 1 - && mediaChunks.get(1).getFirstSampleIndex(0) <= discardToPrimaryStreamIndex) { - mediaChunks.removeFirst(); - } + int discardToMediaChunkIndex = + primaryStreamIndexToMediaChunkIndex(discardToPrimaryStreamIndex, /* minChunkIndex= */ 0); + if (discardToMediaChunkIndex > 0) { + Util.removeRange(mediaChunks, /* fromIndex= */ 0, /* toIndex= */ discardToMediaChunkIndex); } } private void maybeNotifyPrimaryTrackFormatChanged(int toPrimaryStreamReadIndex, int readCount) { - if (!mediaChunks.isEmpty()) { - int fromPrimaryStreamReadIndex = toPrimaryStreamReadIndex - readCount; - int fromChunkIndex = 0; - while (fromChunkIndex < mediaChunks.size() - 1 - && mediaChunks.get(fromChunkIndex + 1).getFirstSampleIndex(0) - <= fromPrimaryStreamReadIndex) { - fromChunkIndex++; - } - int toChunkIndex = fromChunkIndex + 1; - if (readCount > 1) { - while (toChunkIndex < mediaChunks.size() - && mediaChunks.get(toChunkIndex).getFirstSampleIndex(0) < toPrimaryStreamReadIndex) { - toChunkIndex++; - } - } - for (int i = fromChunkIndex; i < toChunkIndex; i++) { - maybeNotifyPrimaryTrackFormatChanged(i); - } + int fromMediaChunkIndex = primaryStreamIndexToMediaChunkIndex( + toPrimaryStreamReadIndex - readCount, /* minChunkIndex= */ 0); + int toMediaChunkIndexInclusive = readCount == 1 ? fromMediaChunkIndex + : primaryStreamIndexToMediaChunkIndex(toPrimaryStreamReadIndex - 1, + /* minChunkIndex= */ fromMediaChunkIndex); + for (int i = fromMediaChunkIndex; i <= toMediaChunkIndexInclusive; i++) { + maybeNotifyPrimaryTrackFormatChanged(i); } } @@ -483,6 +475,23 @@ public class ChunkSampleStream implements SampleStream, S primaryDownstreamTrackFormat = trackFormat; } + /** + * Returns media chunk index for primary stream sample index. May be -1 if the list of media + * chunks is empty or the requested index is less than the first index in the first media chunk. + */ + private int primaryStreamIndexToMediaChunkIndex(int primaryStreamIndex, int minChunkIndex) { + for (int i = minChunkIndex + 1; i < mediaChunks.size(); i++) { + if (mediaChunks.get(i).getFirstSampleIndex(0) > primaryStreamIndex) { + return i - 1; + } + } + return mediaChunks.size() - 1; + } + + private BaseMediaChunk getLastMediaChunk() { + return mediaChunks.get(mediaChunks.size() - 1); + } + /** * Discard upstream media chunks until the queue length is equal to the length specified. * @@ -493,16 +502,14 @@ public class ChunkSampleStream implements SampleStream, S if (mediaChunks.size() <= queueLength) { return false; } - BaseMediaChunk removed; - long startTimeUs; - long endTimeUs = mediaChunks.getLast().endTimeUs; - do { - removed = mediaChunks.removeLast(); - startTimeUs = removed.startTimeUs; - } while (mediaChunks.size() > queueLength); - primarySampleQueue.discardUpstreamSamples(removed.getFirstSampleIndex(0)); + + long endTimeUs = getLastMediaChunk().endTimeUs; + BaseMediaChunk firstRemovedChunk = mediaChunks.get(queueLength); + long startTimeUs = firstRemovedChunk.startTimeUs; + Util.removeRange(mediaChunks, /* fromIndex= */ queueLength, /* toIndex= */ mediaChunks.size()); + primarySampleQueue.discardUpstreamSamples(firstRemovedChunk.getFirstSampleIndex(0)); for (int i = 0; i < embeddedSampleQueues.length; i++) { - embeddedSampleQueues[i].discardUpstreamSamples(removed.getFirstSampleIndex(i + 1)); + embeddedSampleQueues[i].discardUpstreamSamples(firstRemovedChunk.getFirstSampleIndex(i + 1)); } loadingFinished = false; eventDispatcher.upstreamDiscarded(primaryTrackType, startTimeUs, endTimeUs); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java index 4582ab7c86..b5a897dc16 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -191,6 +191,17 @@ public final class Util { return false; } + /** + * Removes an indexed range from a List. + * + * @param list The List to remove the range from. + * @param fromIndex The first index to be removed (inclusive). + * @param toIndex The last index to be removed (exclusive). + */ + public static void removeRange(List list, int fromIndex, int toIndex) { + list.subList(fromIndex, toIndex).clear(); + } + /** * Instantiates a new single threaded executor whose thread has the specified name. * From 2537e883d6750751bd1149a1d1d1a66c37c002c4 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 23 Nov 2017 04:58:03 -0800 Subject: [PATCH 0768/2472] Move HlsSampleStreamWrapper to use ArrayList Also prevent skip when there's a pending reset, and add a TODO to split/fix chunk discard and downstream format change reporting. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176760955 --- .../source/hls/HlsSampleStreamWrapper.java | 36 +++++++++++++------ 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/library/hls/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 index adedee7e83..06d48f1b08 100644 --- a/library/hls/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 @@ -39,8 +39,8 @@ import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import java.io.IOException; +import java.util.ArrayList; import java.util.Arrays; -import java.util.LinkedList; /** * Loads {@link HlsMediaChunk}s obtained from a {@link HlsChunkSource}, and provides @@ -81,7 +81,7 @@ import java.util.LinkedList; private final Loader loader; private final EventDispatcher eventDispatcher; private final HlsChunkSource.HlsChunkHolder nextChunkHolder; - private final LinkedList mediaChunks; + private final ArrayList mediaChunks; private final Runnable maybeFinishPrepareRunnable; private final Handler handler; @@ -137,7 +137,7 @@ import java.util.LinkedList; sampleQueues = new SampleQueue[0]; sampleQueueIsAudioVideoFlags = new boolean[0]; sampleQueuesEnabledStates = new boolean[0]; - mediaChunks = new LinkedList<>(); + mediaChunks = new ArrayList<>(); maybeFinishPrepareRunnable = new Runnable() { @Override public void run() { @@ -260,7 +260,7 @@ import java.util.LinkedList; if (!seenFirstTrackSelection) { long bufferedDurationUs = positionUs < 0 ? -positionUs : 0; primaryTrackSelection.updateSelectedTrack(positionUs, bufferedDurationUs, C.TIME_UNSET); - int chunkIndex = chunkSource.getTrackGroup().indexOf(mediaChunks.getLast().trackFormat); + int chunkIndex = chunkSource.getTrackGroup().indexOf(getLastMediaChunk().trackFormat); if (primaryTrackSelection.getSelectedIndexInTrackGroup() != chunkIndex) { // This is the first selection and the chunk loaded during preparation does not match // the initially selected format. @@ -332,7 +332,7 @@ import java.util.LinkedList; return pendingResetPositionUs; } else { long bufferedPositionUs = lastSeekPositionUs; - HlsMediaChunk lastMediaChunk = mediaChunks.getLast(); + HlsMediaChunk lastMediaChunk = getLastMediaChunk(); HlsMediaChunk lastCompletedMediaChunk = lastMediaChunk.isLoadCompleted() ? lastMediaChunk : mediaChunks.size() > 1 ? mediaChunks.get(mediaChunks.size() - 2) : null; if (lastCompletedMediaChunk != null) { @@ -389,11 +389,17 @@ import java.util.LinkedList; return C.RESULT_NOTHING_READ; } + // TODO: Split into discard (in discardBuffer) and format change (here and in skipData) steps. if (!mediaChunks.isEmpty()) { - while (mediaChunks.size() > 1 && finishedReadingChunk(mediaChunks.getFirst())) { - mediaChunks.removeFirst(); + int discardToMediaChunkIndex = 0; + while (discardToMediaChunkIndex < mediaChunks.size() - 1 + && finishedReadingChunk(mediaChunks.get(discardToMediaChunkIndex))) { + discardToMediaChunkIndex++; } - HlsMediaChunk currentChunk = mediaChunks.getFirst(); + if (discardToMediaChunkIndex > 0) { + Util.removeRange(mediaChunks, 0, discardToMediaChunkIndex); + } + HlsMediaChunk currentChunk = mediaChunks.get(0); Format trackFormat = currentChunk.trackFormat; if (!trackFormat.equals(downstreamTrackFormat)) { eventDispatcher.downstreamFormatChanged(trackType, trackFormat, @@ -408,6 +414,10 @@ import java.util.LinkedList; } public int skipData(int sampleQueueIndex, long positionUs) { + if (isPendingReset()) { + return 0; + } + SampleQueue sampleQueue = sampleQueues[sampleQueueIndex]; if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) { return sampleQueue.advanceToEnd(); @@ -449,7 +459,7 @@ import java.util.LinkedList; previousChunk = null; loadPositionUs = pendingResetPositionUs; } else { - previousChunk = mediaChunks.getLast(); + previousChunk = getLastMediaChunk(); loadPositionUs = previousChunk.endTimeUs; } chunkSource.getNextChunk(previousChunk, positionUs, loadPositionUs, nextChunkHolder); @@ -489,7 +499,7 @@ import java.util.LinkedList; if (isPendingReset()) { return pendingResetPositionUs; } else { - return loadingFinished ? C.TIME_END_OF_SOURCE : mediaChunks.getLast().endTimeUs; + return loadingFinished ? C.TIME_END_OF_SOURCE : getLastMediaChunk().endTimeUs; } } @@ -531,7 +541,7 @@ import java.util.LinkedList; boolean canceled = false; if (chunkSource.onChunkLoadError(loadable, cancelable, error)) { if (isMediaChunk) { - HlsMediaChunk removed = mediaChunks.removeLast(); + HlsMediaChunk removed = mediaChunks.remove(mediaChunks.size() - 1); Assertions.checkState(removed == loadable); if (mediaChunks.isEmpty()) { pendingResetPositionUs = lastSeekPositionUs; @@ -764,6 +774,10 @@ import java.util.LinkedList; containerFormat.language); } + private HlsMediaChunk getLastMediaChunk() { + return mediaChunks.get(mediaChunks.size() - 1); + } + private boolean isMediaChunk(Chunk chunk) { return chunk instanceof HlsMediaChunk; } From 874d1be8529771aac6e193f61b20f2cefbc3cde9 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 23 Nov 2017 05:43:38 -0800 Subject: [PATCH 0769/2472] stopInternal should release MediaSource ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176763841 --- .../com/google/android/exoplayer2/ExoPlayerImplInternal.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 69da4b1f5b..8895b8e03a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -766,7 +766,7 @@ import java.io.IOException; } private void stopInternal() { - resetInternal(/* releaseMediaSource= */ false, /* resetPosition= */ false); + resetInternal(/* releaseMediaSource= */ true, /* resetPosition= */ false); loadControl.onStopped(); setState(Player.STATE_IDLE); } From 5d70b9e02d452aeba8a9bd92b5493dbffa596545 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 23 Nov 2017 06:00:46 -0800 Subject: [PATCH 0770/2472] Partialy revert "Make ExtractorMediaSource timeline dynamic until duration is set" This change broke playback through playlists. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176764830 --- .../android/exoplayer2/source/ExtractorMediaSource.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java index 0839d06fdd..351416df6a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java @@ -327,11 +327,9 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe private void notifySourceInfoRefreshed(long durationUs, boolean isSeekable) { timelineDurationUs = durationUs; timelineIsSeekable = isSeekable; - // If the duration is currently unset, we expect to be able to update the window when its - // duration eventually becomes known. - boolean isDynamic = timelineDurationUs == C.TIME_UNSET; + // TODO: Make timeline dynamic until its duration is known. This is non-trivial. See b/69703223. sourceListener.onSourceInfoRefreshed(this, - new SinglePeriodTimeline(timelineDurationUs, timelineIsSeekable, isDynamic), null); + new SinglePeriodTimeline(timelineDurationUs, timelineIsSeekable, false), null); } } From 91bcde033caeafdb7747a1a85c031e0022777120 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 23 Nov 2017 06:55:50 -0800 Subject: [PATCH 0771/2472] Fix release notes (change was cherry-picked) ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176768835 --- RELEASENOTES.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index c01f2c29ee..3972b06d76 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,10 +2,6 @@ ### dev-v2 (not yet released) ### -* Fix reporting of internal position discontinuities via - `Player.onPositionDiscontinuity`. `DISCONTINUITY_REASON_SEEK_ADJUSTMENT` is - added to disambiguate position adjustments during seeks from other types of - internal position discontinuity. * Allow more flexible loading strategy when playing media containing multiple sub-streams, by allowing injection of custom `CompositeSequenceableLoader` factories through `DashMediaSource.Builder`, `HlsMediaSource.Builder`, @@ -36,6 +32,10 @@ * SimpleExoPlayer: Support for multiple video, text and metadata outputs. * Support for `Renderer`s that don't consume any media ([#3212](https://github.com/google/ExoPlayer/issues/3212)). +* Fix reporting of internal position discontinuities via + `Player.onPositionDiscontinuity`. `DISCONTINUITY_REASON_SEEK_ADJUSTMENT` is + added to disambiguate position adjustments during seeks from other types of + internal position discontinuity. * Fix potential `IndexOutOfBoundsException` when calling `ExoPlayer.getDuration` ([#3362](https://github.com/google/ExoPlayer/issues/3362)). * Fix playbacks involving looping, concatenation and ads getting stuck when From 77d8c13621e1ecb8a8fef8062ac439477c5d3dd1 Mon Sep 17 00:00:00 2001 From: simophin Date: Fri, 24 Nov 2017 17:27:35 +1300 Subject: [PATCH 0772/2472] Guard against out-of-range timestamp We've found that in our production environment, the AAC stream's timestamp exceeds the 33bit limit from time to time, when it happens, `peekId3PrivTimestamp` returns a value that is greater than `TimestampAdjuster.MAX_PTS_PLUS_ONE`, which causes a overflow in `TimestampAdjuster.adjustTsTimestamp` (overflow inside `ptsToUs`) after playing for a while . When the overflow happens, the start time of the stream becomes negative and the playback simply stucks at buffering forever. I fully understand that the 33bit is a spec requirement, thus I asked our stream provider to correct this mistake. But in the mean time, I'd also like ExoPlayer to handle this situation more error tolerance, as in other platforms (iOS, browsers) we see more tolerance behavior. --- .../com/google/android/exoplayer2/source/hls/HlsMediaChunk.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/hls/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 index 5ca8675dd9..83167c152f 100644 --- a/library/hls/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 @@ -306,7 +306,7 @@ import java.util.concurrent.atomic.AtomicInteger; if (PRIV_TIMESTAMP_FRAME_OWNER.equals(privFrame.owner)) { System.arraycopy(privFrame.privateData, 0, id3Data.data, 0, 8 /* timestamp size */); id3Data.reset(8); - return id3Data.readLong(); + return id3Data.readLong() & ((1L << 33) - 1L); } } } From 5cd8869646b0391cd26a194564772d0c64587df0 Mon Sep 17 00:00:00 2001 From: baiming Date: Fri, 24 Nov 2017 01:32:44 -0800 Subject: [PATCH 0773/2472] Really fix the NPE in ExoPlayer. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176821463 --- .../android/exoplayer2/ExoPlayerImplInternal.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 8895b8e03a..4e37211e80 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -947,10 +947,12 @@ import java.io.IOException; MediaPeriodHolder periodHolder = playingPeriodHolder != null ? playingPeriodHolder : loadingPeriodHolder; while (periodHolder != null) { - TrackSelection[] trackSelections = periodHolder.trackSelectorResult.selections.getAll(); - for (TrackSelection trackSelection : trackSelections) { - if (trackSelection != null) { - trackSelection.onPlaybackSpeed(playbackSpeed); + if (periodHolder.trackSelectorResult != null) { + TrackSelection[] trackSelections = periodHolder.trackSelectorResult.selections.getAll(); + for (TrackSelection trackSelection : trackSelections) { + if (trackSelection != null) { + trackSelection.onPlaybackSpeed(playbackSpeed); + } } } periodHolder = periodHolder.next; From de476ba4e67bb3f1b9ad4f5a97ef8f997ebe0463 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 24 Nov 2017 02:20:35 -0800 Subject: [PATCH 0774/2472] Propagate the player error to ExoPlayerTestRunner In a test run where no exceptions were thrown on the main thread and the test did not time out, exceptions from onPlayerError were not correctly propagated to the test thread (handleException would be called with null). Fix ExoPlayerTestRunner.onPlayerError to propagate the actual exception from the player. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176825907 --- .../google/android/exoplayer2/testutil/ExoPlayerTestRunner.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java index 5ada65ef1e..759af41039 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java @@ -567,7 +567,7 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { @Override public void onPlayerError(ExoPlaybackException error) { - handleException(exception); + handleException(error); } @Override From 36255c42cf66c7be10ec2a4336cda22e094c7bf7 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 24 Nov 2017 05:37:49 -0800 Subject: [PATCH 0775/2472] Test setPlaybackParameters before preparation completes ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176837939 --- .../android/exoplayer2/ExoPlayerTest.java | 44 ++++++++++++ .../android/exoplayer2/testutil/Action.java | 25 +++++++ .../exoplayer2/testutil/ActionSchedule.java | 13 ++++ .../exoplayer2/testutil/FakeMediaPeriod.java | 68 +++++++++++++++---- 4 files changed, 136 insertions(+), 14 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index e911778992..5f41e57a6a 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -570,4 +570,48 @@ public final class ExoPlayerTest extends TestCase { testRunner.assertPlayedPeriodIndices(0, 1, 0); } + public void testSetPlaybackParametersBeforePreparationCompletesSucceeds() throws Exception { + // Test that no exception is thrown when playback parameters are updated between creating a + // period and preparation of the period completing. + final CountDownLatch createPeriodCalledCountDownLatch = new CountDownLatch(1); + final FakeMediaPeriod[] fakeMediaPeriodHolder = new FakeMediaPeriod[1]; + MediaSource mediaSource = + new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1), null, Builder.VIDEO_FORMAT) { + @Override + protected FakeMediaPeriod createFakeMediaPeriod( + MediaPeriodId id, TrackGroupArray trackGroupArray, Allocator allocator) { + // Defer completing preparation of the period until playback parameters have been set. + fakeMediaPeriodHolder[0] = + new FakeMediaPeriod(trackGroupArray, /* deferOnPrepared= */ true); + createPeriodCalledCountDownLatch.countDown(); + return fakeMediaPeriodHolder[0]; + } + }; + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSetPlaybackParametersBeforePreparationCompletesSucceeds") + .waitForPlaybackState(Player.STATE_BUFFERING) + // Block until createPeriod has been called on the fake media source. + .executeRunnable(new Runnable() { + @Override + public void run() { + try { + createPeriodCalledCountDownLatch.await(); + } catch (InterruptedException e) { + throw new IllegalStateException(e); + } + } + }) + // Set playback parameters (while the fake media period is not yet prepared). + .setPlaybackParameters(new PlaybackParameters(2f, 2f)) + // Complete preparation of the fake media period. + .executeRunnable(new Runnable() { + @Override + public void run() { + fakeMediaPeriodHolder[0].setPreparationComplete(); + } + }) + .build(); + new ExoPlayerTestRunner.Builder().setMediaSource(mediaSource).setActionSchedule(actionSchedule) + .build().start().blockUntilEnded(TIMEOUT_MS); + } } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java index 357d69df38..003d08cd59 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java @@ -19,6 +19,7 @@ import android.os.Handler; import android.util.Log; import android.view.Surface; import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; @@ -303,6 +304,30 @@ public abstract class Action { } + /** + * Calls {@link Player#setPlaybackParameters(PlaybackParameters)}. + */ + public static final class SetPlaybackParameters extends Action { + + private final PlaybackParameters playbackParameters; + + /** + * @param tag A tag to use for logging. + * @param playbackParameters The playback parameters. + */ + public SetPlaybackParameters(String tag, PlaybackParameters playbackParameters) { + super(tag, "SetPlaybackParameters:" + playbackParameters); + this.playbackParameters = playbackParameters; + } + + @Override + protected void doActionImpl(SimpleExoPlayer player, MappingTrackSelector trackSelector, + Surface surface) { + player.setPlaybackParameters(playbackParameters); + } + + } + /** * Waits for {@link Player.EventListener#onTimelineChanged(Timeline, Object, int)}. */ diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java index ddfa2345ee..2dbb4e18d2 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java @@ -19,6 +19,7 @@ import android.os.Handler; import android.os.Looper; import android.view.Surface; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; @@ -28,6 +29,7 @@ import com.google.android.exoplayer2.testutil.Action.ExecuteRunnable; import com.google.android.exoplayer2.testutil.Action.PrepareSource; import com.google.android.exoplayer2.testutil.Action.Seek; import com.google.android.exoplayer2.testutil.Action.SetPlayWhenReady; +import com.google.android.exoplayer2.testutil.Action.SetPlaybackParameters; import com.google.android.exoplayer2.testutil.Action.SetRendererDisabled; import com.google.android.exoplayer2.testutil.Action.SetRepeatMode; import com.google.android.exoplayer2.testutil.Action.SetShuffleModeEnabled; @@ -151,6 +153,17 @@ public final class ActionSchedule { .apply(new WaitForPlaybackState(tag, Player.STATE_READY)); } + /** + * Schedules a playback parameters setting action to be executed. + * + * @param playbackParameters The playback parameters to set. + * @return The builder, for convenience. + * @see Player#setPlaybackParameters(PlaybackParameters) + */ + public Builder setPlaybackParameters(PlaybackParameters playbackParameters) { + return apply(new SetPlaybackParameters(tag, playbackParameters)); + } + /** * Schedules a stop action to be executed. * diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java index 0b409f5348..c1be199b1e 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.testutil; +import android.os.Handler; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.SampleStream; @@ -32,12 +34,30 @@ public class FakeMediaPeriod implements MediaPeriod { private final TrackGroupArray trackGroupArray; - private boolean preparedPeriod; + @Nullable private Handler playerHandler; + @Nullable private Callback prepareCallback; + + private boolean deferOnPrepared; + private boolean prepared; private long seekOffsetUs; private long discontinuityPositionUs; + /** + * @param trackGroupArray The track group array. + */ public FakeMediaPeriod(TrackGroupArray trackGroupArray) { + this(trackGroupArray, false); + } + + /** + * @param trackGroupArray The track group array. + * @param deferOnPrepared Whether {@link MediaPeriod.Callback#onPrepared(MediaPeriod)} should be + * called only after {@link #setPreparationComplete()} has been called. If {@code false} + * preparation completes immediately. + */ + public FakeMediaPeriod(TrackGroupArray trackGroupArray, boolean deferOnPrepared) { this.trackGroupArray = trackGroupArray; + this.deferOnPrepared = deferOnPrepared; discontinuityPositionUs = C.TIME_UNSET; } @@ -51,6 +71,22 @@ public class FakeMediaPeriod implements MediaPeriod { this.discontinuityPositionUs = discontinuityPositionUs; } + /** + * Allows the fake media period to complete preparation. May be called on any thread. + */ + public synchronized void setPreparationComplete() { + deferOnPrepared = false; + if (playerHandler != null && prepareCallback != null) { + playerHandler.post(new Runnable() { + @Override + public void run() { + prepared = true; + prepareCallback.onPrepared(FakeMediaPeriod.this); + } + }); + } + } + /** * Sets an offset to be applied to positions returned by {@link #seekToUs(long)}. * @@ -61,31 +97,35 @@ public class FakeMediaPeriod implements MediaPeriod { } public void release() { - preparedPeriod = false; + prepared = false; } @Override - public void prepare(Callback callback, long positionUs) { - Assert.assertFalse(preparedPeriod); - preparedPeriod = true; - callback.onPrepared(this); + public synchronized void prepare(Callback callback, long positionUs) { + if (deferOnPrepared) { + playerHandler = new Handler(); + prepareCallback = callback; + } else { + prepared = true; + callback.onPrepared(this); + } } @Override public void maybeThrowPrepareError() throws IOException { - Assert.assertTrue(preparedPeriod); + // Do nothing. } @Override public TrackGroupArray getTrackGroups() { - Assert.assertTrue(preparedPeriod); + Assert.assertTrue(prepared); return trackGroupArray; } @Override public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { - Assert.assertTrue(preparedPeriod); + Assert.assertTrue(prepared); int rendererCount = selections.length; for (int i = 0; i < rendererCount; i++) { if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) { @@ -113,7 +153,7 @@ public class FakeMediaPeriod implements MediaPeriod { @Override public long readDiscontinuity() { - Assert.assertTrue(preparedPeriod); + Assert.assertTrue(prepared); long positionDiscontinuityUs = this.discontinuityPositionUs; this.discontinuityPositionUs = C.TIME_UNSET; return positionDiscontinuityUs; @@ -121,25 +161,25 @@ public class FakeMediaPeriod implements MediaPeriod { @Override public long getBufferedPositionUs() { - Assert.assertTrue(preparedPeriod); + Assert.assertTrue(prepared); return C.TIME_END_OF_SOURCE; } @Override public long seekToUs(long positionUs) { - Assert.assertTrue(preparedPeriod); + Assert.assertTrue(prepared); return positionUs + seekOffsetUs; } @Override public long getNextLoadPositionUs() { - Assert.assertTrue(preparedPeriod); + Assert.assertTrue(prepared); return C.TIME_END_OF_SOURCE; } @Override public boolean continueLoading(long positionUs) { - Assert.assertTrue(preparedPeriod); + Assert.assertTrue(prepared); return false; } From c4385c738f12986702cf94ff6330ae2768bdae7b Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 24 Nov 2017 08:30:53 -0800 Subject: [PATCH 0776/2472] Temporarily fix flakiness of testPlayEmptyTimeline. Fixed by explicitly waiting for the timeline update. This shouldn't be necessary and will be removed as soon as the correct order of events can be guaranteed (timeline change -> state change -> onSeekProcessed). The waiting for the timeline update is implemented by introducing the feature that the test runner also waits until the action schedule has finished before stopping the test. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176848540 --- .../android/exoplayer2/ExoPlayerTest.java | 9 ++- .../exoplayer2/testutil/ActionSchedule.java | 52 +++++++++++++++++- .../exoplayer2/testutil/ExoHostedTest.java | 4 +- .../testutil/ExoPlayerTestRunner.java | 55 ++++++++++++++++--- 4 files changed, 106 insertions(+), 14 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index 5f41e57a6a..8213e6133d 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -56,8 +56,15 @@ public final class ExoPlayerTest extends TestCase { public void testPlayEmptyTimeline() throws Exception { Timeline timeline = Timeline.EMPTY; FakeRenderer renderer = new FakeRenderer(); + // TODO(b/69665207): Without waiting for the timeline update, this test is flaky as the timeline + // update happens after the transition to STATE_ENDED and the test runner may already have been + // stopped. Remove action schedule as soon as state changes are part of the masking and the + // correct order of events is restored. + ActionSchedule actionSchedule = new ActionSchedule.Builder("testPlayEmptyTimeline") + .waitForTimelineChanged(timeline) + .build(); ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() - .setTimeline(timeline).setRenderers(renderer) + .setTimeline(timeline).setRenderers(renderer).setActionSchedule(actionSchedule) .build().start().blockUntilEnded(TIMEOUT_MS); testRunner.assertNoPositionDiscontinuities(); testRunner.assertTimelinesEqual(timeline); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java index 2dbb4e18d2..5e3d6bcb9a 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.testutil; import android.os.Handler; import android.os.Looper; +import android.support.annotation.Nullable; import android.view.Surface; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.PlaybackParameters; @@ -47,13 +48,28 @@ import com.google.android.exoplayer2.util.Clock; */ public final class ActionSchedule { + /** + * Callback to notify listener that the action schedule has finished. + */ + public interface Callback { + + /** + * Called when action schedule finished executing all its actions. + */ + void onActionScheduleFinished(); + + } + private final ActionNode rootNode; + private final CallbackAction callbackAction; /** * @param rootNode The first node in the sequence. + * @param callbackAction The final action which can be used to trigger a callback. */ - private ActionSchedule(ActionNode rootNode) { + private ActionSchedule(ActionNode rootNode, CallbackAction callbackAction) { this.rootNode = rootNode; + this.callbackAction = callbackAction; } /** @@ -63,9 +79,12 @@ public final class ActionSchedule { * @param trackSelector The track selector to which actions should be applied. * @param surface The surface to use when applying actions. * @param mainHandler A handler associated with the main thread of the host activity. + * @param callback A {@link Callback} to notify when the action schedule finishes, or null if no + * notification is needed. */ /* package */ void start(SimpleExoPlayer player, MappingTrackSelector trackSelector, - Surface surface, Handler mainHandler) { + Surface surface, Handler mainHandler, @Nullable Callback callback) { + callbackAction.setCallback(callback); rootNode.schedule(player, trackSelector, surface, mainHandler); } @@ -304,7 +323,9 @@ public final class ActionSchedule { } public ActionSchedule build() { - return new ActionSchedule(rootNode); + CallbackAction callbackAction = new CallbackAction(tag); + apply(callbackAction); + return new ActionSchedule(rootNode, callbackAction); } private Builder appendActionNode(ActionNode actionNode) { @@ -420,4 +441,29 @@ public final class ActionSchedule { } + /** + * An action calling a specified {@link ActionSchedule.Callback}. + */ + private static final class CallbackAction extends Action { + + private @Nullable Callback callback; + + public CallbackAction(String tag) { + super(tag, "FinishedCallback"); + } + + public void setCallback(@Nullable Callback callback) { + this.callback = callback; + } + + @Override + protected void doActionImpl(SimpleExoPlayer player, MappingTrackSelector trackSelector, + Surface surface) { + if (callback != null) { + callback.onActionScheduleFinished(); + } + } + + } + } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java index ee4018ba0e..ab31238983 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java @@ -126,7 +126,7 @@ public abstract class ExoHostedTest extends Player.DefaultEventListener implemen if (player == null) { pendingSchedule = schedule; } else { - schedule.start(player, trackSelector, surface, actionHandler); + schedule.start(player, trackSelector, surface, actionHandler, /* callback= */ null); } } @@ -162,7 +162,7 @@ public abstract class ExoHostedTest extends Player.DefaultEventListener implemen actionHandler = new Handler(); // Schedule any pending actions. if (pendingSchedule != null) { - pendingSchedule.start(player, trackSelector, surface, actionHandler); + pendingSchedule.start(player, trackSelector, surface, actionHandler, /* callback= */ null); pendingSchedule = null; } } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java index 759af41039..7b3292db89 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java @@ -17,6 +17,8 @@ package com.google.android.exoplayer2.testutil; import android.os.Handler; import android.os.HandlerThread; +import android.os.SystemClock; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.DefaultLoadControl; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayerFactory; @@ -48,7 +50,8 @@ import junit.framework.Assert; /** * Helper class to run an ExoPlayer test. */ -public final class ExoPlayerTestRunner extends Player.DefaultEventListener { +public final class ExoPlayerTestRunner extends Player.DefaultEventListener + implements ActionSchedule.Callback { /** * Builder to set-up a {@link ExoPlayerTestRunner}. Default fake implementations will be used for @@ -327,12 +330,13 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { private final RenderersFactory renderersFactory; private final MappingTrackSelector trackSelector; private final LoadControl loadControl; - private final ActionSchedule actionSchedule; - private final Player.EventListener eventListener; + private final @Nullable ActionSchedule actionSchedule; + private final @Nullable Player.EventListener eventListener; private final HandlerThread playerThread; private final Handler handler; private final CountDownLatch endedCountDownLatch; + private final CountDownLatch actionScheduleFinishedCountDownLatch; private final ArrayList timelines; private final ArrayList manifests; private final ArrayList timelineChangeReasons; @@ -346,8 +350,8 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { private ExoPlayerTestRunner(PlayerFactory playerFactory, MediaSource mediaSource, RenderersFactory renderersFactory, MappingTrackSelector trackSelector, - LoadControl loadControl, ActionSchedule actionSchedule, Player.EventListener eventListener, - int expectedPlayerEndedCount) { + LoadControl loadControl, @Nullable ActionSchedule actionSchedule, + @Nullable Player.EventListener eventListener, int expectedPlayerEndedCount) { this.playerFactory = playerFactory; this.mediaSource = mediaSource; this.renderersFactory = renderersFactory; @@ -361,6 +365,7 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { this.periodIndices = new ArrayList<>(); this.discontinuityReasons = new ArrayList<>(); this.endedCountDownLatch = new CountDownLatch(expectedPlayerEndedCount); + this.actionScheduleFinishedCountDownLatch = new CountDownLatch(actionSchedule != null ? 1 : 0); this.playerThread = new HandlerThread("ExoPlayerTest thread"); playerThread.start(); this.handler = new Handler(playerThread.getLooper()); @@ -387,7 +392,7 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { } player.setPlayWhenReady(true); if (actionSchedule != null) { - actionSchedule.start(player, trackSelector, null, handler); + actionSchedule.start(player, trackSelector, null, handler, ExoPlayerTestRunner.this); } player.prepare(mediaSource); } catch (Exception e) { @@ -400,8 +405,9 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { /** * Blocks the current thread until the test runner finishes. A test is deemed to be finished when - * the playback state transitions to {@link Player#STATE_ENDED} or {@link Player#STATE_IDLE}, or - * when am {@link ExoPlaybackException} is thrown. + * the action schedule finished and the playback state transitioned to {@link Player#STATE_ENDED} + * or {@link Player#STATE_IDLE} for the specified number of times. The test also finishes when an + * {@link ExoPlaybackException} is thrown. * * @param timeoutMs The maximum time to wait for the test runner to finish. If this time elapsed * the method will throw a {@link TimeoutException}. @@ -409,6 +415,13 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { * @throws Exception If any exception occurred during playback, release, or due to a timeout. */ public ExoPlayerTestRunner blockUntilEnded(long timeoutMs) throws Exception { + long deadlineMs = SystemClock.elapsedRealtime() + timeoutMs; + try { + blockUntilActionScheduleFinished(timeoutMs); + } catch (TimeoutException error) { + exception = error; + } + timeoutMs = Math.max(0, deadlineMs - SystemClock.elapsedRealtime()); if (!endedCountDownLatch.await(timeoutMs, TimeUnit.MILLISECONDS)) { exception = new TimeoutException("Test playback timed out waiting for playback to end."); } @@ -420,6 +433,24 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { return this; } + /** + * Blocks the current thread until the action schedule finished. Also returns when an + * {@link ExoPlaybackException} is thrown. This does not release the test runner and the test must + * still call {@link #blockUntilEnded(long)}. + * + * @param timeoutMs The maximum time to wait for the action schedule to finish. + * @return This test runner. + * @throws TimeoutException If the action schedule did not finish within the specified timeout. + * @throws InterruptedException If the test thread gets interrupted while waiting. + */ + public ExoPlayerTestRunner blockUntilActionScheduleFinished(long timeoutMs) + throws TimeoutException, InterruptedException { + if (!actionScheduleFinishedCountDownLatch.await(timeoutMs, TimeUnit.MILLISECONDS)) { + throw new TimeoutException("Test playback timed out waiting for action schedule to finish."); + } + return this; + } + // Assertions called on the test thread after test finished. /** @@ -536,6 +567,7 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { while (endedCountDownLatch.getCount() > 0) { endedCountDownLatch.countDown(); } + actionScheduleFinishedCountDownLatch.countDown(); } // Player.EventListener @@ -582,4 +614,11 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { } } + // ActionSchedule.Callback + + @Override + public void onActionScheduleFinished() { + actionScheduleFinishedCountDownLatch.countDown(); + } + } From a9ed6b191dc57c971d70842b07c6a325dffb0574 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 24 Nov 2017 10:00:44 -0800 Subject: [PATCH 0777/2472] Switch from currentTimeMillis to elapsedRealtime currentTimeMillis is not guaranteed to be monotonic and elapsedRealtime is recommend for interval timing. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176853118 --- .../google/android/exoplayer2/util/ConditionVariable.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/ConditionVariable.java b/library/core/src/main/java/com/google/android/exoplayer2/util/ConditionVariable.java index 262d120af8..058a5d6dd2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/ConditionVariable.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/ConditionVariable.java @@ -60,18 +60,18 @@ public final class ConditionVariable { } /** - * Blocks until the condition is opened or until timeout milliseconds have passed. + * Blocks until the condition is opened or until {@code timeout} milliseconds have passed. * * @param timeout The maximum time to wait in milliseconds. - * @return true If the condition was opened, false if the call returns because of the timeout. + * @return True if the condition was opened, false if the call returns because of the timeout. * @throws InterruptedException If the thread is interrupted. */ public synchronized boolean block(long timeout) throws InterruptedException { - long now = System.currentTimeMillis(); + long now = android.os.SystemClock.elapsedRealtime(); long end = now + timeout; while (!isOpen && now < end) { wait(end - now); - now = System.currentTimeMillis(); + now = android.os.SystemClock.elapsedRealtime(); } return isOpen; } From 8833a2930c0f6a7f43dd27787b72fd6d2c866ce5 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 27 Nov 2017 01:43:34 -0800 Subject: [PATCH 0778/2472] Take into account the playback speed for loading Update the default AdaptiveTrackSelection and DefaultLoadControl to use playback speed information. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176989168 --- .../exoplayer2/DefaultLoadControl.java | 5 +++++ .../AdaptiveTrackSelection.java | 22 ++++++++++++++----- .../google/android/exoplayer2/util/Util.java | 13 +++++++++++ 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java index bfafd409f3..d329f6584b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java @@ -172,6 +172,11 @@ public class DefaultLoadControl implements LoadControl { @Override public boolean shouldStartPlayback(long bufferedDurationUs, float playbackSpeed, boolean rebuffering) { + if (bufferedDurationUs >= minBufferUs) { + // It's possible that we're not loading, so allow playback to start unconditionally. + return true; + } + bufferedDurationUs = Util.getPlayoutDurationForMediaDuration(bufferedDurationUs, playbackSpeed); long minBufferDurationUs = rebuffering ? bufferForPlaybackAfterRebufferUs : bufferForPlaybackUs; return minBufferDurationUs <= 0 || bufferedDurationUs >= minBufferDurationUs; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java index f9eddab286..ba45b2b186 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java @@ -21,6 +21,7 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.chunk.MediaChunk; import com.google.android.exoplayer2.upstream.BandwidthMeter; +import com.google.android.exoplayer2.util.Util; import java.util.List; /** @@ -139,6 +140,7 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { private final float bandwidthFraction; private final float bufferedFractionToLiveEdgeForQualityIncrease; + private float playbackSpeed; private int selectedIndex; private int reason; @@ -196,10 +198,16 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { this.bandwidthFraction = bandwidthFraction; this.bufferedFractionToLiveEdgeForQualityIncrease = bufferedFractionToLiveEdgeForQualityIncrease; + playbackSpeed = 1f; selectedIndex = determineIdealSelectedIndex(Long.MIN_VALUE); reason = C.SELECTION_REASON_INITIAL; } + @Override + public void onPlaybackSpeed(float playbackSpeed) { + this.playbackSpeed = playbackSpeed; + } + @Override public void updateSelectedTrack(long playbackPositionUs, long bufferedDurationUs, long availableDurationUs) { @@ -254,8 +262,10 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { return 0; } int queueSize = queue.size(); - long bufferedDurationUs = queue.get(queueSize - 1).endTimeUs - playbackPositionUs; - if (bufferedDurationUs < minDurationToRetainAfterDiscardUs) { + long mediaBufferedDurationUs = queue.get(queueSize - 1).endTimeUs - playbackPositionUs; + long playoutBufferedDurationUs = + Util.getPlayoutDurationForMediaDuration(mediaBufferedDurationUs, playbackSpeed); + if (playoutBufferedDurationUs < minDurationToRetainAfterDiscardUs) { return queueSize; } int idealSelectedIndex = determineIdealSelectedIndex(SystemClock.elapsedRealtime()); @@ -266,8 +276,10 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { for (int i = 0; i < queueSize; i++) { MediaChunk chunk = queue.get(i); Format format = chunk.trackFormat; - long durationBeforeThisChunkUs = chunk.startTimeUs - playbackPositionUs; - if (durationBeforeThisChunkUs >= minDurationToRetainAfterDiscardUs + long mediaDurationBeforeThisChunkUs = chunk.startTimeUs - playbackPositionUs; + long playoutDurationBeforeThisChunkUs = + Util.getPlayoutDurationForMediaDuration(mediaDurationBeforeThisChunkUs, playbackSpeed); + if (playoutDurationBeforeThisChunkUs >= minDurationToRetainAfterDiscardUs && format.bitrate < idealFormat.bitrate && format.height != Format.NO_VALUE && format.height < 720 && format.width != Format.NO_VALUE && format.width < 1280 @@ -292,7 +304,7 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { for (int i = 0; i < length; i++) { if (nowMs == Long.MIN_VALUE || !isBlacklisted(i, nowMs)) { Format format = getFormat(i); - if (format.bitrate <= effectiveBitrate) { + if (Math.round(format.bitrate * playbackSpeed) <= effectiveBitrate) { return i; } else { lowestBitrateNonBlacklistedIndex = i; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java index b5a897dc16..881da0868f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -697,6 +697,19 @@ public final class Util { return Math.round((double) playoutDuration * speed); } + /** + * Returns the playout duration of {@code mediaDuration} of media. + * + * @param mediaDuration The duration to scale. + * @return The scaled duration, in the same units as {@code mediaDuration}. + */ + public static long getPlayoutDurationForMediaDuration(long mediaDuration, float speed) { + if (speed == 1f) { + return mediaDuration; + } + return Math.round((double) mediaDuration / speed); + } + /** * Converts a list of integers to a primitive array. * From 70169af6a09f25f2465650f3e67931a55e860ce5 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 27 Nov 2017 01:50:47 -0800 Subject: [PATCH 0779/2472] Update version strings ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176989632 --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 3902ec5cbd..ecfe3eb96f 100644 --- a/README.md +++ b/README.md @@ -42,18 +42,18 @@ Next add a gradle compile dependency to the `build.gradle` file of your app module. The following will add a dependency to the full library: ```gradle -compile 'com.google.android.exoplayer:exoplayer:r2.X.X' +compile 'com.google.android.exoplayer:exoplayer:2.X.X' ``` -where `r2.X.X` is your preferred version. Alternatively, you can depend on only +where `2.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: ```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' +compile 'com.google.android.exoplayer:exoplayer-core:2.X.X' +compile 'com.google.android.exoplayer:exoplayer-dash:2.X.X' +compile 'com.google.android.exoplayer:exoplayer-ui:2.X.X' ``` The available library modules are listed below. Adding a dependency to the full From a4fbb453252f1318b3d0df775e47490536398451 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 27 Nov 2017 03:45:24 -0800 Subject: [PATCH 0780/2472] Remove race condition when stopping FakeExoPlayer. A message to stop the playback and to quit the playback thread was posted in release(). The stop message removed all other already queued messages which might include the second message to quit the thread. That led to infinite waiting in the release method because the playback thread never got the quit signal. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176997104 --- .../testutil/FakeSimpleExoPlayer.java | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java index 094aaa5273..dc4f191885 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java @@ -166,27 +166,13 @@ public class FakeSimpleExoPlayer extends SimpleExoPlayer { @Override public void stop() { - playbackHandler.post(new Runnable() { - @Override - public void run () { - playbackHandler.removeCallbacksAndMessages(null); - releaseMedia(); - changePlaybackState(Player.STATE_IDLE); - } - }); + stop(/* quitPlaybackThread= */ false); } @Override @SuppressWarnings("ThreadJoinLoop") public void release() { - stop(); - playbackHandler.post(new Runnable() { - @Override - public void run () { - playbackHandler.removeCallbacksAndMessages(null); - playbackThread.quit(); - } - }); + stop(/* quitPlaybackThread= */ true); while (playbackThread.isAlive()) { try { playbackThread.join(); @@ -527,6 +513,20 @@ public class FakeSimpleExoPlayer extends SimpleExoPlayer { } } + private void stop(boolean quitPlaybackThread) { + playbackHandler.post(new Runnable() { + @Override + public void run () { + playbackHandler.removeCallbacksAndMessages(null); + releaseMedia(); + changePlaybackState(Player.STATE_IDLE); + if (quitPlaybackThread) { + playbackThread.quit(); + } + } + }); + } + } } From 818d5a0b0062e846acdd26cda1ffdde8c178eb63 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 27 Nov 2017 03:57:45 -0800 Subject: [PATCH 0781/2472] Add final to boolean used within Runnable. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176997767 --- .../google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java index dc4f191885..58f19ace1e 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java @@ -513,7 +513,7 @@ public class FakeSimpleExoPlayer extends SimpleExoPlayer { } } - private void stop(boolean quitPlaybackThread) { + private void stop(final boolean quitPlaybackThread) { playbackHandler.post(new Runnable() { @Override public void run () { From 1b66908f7d79b6891d8d99705470797640457e72 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 27 Nov 2017 01:50:47 -0800 Subject: [PATCH 0782/2472] Update version strings ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176989632 --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 3902ec5cbd..ecfe3eb96f 100644 --- a/README.md +++ b/README.md @@ -42,18 +42,18 @@ Next add a gradle compile dependency to the `build.gradle` file of your app module. The following will add a dependency to the full library: ```gradle -compile 'com.google.android.exoplayer:exoplayer:r2.X.X' +compile 'com.google.android.exoplayer:exoplayer:2.X.X' ``` -where `r2.X.X` is your preferred version. Alternatively, you can depend on only +where `2.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: ```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' +compile 'com.google.android.exoplayer:exoplayer-core:2.X.X' +compile 'com.google.android.exoplayer:exoplayer-dash:2.X.X' +compile 'com.google.android.exoplayer:exoplayer-ui:2.X.X' ``` The available library modules are listed below. Adding a dependency to the full From 16c43c6bb71a6ee486d79355189e2445ef7753b9 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Mon, 27 Nov 2017 06:29:47 -0800 Subject: [PATCH 0783/2472] Support undefined text track language when preferred is not available Also slightly improve language normalization/documentation. For this CL, it is assumed that null and "und" languages are different entities. Once we fully tackle language tag normalization, we can decide whether to normalize the "undefined" language. Issue:#2867 Issue:#2980 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177008509 --- RELEASENOTES.md | 5 +- .../java/com/google/android/exoplayer2/C.java | 5 + .../trackselection/DefaultTrackSelector.java | 149 ++++++++++++------ .../google/android/exoplayer2/util/Util.java | 14 +- .../DefaultTrackSelectorTest.java | 64 ++++++++ 5 files changed, 181 insertions(+), 56 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 3972b06d76..dd4a6ce655 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -18,8 +18,11 @@ use this with `FfmpegAudioRenderer`. * Support extraction and decoding of Dolby Atmos ([#2465](https://github.com/google/ExoPlayer/issues/2465)). -* Added a reason to `EventListener.onTimelineChanged` to distinguish between +* Added a reason to `EventListener.onTimelineChanged` to distinguish between initial preparation, reset and dynamic updates. +* DefaultTrackSelector: Support undefined language text track selection when the + preferred language is not available + ([#2980](https://github.com/google/ExoPlayer/issues/2980)). ### 2.6.0 ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/C.java b/library/core/src/main/java/com/google/android/exoplayer2/C.java index 592589e221..6a35c0c5e8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/C.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/C.java @@ -424,6 +424,11 @@ public final class C { */ public static final int SELECTION_FLAG_AUTOSELECT = 4; + /** + * Represents an undetermined language as an ISO 639 alpha-3 language code. + */ + public static final String LANGUAGE_UNDETERMINED = "und"; + /** * Represents a streaming or other media type. */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index c789caded4..0029cdbd31 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -46,7 +46,7 @@ import java.util.concurrent.atomic.AtomicReference; * Parameters currentParameters = trackSelector.getParameters(); * // Generate new parameters to prefer German audio and impose a maximum video size constraint. * Parameters newParameters = currentParameters - * .withPreferredAudioLanguage("de") + * .withPreferredAudioLanguage("deu") * .withMaxVideoSize(1024, 768); * // Set the new parameters on the selector. * trackSelector.setParameters(newParameters);} @@ -81,17 +81,22 @@ public class DefaultTrackSelector extends MappingTrackSelector { // Audio /** - * The preferred language for audio, as well as for forced text tracks as defined by RFC 5646. + * The preferred language for audio, as well as for forced text tracks, as an ISO 639-2/T tag. * {@code null} selects the default track, or the first track if there's no default. */ public final String preferredAudioLanguage; // Text /** - * The preferred language for text tracks as defined by RFC 5646. {@code null} selects the + * The preferred language for text tracks as an ISO 639-2/T tag. {@code null} selects the * default track if there is one, or no track otherwise. */ public final String preferredTextLanguage; + /** + * Whether a text track with undetermined language should be selected if no track with + * {@link #preferredTextLanguage} is available, or if {@link #preferredTextLanguage} is unset. + */ + public final boolean selectUndeterminedTextLanguage; // Video /** @@ -150,6 +155,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { *
            *
          • No preferred audio language is set.
          • *
          • No preferred text language is set.
          • + *
          • Text tracks with undetermined language are not selected if no track with + * {@link #preferredTextLanguage} is available.
          • *
          • Lowest bitrate track selections are not forced.
          • *
          • Adaptation between different mime types is not allowed.
          • *
          • Non seamless adaptation is allowed.
          • @@ -161,13 +168,14 @@ public class DefaultTrackSelector extends MappingTrackSelector { *
          */ public Parameters() { - this(null, null, false, false, true, Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE, - true, true, Integer.MAX_VALUE, Integer.MAX_VALUE, true); + this(null, null, false, false, false, true, Integer.MAX_VALUE, Integer.MAX_VALUE, + Integer.MAX_VALUE, true, true, Integer.MAX_VALUE, Integer.MAX_VALUE, true); } /** * @param preferredAudioLanguage See {@link #preferredAudioLanguage} * @param preferredTextLanguage See {@link #preferredTextLanguage} + * @param selectUndeterminedTextLanguage See {@link #selectUndeterminedTextLanguage}. * @param forceLowestBitrate See {@link #forceLowestBitrate}. * @param allowMixedMimeAdaptiveness See {@link #allowMixedMimeAdaptiveness} * @param allowNonSeamlessAdaptiveness See {@link #allowNonSeamlessAdaptiveness} @@ -181,13 +189,14 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @param viewportOrientationMayChange See {@link #viewportOrientationMayChange} */ public Parameters(String preferredAudioLanguage, String preferredTextLanguage, - boolean forceLowestBitrate, boolean allowMixedMimeAdaptiveness, - boolean allowNonSeamlessAdaptiveness, int maxVideoWidth, int maxVideoHeight, - int maxVideoBitrate, boolean exceedVideoConstraintsIfNecessary, + boolean selectUndeterminedTextLanguage, boolean forceLowestBitrate, + boolean allowMixedMimeAdaptiveness, boolean allowNonSeamlessAdaptiveness, int maxVideoWidth, + int maxVideoHeight, int maxVideoBitrate, boolean exceedVideoConstraintsIfNecessary, boolean exceedRendererCapabilitiesIfNecessary, int viewportWidth, int viewportHeight, boolean viewportOrientationMayChange) { this.preferredAudioLanguage = preferredAudioLanguage; this.preferredTextLanguage = preferredTextLanguage; + this.selectUndeterminedTextLanguage = selectUndeterminedTextLanguage; this.forceLowestBitrate = forceLowestBitrate; this.allowMixedMimeAdaptiveness = allowMixedMimeAdaptiveness; this.allowNonSeamlessAdaptiveness = allowNonSeamlessAdaptiveness; @@ -209,10 +218,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (TextUtils.equals(preferredAudioLanguage, this.preferredAudioLanguage)) { return this; } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, forceLowestBitrate, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, viewportOrientationMayChange); + return new Parameters(preferredAudioLanguage, preferredTextLanguage, + selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, + allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, + exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, + viewportHeight, viewportOrientationMayChange); } /** @@ -223,10 +233,26 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (TextUtils.equals(preferredTextLanguage, this.preferredTextLanguage)) { return this; } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, forceLowestBitrate, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, viewportOrientationMayChange); + return new Parameters(preferredAudioLanguage, preferredTextLanguage, + selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, + allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, + exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, + viewportHeight, viewportOrientationMayChange); + } + + /** + * Returns an instance with the provided {@link #selectUndeterminedTextLanguage}. + */ + public Parameters withSelectUndeterminedTextLanguageAsFallback( + boolean selectUndeterminedTextLanguage) { + if (selectUndeterminedTextLanguage == this.selectUndeterminedTextLanguage) { + return this; + } + return new Parameters(preferredAudioLanguage, preferredTextLanguage, + selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, + allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, + exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, + viewportHeight, viewportOrientationMayChange); } /** @@ -236,10 +262,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (forceLowestBitrate == this.forceLowestBitrate) { return this; } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, forceLowestBitrate, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, viewportOrientationMayChange); + return new Parameters(preferredAudioLanguage, preferredTextLanguage, + selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, + allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, + exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, + viewportHeight, viewportOrientationMayChange); } /** @@ -249,10 +276,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (allowMixedMimeAdaptiveness == this.allowMixedMimeAdaptiveness) { return this; } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, forceLowestBitrate, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, viewportOrientationMayChange); + return new Parameters(preferredAudioLanguage, preferredTextLanguage, + selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, + allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, + exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, + viewportHeight, viewportOrientationMayChange); } /** @@ -262,10 +290,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (allowNonSeamlessAdaptiveness == this.allowNonSeamlessAdaptiveness) { return this; } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, forceLowestBitrate, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, viewportOrientationMayChange); + return new Parameters(preferredAudioLanguage, preferredTextLanguage, + selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, + allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, + exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, + viewportHeight, viewportOrientationMayChange); } /** @@ -275,10 +304,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (maxVideoWidth == this.maxVideoWidth && maxVideoHeight == this.maxVideoHeight) { return this; } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, forceLowestBitrate, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, viewportOrientationMayChange); + return new Parameters(preferredAudioLanguage, preferredTextLanguage, + selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, + allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, + exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, + viewportHeight, viewportOrientationMayChange); } /** @@ -288,10 +318,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (maxVideoBitrate == this.maxVideoBitrate) { return this; } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, forceLowestBitrate, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, viewportOrientationMayChange); + return new Parameters(preferredAudioLanguage, preferredTextLanguage, + selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, + allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, + exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, + viewportHeight, viewportOrientationMayChange); } /** @@ -320,10 +351,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (exceedVideoConstraintsIfNecessary == this.exceedVideoConstraintsIfNecessary) { return this; } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, forceLowestBitrate, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, viewportOrientationMayChange); + return new Parameters(preferredAudioLanguage, preferredTextLanguage, + selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, + allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, + exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, + viewportHeight, viewportOrientationMayChange); } /** @@ -334,10 +366,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (exceedRendererCapabilitiesIfNecessary == this.exceedRendererCapabilitiesIfNecessary) { return this; } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, forceLowestBitrate, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, viewportOrientationMayChange); + return new Parameters(preferredAudioLanguage, preferredTextLanguage, + selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, + allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, + exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, + viewportHeight, viewportOrientationMayChange); } /** @@ -350,10 +383,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { && viewportOrientationMayChange == this.viewportOrientationMayChange) { return this; } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, forceLowestBitrate, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, viewportOrientationMayChange); + return new Parameters(preferredAudioLanguage, preferredTextLanguage, + selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, + allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, + exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, + viewportHeight, viewportOrientationMayChange); } /** @@ -880,17 +914,20 @@ public class DefaultTrackSelector extends MappingTrackSelector { boolean isDefault = (format.selectionFlags & C.SELECTION_FLAG_DEFAULT) != 0; boolean isForced = (format.selectionFlags & C.SELECTION_FLAG_FORCED) != 0; int trackScore; - if (formatHasLanguage(format, params.preferredTextLanguage)) { + boolean preferredLanguageFound = formatHasLanguage(format, params.preferredTextLanguage); + if (preferredLanguageFound + || (params.selectUndeterminedTextLanguage && formatHasNoLanguage(format))) { if (isDefault) { - trackScore = 6; + trackScore = 8; } else if (!isForced) { // Prefer non-forced to forced if a preferred text language has been specified. Where // both are provided the non-forced track will usually contain the forced subtitles as // a subset. - trackScore = 5; + trackScore = 6; } else { trackScore = 4; } + trackScore += preferredLanguageFound ? 1 : 0; } else if (isDefault) { trackScore = 3; } else if (isForced) { @@ -980,6 +1017,16 @@ public class DefaultTrackSelector extends MappingTrackSelector { && maskedSupport == RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES); } + /** + * Returns whether a {@link Format} does not define a language. + * + * @param format The {@link Format}. + * @return Whether the {@link Format} does not define a language. + */ + protected static boolean formatHasNoLanguage(Format format) { + return TextUtils.isEmpty(format.language) || formatHasLanguage(format, C.LANGUAGE_UNDETERMINED); + } + /** * Returns whether a {@link Format} specifies a particular language, or {@code false} if * {@code language} is null. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java index 881da0868f..0594f52288 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -50,6 +50,7 @@ import java.util.Formatter; import java.util.GregorianCalendar; import java.util.List; import java.util.Locale; +import java.util.MissingResourceException; import java.util.TimeZone; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -249,13 +250,18 @@ public final class Util { } /** - * Returns a normalized RFC 5646 language code. + * Returns a normalized RFC 639-2/T code for {@code language}. * - * @param language A possibly non-normalized RFC 5646 language code. - * @return The normalized code, or null if the input was null. + * @param language A case-insensitive ISO 639 alpha-2 or alpha-3 language code. + * @return The all-lowercase normalized code, or null if the input was null, or + * {@code language.toLowerCase()} if the language could not be normalized. */ public static String normalizeLanguageCode(String language) { - return language == null ? null : new Locale(language).getLanguage(); + try { + return language == null ? null : new Locale(language).getISO3Language(); + } catch (MissingResourceException e) { + return language.toLowerCase(); + } } /** diff --git a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java index a0e499139c..b2b149b004 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java @@ -36,6 +36,8 @@ public final class DefaultTrackSelectorTest { private static final Parameters DEFAULT_PARAMETERS = new Parameters(); private static final RendererCapabilities ALL_AUDIO_FORMAT_SUPPORTED_RENDERER_CAPABILITIES = new FakeRendererCapabilities(C.TRACK_TYPE_AUDIO); + private static final RendererCapabilities ALL_TEXT_FORMAT_SUPPORTED_RENDERER_CAPABILITIES = + new FakeRendererCapabilities(C.TRACK_TYPE_TEXT); private static final RendererCapabilities ALL_AUDIO_FORMAT_EXCEEDED_RENDERER_CAPABILITIES = new FakeRendererCapabilities(C.TRACK_TYPE_AUDIO, FORMAT_EXCEEDS_CAPABILITIES); @@ -534,6 +536,60 @@ public final class DefaultTrackSelectorTest { .isEqualTo(lowerSampleRateHigherBitrateFormat); } + /** + * Tests that the default track selector will select a text track with undetermined language if no + * text track with the preferred language is available but + * {@link Parameters#selectUndeterminedTextLanguage} is true. + */ + @Test + public void testSelectUndeterminedTextLanguageAsFallback() throws ExoPlaybackException{ + Format spanish = Format.createTextContainerFormat("spanish", null, + MimeTypes.TEXT_VTT, null, Format.NO_VALUE, 0, "spa"); + Format german = Format.createTextContainerFormat("german", null, + MimeTypes.TEXT_VTT, null, Format.NO_VALUE, 0, "de"); + Format undeterminedUnd = Format.createTextContainerFormat("undeterminedUnd", null, + MimeTypes.TEXT_VTT, null, Format.NO_VALUE, 0, "und"); + Format undeterminedNull = Format.createTextContainerFormat("undeterminedNull", null, + MimeTypes.TEXT_VTT, null, Format.NO_VALUE, 0, null); + + RendererCapabilities[] textRendererCapabilites = + new RendererCapabilities[] {ALL_TEXT_FORMAT_SUPPORTED_RENDERER_CAPABILITIES}; + + TrackSelectorResult result; + + result = trackSelector.selectTracks(textRendererCapabilites, + wrapFormats(spanish, german, undeterminedUnd, undeterminedNull)); + assertThat(result.selections.get(0)).isNull(); + + trackSelector.setParameters( + DEFAULT_PARAMETERS.withSelectUndeterminedTextLanguageAsFallback(true)); + result = trackSelector.selectTracks(textRendererCapabilites, + wrapFormats(spanish, german, undeterminedUnd, undeterminedNull)); + assertThat(result.selections.get(0).getFormat(0)).isSameAs(undeterminedUnd); + + trackSelector.setParameters(DEFAULT_PARAMETERS.withPreferredTextLanguage("spa")); + result = trackSelector.selectTracks(textRendererCapabilites, + wrapFormats(spanish, german, undeterminedUnd, undeterminedNull)); + assertThat(result.selections.get(0).getFormat(0)).isSameAs(spanish); + + result = trackSelector.selectTracks(textRendererCapabilites, + wrapFormats(german, undeterminedUnd, undeterminedNull)); + assertThat(result.selections.get(0)).isNull(); + + trackSelector.setParameters( + trackSelector.getParameters().withSelectUndeterminedTextLanguageAsFallback(true)); + result = trackSelector.selectTracks(textRendererCapabilites, + wrapFormats(german, undeterminedUnd, undeterminedNull)); + assertThat(result.selections.get(0).getFormat(0)).isSameAs(undeterminedUnd); + + result = trackSelector.selectTracks(textRendererCapabilites, + wrapFormats(german, undeterminedNull)); + assertThat(result.selections.get(0).getFormat(0)).isSameAs(undeterminedNull); + + result = trackSelector.selectTracks(textRendererCapabilites, wrapFormats(german)); + assertThat(result.selections.get(0)).isNull(); + } + /** * Tests that track selector will select audio tracks with lower bitrate when {@link Parameters} * indicate lowest bitrate preference, even when tracks are within capabilities. @@ -562,6 +618,14 @@ public final class DefaultTrackSelectorTest { return new TrackGroupArray(new TrackGroup(formats)); } + private static TrackGroupArray wrapFormats(Format... formats) { + TrackGroup[] trackGroups = new TrackGroup[formats.length]; + for (int i = 0; i < trackGroups.length; i++) { + trackGroups[i] = new TrackGroup(formats[i]); + } + return new TrackGroupArray(trackGroups); + } + /** * A {@link RendererCapabilities} that advertises support for all formats of a given type using * a provided support value. For any format that does not have the given track type, From ee26da682c495d0a8b6ed4022273bf5d0fa4d196 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 27 Nov 2017 07:02:33 -0800 Subject: [PATCH 0784/2472] Add throws IllegalSeekPositionException doc to seekTo(windowIndex, positionMs). ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177011497 --- .../src/main/java/com/google/android/exoplayer2/Player.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Player.java b/library/core/src/main/java/com/google/android/exoplayer2/Player.java index 77fced0832..a036a2021d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Player.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Player.java @@ -401,6 +401,8 @@ public interface Player { * @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. + * @throws IllegalSeekPositionException If the player has a non-empty timeline and the provided + * {@code windowIndex} is not within the bounds of the current timeline. */ void seekTo(int windowIndex, long positionMs); From 95de9c96fe0eedee5b7f34bcc939ed5f3da6ea41 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 27 Nov 2017 08:58:57 -0800 Subject: [PATCH 0785/2472] Don't always wait for action schedule in ExoPlayerTestRunner. Unconditionally waiting for the action schedule to finish in ExoPlayerTestRunner doesn't work if the action schedule is not intended to be finished. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177024139 --- .../google/android/exoplayer2/ExoPlayerTest.java | 2 +- .../exoplayer2/testutil/ExoPlayerTestRunner.java | 14 +++----------- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index 8213e6133d..27e4a97ac5 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -65,7 +65,7 @@ public final class ExoPlayerTest extends TestCase { .build(); ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() .setTimeline(timeline).setRenderers(renderer).setActionSchedule(actionSchedule) - .build().start().blockUntilEnded(TIMEOUT_MS); + .build().start().blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS); testRunner.assertNoPositionDiscontinuities(); testRunner.assertTimelinesEqual(timeline); assertEquals(0, renderer.formatReadCount); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java index 7b3292db89..62e950091b 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.testutil; import android.os.Handler; import android.os.HandlerThread; -import android.os.SystemClock; import android.support.annotation.Nullable; import com.google.android.exoplayer2.DefaultLoadControl; import com.google.android.exoplayer2.ExoPlaybackException; @@ -405,9 +404,9 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener /** * Blocks the current thread until the test runner finishes. A test is deemed to be finished when - * the action schedule finished and the playback state transitioned to {@link Player#STATE_ENDED} - * or {@link Player#STATE_IDLE} for the specified number of times. The test also finishes when an - * {@link ExoPlaybackException} is thrown. + * the playback state transitioned to {@link Player#STATE_ENDED} or {@link Player#STATE_IDLE} for + * the specified number of times. The test also finishes when an {@link ExoPlaybackException} is + * thrown. * * @param timeoutMs The maximum time to wait for the test runner to finish. If this time elapsed * the method will throw a {@link TimeoutException}. @@ -415,13 +414,6 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener * @throws Exception If any exception occurred during playback, release, or due to a timeout. */ public ExoPlayerTestRunner blockUntilEnded(long timeoutMs) throws Exception { - long deadlineMs = SystemClock.elapsedRealtime() + timeoutMs; - try { - blockUntilActionScheduleFinished(timeoutMs); - } catch (TimeoutException error) { - exception = error; - } - timeoutMs = Math.max(0, deadlineMs - SystemClock.elapsedRealtime()); if (!endedCountDownLatch.await(timeoutMs, TimeUnit.MILLISECONDS)) { exception = new TimeoutException("Test playback timed out waiting for playback to end."); } From a0f6bba842d8aeb8716c31a2ff2a4b207b970861 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 27 Nov 2017 12:42:53 -0800 Subject: [PATCH 0786/2472] Force wrapping of HLS ID3 timestamp Merge of https://github.com/google/ExoPlayer/pull/3495 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177057183 --- .../com/google/android/exoplayer2/source/hls/HlsMediaChunk.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/hls/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 index 83167c152f..1ad5acc5c5 100644 --- a/library/hls/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 @@ -306,7 +306,7 @@ import java.util.concurrent.atomic.AtomicInteger; if (PRIV_TIMESTAMP_FRAME_OWNER.equals(privFrame.owner)) { System.arraycopy(privFrame.privateData, 0, id3Data.data, 0, 8 /* timestamp size */); id3Data.reset(8); - return id3Data.readLong() & ((1L << 33) - 1L); + return id3Data.readLong() & 0x1FFFFFFFFL; } } } From 117608edef8c896f9496f0ed3651d0826c4ccf28 Mon Sep 17 00:00:00 2001 From: ojw28 Date: Tue, 28 Nov 2017 17:02:04 +0000 Subject: [PATCH 0787/2472] Update ISSUE_TEMPLATE --- ISSUE_TEMPLATE | 2 -- 1 file changed, 2 deletions(-) diff --git a/ISSUE_TEMPLATE b/ISSUE_TEMPLATE index 1b912312d1..e85c0c28c7 100644 --- a/ISSUE_TEMPLATE +++ b/ISSUE_TEMPLATE @@ -1,5 +1,3 @@ -*** ISSUES THAT IGNORE THIS TEMPLATE WILL BE CLOSED WITHOUT INVESTIGATION *** - Before filing an issue: ----------------------- - Search existing issues, including issues that are closed. From ad16efdf5688b1f25fb3d6f694b41d9c928287e3 Mon Sep 17 00:00:00 2001 From: Pavel Stambrecht Date: Mon, 4 Dec 2017 15:45:54 +0100 Subject: [PATCH 0788/2472] Iso8601Parser improved to be able to parse timestamp offsets from UTC --- .../source/dash/DashMediaSource.java | 36 ++++++++++++++----- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/library/dash/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 index 68d39b5a18..fbaf9ea111 100644 --- a/library/dash/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 @@ -962,19 +962,39 @@ public final class DashMediaSource implements MediaSource { private static final class Iso8601Parser implements ParsingLoadable.Parser { + private static final String ISO_8601_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'"; + private static final String ISO_8601_FORMAT_2 = "yyyy-MM-dd'T'HH:mm:ssZ"; + private static final String ISO_8601_FORMAT_3 = "yyyy-MM-dd'T'HH:mm:ssZ"; + private static final String ISO_8601_FORMAT_2_REGEX_PATTERN = ".*[+\\-]\\d{2}:\\d{2}$"; + private static final String ISO_8601_FORMAT_3_REGEX_PATTERN = ".*[+\\-]\\d{4}$"; + @Override public Long parse(Uri uri, InputStream inputStream) throws IOException { String firstLine = new BufferedReader(new InputStreamReader(inputStream)).readLine(); - try { - // TODO: It may be necessary to handle timestamp offsets from UTC. - SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US); - format.setTimeZone(TimeZone.getTimeZone("UTC")); - return format.parse(firstLine).getTime(); - } catch (ParseException e) { - throw new ParserException(e); + + if (firstLine != null) { + //determine format pattern + String formatPattern; + if (firstLine.matches(ISO_8601_FORMAT_2_REGEX_PATTERN)) { + formatPattern = ISO_8601_FORMAT_2; + } else if (firstLine.matches(ISO_8601_FORMAT_3_REGEX_PATTERN)) { + formatPattern = ISO_8601_FORMAT_3; + } else { + formatPattern = ISO_8601_FORMAT; + } + //parse + try { + SimpleDateFormat format = new SimpleDateFormat(formatPattern, Locale.US); + format.setTimeZone(TimeZone.getTimeZone("UTC")); + return format.parse(firstLine).getTime(); + } catch (ParseException e) { + throw new ParserException(e); + } + + } else { + throw new ParserException("Unable to parse ISO 8601. Input value is null"); } } - } } From 28d709aa8f7b5a4c57f0e68515b3a099e3da4a57 Mon Sep 17 00:00:00 2001 From: Pavel Stambrecht Date: Mon, 4 Dec 2017 15:52:12 +0100 Subject: [PATCH 0789/2472] Iso8601Parser improved to be able to parse timestamp offsets from UTC --- .../source/dash/DashMediaSource.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/library/dash/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 index fbaf9ea111..e2143b4bf5 100644 --- a/library/dash/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 @@ -963,10 +963,9 @@ public final class DashMediaSource implements MediaSource { private static final class Iso8601Parser implements ParsingLoadable.Parser { private static final String ISO_8601_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'"; - private static final String ISO_8601_FORMAT_2 = "yyyy-MM-dd'T'HH:mm:ssZ"; - private static final String ISO_8601_FORMAT_3 = "yyyy-MM-dd'T'HH:mm:ssZ"; - private static final String ISO_8601_FORMAT_2_REGEX_PATTERN = ".*[+\\-]\\d{2}:\\d{2}$"; - private static final String ISO_8601_FORMAT_3_REGEX_PATTERN = ".*[+\\-]\\d{4}$"; + private static final String ISO_8601_WITH_OFFSET_FORMAT = "yyyy-MM-dd'T'HH:mm:ssZ"; + private static final String ISO_8601_WITH_OFFSET_FORMAT_REGEX_PATTERN = ".*[+\\-]\\d{2}:\\d{2}$"; + private static final String ISO_8601_WITH_OFFSET_FORMAT_REGEX_PATTERN_2 = ".*[+\\-]\\d{4}$"; @Override public Long parse(Uri uri, InputStream inputStream) throws IOException { @@ -975,10 +974,10 @@ public final class DashMediaSource implements MediaSource { if (firstLine != null) { //determine format pattern String formatPattern; - if (firstLine.matches(ISO_8601_FORMAT_2_REGEX_PATTERN)) { - formatPattern = ISO_8601_FORMAT_2; - } else if (firstLine.matches(ISO_8601_FORMAT_3_REGEX_PATTERN)) { - formatPattern = ISO_8601_FORMAT_3; + if (firstLine.matches(ISO_8601_WITH_OFFSET_FORMAT_REGEX_PATTERN)) { + formatPattern = ISO_8601_WITH_OFFSET_FORMAT; + } else if (firstLine.matches(ISO_8601_WITH_OFFSET_FORMAT_REGEX_PATTERN_2)) { + formatPattern = ISO_8601_WITH_OFFSET_FORMAT; } else { formatPattern = ISO_8601_FORMAT; } @@ -995,6 +994,7 @@ public final class DashMediaSource implements MediaSource { throw new ParserException("Unable to parse ISO 8601. Input value is null"); } } - } + } + } From d84398788ab67871c4a511df06248464c412e5dc Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 27 Nov 2017 13:32:44 -0800 Subject: [PATCH 0790/2472] Update moe equivalence ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177063576 --- .../com/google/android/exoplayer2/source/hls/HlsMediaChunk.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/library/hls/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 index 1ad5acc5c5..c4e54d4bd3 100644 --- a/library/hls/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 @@ -306,6 +306,8 @@ import java.util.concurrent.atomic.AtomicInteger; if (PRIV_TIMESTAMP_FRAME_OWNER.equals(privFrame.owner)) { System.arraycopy(privFrame.privateData, 0, id3Data.data, 0, 8 /* timestamp size */); id3Data.reset(8); + // The top 31 bits should be zeros, but explicitly zero them to wrap in the case that the + // streaming provider forgot. See: https://github.com/google/ExoPlayer/pull/3495. return id3Data.readLong() & 0x1FFFFFFFFL; } } From f46cb907b7f88eb753497ff4bb92ceb694025329 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 28 Nov 2017 01:53:15 -0800 Subject: [PATCH 0791/2472] Add stop with position reset to Player interface. The ExoPlayerImpl implementation forwards the stop request with this optional parameter. To ensure correct masking (e.g. when timeline updates arrive after calling reset in ExoPlayerImpl but before resetInternal in ExoPlayerImplInternal), we use the existing prepareAck counter and extend it also count stop operations. For this to work, we also return the updated empty timeline after finishing the reset. The CastPlayer doesn't support the two reset options so far. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177132107 --- RELEASENOTES.md | 3 +- .../exoplayer2/ext/cast/CastPlayer.java | 6 + .../android/exoplayer2/ExoPlayerTest.java | 154 ++++++++++++++++++ .../android/exoplayer2/ExoPlayerImpl.java | 85 +++++----- .../exoplayer2/ExoPlayerImplInternal.java | 44 +++-- .../com/google/android/exoplayer2/Player.java | 24 ++- .../android/exoplayer2/SimpleExoPlayer.java | 8 + .../android/exoplayer2/testutil/Action.java | 54 +++++- .../exoplayer2/testutil/ActionSchedule.java | 21 +++ .../testutil/FakeSimpleExoPlayer.java | 11 +- .../exoplayer2/testutil/StubExoPlayer.java | 5 + 11 files changed, 349 insertions(+), 66 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index dd4a6ce655..2c07ad6118 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -18,11 +18,12 @@ use this with `FfmpegAudioRenderer`. * Support extraction and decoding of Dolby Atmos ([#2465](https://github.com/google/ExoPlayer/issues/2465)). -* Added a reason to `EventListener.onTimelineChanged` to distinguish between +* Add a reason to `EventListener.onTimelineChanged` to distinguish between initial preparation, reset and dynamic updates. * DefaultTrackSelector: Support undefined language text track selection when the preferred language is not available ([#2980](https://github.com/google/ExoPlayer/issues/2980)). +* Add optional parameter to `Player.stop` to reset the player when stopping. ### 2.6.0 ### diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java index 32e064e834..92e36c7f2d 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java @@ -359,7 +359,13 @@ public final class CastPlayer implements Player { @Override public void stop() { + stop(/* reset= */ false); + } + + @Override + public void stop(boolean reset) { if (remoteMediaClient != null) { + // TODO(b/69792021): Support or emulate stop without position reset. remoteMediaClient.stop(); } } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index 27e4a97ac5..4c5ac1ac0f 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -621,4 +621,158 @@ public final class ExoPlayerTest extends TestCase { new ExoPlayerTestRunner.Builder().setMediaSource(mediaSource).setActionSchedule(actionSchedule) .build().start().blockUntilEnded(TIMEOUT_MS); } + + public void testStopDoesNotResetPosition() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + ActionSchedule actionSchedule = new ActionSchedule.Builder("testStopDoesNotResetPosition") + .waitForPlaybackState(Player.STATE_READY) + .stop() + .build(); + ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + testRunner.assertTimelinesEqual(timeline); + testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); + testRunner.assertNoPositionDiscontinuities(); + } + + public void testStopWithoutResetDoesNotResetPosition() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + ActionSchedule actionSchedule = new ActionSchedule.Builder("testStopWithoutResetDoesNotReset") + .waitForPlaybackState(Player.STATE_READY) + .stop(/* reset= */ false) + .build(); + ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + testRunner.assertTimelinesEqual(timeline); + testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); + testRunner.assertNoPositionDiscontinuities(); + } + + public void testStopWithResetDoesResetPosition() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + ActionSchedule actionSchedule = new ActionSchedule.Builder("testStopWithResetDoesReset") + .waitForPlaybackState(Player.STATE_READY) + .stop(/* reset= */ true) + .build(); + ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + testRunner.assertTimelinesEqual(timeline, Timeline.EMPTY); + testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED, + Player.TIMELINE_CHANGE_REASON_RESET); + testRunner.assertNoPositionDiscontinuities(); + } + + public void testStopWithoutResetReleasesMediaSource() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + final FakeMediaSource mediaSource = + new FakeMediaSource(timeline, /* manifest= */ null, Builder.VIDEO_FORMAT); + ActionSchedule actionSchedule = new ActionSchedule.Builder("testStopReleasesMediaSource") + .waitForPlaybackState(Player.STATE_READY) + .stop(/* reset= */ false) + .build(); + ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS); + mediaSource.assertReleased(); + testRunner.blockUntilEnded(TIMEOUT_MS); + } + + public void testStopWithResetReleasesMediaSource() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + final FakeMediaSource mediaSource = + new FakeMediaSource(timeline, /* manifest= */ null, Builder.VIDEO_FORMAT); + ActionSchedule actionSchedule = new ActionSchedule.Builder("testStopReleasesMediaSource") + .waitForPlaybackState(Player.STATE_READY) + .stop(/* reset= */ true) + .build(); + ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS); + mediaSource.assertReleased(); + testRunner.blockUntilEnded(TIMEOUT_MS); + } + + public void testRepreparationDoesNotResetAfterStopWithReset() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + MediaSource secondSource = new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT); + ActionSchedule actionSchedule = new ActionSchedule.Builder("testRepreparationAfterStop") + .waitForPlaybackState(Player.STATE_READY) + .stop(/* reset= */ true) + .waitForPlaybackState(Player.STATE_IDLE) + .prepareSource(secondSource) + .build(); + ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .setExpectedPlayerEndedCount(2) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + testRunner.assertTimelinesEqual(timeline, Timeline.EMPTY, timeline); + testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED, + Player.TIMELINE_CHANGE_REASON_RESET, Player.TIMELINE_CHANGE_REASON_PREPARED); + testRunner.assertNoPositionDiscontinuities(); + } + + public void testSeekBeforeRepreparationPossibleAfterStopWithReset() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + Timeline secondTimeline = new FakeTimeline(/* windowCount= */ 2); + MediaSource secondSource = new FakeMediaSource(secondTimeline, null, Builder.VIDEO_FORMAT); + ActionSchedule actionSchedule = new ActionSchedule.Builder("testSeekAfterStopWithReset") + .waitForPlaybackState(Player.STATE_READY) + .stop(/* reset= */ true) + .waitForPlaybackState(Player.STATE_IDLE) + // If we were still using the first timeline, this would throw. + .seek(/* windowIndex= */ 1, /* positionMs= */ 0) + .prepareSource(secondSource) + .build(); + ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .setExpectedPlayerEndedCount(2) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + testRunner.assertTimelinesEqual(timeline, Timeline.EMPTY, secondTimeline); + testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED, + Player.TIMELINE_CHANGE_REASON_RESET, Player.TIMELINE_CHANGE_REASON_PREPARED); + testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_SEEK); + testRunner.assertPlayedPeriodIndices(0, 1); + } + + public void testStopDuringPreparationOverwritesPreparation() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + ActionSchedule actionSchedule = new ActionSchedule.Builder("testStopOverwritesPrepare") + .waitForPlaybackState(Player.STATE_BUFFERING) + .stop(true) + .build(); + ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + testRunner.assertTimelinesEqual(Timeline.EMPTY); + testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); + testRunner.assertNoPositionDiscontinuities(); + } + } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index 77131f5ded..34dffd0e73 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -55,7 +55,7 @@ import java.util.concurrent.CopyOnWriteArraySet; private boolean shuffleModeEnabled; private int playbackState; private int pendingSeekAcks; - private int pendingPrepareAcks; + private int pendingPrepareOrStopAcks; private boolean waitingForInitialTimeline; private boolean isLoading; private TrackGroupArray trackGroups; @@ -134,35 +134,9 @@ import java.util.concurrent.CopyOnWriteArraySet; @Override public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) { - if (!resetPosition) { - maskingWindowIndex = getCurrentWindowIndex(); - maskingPeriodIndex = getCurrentPeriodIndex(); - maskingWindowPositionMs = getCurrentPosition(); - } else { - maskingWindowIndex = 0; - maskingPeriodIndex = 0; - maskingWindowPositionMs = 0; - } - if (resetState) { - if (!playbackInfo.timeline.isEmpty() || playbackInfo.manifest != null) { - playbackInfo = playbackInfo.copyWithTimeline(Timeline.EMPTY, null); - for (Player.EventListener listener : listeners) { - listener.onTimelineChanged(playbackInfo.timeline, playbackInfo.manifest, - Player.TIMELINE_CHANGE_REASON_RESET); - } - } - if (tracksSelected) { - tracksSelected = false; - trackGroups = TrackGroupArray.EMPTY; - trackSelections = emptyTrackSelections; - trackSelector.onSelectionActivated(null); - for (Player.EventListener listener : listeners) { - listener.onTracksChanged(trackGroups, trackSelections); - } - } - } waitingForInitialTimeline = true; - pendingPrepareAcks++; + pendingPrepareOrStopAcks++; + reset(resetPosition, resetState); internalPlayer.prepare(mediaSource, resetPosition); } @@ -286,7 +260,14 @@ import java.util.concurrent.CopyOnWriteArraySet; @Override public void stop() { - internalPlayer.stop(); + stop(/* reset= */ false); + } + + @Override + public void stop(boolean reset) { + pendingPrepareOrStopAcks++; + reset(/* resetPosition= */ reset, /* resetState= */ reset); + internalPlayer.stop(reset); } @Override @@ -468,14 +449,14 @@ import java.util.concurrent.CopyOnWriteArraySet; break; } case ExoPlayerImplInternal.MSG_SOURCE_INFO_REFRESHED: { - int prepareAcks = msg.arg1; + int prepareOrStopAcks = msg.arg1; int seekAcks = msg.arg2; - handlePlaybackInfo((PlaybackInfo) msg.obj, prepareAcks, seekAcks, false, + handlePlaybackInfo((PlaybackInfo) msg.obj, prepareOrStopAcks, seekAcks, false, /* ignored */ DISCONTINUITY_REASON_INTERNAL); break; } case ExoPlayerImplInternal.MSG_TRACKS_CHANGED: { - if (pendingPrepareAcks == 0) { + if (pendingPrepareOrStopAcks == 0) { TrackSelectorResult trackSelectorResult = (TrackSelectorResult) msg.obj; tracksSelected = true; trackGroups = trackSelectorResult.groups; @@ -520,12 +501,12 @@ import java.util.concurrent.CopyOnWriteArraySet; } } - private void handlePlaybackInfo(PlaybackInfo playbackInfo, int prepareAcks, int seekAcks, + private void handlePlaybackInfo(PlaybackInfo playbackInfo, int prepareOrStopAcks, int seekAcks, boolean positionDiscontinuity, @DiscontinuityReason int positionDiscontinuityReason) { Assertions.checkNotNull(playbackInfo.timeline); - pendingPrepareAcks -= prepareAcks; + pendingPrepareOrStopAcks -= prepareOrStopAcks; pendingSeekAcks -= seekAcks; - if (pendingPrepareAcks == 0 && pendingSeekAcks == 0) { + if (pendingPrepareOrStopAcks == 0 && pendingSeekAcks == 0) { boolean timelineOrManifestChanged = this.playbackInfo.timeline != playbackInfo.timeline || this.playbackInfo.manifest != playbackInfo.manifest; this.playbackInfo = playbackInfo; @@ -556,6 +537,36 @@ import java.util.concurrent.CopyOnWriteArraySet; } } + private void reset(boolean resetPosition, boolean resetState) { + if (resetPosition) { + maskingWindowIndex = 0; + maskingPeriodIndex = 0; + maskingWindowPositionMs = 0; + } else { + maskingWindowIndex = getCurrentWindowIndex(); + maskingPeriodIndex = getCurrentPeriodIndex(); + maskingWindowPositionMs = getCurrentPosition(); + } + if (resetState) { + if (!playbackInfo.timeline.isEmpty() || playbackInfo.manifest != null) { + playbackInfo = playbackInfo.copyWithTimeline(Timeline.EMPTY, null); + for (Player.EventListener listener : listeners) { + listener.onTimelineChanged(playbackInfo.timeline, playbackInfo.manifest, + Player.TIMELINE_CHANGE_REASON_RESET); + } + } + if (tracksSelected) { + tracksSelected = false; + trackGroups = TrackGroupArray.EMPTY; + trackSelections = emptyTrackSelections; + trackSelector.onSelectionActivated(null); + for (Player.EventListener listener : listeners) { + listener.onTracksChanged(trackGroups, trackSelections); + } + } + } + } + private long playbackInfoPositionUsToWindowPositionMs(long positionUs) { long positionMs = C.usToMs(positionUs); if (!playbackInfo.periodId.isAd()) { @@ -566,7 +577,7 @@ import java.util.concurrent.CopyOnWriteArraySet; } private boolean shouldMaskPosition() { - return playbackInfo.timeline.isEmpty() || pendingSeekAcks > 0 || pendingPrepareAcks > 0; + return playbackInfo.timeline.isEmpty() || pendingSeekAcks > 0 || pendingPrepareOrStopAcks > 0; } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 4e37211e80..f62d36e48b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -196,8 +196,8 @@ import java.io.IOException; handler.obtainMessage(MSG_SET_PLAYBACK_PARAMETERS, playbackParameters).sendToTarget(); } - public void stop() { - handler.sendEmptyMessage(MSG_STOP); + public void stop(boolean reset) { + handler.obtainMessage(MSG_STOP, reset ? 1 : 0, 0).sendToTarget(); } public void sendMessages(ExoPlayerMessage... messages) { @@ -324,7 +324,7 @@ import java.io.IOException; return true; } case MSG_STOP: { - stopInternal(); + stopInternal(/* reset= */ msg.arg1 != 0); return true; } case MSG_RELEASE: { @@ -357,18 +357,18 @@ import java.io.IOException; } catch (ExoPlaybackException e) { Log.e(TAG, "Renderer error.", e); eventHandler.obtainMessage(MSG_ERROR, e).sendToTarget(); - stopInternal(); + stopInternal(/* reset= */ false); return true; } catch (IOException e) { Log.e(TAG, "Source error.", e); eventHandler.obtainMessage(MSG_ERROR, ExoPlaybackException.createForSource(e)).sendToTarget(); - stopInternal(); + stopInternal(/* reset= */ false); return true; } catch (RuntimeException e) { Log.e(TAG, "Internal runtime error.", e); eventHandler.obtainMessage(MSG_ERROR, ExoPlaybackException.createForUnexpected(e)) .sendToTarget(); - stopInternal(); + stopInternal(/* reset= */ false); return true; } } @@ -394,8 +394,8 @@ import java.io.IOException; resetInternal(/* releaseMediaSource= */ true, resetPosition); loadControl.onPrepared(); this.mediaSource = mediaSource; - mediaSource.prepareSource(player, /* isTopLevelSource= */ true, /* listener = */ this); setState(Player.STATE_BUFFERING); + mediaSource.prepareSource(player, /* isTopLevelSource= */ true, /* listener = */ this); handler.sendEmptyMessage(MSG_DO_SOME_WORK); } @@ -765,8 +765,23 @@ import java.io.IOException; mediaClock.setPlaybackParameters(playbackParameters); } - private void stopInternal() { - resetInternal(/* releaseMediaSource= */ true, /* resetPosition= */ false); + private void stopInternal(boolean reset) { + // Releasing the internal player sets the timeline to null. Use the current timeline or + // Timeline.EMPTY for notifying the eventHandler. + Timeline publicTimeline = reset || playbackInfo.timeline == null + ? Timeline.EMPTY : playbackInfo.timeline; + Object publicManifest = reset ? null : playbackInfo.manifest; + resetInternal(/* releaseMediaSource= */ true, reset); + PlaybackInfo publicPlaybackInfo = playbackInfo.copyWithTimeline(publicTimeline, publicManifest); + if (reset) { + // When resetting the state, set the playback position to 0 (instead of C.TIME_UNSET) for + // notifying the eventHandler. + publicPlaybackInfo = + publicPlaybackInfo.fromNewPosition(playbackInfo.periodId.periodIndex, 0, C.TIME_UNSET); + } + int prepareOrStopAcks = pendingPrepareCount + 1; + pendingPrepareCount = 0; + notifySourceInfoRefresh(prepareOrStopAcks, 0, publicPlaybackInfo); loadControl.onStopped(); setState(Player.STATE_IDLE); } @@ -1170,13 +1185,14 @@ import java.io.IOException; notifySourceInfoRefresh(0, 0); } - private void notifySourceInfoRefresh(int prepareAcks, int seekAcks) { - notifySourceInfoRefresh(prepareAcks, seekAcks, playbackInfo); + private void notifySourceInfoRefresh(int prepareOrStopAcks, int seekAcks) { + notifySourceInfoRefresh(prepareOrStopAcks, seekAcks, playbackInfo); } - private void notifySourceInfoRefresh(int prepareAcks, int seekAcks, PlaybackInfo playbackInfo) { - eventHandler.obtainMessage(MSG_SOURCE_INFO_REFRESHED, prepareAcks, seekAcks, playbackInfo) - .sendToTarget(); + private void notifySourceInfoRefresh(int prepareOrStopAcks, int seekAcks, + PlaybackInfo playbackInfo) { + eventHandler.obtainMessage(MSG_SOURCE_INFO_REFRESHED, prepareOrStopAcks, seekAcks, + playbackInfo).sendToTarget(); } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Player.java b/library/core/src/main/java/com/google/android/exoplayer2/Player.java index a036a2021d..b3ae4c28c6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Player.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Player.java @@ -429,17 +429,29 @@ public interface Player { PlaybackParameters getPlaybackParameters(); /** - * Stops playback. Use {@code setPlayWhenReady(false)} rather than this method if the intention - * is to pause playback. - *

          - * Calling this method will cause the playback state to transition to {@link #STATE_IDLE}. The + * Stops playback without resetting the player. Use {@code setPlayWhenReady(false)} rather than + * this method if the intention is to pause playback. + * + *

          Calling this method will cause the playback state to transition to {@link #STATE_IDLE}. The * player instance can still be used, and {@link #release()} must still be called on the player if * it's no longer required. - *

          - * Calling this method does not reset the playback position. + * + *

          Calling this method does not reset the playback position. */ void stop(); + /** + * Stops playback and optionally resets the player. Use {@code setPlayWhenReady(false)} rather + * than this method if the intention is to pause playback. + * + *

          Calling this method will cause the playback state to transition to {@link #STATE_IDLE}. The + * player instance can still be used, and {@link #release()} must still be called on the player if + * it's no longer required. + * + * @param reset Whether the player should be reset. + */ + void stop(boolean reset); + /** * Releases the player. This method must be called when the player is no longer required. The * player must not be used after calling this method. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 5a5a948d58..a153e4ed43 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -133,6 +133,9 @@ public class SimpleExoPlayer implements ExoPlayer { case C.TRACK_TYPE_AUDIO: audioRendererCount++; break; + default: + // Don't count other track types. + break; } } this.videoRendererCount = videoRendererCount; @@ -692,6 +695,11 @@ public class SimpleExoPlayer implements ExoPlayer { player.stop(); } + @Override + public void stop(boolean reset) { + player.stop(reset); + } + @Override public void release() { player.release(); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java index 003d08cd59..ff0b8a6bc0 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java @@ -89,45 +89,89 @@ public abstract class Action { Surface surface); /** - * Calls {@link Player#seekTo(long)}. + * Calls {@link Player#seekTo(long)} or {@link Player#seekTo(int, long)}. */ public static final class Seek extends Action { + private final Integer windowIndex; private final long positionMs; /** + * Action calls {@link Player#seekTo(long)}. + * * @param tag A tag to use for logging. * @param positionMs The seek position. */ public Seek(String tag, long positionMs) { super(tag, "Seek:" + positionMs); + this.windowIndex = null; + this.positionMs = positionMs; + } + + /** + * Action calls {@link Player#seekTo(int, long)}. + * + * @param tag A tag to use for logging. + * @param windowIndex The window to seek to. + * @param positionMs The seek position. + */ + public Seek(String tag, int windowIndex, long positionMs) { + super(tag, "Seek:" + positionMs); + this.windowIndex = windowIndex; this.positionMs = positionMs; } @Override protected void doActionImpl(SimpleExoPlayer player, MappingTrackSelector trackSelector, Surface surface) { - player.seekTo(positionMs); + if (windowIndex == null) { + player.seekTo(positionMs); + } else { + player.seekTo(windowIndex, positionMs); + } } } /** - * Calls {@link Player#stop()}. + * Calls {@link Player#stop()} or {@link Player#stop(boolean)}. */ public static final class Stop extends Action { + private static final String STOP_ACTION_TAG = "Stop"; + + private final Boolean reset; + /** + * Action will call {@link Player#stop()}. + * * @param tag A tag to use for logging. */ public Stop(String tag) { - super(tag, "Stop"); + super(tag, STOP_ACTION_TAG); + this.reset = null; + } + + /** + * Action will call {@link Player#stop(boolean)}. + * + * @param tag A tag to use for logging. + * @param reset The value to pass to {@link Player#stop(boolean)}. + */ + public Stop(String tag, boolean reset) { + super(tag, STOP_ACTION_TAG); + this.reset = reset; } @Override protected void doActionImpl(SimpleExoPlayer player, MappingTrackSelector trackSelector, Surface surface) { - player.stop(); + if (reset == null) { + player.stop(); + } else { + player.stop(reset); + } + } } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java index 5e3d6bcb9a..abca2cafdb 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java @@ -160,6 +160,17 @@ public final class ActionSchedule { return apply(new Seek(tag, positionMs)); } + /** + * Schedules a seek action to be executed. + * + * @param windowIndex The window to seek to. + * @param positionMs The seek position. + * @return The builder, for convenience. + */ + public Builder seek(int windowIndex, long positionMs) { + return apply(new Seek(tag, windowIndex, positionMs)); + } + /** * Schedules a seek action to be executed and waits until playback resumes after the seek. * @@ -192,6 +203,16 @@ public final class ActionSchedule { return apply(new Stop(tag)); } + /** + * Schedules a stop action to be executed. + * + * @param reset Whether the player should be reset. + * @return The builder, for convenience. + */ + public Builder stop(boolean reset) { + return apply(new Stop(tag, reset)); + } + /** * Schedules a play action to be executed. * diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java index 58f19ace1e..0358e5d980 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java @@ -166,13 +166,18 @@ public class FakeSimpleExoPlayer extends SimpleExoPlayer { @Override public void stop() { - stop(/* quitPlaybackThread= */ false); + stop(/* reset= */ false); + } + + @Override + public void stop(boolean reset) { + stopPlayback(/* quitPlaybackThread= */ false); } @Override @SuppressWarnings("ThreadJoinLoop") public void release() { - stop(/* quitPlaybackThread= */ true); + stopPlayback(/* quitPlaybackThread= */ true); while (playbackThread.isAlive()) { try { playbackThread.join(); @@ -513,7 +518,7 @@ public class FakeSimpleExoPlayer extends SimpleExoPlayer { } } - private void stop(final boolean quitPlaybackThread) { + private void stopPlayback(final boolean quitPlaybackThread) { playbackHandler.post(new Runnable() { @Override public void run () { diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java index e03f6fbad9..0d94b8fa03 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java @@ -130,6 +130,11 @@ public abstract class StubExoPlayer implements ExoPlayer { throw new UnsupportedOperationException(); } + @Override + public void stop(boolean resetStateAndPosition) { + throw new UnsupportedOperationException(); + } + @Override public void release() { throw new UnsupportedOperationException(); From 1ae50cb9e56e6b6a00606f1fac3066d2461db17d Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 28 Nov 2017 03:58:30 -0800 Subject: [PATCH 0792/2472] Add some clarifications to MediaSource documentation ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177141094 --- .../android/exoplayer2/source/MediaSource.java | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java index 7288b39897..4a0d8e196d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java @@ -35,7 +35,8 @@ import java.io.IOException; * player to load and read the media. * * All methods are called on the player's internal playback thread, as described in the - * {@link ExoPlayer} Javadoc. + * {@link ExoPlayer} Javadoc. They should not be called directly from application code. Instances + * should not be re-used, meaning they should be passed to {@link ExoPlayer#prepare} at most once. */ public interface MediaSource { @@ -150,6 +151,8 @@ public interface MediaSource { /** * Starts preparation of the source. + *

          + * Should not be called directly from application code. * * @param player The player for which this source is being prepared. * @param isTopLevelSource Whether this source has been passed directly to @@ -162,6 +165,8 @@ public interface MediaSource { /** * Throws any pending error encountered while loading or refreshing source information. + *

          + * Should not be called directly from application code. */ void maybeThrowSourceInfoRefreshError() throws IOException; @@ -169,6 +174,8 @@ public interface MediaSource { * Returns a new {@link MediaPeriod} identified by {@code periodId}. This method may be called * multiple times with the same period identifier without an intervening call to * {@link #releasePeriod(MediaPeriod)}. + *

          + * Should not be called directly from application code. * * @param id The identifier of the period. * @param allocator An {@link Allocator} from which to obtain media buffer allocations. @@ -178,6 +185,8 @@ public interface MediaSource { /** * Releases the period. + *

          + * Should not be called directly from application code. * * @param mediaPeriod The period to release. */ @@ -186,8 +195,7 @@ public interface MediaSource { /** * Releases the source. *

          - * This method should be called when the source is no longer required. It may be called in any - * state. + * Should not be called directly from application code. */ void releaseSource(); From efc709f36616d7bdce397f307d69e9fd09b4192b Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 28 Nov 2017 04:40:06 -0800 Subject: [PATCH 0793/2472] Remove initial seek counting in ExoPlayerImplInternal. We can acknoledge seeks before preparation finished immediately now, because ExoPlayerImpl won't leave the masking state until the first prepare operation is processed. As a side effect, it also cleans up the responsibility of the callbacks. Prepares are always acknowledged with a SOURCE_INFO_REFRESHED, while seeks are always acknowledged with a SEEK_ACK. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177144089 --- .../android/exoplayer2/ExoPlayerTest.java | 29 +++++++---- .../android/exoplayer2/ExoPlayerImpl.java | 15 +++--- .../exoplayer2/ExoPlayerImplInternal.java | 48 +++++++++---------- 3 files changed, 49 insertions(+), 43 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index 4c5ac1ac0f..efb7b0e96c 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -306,11 +306,19 @@ public final class ExoPlayerTest extends TestCase { public void testSeekProcessedCallback() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 2); ActionSchedule actionSchedule = new ActionSchedule.Builder("testSeekProcessedCallback") - // Initial seek before timeline preparation finished. - .pause().seek(10).waitForPlaybackState(Player.STATE_READY) - // Re-seek to same position, start playback and wait until playback reaches second window. - .seek(10).play().waitForPositionDiscontinuity() - // Seek twice in concession, expecting the first seek to be replaced. + // Initial seek before timeline preparation started. Expect immediate seek processed while + // the player is still in STATE_IDLE. + .pause().seek(5) + // Wait until the media source starts preparing and issue more initial seeks. Expect only + // one seek processed after the source has been prepared. + .waitForPlaybackState(Player.STATE_BUFFERING).seek(2).seek(10) + // Wait until media source prepared and re-seek to same position. Expect a seek processed + // while still being in STATE_READY. + .waitForPlaybackState(Player.STATE_READY).seek(10) + // Start playback and wait until playback reaches second window. + .play().waitForPositionDiscontinuity() + // Seek twice in concession, expecting the first seek to be replaced (and thus except only + // on seek processed callback). .seek(5).seek(60).build(); final List playbackStatesWhenSeekProcessed = new ArrayList<>(); Player.EventListener eventListener = new Player.DefaultEventListener() { @@ -329,10 +337,11 @@ public final class ExoPlayerTest extends TestCase { new ExoPlayerTestRunner.Builder() .setTimeline(timeline).setEventListener(eventListener).setActionSchedule(actionSchedule) .build().start().blockUntilEnded(TIMEOUT_MS); - assertEquals(3, playbackStatesWhenSeekProcessed.size()); - assertEquals(Player.STATE_BUFFERING, (int) playbackStatesWhenSeekProcessed.get(0)); - assertEquals(Player.STATE_READY, (int) playbackStatesWhenSeekProcessed.get(1)); - assertEquals(Player.STATE_BUFFERING, (int) playbackStatesWhenSeekProcessed.get(2)); + assertEquals(4, playbackStatesWhenSeekProcessed.size()); + assertEquals(Player.STATE_IDLE, (int) playbackStatesWhenSeekProcessed.get(0)); + assertEquals(Player.STATE_BUFFERING, (int) playbackStatesWhenSeekProcessed.get(1)); + assertEquals(Player.STATE_READY, (int) playbackStatesWhenSeekProcessed.get(2)); + assertEquals(Player.STATE_BUFFERING, (int) playbackStatesWhenSeekProcessed.get(3)); } public void testSeekDiscontinuity() throws Exception { @@ -742,7 +751,7 @@ public final class ExoPlayerTest extends TestCase { .waitForPlaybackState(Player.STATE_IDLE) // If we were still using the first timeline, this would throw. .seek(/* windowIndex= */ 1, /* positionMs= */ 0) - .prepareSource(secondSource) + .prepareSource(secondSource, /* resetPosition= */ false, /* resetState= */ true) .build(); ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() .setTimeline(timeline) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index 34dffd0e73..37fccafd08 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -450,8 +450,7 @@ import java.util.concurrent.CopyOnWriteArraySet; } case ExoPlayerImplInternal.MSG_SOURCE_INFO_REFRESHED: { int prepareOrStopAcks = msg.arg1; - int seekAcks = msg.arg2; - handlePlaybackInfo((PlaybackInfo) msg.obj, prepareOrStopAcks, seekAcks, false, + handlePlaybackInfo((PlaybackInfo) msg.obj, prepareOrStopAcks, 0, false, /* ignored */ DISCONTINUITY_REASON_INTERNAL); break; } @@ -510,13 +509,13 @@ import java.util.concurrent.CopyOnWriteArraySet; boolean timelineOrManifestChanged = this.playbackInfo.timeline != playbackInfo.timeline || this.playbackInfo.manifest != playbackInfo.manifest; this.playbackInfo = playbackInfo; - if (playbackInfo.timeline.isEmpty()) { - // Update the masking variables, which are used when the timeline is empty. - maskingPeriodIndex = 0; - maskingWindowIndex = 0; - maskingWindowPositionMs = 0; - } if (timelineOrManifestChanged || waitingForInitialTimeline) { + if (playbackInfo.timeline.isEmpty()) { + // Update the masking variables, which are used when the timeline becomes empty. + maskingPeriodIndex = 0; + maskingWindowIndex = 0; + maskingWindowPositionMs = 0; + } @Player.TimelineChangeReason int reason = waitingForInitialTimeline ? Player.TIMELINE_CHANGE_REASON_PREPARED : Player.TIMELINE_CHANGE_REASON_DYNAMIC; waitingForInitialTimeline = false; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index f62d36e48b..909f52fad8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -125,8 +125,7 @@ import java.io.IOException; private long elapsedRealtimeUs; private int pendingPrepareCount; - private int pendingInitialSeekCount; - private SeekPosition pendingSeekPosition; + private SeekPosition pendingInitialSeekPosition; private long rendererPositionUs; private MediaPeriodHolder loadingPeriodHolder; @@ -631,8 +630,9 @@ import java.io.IOException; private void seekToInternal(SeekPosition seekPosition) throws ExoPlaybackException { Timeline timeline = playbackInfo.timeline; if (timeline == null) { - pendingInitialSeekCount++; - pendingSeekPosition = seekPosition; + pendingInitialSeekPosition = seekPosition; + eventHandler.obtainMessage(MSG_SEEK_ACK, /* seekAdjusted = */ 0, 0, + playbackInfo.copyWithTimeline(Timeline.EMPTY, null)).sendToTarget(); return; } @@ -781,7 +781,7 @@ import java.io.IOException; } int prepareOrStopAcks = pendingPrepareCount + 1; pendingPrepareCount = 0; - notifySourceInfoRefresh(prepareOrStopAcks, 0, publicPlaybackInfo); + notifySourceInfoRefresh(prepareOrStopAcks, publicPlaybackInfo); loadControl.onStopped(); setState(Player.STATE_IDLE); } @@ -825,6 +825,7 @@ import java.io.IOException; ? 0 : timeline.getWindow(timeline.getFirstWindowIndex(shuffleModeEnabled), window) .firstPeriodIndex; + pendingInitialSeekPosition = null; playbackInfo = playbackInfo.fromNewPosition(firstPeriodIndex, C.TIME_UNSET, C.TIME_UNSET); } else { // The new start position is the current playback position. @@ -1009,15 +1010,13 @@ import java.io.IOException; if (oldTimeline == null) { int processedPrepareAcks = pendingPrepareCount; pendingPrepareCount = 0; - if (pendingInitialSeekCount > 0) { - Pair periodPosition = resolveSeekPosition(pendingSeekPosition); - int processedInitialSeekCount = pendingInitialSeekCount; - pendingInitialSeekCount = 0; - pendingSeekPosition = null; + if (pendingInitialSeekPosition != null) { + Pair periodPosition = resolveSeekPosition(pendingInitialSeekPosition); + pendingInitialSeekPosition = null; if (periodPosition == null) { // The seek position was valid for the timeline that it was performed into, but the // timeline has changed and a suitable seek position could not be resolved in the new one. - handleSourceInfoRefreshEndedPlayback(processedPrepareAcks, processedInitialSeekCount); + handleSourceInfoRefreshEndedPlayback(processedPrepareAcks); } else { int periodIndex = periodPosition.first; long positionUs = periodPosition.second; @@ -1025,11 +1024,11 @@ import java.io.IOException; mediaPeriodInfoSequence.resolvePeriodPositionForAds(periodIndex, positionUs); playbackInfo = playbackInfo.fromNewPosition(periodId, periodId.isAd() ? 0 : positionUs, positionUs); - notifySourceInfoRefresh(processedPrepareAcks, processedInitialSeekCount); + notifySourceInfoRefresh(processedPrepareAcks); } } else if (playbackInfo.startPositionUs == C.TIME_UNSET) { if (timeline.isEmpty()) { - handleSourceInfoRefreshEndedPlayback(processedPrepareAcks, 0); + handleSourceInfoRefreshEndedPlayback(processedPrepareAcks); } else { Pair defaultPosition = getPeriodPosition(timeline, timeline.getFirstWindowIndex(shuffleModeEnabled), C.TIME_UNSET); @@ -1039,10 +1038,10 @@ import java.io.IOException; startPositionUs); playbackInfo = playbackInfo.fromNewPosition(periodId, periodId.isAd() ? 0 : startPositionUs, startPositionUs); - notifySourceInfoRefresh(processedPrepareAcks, 0); + notifySourceInfoRefresh(processedPrepareAcks); } } else { - notifySourceInfoRefresh(processedPrepareAcks, 0); + notifySourceInfoRefresh(processedPrepareAcks); } return; } @@ -1169,30 +1168,29 @@ import java.io.IOException; } private void handleSourceInfoRefreshEndedPlayback() { - handleSourceInfoRefreshEndedPlayback(0, 0); + handleSourceInfoRefreshEndedPlayback(0); } - private void handleSourceInfoRefreshEndedPlayback(int prepareAcks, int seekAcks) { + private void handleSourceInfoRefreshEndedPlayback(int prepareAcks) { setState(Player.STATE_ENDED); // Reset, but retain the source so that it can still be used should a seek occur. resetInternal(false, true); // Set the playback position to 0 for notifying the eventHandler (instead of C.TIME_UNSET). - notifySourceInfoRefresh(prepareAcks, seekAcks, + notifySourceInfoRefresh(prepareAcks, playbackInfo.fromNewPosition(playbackInfo.periodId.periodIndex, 0, C.TIME_UNSET)); } private void notifySourceInfoRefresh() { - notifySourceInfoRefresh(0, 0); + notifySourceInfoRefresh(0); } - private void notifySourceInfoRefresh(int prepareOrStopAcks, int seekAcks) { - notifySourceInfoRefresh(prepareOrStopAcks, seekAcks, playbackInfo); + private void notifySourceInfoRefresh(int prepareOrStopAcks) { + notifySourceInfoRefresh(prepareOrStopAcks, playbackInfo); } - private void notifySourceInfoRefresh(int prepareOrStopAcks, int seekAcks, - PlaybackInfo playbackInfo) { - eventHandler.obtainMessage(MSG_SOURCE_INFO_REFRESHED, prepareOrStopAcks, seekAcks, - playbackInfo).sendToTarget(); + private void notifySourceInfoRefresh(int prepareOrStopAcks, PlaybackInfo playbackInfo) { + eventHandler.obtainMessage(MSG_SOURCE_INFO_REFRESHED, prepareOrStopAcks, 0, playbackInfo) + .sendToTarget(); } /** From 20567633a03fb4c90b36e31b8cfad2e25f81f3fd Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 28 Nov 2017 05:02:15 -0800 Subject: [PATCH 0794/2472] Add queue manipulation to the Cast demo Against all odds, samples can be reordered by using drag & drop. Issue:#1706 Issue:#2283 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177145553 --- demos/cast/build.gradle | 1 + .../exoplayer2/castdemo/MainActivity.java | 178 +++++++-- .../exoplayer2/castdemo/PlayerManager.java | 347 ++++++++++++++---- .../res/drawable/ic_add_circle_white_24dp.xml | 20 + .../src/main/res/layout/main_activity.xml | 32 +- .../cast/src/main/res/layout/sample_list.xml | 25 ++ demos/cast/src/main/res/values/strings.xml | 2 + .../exoplayer2/ext/cast/CastPlayer.java | 19 +- 8 files changed, 517 insertions(+), 107 deletions(-) create mode 100644 demos/cast/src/main/res/drawable/ic_add_circle_white_24dp.xml create mode 100644 demos/cast/src/main/res/layout/sample_list.xml diff --git a/demos/cast/build.gradle b/demos/cast/build.gradle index a9fa27ad58..8f074c9238 100644 --- a/demos/cast/build.gradle +++ b/demos/cast/build.gradle @@ -48,4 +48,5 @@ dependencies { compile project(modulePrefix + 'library-smoothstreaming') compile project(modulePrefix + 'library-ui') compile project(modulePrefix + 'extension-cast') + compile 'com.android.support:recyclerview-v7:' + supportLibraryVersion } diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java index 094e9f9e6e..d34888352f 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java @@ -15,37 +15,54 @@ */ package com.google.android.exoplayer2.castdemo; -import android.graphics.Color; +import android.content.Context; import android.os.Bundle; -import android.support.annotation.NonNull; +import android.support.v4.graphics.ColorUtils; +import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.RecyclerView.ViewHolder; +import android.support.v7.widget.helper.ItemTouchHelper; import android.view.KeyEvent; +import android.view.LayoutInflater; import android.view.Menu; import android.view.View; +import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; import android.widget.ArrayAdapter; import android.widget.ListView; +import android.widget.TextView; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.castdemo.DemoUtil.Sample; import com.google.android.exoplayer2.ext.cast.CastPlayer; import com.google.android.exoplayer2.ui.PlaybackControlView; import com.google.android.exoplayer2.ui.SimpleExoPlayerView; import com.google.android.gms.cast.framework.CastButtonFactory; +import com.google.android.gms.cast.framework.CastContext; /** * An activity that plays video using {@link SimpleExoPlayer} and {@link CastPlayer}. */ -public class MainActivity extends AppCompatActivity { +public class MainActivity extends AppCompatActivity implements OnClickListener, + PlayerManager.QueuePositionListener { private SimpleExoPlayerView simpleExoPlayerView; private PlaybackControlView castControlView; private PlayerManager playerManager; + private MediaQueueAdapter listAdapter; + private CastContext castContext; // Activity lifecycle methods. @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + // Getting the cast context later than onStart can cause device discovery not to take place. + castContext = CastContext.getSharedInstance(this); setContentView(R.layout.main_activity); @@ -54,24 +71,30 @@ public class MainActivity extends AppCompatActivity { castControlView = findViewById(R.id.cast_control_view); - ListView sampleList = findViewById(R.id.sample_list); - sampleList.setAdapter(new SampleListAdapter()); - sampleList.setOnItemClickListener(new SampleClickListener()); + RecyclerView sampleList = findViewById(R.id.sample_list); + ItemTouchHelper helper = new ItemTouchHelper(new RecyclerViewCallback()); + helper.attachToRecyclerView(sampleList); + sampleList.setLayoutManager(new LinearLayoutManager(this)); + sampleList.setHasFixedSize(true); + listAdapter = new MediaQueueAdapter(); + sampleList.setAdapter(listAdapter); + + findViewById(R.id.add_sample_button).setOnClickListener(this); } @Override public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); getMenuInflater().inflate(R.menu.menu, menu); - CastButtonFactory.setUpMediaRouteButton(getApplicationContext(), menu, - R.id.media_route_menu_item); + CastButtonFactory.setUpMediaRouteButton(this, menu, R.id.media_route_menu_item); return true; } @Override public void onResume() { super.onResume(); - playerManager = new PlayerManager(simpleExoPlayerView, castControlView, this); + playerManager = PlayerManager.createPlayerManager(this, simpleExoPlayerView, castControlView, + this, castContext); } @Override @@ -89,32 +112,141 @@ public class MainActivity extends AppCompatActivity { return super.dispatchKeyEvent(event) || playerManager.dispatchKeyEvent(event); } - // User controls. + @Override + public void onClick(View view) { + new AlertDialog.Builder(this).setTitle(R.string.sample_list_dialog_title) + .setView(buildSampleListView()).setPositiveButton(android.R.string.ok, null).create() + .show(); + } - private final class SampleListAdapter extends ArrayAdapter { + // PlayerManager.QueuePositionListener implementation. - public SampleListAdapter() { - super(getApplicationContext(), android.R.layout.simple_list_item_1, DemoUtil.SAMPLES); + @Override + public void onQueuePositionChanged(int previousIndex, int newIndex) { + if (previousIndex != C.INDEX_UNSET) { + listAdapter.notifyItemChanged(previousIndex); + } + if (newIndex != C.INDEX_UNSET) { + listAdapter.notifyItemChanged(newIndex); + } + } + + // Internal methods. + + private View buildSampleListView() { + View dialogList = getLayoutInflater().inflate(R.layout.sample_list, null); + ListView sampleList = dialogList.findViewById(R.id.sample_list); + sampleList.setAdapter(new SampleListAdapter(this)); + sampleList.setOnItemClickListener(new OnItemClickListener() { + + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + playerManager.addItem(DemoUtil.SAMPLES.get(position)); + listAdapter.notifyItemInserted(playerManager.getMediaQueueSize() - 1); + } + + }); + return dialogList; + } + + // Internal classes. + + private class QueueItemViewHolder extends RecyclerView.ViewHolder implements OnClickListener { + + public final TextView textView; + + public QueueItemViewHolder(TextView textView) { + super(textView); + this.textView = textView; + textView.setOnClickListener(this); } @Override - @NonNull - public View getView(int position, View convertView, @NonNull ViewGroup parent) { - View view = super.getView(position, convertView, parent); - view.setBackgroundColor(Color.WHITE); - return view; + public void onClick(View v) { + playerManager.selectQueueItem(getAdapterPosition()); } } - private class SampleClickListener implements AdapterView.OnItemClickListener { + private class MediaQueueAdapter extends RecyclerView.Adapter { @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - if (parent.getSelectedItemPosition() != position) { - DemoUtil.Sample currentSample = DemoUtil.SAMPLES.get(position); - playerManager.setCurrentSample(currentSample, 0, true); + public QueueItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + TextView v = (TextView) LayoutInflater.from(parent.getContext()) + .inflate(android.R.layout.simple_list_item_1, parent, false); + return new QueueItemViewHolder(v); + } + + @Override + public void onBindViewHolder(QueueItemViewHolder holder, int position) { + TextView view = holder.textView; + view.setText(playerManager.getItem(position).name); + // TODO: Solve coloring using the theme's ColorStateList. + view.setTextColor(ColorUtils.setAlphaComponent(view.getCurrentTextColor(), + position == playerManager.getCurrentItemIndex() ? 255 : 100)); + } + + @Override + public int getItemCount() { + return playerManager.getMediaQueueSize(); + } + + } + + private class RecyclerViewCallback extends ItemTouchHelper.SimpleCallback { + + private int draggingFromPosition; + private int draggingToPosition; + + public RecyclerViewCallback() { + super(ItemTouchHelper.UP | ItemTouchHelper.DOWN, ItemTouchHelper.START | ItemTouchHelper.END); + draggingFromPosition = C.INDEX_UNSET; + draggingToPosition = C.INDEX_UNSET; + } + + @Override + public boolean onMove(RecyclerView list, RecyclerView.ViewHolder origin, + RecyclerView.ViewHolder target) { + int fromPosition = origin.getAdapterPosition(); + int toPosition = target.getAdapterPosition(); + if (draggingFromPosition == C.INDEX_UNSET) { + // A drag has started, but changes to the media queue will be reflected in clearView(). + draggingFromPosition = fromPosition; } + draggingToPosition = toPosition; + listAdapter.notifyItemMoved(fromPosition, toPosition); + return true; + } + + @Override + public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) { + int position = viewHolder.getAdapterPosition(); + if (playerManager.removeItem(position)) { + listAdapter.notifyItemRemoved(position); + } + } + + @Override + public void clearView(RecyclerView recyclerView, ViewHolder viewHolder) { + super.clearView(recyclerView, viewHolder); + if (draggingFromPosition != C.INDEX_UNSET) { + // A drag has ended. We reflect the media queue change in the player. + if (!playerManager.moveItem(draggingFromPosition, draggingToPosition)) { + // The move failed. The entire sequence of onMove calls since the drag started needs to be + // invalidated. + listAdapter.notifyDataSetChanged(); + } + } + draggingFromPosition = C.INDEX_UNSET; + draggingToPosition = C.INDEX_UNSET; + } + + } + + private static final class SampleListAdapter extends ArrayAdapter { + + public SampleListAdapter(Context context) { + super(context, android.R.layout.simple_list_item_1, DemoUtil.SAMPLES); } } diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java index f00d27a067..0f4adfae99 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java @@ -19,11 +19,19 @@ import android.content.Context; import android.net.Uri; import android.view.KeyEvent; import android.view.View; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.DefaultRenderersFactory; import com.google.android.exoplayer2.ExoPlayerFactory; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Player.DefaultEventListener; +import com.google.android.exoplayer2.Player.DiscontinuityReason; import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.Timeline.Period; +import com.google.android.exoplayer2.castdemo.DemoUtil.Sample; import com.google.android.exoplayer2.ext.cast.CastPlayer; +import com.google.android.exoplayer2.source.DynamicConcatenatingMediaSource; import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.dash.DashMediaSource; @@ -40,14 +48,25 @@ import com.google.android.gms.cast.MediaInfo; import com.google.android.gms.cast.MediaMetadata; import com.google.android.gms.cast.MediaQueueItem; import com.google.android.gms.cast.framework.CastContext; +import java.util.ArrayList; /** - * Manages players for the ExoPlayer/Cast integration app. + * Manages players and an internal media queue for the ExoPlayer/Cast demo app. */ -/* package */ final class PlayerManager implements CastPlayer.SessionAvailabilityListener { +/* package */ final class PlayerManager extends DefaultEventListener + implements CastPlayer.SessionAvailabilityListener { - private static final int PLAYBACK_REMOTE = 1; - private static final int PLAYBACK_LOCAL = 2; + /** + * Listener for changes in the media queue playback position. + */ + public interface QueuePositionListener { + + /** + * Called when the currently played item of the media queue changes. + */ + void onQueuePositionChanged(int previousIndex, int newIndex); + + } private static final String USER_AGENT = "ExoCastDemoPlayer"; private static final DefaultBandwidthMeter BANDWIDTH_METER = new DefaultBandwidthMeter(); @@ -58,64 +77,174 @@ import com.google.android.gms.cast.framework.CastContext; private final PlaybackControlView castControlView; private final SimpleExoPlayer exoPlayer; private final CastPlayer castPlayer; + private final ArrayList mediaQueue; + private final QueuePositionListener listener; - private int playbackLocation; - private DemoUtil.Sample currentSample; + private DynamicConcatenatingMediaSource dynamicConcatenatingMediaSource; + private boolean castMediaQueueCreationPending; + private int currentItemIndex; + private Player currentPlayer; /** + * @param listener A {@link QueuePositionListener} for queue position changes. * @param exoPlayerView The {@link SimpleExoPlayerView} for local playback. * @param castControlView The {@link PlaybackControlView} to control remote playback. * @param context A {@link Context}. + * @param castContext The {@link CastContext}. */ - public PlayerManager(SimpleExoPlayerView exoPlayerView, PlaybackControlView castControlView, - Context context) { + public static PlayerManager createPlayerManager(QueuePositionListener listener, + SimpleExoPlayerView exoPlayerView, PlaybackControlView castControlView, Context context, + CastContext castContext) { + PlayerManager playerManager = new PlayerManager(listener, exoPlayerView, castControlView, + context, castContext); + playerManager.init(); + return playerManager; + } + + private PlayerManager(QueuePositionListener listener, SimpleExoPlayerView exoPlayerView, + PlaybackControlView castControlView, Context context, CastContext castContext) { + this.listener = listener; this.exoPlayerView = exoPlayerView; this.castControlView = castControlView; + mediaQueue = new ArrayList<>(); + currentItemIndex = C.INDEX_UNSET; DefaultTrackSelector trackSelector = new DefaultTrackSelector(BANDWIDTH_METER); RenderersFactory renderersFactory = new DefaultRenderersFactory(context, null); exoPlayer = ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector); + exoPlayer.addListener(this); exoPlayerView.setPlayer(exoPlayer); - castPlayer = new CastPlayer(CastContext.getSharedInstance(context)); + castPlayer = new CastPlayer(castContext); + castPlayer.addListener(this); castPlayer.setSessionAvailabilityListener(this); castControlView.setPlayer(castPlayer); + } - setPlaybackLocation(castPlayer.isCastSessionAvailable() ? PLAYBACK_REMOTE : PLAYBACK_LOCAL); + // Queue manipulation methods. + + /** + * Plays a specified queue item in the current player. + * + * @param itemIndex The index of the item to play. + */ + public void selectQueueItem(int itemIndex) { + setCurrentItem(itemIndex, C.TIME_UNSET, true); } /** - * Starts playback of the given sample at the given position. - * - * @param currentSample The {@link DemoUtil} to play. - * @param positionMs The position at which playback should start. - * @param playWhenReady Whether the player should proceed when ready to do so. + * Returns the index of the currently played item. */ - public void setCurrentSample(DemoUtil.Sample currentSample, long positionMs, - boolean playWhenReady) { - this.currentSample = currentSample; - if (playbackLocation == PLAYBACK_REMOTE) { - castPlayer.loadItem(buildMediaQueueItem(currentSample), positionMs); - castPlayer.setPlayWhenReady(playWhenReady); - } else /* playbackLocation == PLAYBACK_LOCAL */ { - exoPlayer.prepare(buildMediaSource(currentSample), true, true); - exoPlayer.setPlayWhenReady(playWhenReady); - exoPlayer.seekTo(positionMs); + public int getCurrentItemIndex() { + return currentItemIndex; + } + + /** + * Appends {@code sample} to the media queue. + * + * @param sample The {@link Sample} to append. + */ + public void addItem(Sample sample) { + mediaQueue.add(sample); + if (currentPlayer == exoPlayer) { + dynamicConcatenatingMediaSource.addMediaSource(buildMediaSource(sample)); + } else { + castPlayer.addItems(buildMediaQueueItem(sample)); } } /** - * Dispatches a given {@link KeyEvent} to whichever view corresponds according to the current - * playback location. + * Returns the size of the media queue. + */ + public int getMediaQueueSize() { + return mediaQueue.size(); + } + + /** + * Returns the item at the given index in the media queue. + * + * @param position The index of the item. + * @return The item at the given index in the media queue. + */ + public Sample getItem(int position) { + return mediaQueue.get(position); + } + + /** + * Removes the item at the given index from the media queue. + * + * @param itemIndex The index of the item to remove. + * @return Whether the removal was successful. + */ + public boolean removeItem(int itemIndex) { + if (currentPlayer == exoPlayer) { + dynamicConcatenatingMediaSource.removeMediaSource(itemIndex); + } else { + if (castPlayer.getPlaybackState() != Player.STATE_IDLE) { + Timeline castTimeline = castPlayer.getCurrentTimeline(); + if (castTimeline.getPeriodCount() <= itemIndex) { + return false; + } + castPlayer.removeItem((int) castTimeline.getPeriod(itemIndex, new Period()).id); + } + } + mediaQueue.remove(itemIndex); + if (itemIndex == currentItemIndex && itemIndex == mediaQueue.size()) { + maybeSetCurrentItemAndNotify(C.INDEX_UNSET); + } else if (itemIndex < currentItemIndex) { + maybeSetCurrentItemAndNotify(currentItemIndex - 1); + } + return true; + } + + /** + * Moves an item within the queue. + * + * @param fromIndex The index of the item to move. + * @param toIndex The target index of the item in the queue. + * @return Whether the item move was successful. + */ + public boolean moveItem(int fromIndex, int toIndex) { + // Player update. + if (currentPlayer == exoPlayer) { + dynamicConcatenatingMediaSource.moveMediaSource(fromIndex, toIndex); + } else if (castPlayer.getPlaybackState() != Player.STATE_IDLE) { + Timeline castTimeline = castPlayer.getCurrentTimeline(); + int periodCount = castTimeline.getPeriodCount(); + if (periodCount <= fromIndex || periodCount <= toIndex) { + return false; + } + int elementId = (int) castTimeline.getPeriod(fromIndex, new Period()).id; + castPlayer.moveItem(elementId, toIndex); + } + + mediaQueue.add(toIndex, mediaQueue.remove(fromIndex)); + + // Index update. + if (fromIndex == currentItemIndex) { + maybeSetCurrentItemAndNotify(toIndex); + } else if (fromIndex < currentItemIndex && toIndex >= currentItemIndex) { + maybeSetCurrentItemAndNotify(currentItemIndex - 1); + } else if (fromIndex > currentItemIndex && toIndex <= currentItemIndex) { + maybeSetCurrentItemAndNotify(currentItemIndex + 1); + } + + return true; + } + + // Miscellaneous methods. + + /** + * Dispatches a given {@link KeyEvent} to the corresponding view of the current player. * * @param event The {@link KeyEvent}. * @return Whether the event was handled by the target view. */ public boolean dispatchKeyEvent(KeyEvent event) { - if (playbackLocation == PLAYBACK_REMOTE) { - return castControlView.dispatchKeyEvent(event); - } else /* playbackLocation == PLAYBACK_REMOTE */ { + if (currentPlayer == exoPlayer) { return exoPlayerView.dispatchKeyEvent(event); + } else /* currentPlayer == castPlayer */ { + return castControlView.dispatchKeyEvent(event); } } @@ -123,33 +252,136 @@ import com.google.android.gms.cast.framework.CastContext; * Releases the manager and the players that it holds. */ public void release() { + currentItemIndex = C.INDEX_UNSET; + mediaQueue.clear(); castPlayer.setSessionAvailabilityListener(null); castPlayer.release(); exoPlayerView.setPlayer(null); exoPlayer.release(); } + // Player.EventListener implementation. + + @Override + public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + updateCurrentItemIndex(); + } + + @Override + public void onPositionDiscontinuity(@DiscontinuityReason int reason) { + updateCurrentItemIndex(); + } + + @Override + public void onTimelineChanged(Timeline timeline, Object manifest) { + updateCurrentItemIndex(); + } + // CastPlayer.SessionAvailabilityListener implementation. @Override public void onCastSessionAvailable() { - setPlaybackLocation(PLAYBACK_REMOTE); + setCurrentPlayer(castPlayer); } @Override public void onCastSessionUnavailable() { - setPlaybackLocation(PLAYBACK_LOCAL); + setCurrentPlayer(exoPlayer); } // Internal methods. - private static MediaQueueItem buildMediaQueueItem(DemoUtil.Sample sample) { - MediaMetadata movieMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE); - movieMetadata.putString(MediaMetadata.KEY_TITLE, sample.name); - MediaInfo mediaInfo = new MediaInfo.Builder(sample.uri) - .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED).setContentType(sample.mimeType) - .setMetadata(movieMetadata).build(); - return new MediaQueueItem.Builder(mediaInfo).build(); + private void init() { + setCurrentPlayer(castPlayer.isCastSessionAvailable() ? castPlayer : exoPlayer); + } + + private void updateCurrentItemIndex() { + int playbackState = currentPlayer.getPlaybackState(); + maybeSetCurrentItemAndNotify( + playbackState != Player.STATE_IDLE && playbackState != Player.STATE_ENDED + ? currentPlayer.getCurrentWindowIndex() : C.INDEX_UNSET); + } + + private void setCurrentPlayer(Player currentPlayer) { + if (this.currentPlayer == currentPlayer) { + return; + } + + // View management. + if (currentPlayer == exoPlayer) { + exoPlayerView.setVisibility(View.VISIBLE); + castControlView.hide(); + } else /* currentPlayer == castPlayer */ { + exoPlayerView.setVisibility(View.GONE); + castControlView.show(); + } + + // Player state management. + long playbackPositionMs = C.TIME_UNSET; + int windowIndex = C.INDEX_UNSET; + boolean playWhenReady = false; + if (this.currentPlayer != null) { + int playbackState = this.currentPlayer.getPlaybackState(); + if (playbackState != Player.STATE_ENDED) { + playbackPositionMs = this.currentPlayer.getCurrentPosition(); + playWhenReady = this.currentPlayer.getPlayWhenReady(); + windowIndex = this.currentPlayer.getCurrentWindowIndex(); + if (windowIndex != currentItemIndex) { + playbackPositionMs = C.TIME_UNSET; + windowIndex = currentItemIndex; + } + } + this.currentPlayer.stop(true); + } else { + // This is the initial setup. No need to save any state. + } + + this.currentPlayer = currentPlayer; + + // Media queue management. + castMediaQueueCreationPending = currentPlayer == castPlayer; + if (currentPlayer == exoPlayer) { + dynamicConcatenatingMediaSource = new DynamicConcatenatingMediaSource(); + for (int i = 0; i < mediaQueue.size(); i++) { + dynamicConcatenatingMediaSource.addMediaSource(buildMediaSource(mediaQueue.get(i))); + } + exoPlayer.prepare(dynamicConcatenatingMediaSource); + } + + // Playback transition. + if (windowIndex != C.INDEX_UNSET) { + setCurrentItem(windowIndex, playbackPositionMs, playWhenReady); + } + } + + /** + * Starts playback of the item at the given position. + * + * @param itemIndex The index of the item to play. + * @param positionMs The position at which playback should start. + * @param playWhenReady Whether the player should proceed when ready to do so. + */ + private void setCurrentItem(int itemIndex, long positionMs, boolean playWhenReady) { + maybeSetCurrentItemAndNotify(itemIndex); + if (castMediaQueueCreationPending) { + MediaQueueItem[] items = new MediaQueueItem[mediaQueue.size()]; + for (int i = 0; i < items.length; i++) { + items[i] = buildMediaQueueItem(mediaQueue.get(i)); + } + castMediaQueueCreationPending = false; + castPlayer.loadItems(items, itemIndex, positionMs, Player.REPEAT_MODE_OFF); + } else { + currentPlayer.seekTo(itemIndex, positionMs); + currentPlayer.setPlayWhenReady(playWhenReady); + } + } + + private void maybeSetCurrentItemAndNotify(int currentItemIndex) { + if (this.currentItemIndex != currentItemIndex) { + int oldIndex = this.currentItemIndex; + this.currentItemIndex = currentItemIndex; + listener.onQueuePositionChanged(oldIndex, currentItemIndex); + } } private static MediaSource buildMediaSource(DemoUtil.Sample sample) { @@ -177,36 +409,13 @@ import com.google.android.gms.cast.framework.CastContext; } } - private void setPlaybackLocation(int playbackLocation) { - if (this.playbackLocation == playbackLocation) { - return; - } - - // View management. - if (playbackLocation == PLAYBACK_LOCAL) { - exoPlayerView.setVisibility(View.VISIBLE); - castControlView.hide(); - } else { - exoPlayerView.setVisibility(View.GONE); - castControlView.show(); - } - - long playbackPositionMs; - boolean playWhenReady; - if (this.playbackLocation == PLAYBACK_LOCAL) { - playbackPositionMs = exoPlayer.getCurrentPosition(); - playWhenReady = exoPlayer.getPlayWhenReady(); - exoPlayer.stop(); - } else /* this.playbackLocation == PLAYBACK_REMOTE */ { - playbackPositionMs = castPlayer.getCurrentPosition(); - playWhenReady = castPlayer.getPlayWhenReady(); - castPlayer.stop(); - } - - this.playbackLocation = playbackLocation; - if (currentSample != null) { - setCurrentSample(currentSample, playbackPositionMs, playWhenReady); - } + private static MediaQueueItem buildMediaQueueItem(DemoUtil.Sample sample) { + MediaMetadata movieMetadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE); + movieMetadata.putString(MediaMetadata.KEY_TITLE, sample.name); + MediaInfo mediaInfo = new MediaInfo.Builder(sample.uri) + .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED).setContentType(sample.mimeType) + .setMetadata(movieMetadata).build(); + return new MediaQueueItem.Builder(mediaInfo).build(); } } diff --git a/demos/cast/src/main/res/drawable/ic_add_circle_white_24dp.xml b/demos/cast/src/main/res/drawable/ic_add_circle_white_24dp.xml new file mode 100644 index 0000000000..5f3c8961ef --- /dev/null +++ b/demos/cast/src/main/res/drawable/ic_add_circle_white_24dp.xml @@ -0,0 +1,20 @@ + + + + + diff --git a/demos/cast/src/main/res/layout/main_activity.xml b/demos/cast/src/main/res/layout/main_activity.xml index 5d94931b64..1cce287b28 100644 --- a/demos/cast/src/main/res/layout/main_activity.xml +++ b/demos/cast/src/main/res/layout/main_activity.xml @@ -13,8 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. --> - - + - + + + + + + + + + + diff --git a/demos/cast/src/main/res/values/strings.xml b/demos/cast/src/main/res/values/strings.xml index d277bb3cdf..3505c40400 100644 --- a/demos/cast/src/main/res/values/strings.xml +++ b/demos/cast/src/main/res/values/strings.xml @@ -20,4 +20,6 @@ Cast + Add samples + diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java index 92e36c7f2d..1f39fe0023 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java @@ -151,7 +151,8 @@ public final class CastPlayer implements Player { * * @param item The item to load. * @param positionMs The position at which the playback should start in milliseconds relative to - * the start of the item at {@code startIndex}. + * the start of the item at {@code startIndex}. If {@link C#TIME_UNSET} is passed, playback + * starts at position 0. * @return The Cast {@code PendingResult}, or null if no session is available. */ public PendingResult loadItem(MediaQueueItem item, long positionMs) { @@ -164,13 +165,15 @@ public final class CastPlayer implements Player { * @param items The items to load. * @param startIndex The index of the item at which playback should start. * @param positionMs The position at which the playback should start in milliseconds relative to - * the start of the item at {@code startIndex}. + * the start of the item at {@code startIndex}. If {@link C#TIME_UNSET} is passed, playback + * starts at position 0. * @param repeatMode The repeat mode for the created media queue. * @return The Cast {@code PendingResult}, or null if no session is available. */ public PendingResult loadItems(MediaQueueItem[] items, int startIndex, long positionMs, @RepeatMode int repeatMode) { if (remoteMediaClient != null) { + positionMs = positionMs != C.TIME_UNSET ? positionMs : 0; waitingForInitialTimeline = true; return remoteMediaClient.queueLoad(items, startIndex, getCastRepeatMode(repeatMode), positionMs, null); @@ -327,6 +330,9 @@ public final class CastPlayer implements Player { @Override public void seekTo(int windowIndex, long positionMs) { MediaStatus mediaStatus = getMediaStatus(); + // We assume the default position is 0. There is no support for seeking to the default position + // in RemoteMediaClient. + positionMs = positionMs != C.TIME_UNSET ? positionMs : 0; if (mediaStatus != null) { if (getCurrentWindowIndex() != windowIndex) { remoteMediaClient.queueJumpToItem((int) currentTimeline.getPeriod(windowIndex, period).uid, @@ -364,6 +370,7 @@ public final class CastPlayer implements Player { @Override public void stop(boolean reset) { + playbackState = STATE_IDLE; if (remoteMediaClient != null) { // TODO(b/69792021): Support or emulate stop without position reset. remoteMediaClient.stop(); @@ -450,14 +457,18 @@ public final class CastPlayer implements Player { @Override public int getNextWindowIndex() { - return C.INDEX_UNSET; + return currentTimeline.isEmpty() ? C.INDEX_UNSET + : currentTimeline.getNextWindowIndex(getCurrentWindowIndex(), repeatMode, false); } @Override public int getPreviousWindowIndex() { - return C.INDEX_UNSET; + return currentTimeline.isEmpty() ? C.INDEX_UNSET + : currentTimeline.getPreviousWindowIndex(getCurrentWindowIndex(), repeatMode, false); } + // TODO: Fill the cast timeline information with ProgressListener's duration updates. + // See [Internal: b/65152553]. @Override public long getDuration() { return currentTimeline.isEmpty() ? C.TIME_UNSET From 69f8b250d5c60470394f7a1e5787345ae411cd6b Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 28 Nov 2017 05:18:35 -0800 Subject: [PATCH 0795/2472] Match SequenceableLoader method order in HlsSampleStreamWrapper ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177146923 --- .../source/hls/HlsSampleStreamWrapper.java | 108 +++++++++--------- 1 file changed, 54 insertions(+), 54 deletions(-) diff --git a/library/hls/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 index 06d48f1b08..87585a52da 100644 --- a/library/hls/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 @@ -324,28 +324,6 @@ import java.util.Arrays; return true; } - @Override - public long getBufferedPositionUs() { - if (loadingFinished) { - return C.TIME_END_OF_SOURCE; - } else if (isPendingReset()) { - return pendingResetPositionUs; - } else { - long bufferedPositionUs = lastSeekPositionUs; - HlsMediaChunk lastMediaChunk = getLastMediaChunk(); - HlsMediaChunk lastCompletedMediaChunk = lastMediaChunk.isLoadCompleted() ? lastMediaChunk - : mediaChunks.size() > 1 ? mediaChunks.get(mediaChunks.size() - 2) : null; - if (lastCompletedMediaChunk != null) { - bufferedPositionUs = Math.max(bufferedPositionUs, lastCompletedMediaChunk.endTimeUs); - } - for (SampleQueue sampleQueue : sampleQueues) { - bufferedPositionUs = Math.max(bufferedPositionUs, - sampleQueue.getLargestQueuedTimestampUs()); - } - return bufferedPositionUs; - } - } - public void release() { boolean releasedSynchronously = loader.release(this); if (prepared && !releasedSynchronously) { @@ -447,6 +425,37 @@ import java.util.Arrays; // SequenceableLoader implementation + @Override + public long getBufferedPositionUs() { + if (loadingFinished) { + return C.TIME_END_OF_SOURCE; + } else if (isPendingReset()) { + return pendingResetPositionUs; + } else { + long bufferedPositionUs = lastSeekPositionUs; + HlsMediaChunk lastMediaChunk = getLastMediaChunk(); + HlsMediaChunk lastCompletedMediaChunk = lastMediaChunk.isLoadCompleted() ? lastMediaChunk + : mediaChunks.size() > 1 ? mediaChunks.get(mediaChunks.size() - 2) : null; + if (lastCompletedMediaChunk != null) { + bufferedPositionUs = Math.max(bufferedPositionUs, lastCompletedMediaChunk.endTimeUs); + } + for (SampleQueue sampleQueue : sampleQueues) { + bufferedPositionUs = Math.max(bufferedPositionUs, + sampleQueue.getLargestQueuedTimestampUs()); + } + return bufferedPositionUs; + } + } + + @Override + public long getNextLoadPositionUs() { + if (isPendingReset()) { + return pendingResetPositionUs; + } else { + return loadingFinished ? C.TIME_END_OF_SOURCE : getLastMediaChunk().endTimeUs; + } + } + @Override public boolean continueLoading(long positionUs) { if (loadingFinished || loader.isLoading()) { @@ -494,15 +503,6 @@ import java.util.Arrays; return true; } - @Override - public long getNextLoadPositionUs() { - if (isPendingReset()) { - return pendingResetPositionUs; - } else { - return loadingFinished ? C.TIME_END_OF_SOURCE : getLastMediaChunk().endTimeUs; - } - } - // Loader.Callback implementation. @Override @@ -755,33 +755,10 @@ import java.util.Arrays; enabledSampleQueueCount = enabledSampleQueueCount + (enabledState ? 1 : -1); } - /** - * Derives a track format corresponding to a given container format, by combining it with sample - * level information obtained from the samples. - * - * @param containerFormat The container format for which the track format should be derived. - * @param sampleFormat A sample format from which to obtain sample level information. - * @return The derived track format. - */ - private static Format deriveFormat(Format containerFormat, Format sampleFormat) { - if (containerFormat == null) { - return sampleFormat; - } - int sampleTrackType = MimeTypes.getTrackType(sampleFormat.sampleMimeType); - String codecs = Util.getCodecsOfType(containerFormat.codecs, sampleTrackType); - return sampleFormat.copyWithContainerInfo(containerFormat.id, codecs, containerFormat.bitrate, - containerFormat.width, containerFormat.height, containerFormat.selectionFlags, - containerFormat.language); - } - private HlsMediaChunk getLastMediaChunk() { return mediaChunks.get(mediaChunks.size() - 1); } - private boolean isMediaChunk(Chunk chunk) { - return chunk instanceof HlsMediaChunk; - } - private boolean isPendingReset() { return pendingResetPositionUs != C.TIME_UNSET; } @@ -810,4 +787,27 @@ import java.util.Arrays; return true; } + /** + * Derives a track format corresponding to a given container format, by combining it with sample + * level information obtained from the samples. + * + * @param containerFormat The container format for which the track format should be derived. + * @param sampleFormat A sample format from which to obtain sample level information. + * @return The derived track format. + */ + private static Format deriveFormat(Format containerFormat, Format sampleFormat) { + if (containerFormat == null) { + return sampleFormat; + } + int sampleTrackType = MimeTypes.getTrackType(sampleFormat.sampleMimeType); + String codecs = Util.getCodecsOfType(containerFormat.codecs, sampleTrackType); + return sampleFormat.copyWithContainerInfo(containerFormat.id, codecs, containerFormat.bitrate, + containerFormat.width, containerFormat.height, containerFormat.selectionFlags, + containerFormat.language); + } + + private static boolean isMediaChunk(Chunk chunk) { + return chunk instanceof HlsMediaChunk; + } + } From f2d554175297c3f8ae6c42a25d20fad6d2db14cf Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 28 Nov 2017 08:40:21 -0800 Subject: [PATCH 0796/2472] Extractor cleanup - Align class summary Javadoc - Fix ErrorProne + Style warnings ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177165593 --- .../extractor/flv/FlvExtractor.java | 46 ++++++++++++------- .../extractor/mkv/MatroskaExtractor.java | 2 +- .../extractor/mp3/Mp3Extractor.java | 2 +- .../exoplayer2/extractor/mp3/XingSeeker.java | 3 +- .../extractor/mp4/FragmentedMp4Extractor.java | 2 +- .../extractor/mp4/Mp4Extractor.java | 2 +- .../extractor/ogg/DefaultOggSeeker.java | 2 +- .../exoplayer2/extractor/ogg/FlacReader.java | 3 +- .../extractor/ogg/OggExtractor.java | 2 +- .../extractor/rawcc/RawCcExtractor.java | 2 +- .../exoplayer2/extractor/ts/Ac3Extractor.java | 2 +- .../extractor/ts/AdtsExtractor.java | 2 +- .../exoplayer2/extractor/ts/PsExtractor.java | 2 +- .../exoplayer2/extractor/ts/TsExtractor.java | 2 +- .../extractor/wav/WavExtractor.java | 4 +- 15 files changed, 47 insertions(+), 31 deletions(-) diff --git a/library/core/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 index 218e6ffd82..30b66d65fd 100644 --- a/library/core/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 @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor.flv; +import android.support.annotation.IntDef; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; @@ -25,9 +26,11 @@ import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; /** - * Facilitates the extraction of data from the FLV container format. + * Extracts data from the FLV container format. */ public final class FlvExtractor implements Extractor, SeekMap { @@ -43,16 +46,22 @@ public final class FlvExtractor implements Extractor, SeekMap { }; - // Header sizes. - private static final int FLV_HEADER_SIZE = 9; - private static final int FLV_TAG_HEADER_SIZE = 11; - - // Parser states. + /** + * Extractor states. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({STATE_READING_FLV_HEADER, STATE_SKIPPING_TO_TAG_HEADER, STATE_READING_TAG_HEADER, + STATE_READING_TAG_DATA}) + private @interface States {} private static final int STATE_READING_FLV_HEADER = 1; private static final int STATE_SKIPPING_TO_TAG_HEADER = 2; private static final int STATE_READING_TAG_HEADER = 3; private static final int STATE_READING_TAG_DATA = 4; + // Header sizes. + private static final int FLV_HEADER_SIZE = 9; + private static final int FLV_TAG_HEADER_SIZE = 11; + // Tag types. private static final int TAG_TYPE_AUDIO = 8; private static final int TAG_TYPE_VIDEO = 9; @@ -71,11 +80,11 @@ public final class FlvExtractor implements Extractor, SeekMap { private ExtractorOutput extractorOutput; // State variables. - private int parserState; + private @States int state; private int bytesToNextTagHeader; - public int tagType; - public int tagDataSize; - public long tagTimestampUs; + private int tagType; + private int tagDataSize; + private long tagTimestampUs; // Tags readers. private AudioTagPayloadReader audioReader; @@ -87,7 +96,7 @@ public final class FlvExtractor implements Extractor, SeekMap { headerBuffer = new ParsableByteArray(FLV_HEADER_SIZE); tagHeaderBuffer = new ParsableByteArray(FLV_TAG_HEADER_SIZE); tagData = new ParsableByteArray(); - parserState = STATE_READING_FLV_HEADER; + state = STATE_READING_FLV_HEADER; } @Override @@ -128,7 +137,7 @@ public final class FlvExtractor implements Extractor, SeekMap { @Override public void seek(long position, long timeUs) { - parserState = STATE_READING_FLV_HEADER; + state = STATE_READING_FLV_HEADER; bytesToNextTagHeader = 0; } @@ -141,7 +150,7 @@ public final class FlvExtractor implements Extractor, SeekMap { public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException, InterruptedException { while (true) { - switch (parserState) { + switch (state) { case STATE_READING_FLV_HEADER: if (!readFlvHeader(input)) { return RESULT_END_OF_INPUT; @@ -160,6 +169,9 @@ public final class FlvExtractor implements Extractor, SeekMap { return RESULT_CONTINUE; } break; + default: + // Never happens. + throw new IllegalStateException(); } } } @@ -199,7 +211,7 @@ public final class FlvExtractor implements Extractor, SeekMap { // We need to skip any additional content in the FLV header, plus the 4 byte previous tag size. bytesToNextTagHeader = headerBuffer.readInt() - FLV_HEADER_SIZE + 4; - parserState = STATE_SKIPPING_TO_TAG_HEADER; + state = STATE_SKIPPING_TO_TAG_HEADER; return true; } @@ -213,7 +225,7 @@ public final class FlvExtractor implements Extractor, SeekMap { private void skipToTagHeader(ExtractorInput input) throws IOException, InterruptedException { input.skipFully(bytesToNextTagHeader); bytesToNextTagHeader = 0; - parserState = STATE_READING_TAG_HEADER; + state = STATE_READING_TAG_HEADER; } /** @@ -236,7 +248,7 @@ public final class FlvExtractor implements Extractor, SeekMap { tagTimestampUs = tagHeaderBuffer.readUnsignedInt24(); tagTimestampUs = ((tagHeaderBuffer.readUnsignedByte() << 24) | tagTimestampUs) * 1000L; tagHeaderBuffer.skipBytes(3); // streamId - parserState = STATE_READING_TAG_DATA; + state = STATE_READING_TAG_DATA; return true; } @@ -261,7 +273,7 @@ public final class FlvExtractor implements Extractor, SeekMap { wasConsumed = false; } bytesToNextTagHeader = 4; // There's a 4 byte previous tag size before the next header. - parserState = STATE_SKIPPING_TO_TAG_HEADER; + state = STATE_SKIPPING_TO_TAG_HEADER; return wasConsumed; } diff --git a/library/core/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 index 5aefd041c4..4b0bbda275 100644 --- a/library/core/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 @@ -53,7 +53,7 @@ import java.util.Locale; import java.util.UUID; /** - * Extracts data from a Matroska or WebM file. + * Extracts data from the Matroska and WebM container formats. */ public final class MatroskaExtractor implements Extractor { diff --git a/library/core/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 index a4349ada09..dc7d21851a 100644 --- a/library/core/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 @@ -38,7 +38,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; /** - * Extracts data from an MP3 file. + * Extracts data from the MP3 container format. */ public final class Mp3Extractor implements Extractor { diff --git a/library/core/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 index 5e8d72f18d..9b1158dfa8 100644 --- a/library/core/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 @@ -113,12 +113,13 @@ import com.google.android.exoplayer2.util.Util; fx = 256f; } else { int a = (int) percent; - float fa, fb; + float fa; if (a == 0) { fa = 0f; } else { fa = tableOfContents[a - 1]; } + float fb; if (a < 99) { fb = tableOfContents[a]; } else { diff --git a/library/core/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 index e86157dd92..4bc1b04418 100644 --- a/library/core/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 @@ -53,7 +53,7 @@ import java.util.Stack; import java.util.UUID; /** - * Facilitates the extraction of data from the fragmented mp4 container format. + * Extracts data from the FMP4 container format. */ public final class FragmentedMp4Extractor implements Extractor { diff --git a/library/core/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 index f23af98e7f..f2412bf4ba 100644 --- a/library/core/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 @@ -41,7 +41,7 @@ import java.util.List; import java.util.Stack; /** - * Extracts data from an unfragmented MP4 file. + * Extracts data from the MP4 container format. */ public final class Mp4Extractor implements Extractor, SeekMap { diff --git a/library/core/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 index 5470e2badc..77def57275 100644 --- a/library/core/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 @@ -186,7 +186,7 @@ import java.io.IOException; return start; } - long offset = pageSize * (granuleDistance <= 0 ? 2 : 1); + long offset = pageSize * (granuleDistance <= 0 ? 2L : 1L); long nextPosition = input.getPosition() - offset + (granuleDistance * (end - start) / (endGranule - startGranule)); diff --git a/library/core/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 index f4da6e3960..304fb3dd96 100644 --- a/library/core/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 @@ -118,8 +118,9 @@ import java.util.List; case 14: case 15: return 256 << (blockSizeCode - 8); + default: + return -1; } - return -1; } private class FlacOggSeeker implements OggSeeker, SeekMap { diff --git a/library/core/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 index 54e168c665..a4d8f97d5b 100644 --- a/library/core/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 @@ -27,7 +27,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray; import java.io.IOException; /** - * Ogg {@link Extractor}. + * Extracts data from the Ogg container format. */ public class OggExtractor implements Extractor { diff --git a/library/core/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 index 7840eafce6..aa77aba30e 100644 --- a/library/core/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 @@ -29,7 +29,7 @@ import com.google.android.exoplayer2.util.Util; import java.io.IOException; /** - * Extracts CEA data from a RawCC file. + * Extracts data from the RawCC container format. */ public final class RawCcExtractor implements Extractor { diff --git a/library/core/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 index 4d54600c6d..bc37277c57 100644 --- a/library/core/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 @@ -29,7 +29,7 @@ import com.google.android.exoplayer2.util.Util; import java.io.IOException; /** - * Extracts samples from (E-)AC-3 bitstreams. + * Extracts data from (E-)AC-3 bitstreams. */ public final class Ac3Extractor implements Extractor { diff --git a/library/core/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 index 5ce15952a5..a0a748660e 100644 --- a/library/core/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 @@ -29,7 +29,7 @@ import com.google.android.exoplayer2.util.Util; import java.io.IOException; /** - * Extracts samples from AAC bit streams with ADTS framing. + * Extracts data from AAC bit streams with ADTS framing. */ public final class AdtsExtractor implements Extractor { diff --git a/library/core/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 index 69c5745eaa..f3aad6ba6b 100644 --- a/library/core/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 @@ -31,7 +31,7 @@ import com.google.android.exoplayer2.util.TimestampAdjuster; import java.io.IOException; /** - * Facilitates the extraction of data from the MPEG-2 PS container format. + * Extracts data from the MPEG-2 PS container format. */ public final class PsExtractor implements Extractor { diff --git a/library/core/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 index 213d30d47d..13e669da23 100644 --- a/library/core/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 @@ -45,7 +45,7 @@ import java.util.Collections; import java.util.List; /** - * Facilitates the extraction of data from the MPEG-2 TS container format. + * Extracts data from the MPEG-2 TS container format. */ public final class TsExtractor implements Extractor { diff --git a/library/core/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 index cb46aa5519..cb9a2653d7 100644 --- a/library/core/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 @@ -28,7 +28,9 @@ import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.util.MimeTypes; import java.io.IOException; -/** {@link Extractor} to extract samples from a WAV byte stream. */ +/** + * Extracts data from WAV byte streams. + */ public final class WavExtractor implements Extractor, SeekMap { /** From 54a1bb186e860c172c473713eb24f1cc5d073d07 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 28 Nov 2017 09:00:43 -0800 Subject: [PATCH 0797/2472] Allow resetInternal to release MediaSource but keep timeline. This allows to keep the state synced with ExoPlayerImpl after stopping the player, but still releases the media source immediately as it needs to be reprepared. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177167980 --- .../android/exoplayer2/ExoPlayerTest.java | 24 ++++++++ .../exoplayer2/ExoPlayerImplInternal.java | 58 ++++++++++++------- .../exoplayer2/testutil/ActionSchedule.java | 9 +++ 3 files changed, 70 insertions(+), 21 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index efb7b0e96c..2443f8b892 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -784,4 +784,28 @@ public final class ExoPlayerTest extends TestCase { testRunner.assertNoPositionDiscontinuities(); } + public void testStopAndSeekAfterStopDoesNotResetTimeline() throws Exception { + // Combining additional stop and seek after initial stop in one test to get the seek processed + // callback which ensures that all operations have been processed by the player. + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testStopTwice") + .waitForPlaybackState(Player.STATE_READY) + .stop(false) + .stop(false) + .seek(0) + .waitForSeekProcessed() + .build(); + ExoPlayerTestRunner testRunner = + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + testRunner.assertTimelinesEqual(timeline); + testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); + testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_SEEK); + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 909f52fad8..3bd1d2b00f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -390,11 +390,11 @@ import java.io.IOException; private void prepareInternal(MediaSource mediaSource, boolean resetPosition) { pendingPrepareCount++; - resetInternal(/* releaseMediaSource= */ true, resetPosition); + resetInternal(/* releaseMediaSource= */ true, resetPosition, /* resetState= */ true); loadControl.onPrepared(); this.mediaSource = mediaSource; setState(Player.STATE_BUFFERING); - mediaSource.prepareSource(player, /* isTopLevelSource= */ true, /* listener = */ this); + mediaSource.prepareSource(player, /* isTopLevelSource= */ true, /* listener= */ this); handler.sendEmptyMessage(MSG_DO_SOME_WORK); } @@ -629,10 +629,15 @@ import java.io.IOException; private void seekToInternal(SeekPosition seekPosition) throws ExoPlaybackException { Timeline timeline = playbackInfo.timeline; - if (timeline == null) { + if (mediaSource == null || timeline == null) { pendingInitialSeekPosition = seekPosition; - eventHandler.obtainMessage(MSG_SEEK_ACK, /* seekAdjusted = */ 0, 0, - playbackInfo.copyWithTimeline(Timeline.EMPTY, null)).sendToTarget(); + eventHandler + .obtainMessage( + MSG_SEEK_ACK, + /* seekAdjusted */ 0, + 0, + timeline == null ? playbackInfo.copyWithTimeline(Timeline.EMPTY, null) : playbackInfo) + .sendToTarget(); return; } @@ -642,10 +647,11 @@ import java.io.IOException; // timeline has changed and a suitable seek position could not be resolved in the new one. setState(Player.STATE_ENDED); // Reset, but retain the source so that it can still be used should a seek occur. - resetInternal(false, true); + resetInternal( + /* releaseMediaSource= */ false, /* resetPosition= */ true, /* resetState= */ false); // Set the playback position to 0 for notifying the eventHandler (instead of C.TIME_UNSET). - eventHandler.obtainMessage(MSG_SEEK_ACK, /* seekAdjusted = */ 1, 0, - playbackInfo.fromNewPosition(playbackInfo.periodId.periodIndex, /* startPositionUs = */ 0, + eventHandler.obtainMessage(MSG_SEEK_ACK, /* seekAdjusted */ 1, 0, + playbackInfo.fromNewPosition(playbackInfo.periodId.periodIndex, /* startPositionUs= */ 0, /* contentPositionUs= */ C.TIME_UNSET)) .sendToTarget(); return; @@ -766,14 +772,15 @@ import java.io.IOException; } private void stopInternal(boolean reset) { - // Releasing the internal player sets the timeline to null. Use the current timeline or - // Timeline.EMPTY for notifying the eventHandler. - Timeline publicTimeline = reset || playbackInfo.timeline == null - ? Timeline.EMPTY : playbackInfo.timeline; - Object publicManifest = reset ? null : playbackInfo.manifest; - resetInternal(/* releaseMediaSource= */ true, reset); - PlaybackInfo publicPlaybackInfo = playbackInfo.copyWithTimeline(publicTimeline, publicManifest); - if (reset) { + resetInternal( + /* releaseMediaSource= */ true, /* resetPosition= */ reset, /* resetState= */ reset); + PlaybackInfo publicPlaybackInfo = playbackInfo; + if (playbackInfo.timeline == null) { + // Resetting the state sets the timeline to null. Use Timeline.EMPTY for notifying the + // eventHandler. + publicPlaybackInfo = publicPlaybackInfo.copyWithTimeline(Timeline.EMPTY, null); + } + if (playbackInfo.startPositionUs == C.TIME_UNSET) { // When resetting the state, set the playback position to 0 (instead of C.TIME_UNSET) for // notifying the eventHandler. publicPlaybackInfo = @@ -787,7 +794,8 @@ import java.io.IOException; } private void releaseInternal() { - resetInternal(/* releaseMediaSource= */ true, /* resetPosition= */ true); + resetInternal( + /* releaseMediaSource= */ true, /* resetPosition= */ true, /* resetState= */ true); loadControl.onReleased(); setState(Player.STATE_IDLE); internalPlaybackThread.quit(); @@ -797,7 +805,8 @@ import java.io.IOException; } } - private void resetInternal(boolean releaseMediaSource, boolean resetPosition) { + private void resetInternal( + boolean releaseMediaSource, boolean resetPosition, boolean resetState) { handler.removeMessages(MSG_DO_SOME_WORK); rebuffering = false; mediaClock.stop(); @@ -832,13 +841,15 @@ import java.io.IOException; playbackInfo = playbackInfo.fromNewPosition(playbackInfo.periodId, playbackInfo.positionUs, playbackInfo.contentPositionUs); } + if (resetState) { + mediaPeriodInfoSequence.setTimeline(null); + playbackInfo = playbackInfo.copyWithTimeline(null, null); + } if (releaseMediaSource) { if (mediaSource != null) { mediaSource.releaseSource(); mediaSource = null; } - mediaPeriodInfoSequence.setTimeline(null); - playbackInfo = playbackInfo.copyWithTimeline(null, null); } } @@ -1174,7 +1185,8 @@ import java.io.IOException; private void handleSourceInfoRefreshEndedPlayback(int prepareAcks) { setState(Player.STATE_ENDED); // Reset, but retain the source so that it can still be used should a seek occur. - resetInternal(false, true); + resetInternal( + /* releaseMediaSource= */ false, /* resetPosition= */ true, /* resetState= */ false); // Set the playback position to 0 for notifying the eventHandler (instead of C.TIME_UNSET). notifySourceInfoRefresh(prepareAcks, playbackInfo.fromNewPosition(playbackInfo.periodId.periodIndex, 0, C.TIME_UNSET)); @@ -1279,6 +1291,10 @@ import java.io.IOException; } private void updatePeriods() throws ExoPlaybackException, IOException { + if (mediaSource == null) { + // The player has no media source yet. + return; + } if (playbackInfo.timeline == null) { // We're waiting to get information about periods. mediaSource.maybeThrowSourceInfoRefreshError(); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java index abca2cafdb..7a2ce9270c 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java @@ -183,6 +183,15 @@ public final class ActionSchedule { .apply(new WaitForPlaybackState(tag, Player.STATE_READY)); } + /** + * Schedules a delay until the player indicates that a seek has been processed. + * + * @return The builder, for convenience. + */ + public Builder waitForSeekProcessed() { + return apply(new WaitForSeekProcessed(tag)); + } + /** * Schedules a playback parameters setting action to be executed. * From baa80a1b68329c8c6ea2e746b332dfae1a00a024 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 28 Nov 2017 09:22:32 -0800 Subject: [PATCH 0798/2472] Fix DefaultTrackSelector#Parameter withSelectUndeterminedTextLanguage ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177170994 --- .../exoplayer2/trackselection/DefaultTrackSelector.java | 3 +-- .../exoplayer2/trackselection/DefaultTrackSelectorTest.java | 5 ++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index 0029cdbd31..49b8e8964b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -243,8 +243,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { /** * Returns an instance with the provided {@link #selectUndeterminedTextLanguage}. */ - public Parameters withSelectUndeterminedTextLanguageAsFallback( - boolean selectUndeterminedTextLanguage) { + public Parameters withSelectUndeterminedTextLanguage(boolean selectUndeterminedTextLanguage) { if (selectUndeterminedTextLanguage == this.selectUndeterminedTextLanguage) { return this; } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java index b2b149b004..6b14d139ae 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java @@ -561,8 +561,7 @@ public final class DefaultTrackSelectorTest { wrapFormats(spanish, german, undeterminedUnd, undeterminedNull)); assertThat(result.selections.get(0)).isNull(); - trackSelector.setParameters( - DEFAULT_PARAMETERS.withSelectUndeterminedTextLanguageAsFallback(true)); + trackSelector.setParameters(DEFAULT_PARAMETERS.withSelectUndeterminedTextLanguage(true)); result = trackSelector.selectTracks(textRendererCapabilites, wrapFormats(spanish, german, undeterminedUnd, undeterminedNull)); assertThat(result.selections.get(0).getFormat(0)).isSameAs(undeterminedUnd); @@ -577,7 +576,7 @@ public final class DefaultTrackSelectorTest { assertThat(result.selections.get(0)).isNull(); trackSelector.setParameters( - trackSelector.getParameters().withSelectUndeterminedTextLanguageAsFallback(true)); + trackSelector.getParameters().withSelectUndeterminedTextLanguage(true)); result = trackSelector.selectTracks(textRendererCapabilites, wrapFormats(german, undeterminedUnd, undeterminedNull)); assertThat(result.selections.get(0).getFormat(0)).isSameAs(undeterminedUnd); From ff49bc97c459062a87310557197c72162dfa7dcb Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 28 Nov 2017 09:43:08 -0800 Subject: [PATCH 0799/2472] Clean up some extrator SeekMap implementations ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177173618 --- .../extractor/flv/FlvExtractor.java | 45 +++++------ .../extractor/flv/ScriptTagPayloadReader.java | 8 +- .../extractor/wav/WavExtractor.java | 21 +---- .../exoplayer2/extractor/wav/WavHeader.java | 76 ++++++++++++------- 4 files changed, 69 insertions(+), 81 deletions(-) diff --git a/library/core/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 index 30b66d65fd..2da075ff53 100644 --- a/library/core/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 @@ -32,7 +32,7 @@ import java.lang.annotation.RetentionPolicy; /** * Extracts data from the FLV container format. */ -public final class FlvExtractor implements Extractor, SeekMap { +public final class FlvExtractor implements Extractor { /** * Factory for {@link FlvExtractor} instances. @@ -70,32 +70,28 @@ public final class FlvExtractor implements Extractor, SeekMap { // FLV container identifier. private static final int FLV_TAG = Util.getIntegerCodeForString("FLV"); - // Temporary buffers. private final ParsableByteArray scratch; private final ParsableByteArray headerBuffer; private final ParsableByteArray tagHeaderBuffer; private final ParsableByteArray tagData; + private final ScriptTagPayloadReader metadataReader; - // Extractor outputs. private ExtractorOutput extractorOutput; - - // State variables. private @States int state; private int bytesToNextTagHeader; private int tagType; private int tagDataSize; private long tagTimestampUs; - - // Tags readers. + private boolean outputSeekMap; private AudioTagPayloadReader audioReader; private VideoTagPayloadReader videoReader; - private ScriptTagPayloadReader metadataReader; public FlvExtractor() { scratch = new ParsableByteArray(4); headerBuffer = new ParsableByteArray(FLV_HEADER_SIZE); tagHeaderBuffer = new ParsableByteArray(FLV_TAG_HEADER_SIZE); tagData = new ParsableByteArray(); + metadataReader = new ScriptTagPayloadReader(); state = STATE_READING_FLV_HEADER; } @@ -203,11 +199,7 @@ public final class FlvExtractor implements Extractor, SeekMap { videoReader = new VideoTagPayloadReader( extractorOutput.track(TAG_TYPE_VIDEO, C.TRACK_TYPE_VIDEO)); } - if (metadataReader == null) { - metadataReader = new ScriptTagPayloadReader(null); - } extractorOutput.endTracks(); - extractorOutput.seekMap(this); // We need to skip any additional content in the FLV header, plus the 4 byte previous tag size. bytesToNextTagHeader = headerBuffer.readInt() - FLV_HEADER_SIZE + 4; @@ -263,11 +255,18 @@ public final class FlvExtractor implements Extractor, SeekMap { private boolean readTagData(ExtractorInput input) throws IOException, InterruptedException { boolean wasConsumed = true; if (tagType == TAG_TYPE_AUDIO && audioReader != null) { + ensureOutputSeekMap(); audioReader.consume(prepareTagData(input), tagTimestampUs); } else if (tagType == TAG_TYPE_VIDEO && videoReader != null) { + ensureOutputSeekMap(); videoReader.consume(prepareTagData(input), tagTimestampUs); - } else if (tagType == TAG_TYPE_SCRIPT_DATA && metadataReader != null) { + } else if (tagType == TAG_TYPE_SCRIPT_DATA && !outputSeekMap) { metadataReader.consume(prepareTagData(input), tagTimestampUs); + long durationUs = metadataReader.getDurationUs(); + if (durationUs != C.TIME_UNSET) { + extractorOutput.seekMap(new SeekMap.Unseekable(durationUs)); + outputSeekMap = true; + } } else { input.skipFully(tagDataSize); wasConsumed = false; @@ -289,21 +288,11 @@ public final class FlvExtractor implements Extractor, SeekMap { return tagData; } - // SeekMap implementation. - - @Override - public boolean isSeekable() { - return false; - } - - @Override - public long getDurationUs() { - return metadataReader.getDurationUs(); - } - - @Override - public long getPosition(long timeUs) { - return 0; + private void ensureOutputSeekMap() { + if (!outputSeekMap) { + extractorOutput.seekMap(new SeekMap.Unseekable(C.TIME_UNSET)); + outputSeekMap = true; + } } } diff --git a/library/core/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 index 1a4f8f3e88..2dec85ffcc 100644 --- a/library/core/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 @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.extractor.flv; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; -import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.util.ParsableByteArray; import java.util.ArrayList; import java.util.Date; @@ -44,11 +43,8 @@ import java.util.Map; private long durationUs; - /** - * @param output A {@link TrackOutput} to which samples should be written. - */ - public ScriptTagPayloadReader(TrackOutput output) { - super(output); + public ScriptTagPayloadReader() { + super(null); durationUs = C.TIME_UNSET; } diff --git a/library/core/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 index cb9a2653d7..4f2be71a69 100644 --- a/library/core/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 @@ -23,7 +23,6 @@ import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorOutput; 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.util.MimeTypes; import java.io.IOException; @@ -31,7 +30,7 @@ import java.io.IOException; /** * Extracts data from WAV byte streams. */ -public final class WavExtractor implements Extractor, SeekMap { +public final class WavExtractor implements Extractor { /** * Factory for {@link WavExtractor} instances. @@ -95,7 +94,7 @@ public final class WavExtractor implements Extractor, SeekMap { if (!wavHeader.hasDataBounds()) { WavHeaderReader.skipToData(input, wavHeader); - extractorOutput.seekMap(this); + extractorOutput.seekMap(wavHeader); } int bytesAppended = trackOutput.sampleData(input, MAX_INPUT_SIZE - pendingBytes, true); @@ -115,20 +114,4 @@ public final class WavExtractor implements Extractor, SeekMap { return bytesAppended == RESULT_END_OF_INPUT ? RESULT_END_OF_INPUT : RESULT_CONTINUE; } - // SeekMap implementation. - - @Override - public long getDurationUs() { - return wavHeader.getDurationUs(); - } - - @Override - public boolean isSeekable() { - return true; - } - - @Override - public long getPosition(long timeUs) { - return wavHeader.getPosition(timeUs); - } } diff --git a/library/core/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 index a57060f604..1c1fc97a22 100644 --- a/library/core/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 @@ -16,9 +16,10 @@ package com.google.android.exoplayer2.extractor.wav; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.extractor.SeekMap; /** Header for a WAV file. */ -/*package*/ final class WavHeader { +/* package */ final class WavHeader implements SeekMap { /** Number of audio chanels. */ private final int numChannels; @@ -49,12 +50,56 @@ import com.google.android.exoplayer2.C; this.encoding = encoding; } - /** Returns the duration in microseconds of this WAV. */ + // Setting bounds. + + /** + * Sets the data start position and size in bytes of sample data in this WAV. + * + * @param dataStartPosition The data start position in bytes. + * @param dataSize The data size in bytes. + */ + public void setDataBounds(long dataStartPosition, long dataSize) { + this.dataStartPosition = dataStartPosition; + this.dataSize = dataSize; + } + + /** Returns whether the data start position and size have been set. */ + public boolean hasDataBounds() { + return dataStartPosition != 0 && dataSize != 0; + } + + // SeekMap implementation. + + @Override + public boolean isSeekable() { + return true; + } + + @Override public long getDurationUs() { long numFrames = dataSize / blockAlignment; return (numFrames * C.MICROS_PER_SECOND) / sampleRateHz; } + @Override + public long getPosition(long timeUs) { + long unroundedPosition = (timeUs * averageBytesPerSecond) / C.MICROS_PER_SECOND; + // Round down to nearest frame. + long position = (unroundedPosition / blockAlignment) * blockAlignment; + return Math.min(position, dataSize - blockAlignment) + dataStartPosition; + } + + // Misc getters. + + /** + * Returns the time in microseconds for the given position in bytes. + * + * @param position The position in bytes. + */ + public long getTimeUs(long position) { + return position * C.MICROS_PER_SECOND / averageBytesPerSecond; + } + /** Returns the bytes per frame of this WAV. */ public int getBytesPerFrame() { return blockAlignment; @@ -75,33 +120,8 @@ import com.google.android.exoplayer2.C; return numChannels; } - /** Returns the position in bytes in this WAV for the given time in microseconds. */ - public long getPosition(long timeUs) { - long unroundedPosition = (timeUs * averageBytesPerSecond) / C.MICROS_PER_SECOND; - // Round down to nearest frame. - long position = (unroundedPosition / blockAlignment) * blockAlignment; - return Math.min(position, dataSize - blockAlignment) + dataStartPosition; - } - - /** Returns the time in microseconds for the given position in bytes in this WAV. */ - public long getTimeUs(long position) { - return position * C.MICROS_PER_SECOND / averageBytesPerSecond; - } - - /** Returns true if the data start position and size have been set. */ - public boolean hasDataBounds() { - return dataStartPosition != 0 && dataSize != 0; - } - - /** Sets the start position and size in bytes of sample data in this WAV. */ - public void setDataBounds(long dataStartPosition, long dataSize) { - this.dataStartPosition = dataStartPosition; - this.dataSize = dataSize; - } - /** Returns the PCM encoding. **/ - @C.PcmEncoding - public int getEncoding() { + public @C.PcmEncoding int getEncoding() { return encoding; } From 2282527821a0232e5d80f554d417828e0f7cf771 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 28 Nov 2017 09:53:30 -0800 Subject: [PATCH 0800/2472] Allow setting supported formats on AdsLoaders ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177175377 --- .../android/exoplayer2/castdemo/DemoUtil.java | 6 ++--- .../exoplayer2/ext/ima/ImaAdsLoader.java | 24 +++++++++++++++++++ .../exoplayer2/source/ads/AdsLoader.java | 10 ++++++++ .../exoplayer2/source/ads/AdsMediaSource.java | 1 + .../android/exoplayer2/util/MimeTypes.java | 3 +++ .../source/dash/DashMediaSource.java | 3 +-- 6 files changed, 42 insertions(+), 5 deletions(-) diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java index d36f8c319e..26ab5eb0dd 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java @@ -26,9 +26,9 @@ import java.util.List; */ /* package */ final class DemoUtil { - public static final String MIME_TYPE_DASH = "application/dash+xml"; - public static final String MIME_TYPE_HLS = "application/vnd.apple.mpegurl"; - public static final String MIME_TYPE_SS = "application/vnd.ms-sstr+xml"; + public static final String MIME_TYPE_DASH = MimeTypes.APPLICATION_MPD; + public static final String MIME_TYPE_HLS = MimeTypes.APPLICATION_M3U8; + public static final String MIME_TYPE_SS = MimeTypes.APPLICATION_SS; public static final String MIME_TYPE_VIDEO_MP4 = MimeTypes.VIDEO_MP4; /** diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index fe6a6d6196..4bf88fe18f 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -49,10 +49,13 @@ import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.ads.AdPlaybackState; import com.google.android.exoplayer2.source.ads.AdsLoader; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.MimeTypes; import java.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Map; @@ -117,6 +120,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A private final AdDisplayContainer adDisplayContainer; private final com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader; + private List supportedMimeTypes; private EventListener eventListener; private Player player; private ViewGroup adUiViewGroup; @@ -238,6 +242,25 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A // AdsLoader implementation. + @Override + public void setSupportedContentTypes(@C.ContentType int... contentTypes) { + List supportedMimeTypes = new ArrayList<>(); + for (@C.ContentType int contentType : contentTypes) { + if (contentType == C.TYPE_DASH) { + supportedMimeTypes.add(MimeTypes.APPLICATION_MPD); + } else if (contentType == C.TYPE_HLS) { + supportedMimeTypes.add(MimeTypes.APPLICATION_M3U8); + } else if (contentType == C.TYPE_OTHER) { + supportedMimeTypes.addAll(Arrays.asList( + MimeTypes.VIDEO_MP4, MimeTypes.VIDEO_WEBM, MimeTypes.VIDEO_H263, MimeTypes.VIDEO_MPEG, + MimeTypes.AUDIO_MP4, MimeTypes.AUDIO_MPEG)); + } else if (contentType == C.TYPE_SS) { + // IMA does not support SmoothStreaming ad media. + } + } + this.supportedMimeTypes = Collections.unmodifiableList(supportedMimeTypes); + } + @Override public void attachPlayer(ExoPlayer player, EventListener eventListener, ViewGroup adUiViewGroup) { this.player = player; @@ -296,6 +319,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A ImaSdkFactory imaSdkFactory = ImaSdkFactory.getInstance(); AdsRenderingSettings adsRenderingSettings = imaSdkFactory.createAdsRenderingSettings(); adsRenderingSettings.setEnablePreloading(true); + adsRenderingSettings.setMimeTypes(supportedMimeTypes); adsManager.init(adsRenderingSettings); if (DEBUG) { Log.d(TAG, "Initialized with preloading"); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsLoader.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsLoader.java index 241750a21f..99feccd2f3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsLoader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsLoader.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.source.ads; import android.view.ViewGroup; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import java.io.IOException; @@ -71,6 +72,15 @@ public interface AdsLoader { } + /** + * Sets the supported content types for ad media. Must be called before the first call to + * {@link #attachPlayer(ExoPlayer, EventListener, ViewGroup)}. Subsequent calls may be ignored. + * + * @param contentTypes The supported content types for ad media. Each element must be one of + * {@link C#TYPE_DASH}, {@link C#TYPE_HLS}, {@link C#TYPE_SS} and {@link C#TYPE_OTHER}. + */ + void setSupportedContentTypes(@C.ContentType int... contentTypes); + /** * Attaches a player that will play ads loaded using this instance. Called on the main thread by * {@link AdsMediaSource}. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java index 397b8effd3..202e31cba1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java @@ -132,6 +132,7 @@ public final class AdsMediaSource implements MediaSource { period = new Timeline.Period(); adGroupMediaSources = new MediaSource[0][]; adDurationsUs = new long[0][]; + adsLoader.setSupportedContentTypes(C.TYPE_OTHER); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java b/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java index a68e0142d6..8307e998a0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java @@ -36,6 +36,7 @@ public final class MimeTypes { public static final String VIDEO_VP8 = BASE_TYPE_VIDEO + "/x-vnd.on2.vp8"; public static final String VIDEO_VP9 = BASE_TYPE_VIDEO + "/x-vnd.on2.vp9"; public static final String VIDEO_MP4V = BASE_TYPE_VIDEO + "/mp4v-es"; + public static final String VIDEO_MPEG = BASE_TYPE_VIDEO + "/mpeg"; public static final String VIDEO_MPEG2 = BASE_TYPE_VIDEO + "/mpeg2"; public static final String VIDEO_VC1 = BASE_TYPE_VIDEO + "/wvc1"; public static final String VIDEO_UNKNOWN = BASE_TYPE_VIDEO + "/x-unknown"; @@ -70,7 +71,9 @@ public final class MimeTypes { public static final String APPLICATION_MP4 = BASE_TYPE_APPLICATION + "/mp4"; public static final String APPLICATION_WEBM = BASE_TYPE_APPLICATION + "/webm"; + public static final String APPLICATION_MPD = BASE_TYPE_APPLICATION + "/dash+xml"; public static final String APPLICATION_M3U8 = BASE_TYPE_APPLICATION + "/x-mpegURL"; + public static final String APPLICATION_SS = BASE_TYPE_APPLICATION + "/vnd.ms-sstr+xml"; public static final String APPLICATION_ID3 = BASE_TYPE_APPLICATION + "/id3"; public static final String APPLICATION_CEA608 = BASE_TYPE_APPLICATION + "/cea-608"; public static final String APPLICATION_CEA708 = BASE_TYPE_APPLICATION + "/cea-708"; diff --git a/library/dash/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 index e2143b4bf5..2562b27237 100644 --- a/library/dash/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 @@ -905,8 +905,7 @@ public final class DashMediaSource implements MediaSource { } - private final class ManifestCallback implements - Loader.Callback> { + private final class ManifestCallback implements Loader.Callback> { @Override public void onLoadCompleted(ParsingLoadable loadable, From 9e8f50a9c06e4e7cbae6b31eca9b3acee761f7d0 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 28 Nov 2017 10:45:38 -0800 Subject: [PATCH 0801/2472] Allow multiple video and audio debug listeners in SimpleExoPlayer. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177184331 --- .../exoplayer2/demo/PlayerActivity.java | 4 +- .../android/exoplayer2/SimpleExoPlayer.java | 82 +++++++++++++++---- .../exoplayer2/testutil/ExoHostedTest.java | 32 +++++++- 3 files changed, 97 insertions(+), 21 deletions(-) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index efde775176..cf0f8b8dc8 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -298,8 +298,8 @@ public class PlayerActivity extends Activity implements OnClickListener, player.addListener(new PlayerEventListener()); player.addListener(eventLogger); player.addMetadataOutput(eventLogger); - player.setAudioDebugListener(eventLogger); - player.setVideoDebugListener(eventLogger); + player.addAudioDebugListener(eventLogger); + player.addVideoDebugListener(eventLogger); simpleExoPlayerView.setPlayer(player); player.setPlayWhenReady(shouldAutoPlay); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index a153e4ed43..909a5d0fd5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -91,6 +91,8 @@ public class SimpleExoPlayer implements ExoPlayer { private final CopyOnWriteArraySet videoListeners; private final CopyOnWriteArraySet textOutputs; private final CopyOnWriteArraySet metadataOutputs; + private final CopyOnWriteArraySet videoDebugListeners; + private final CopyOnWriteArraySet audioDebugListeners; private final int videoRendererCount; private final int audioRendererCount; @@ -103,8 +105,6 @@ public class SimpleExoPlayer implements ExoPlayer { private int videoScalingMode; private SurfaceHolder surfaceHolder; private TextureView textureView; - private AudioRendererEventListener audioDebugListener; - private VideoRendererEventListener videoDebugListener; private DecoderCounters videoDecoderCounters; private DecoderCounters audioDecoderCounters; private int audioSessionId; @@ -117,6 +117,8 @@ public class SimpleExoPlayer implements ExoPlayer { videoListeners = new CopyOnWriteArraySet<>(); textOutputs = new CopyOnWriteArraySet<>(); metadataOutputs = new CopyOnWriteArraySet<>(); + videoDebugListeners = new CopyOnWriteArraySet<>(); + audioDebugListeners = new CopyOnWriteArraySet<>(); Looper eventLooper = Looper.myLooper() != null ? Looper.myLooper() : Looper.getMainLooper(); Handler eventHandler = new Handler(eventLooper); renderers = renderersFactory.createRenderers(eventHandler, componentListener, componentListener, @@ -579,18 +581,64 @@ public class SimpleExoPlayer implements ExoPlayer { * Sets a listener to receive debug events from the video renderer. * * @param listener The listener. + * @deprecated Use {@link #addVideoDebugListener(VideoRendererEventListener)}. */ + @Deprecated public void setVideoDebugListener(VideoRendererEventListener listener) { - videoDebugListener = listener; + videoDebugListeners.clear(); + if (listener != null) { + addVideoDebugListener(listener); + } + } + + /** + * Adds a listener to receive debug events from the video renderer. + * + * @param listener The listener. + */ + public void addVideoDebugListener(VideoRendererEventListener listener) { + videoDebugListeners.add(listener); + } + + /** + * Removes a listener to receive debug events from the video renderer. + * + * @param listener The listener. + */ + public void removeVideoDebugListener(VideoRendererEventListener listener) { + videoDebugListeners.remove(listener); } /** * Sets a listener to receive debug events from the audio renderer. * * @param listener The listener. + * @deprecated Use {@link #addAudioDebugListener(AudioRendererEventListener)}. */ + @Deprecated public void setAudioDebugListener(AudioRendererEventListener listener) { - audioDebugListener = listener; + audioDebugListeners.clear(); + if (listener != null) { + addAudioDebugListener(listener); + } + } + + /** + * Adds a listener to receive debug events from the audio renderer. + * + * @param listener The listener. + */ + public void addAudioDebugListener(AudioRendererEventListener listener) { + audioDebugListeners.add(listener); + } + + /** + * Removes a listener to receive debug events from the audio renderer. + * + * @param listener The listener. + */ + public void removeAudioDebugListener(AudioRendererEventListener listener) { + audioDebugListeners.remove(listener); } // ExoPlayer implementation @@ -885,7 +933,7 @@ public class SimpleExoPlayer implements ExoPlayer { @Override public void onVideoEnabled(DecoderCounters counters) { videoDecoderCounters = counters; - if (videoDebugListener != null) { + for (VideoRendererEventListener videoDebugListener : videoDebugListeners) { videoDebugListener.onVideoEnabled(counters); } } @@ -893,7 +941,7 @@ public class SimpleExoPlayer implements ExoPlayer { @Override public void onVideoDecoderInitialized(String decoderName, long initializedTimestampMs, long initializationDurationMs) { - if (videoDebugListener != null) { + for (VideoRendererEventListener videoDebugListener : videoDebugListeners) { videoDebugListener.onVideoDecoderInitialized(decoderName, initializedTimestampMs, initializationDurationMs); } @@ -902,14 +950,14 @@ public class SimpleExoPlayer implements ExoPlayer { @Override public void onVideoInputFormatChanged(Format format) { videoFormat = format; - if (videoDebugListener != null) { + for (VideoRendererEventListener videoDebugListener : videoDebugListeners) { videoDebugListener.onVideoInputFormatChanged(format); } } @Override public void onDroppedFrames(int count, long elapsed) { - if (videoDebugListener != null) { + for (VideoRendererEventListener videoDebugListener : videoDebugListeners) { videoDebugListener.onDroppedFrames(count, elapsed); } } @@ -921,7 +969,7 @@ public class SimpleExoPlayer implements ExoPlayer { videoListener.onVideoSizeChanged(width, height, unappliedRotationDegrees, pixelWidthHeightRatio); } - if (videoDebugListener != null) { + for (VideoRendererEventListener videoDebugListener : videoDebugListeners) { videoDebugListener.onVideoSizeChanged(width, height, unappliedRotationDegrees, pixelWidthHeightRatio); } @@ -934,14 +982,14 @@ public class SimpleExoPlayer implements ExoPlayer { videoListener.onRenderedFirstFrame(); } } - if (videoDebugListener != null) { + for (VideoRendererEventListener videoDebugListener : videoDebugListeners) { videoDebugListener.onRenderedFirstFrame(surface); } } @Override public void onVideoDisabled(DecoderCounters counters) { - if (videoDebugListener != null) { + for (VideoRendererEventListener videoDebugListener : videoDebugListeners) { videoDebugListener.onVideoDisabled(counters); } videoFormat = null; @@ -953,7 +1001,7 @@ public class SimpleExoPlayer implements ExoPlayer { @Override public void onAudioEnabled(DecoderCounters counters) { audioDecoderCounters = counters; - if (audioDebugListener != null) { + for (AudioRendererEventListener audioDebugListener : audioDebugListeners) { audioDebugListener.onAudioEnabled(counters); } } @@ -961,7 +1009,7 @@ public class SimpleExoPlayer implements ExoPlayer { @Override public void onAudioSessionId(int sessionId) { audioSessionId = sessionId; - if (audioDebugListener != null) { + for (AudioRendererEventListener audioDebugListener : audioDebugListeners) { audioDebugListener.onAudioSessionId(sessionId); } } @@ -969,7 +1017,7 @@ public class SimpleExoPlayer implements ExoPlayer { @Override public void onAudioDecoderInitialized(String decoderName, long initializedTimestampMs, long initializationDurationMs) { - if (audioDebugListener != null) { + for (AudioRendererEventListener audioDebugListener : audioDebugListeners) { audioDebugListener.onAudioDecoderInitialized(decoderName, initializedTimestampMs, initializationDurationMs); } @@ -978,7 +1026,7 @@ public class SimpleExoPlayer implements ExoPlayer { @Override public void onAudioInputFormatChanged(Format format) { audioFormat = format; - if (audioDebugListener != null) { + for (AudioRendererEventListener audioDebugListener : audioDebugListeners) { audioDebugListener.onAudioInputFormatChanged(format); } } @@ -986,14 +1034,14 @@ public class SimpleExoPlayer implements ExoPlayer { @Override public void onAudioSinkUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) { - if (audioDebugListener != null) { + for (AudioRendererEventListener audioDebugListener : audioDebugListeners) { audioDebugListener.onAudioSinkUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs); } } @Override public void onAudioDisabled(DecoderCounters counters) { - if (audioDebugListener != null) { + for (AudioRendererEventListener audioDebugListener : audioDebugListeners) { audioDebugListener.onAudioDisabled(counters); } audioFormat = null; diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java index ab31238983..ab63087f95 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java @@ -78,6 +78,8 @@ public abstract class ExoHostedTest extends Player.DefaultEventListener implemen private Surface surface; private ExoPlaybackException playerError; private Player.EventListener playerEventListener; + private VideoRendererEventListener videoDebugListener; + private AudioRendererEventListener audioDebugListener; private boolean playerWasPrepared; private boolean playing; @@ -140,6 +142,26 @@ public abstract class ExoHostedTest extends Player.DefaultEventListener implemen } } + /** + * Sets an {@link VideoRendererEventListener} to listen for video debug events during the test. + */ + public final void setVideoDebugListener(VideoRendererEventListener videoDebugListener) { + this.videoDebugListener = videoDebugListener; + if (player != null) { + player.addVideoDebugListener(videoDebugListener); + } + } + + /** + * Sets an {@link AudioRendererEventListener} to listen for audio debug events during the test. + */ + public final void setAudioDebugListener(AudioRendererEventListener audioDebugListener) { + this.audioDebugListener = audioDebugListener; + if (player != null) { + player.addAudioDebugListener(audioDebugListener); + } + } + // HostedTest implementation @Override @@ -155,9 +177,15 @@ public abstract class ExoHostedTest extends Player.DefaultEventListener implemen if (playerEventListener != null) { player.addListener(playerEventListener); } + if (videoDebugListener != null) { + player.addVideoDebugListener(videoDebugListener); + } + if (audioDebugListener != null) { + player.addAudioDebugListener(audioDebugListener); + } player.addListener(this); - player.setAudioDebugListener(this); - player.setVideoDebugListener(this); + player.addAudioDebugListener(this); + player.addVideoDebugListener(this); player.setPlayWhenReady(true); actionHandler = new Handler(); // Schedule any pending actions. From 835b6382acf17835798883a518376a5a2c382407 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 29 Nov 2017 02:07:44 -0800 Subject: [PATCH 0802/2472] Move external timeline and start position overwrites to ExoPlayerImpl. Makes it less error-prone to accidentatly forget to set the right overwrites. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177282089 --- .../android/exoplayer2/ExoPlayerImpl.java | 11 ++++++- .../exoplayer2/ExoPlayerImplInternal.java | 30 ++++--------------- 2 files changed, 15 insertions(+), 26 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index 37fccafd08..ee96cb0c47 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -502,10 +502,19 @@ import java.util.concurrent.CopyOnWriteArraySet; private void handlePlaybackInfo(PlaybackInfo playbackInfo, int prepareOrStopAcks, int seekAcks, boolean positionDiscontinuity, @DiscontinuityReason int positionDiscontinuityReason) { - Assertions.checkNotNull(playbackInfo.timeline); pendingPrepareOrStopAcks -= prepareOrStopAcks; pendingSeekAcks -= seekAcks; if (pendingPrepareOrStopAcks == 0 && pendingSeekAcks == 0) { + if (playbackInfo.timeline == null) { + // Replace internal null timeline with externally visible empty timeline. + playbackInfo = playbackInfo.copyWithTimeline(Timeline.EMPTY, playbackInfo.manifest); + } + if (playbackInfo.startPositionUs == C.TIME_UNSET) { + // Replace internal unset start position with externally visible start position of zero. + playbackInfo = + playbackInfo.fromNewPosition( + playbackInfo.periodId, /* startPositionUs= */ 0, playbackInfo.contentPositionUs); + } boolean timelineOrManifestChanged = this.playbackInfo.timeline != playbackInfo.timeline || this.playbackInfo.manifest != playbackInfo.manifest; this.playbackInfo = playbackInfo; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 3bd1d2b00f..b0ef675e71 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -632,11 +632,7 @@ import java.io.IOException; if (mediaSource == null || timeline == null) { pendingInitialSeekPosition = seekPosition; eventHandler - .obtainMessage( - MSG_SEEK_ACK, - /* seekAdjusted */ 0, - 0, - timeline == null ? playbackInfo.copyWithTimeline(Timeline.EMPTY, null) : playbackInfo) + .obtainMessage(MSG_SEEK_ACK, /* seekAdjusted */ 0, 0, playbackInfo) .sendToTarget(); return; } @@ -649,10 +645,8 @@ import java.io.IOException; // Reset, but retain the source so that it can still be used should a seek occur. resetInternal( /* releaseMediaSource= */ false, /* resetPosition= */ true, /* resetState= */ false); - // Set the playback position to 0 for notifying the eventHandler (instead of C.TIME_UNSET). - eventHandler.obtainMessage(MSG_SEEK_ACK, /* seekAdjusted */ 1, 0, - playbackInfo.fromNewPosition(playbackInfo.periodId.periodIndex, /* startPositionUs= */ 0, - /* contentPositionUs= */ C.TIME_UNSET)) + eventHandler + .obtainMessage(MSG_SEEK_ACK, /* seekAdjusted */ 1, 0, playbackInfo) .sendToTarget(); return; } @@ -774,21 +768,9 @@ import java.io.IOException; private void stopInternal(boolean reset) { resetInternal( /* releaseMediaSource= */ true, /* resetPosition= */ reset, /* resetState= */ reset); - PlaybackInfo publicPlaybackInfo = playbackInfo; - if (playbackInfo.timeline == null) { - // Resetting the state sets the timeline to null. Use Timeline.EMPTY for notifying the - // eventHandler. - publicPlaybackInfo = publicPlaybackInfo.copyWithTimeline(Timeline.EMPTY, null); - } - if (playbackInfo.startPositionUs == C.TIME_UNSET) { - // When resetting the state, set the playback position to 0 (instead of C.TIME_UNSET) for - // notifying the eventHandler. - publicPlaybackInfo = - publicPlaybackInfo.fromNewPosition(playbackInfo.periodId.periodIndex, 0, C.TIME_UNSET); - } int prepareOrStopAcks = pendingPrepareCount + 1; pendingPrepareCount = 0; - notifySourceInfoRefresh(prepareOrStopAcks, publicPlaybackInfo); + notifySourceInfoRefresh(prepareOrStopAcks, playbackInfo); loadControl.onStopped(); setState(Player.STATE_IDLE); } @@ -1187,9 +1169,7 @@ import java.io.IOException; // Reset, but retain the source so that it can still be used should a seek occur. resetInternal( /* releaseMediaSource= */ false, /* resetPosition= */ true, /* resetState= */ false); - // Set the playback position to 0 for notifying the eventHandler (instead of C.TIME_UNSET). - notifySourceInfoRefresh(prepareAcks, - playbackInfo.fromNewPosition(playbackInfo.periodId.periodIndex, 0, C.TIME_UNSET)); + notifySourceInfoRefresh(prepareAcks, playbackInfo); } private void notifySourceInfoRefresh() { From 21ea9a821df397df9e52bfb61f18829c1c7333e9 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 29 Nov 2017 08:53:27 -0800 Subject: [PATCH 0803/2472] Fix weird XingSeeker indexing There are still things broken about the seeker, but this cleans up some of the weird bits. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177315136 --- .../exoplayer2/extractor/mp3/XingSeeker.java | 43 +++++++------------ 1 file changed, 16 insertions(+), 27 deletions(-) diff --git a/library/core/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 index 9b1158dfa8..55888066e7 100644 --- a/library/core/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 @@ -58,9 +58,8 @@ import com.google.android.exoplayer2.util.Util; } long sizeBytes = frame.readUnsignedIntToInt(); - frame.skipBytes(1); - long[] tableOfContents = new long[99]; - for (int i = 0; i < 99; i++) { + long[] tableOfContents = new long[100]; + for (int i = 0; i < 100; i++) { tableOfContents[i] = frame.readUnsignedByte(); } @@ -105,30 +104,20 @@ import com.google.android.exoplayer2.util.Util; if (!isSeekable()) { return firstFramePosition; } - float percent = timeUs * 100f / durationUs; - float fx; - if (percent <= 0f) { - fx = 0f; - } else if (percent >= 100f) { - fx = 256f; + double percent = (timeUs * 100d) / durationUs; + double fx; + if (percent <= 0) { + fx = 0; + } else if (percent >= 100) { + fx = 256; } else { int a = (int) percent; - float fa; - if (a == 0) { - fa = 0f; - } else { - fa = tableOfContents[a - 1]; - } - float fb; - if (a < 99) { - fb = tableOfContents[a]; - } else { - fb = 256f; - } + float fa = tableOfContents[a]; + float fb = a == 99 ? 256 : tableOfContents[a + 1]; fx = fa + (fb - fa) * (percent - a); } - long position = Math.round((1.0 / 256) * fx * sizeBytes) + firstFramePosition; + long position = Math.round((fx / 256) * sizeBytes) + firstFramePosition; long maximumPosition = inputLength != C.LENGTH_UNSET ? inputLength - 1 : firstFramePosition - headerSize + sizeBytes - 1; return Math.min(position, maximumPosition); @@ -139,14 +128,14 @@ import com.google.android.exoplayer2.util.Util; if (!isSeekable() || position < firstFramePosition) { return 0L; } - double offsetByte = 256.0 * (position - firstFramePosition) / sizeBytes; + double offsetByte = (256d * (position - firstFramePosition)) / sizeBytes; int previousTocPosition = - Util.binarySearchFloor(tableOfContents, (long) offsetByte, true, false) + 1; + Util.binarySearchFloor(tableOfContents, (long) offsetByte, true, true); long previousTime = getTimeUsForTocPosition(previousTocPosition); // Linearly interpolate the time taking into account the next entry. - long previousByte = previousTocPosition == 0 ? 0 : tableOfContents[previousTocPosition - 1]; - long nextByte = previousTocPosition == 99 ? 256 : tableOfContents[previousTocPosition]; + long previousByte = tableOfContents[previousTocPosition]; + long nextByte = previousTocPosition == 99 ? 256 : tableOfContents[previousTocPosition + 1]; long nextTime = getTimeUsForTocPosition(previousTocPosition + 1); long timeOffset = nextByte == previousByte ? 0 : (long) ((nextTime - previousTime) * (offsetByte - previousByte) / (nextByte - previousByte)); @@ -163,7 +152,7 @@ import com.google.android.exoplayer2.util.Util; * interpreted as a percentage of the stream's duration between 0 and 100. */ private long getTimeUsForTocPosition(int tocPosition) { - return durationUs * tocPosition / 100; + return (durationUs * tocPosition) / 100; } } From a99ef01d3a01dc9619fe2b6751cb44a107c58c33 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 29 Nov 2017 09:09:17 -0800 Subject: [PATCH 0804/2472] Optimize seeking for unseekable SeekMaps - Avoid re-downloading data prior to the first mdat box when seeking back to the start of an unseekable FMP4. - Avoid re-downloading data prior to the first frame for constant bitrate MP3. - Update SeekMap.getPosition documentation to allow a non-zero position for the unseekable case. Note that XingSeeker was already returning a non-zero position if unseekable. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177317256 --- .../android/exoplayer2/extractor/SeekMap.java | 16 ++++++++++++++-- .../extractor/mp3/ConstantBitrateSeeker.java | 2 +- .../extractor/mp4/FragmentedMp4Extractor.java | 3 ++- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekMap.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekMap.java index 778aa4d715..964c43a45a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekMap.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekMap.java @@ -28,13 +28,24 @@ public interface SeekMap { final class Unseekable implements SeekMap { private final long durationUs; + private final long startPosition; /** * @param durationUs The duration of the stream in microseconds, or {@link C#TIME_UNSET} if * the duration is unknown. */ public Unseekable(long durationUs) { + this(durationUs, 0); + } + + /** + * @param durationUs The duration of the stream in microseconds, or {@link C#TIME_UNSET} if + * the duration is unknown. + * @param startPosition The position (byte offset) of the start of the media. + */ + public Unseekable(long durationUs, long startPosition) { this.durationUs = durationUs; + this.startPosition = startPosition; } @Override @@ -49,7 +60,7 @@ public interface SeekMap { @Override public long getPosition(long timeUs) { - return 0; + return startPosition; } } @@ -78,7 +89,8 @@ public interface SeekMap { * * @param timeUs A seek position in microseconds. * @return The corresponding position (byte offset) in the stream from which data can be provided - * to the extractor, or 0 if {@code #isSeekable()} returns false. + * to the extractor. If {@link #isSeekable()} returns false then the returned value will be + * independent of {@code timeUs}, and will indicate the start of the media in the stream. */ long getPosition(long timeUs); diff --git a/library/core/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 index df7748a910..47e12161a8 100644 --- a/library/core/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 @@ -43,7 +43,7 @@ import com.google.android.exoplayer2.util.Util; @Override public long getPosition(long timeUs) { if (durationUs == C.TIME_UNSET) { - return 0; + return firstFramePosition; } timeUs = Util.constrainValue(timeUs, 0, durationUs); return firstFramePosition + (timeUs * bitrate) / (C.MICROS_PER_SECOND * BITS_PER_BYTE); diff --git a/library/core/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 index 4bc1b04418..28a1ffaa7b 100644 --- a/library/core/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 @@ -345,7 +345,8 @@ public final class FragmentedMp4Extractor implements Extractor { currentTrackBundle = null; endOfMdatPosition = atomPosition + atomSize; if (!haveOutputSeekMap) { - extractorOutput.seekMap(new SeekMap.Unseekable(durationUs)); + // This must be the first mdat in the stream. + extractorOutput.seekMap(new SeekMap.Unseekable(durationUs, atomPosition)); haveOutputSeekMap = true; } parserState = STATE_READING_ENCRYPTION_DATA; From 3afdb24f25d7a224e5963384d0955e3393548527 Mon Sep 17 00:00:00 2001 From: hoangtc Date: Wed, 29 Nov 2017 09:23:57 -0800 Subject: [PATCH 0805/2472] Add support for outputing Emsg messages to a sideloaded track output. Currently FragmentedMp4Extractor only parses and outputs emsg messages if the flag FLAG_ENABLE_EMSG_TRACK is set (when there's a metadata renderer that handles emsg messages). Since there are emsg messages that only targets the player, which we want to handle independently from MetadateRenderer, this CL adds the ability for FragmentedMp4Extractor to output emsg messages to an additional TrackOutput if provided, independently from FLAG_ENABLED_EMSG_TRACK. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177318983 --- .../extractor/mp4/FragmentedMp4Extractor.java | 98 ++++++++++++++----- 1 file changed, 73 insertions(+), 25 deletions(-) diff --git a/library/core/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 index 28a1ffaa7b..9a70dfbf90 100644 --- a/library/core/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 @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.extractor.mp4; import android.support.annotation.IntDef; +import android.support.annotation.Nullable; import android.util.Log; import android.util.Pair; import android.util.SparseArray; @@ -44,10 +45,10 @@ import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.LinkedList; import java.util.List; import java.util.Stack; import java.util.UUID; @@ -108,6 +109,8 @@ public final class FragmentedMp4Extractor implements Extractor { private static final int SAMPLE_GROUP_TYPE_seig = Util.getIntegerCodeForString("seig"); private static final byte[] PIFF_SAMPLE_ENCRYPTION_BOX_EXTENDED_TYPE = new byte[] {-94, 57, 79, 82, 90, -101, 79, 20, -94, 68, 108, 66, 124, 100, -115, -12}; + private static final Format EMSG_FORMAT = + Format.createSampleFormat(null, MimeTypes.APPLICATION_EMSG, Format.OFFSET_SAMPLE_RELATIVE); // Parser states. private static final int STATE_READING_ATOM_HEADER = 0; @@ -141,7 +144,8 @@ public final class FragmentedMp4Extractor implements Extractor { private final ParsableByteArray atomHeader; private final byte[] extendedTypeScratch; private final Stack containerAtoms; - private final LinkedList pendingMetadataSampleInfos; + private final ArrayDeque pendingMetadataSampleInfos; + private final @Nullable TrackOutput additionalEmsgTrackOutput; private int parserState; private int atomType; @@ -161,7 +165,7 @@ public final class FragmentedMp4Extractor implements Extractor { // Extractor output. private ExtractorOutput extractorOutput; - private TrackOutput eventMessageTrackOutput; + private TrackOutput[] emsgTrackOutputs; private TrackOutput[] cea608TrackOutputs; // Whether extractorOutput.seekMap has been called. @@ -212,11 +216,32 @@ public final class FragmentedMp4Extractor implements Extractor { */ public FragmentedMp4Extractor(@Flags int flags, TimestampAdjuster timestampAdjuster, Track sideloadedTrack, DrmInitData sideloadedDrmInitData, List closedCaptionFormats) { + this(flags, timestampAdjuster, sideloadedTrack, sideloadedDrmInitData, + closedCaptionFormats, null); + } + + /** + * @param flags Flags that control the extractor's behavior. + * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed. + * @param sideloadedTrack Sideloaded track information, in the case that the extractor + * will not receive a moov box in the input data. Null if a moov box is expected. + * @param sideloadedDrmInitData The {@link DrmInitData} to use for encrypted tracks. If null, the + * pssh boxes (if present) will be used. + * @param closedCaptionFormats For tracks that contain SEI messages, the formats of the closed + * caption channels to expose. + * @param additionalEmsgTrackOutput An extra track output that will receive all emsg messages + * targeting the player, even if {@link #FLAG_ENABLE_EMSG_TRACK} is not set. Null if special + * handling of emsg messages for players is not required. + */ + public FragmentedMp4Extractor(@Flags int flags, TimestampAdjuster timestampAdjuster, + Track sideloadedTrack, DrmInitData sideloadedDrmInitData, List closedCaptionFormats, + @Nullable TrackOutput additionalEmsgTrackOutput) { this.flags = flags | (sideloadedTrack != null ? FLAG_SIDELOADED : 0); this.timestampAdjuster = timestampAdjuster; this.sideloadedTrack = sideloadedTrack; this.sideloadedDrmInitData = sideloadedDrmInitData; this.closedCaptionFormats = Collections.unmodifiableList(closedCaptionFormats); + this.additionalEmsgTrackOutput = additionalEmsgTrackOutput; atomHeader = new ParsableByteArray(Atom.LONG_HEADER_SIZE); nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE); nalPrefix = new ParsableByteArray(5); @@ -225,7 +250,7 @@ public final class FragmentedMp4Extractor implements Extractor { defaultInitializationVector = new ParsableByteArray(); extendedTypeScratch = new byte[16]; containerAtoms = new Stack<>(); - pendingMetadataSampleInfos = new LinkedList<>(); + pendingMetadataSampleInfos = new ArrayDeque<>(); trackBundles = new SparseArray<>(); durationUs = C.TIME_UNSET; segmentIndexEarliestPresentationTimeUs = C.TIME_UNSET; @@ -494,10 +519,21 @@ public final class FragmentedMp4Extractor implements Extractor { } private void maybeInitExtraTracks() { - if ((flags & FLAG_ENABLE_EMSG_TRACK) != 0 && eventMessageTrackOutput == null) { - eventMessageTrackOutput = extractorOutput.track(trackBundles.size(), C.TRACK_TYPE_METADATA); - eventMessageTrackOutput.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_EMSG, - Format.OFFSET_SAMPLE_RELATIVE)); + if (emsgTrackOutputs == null) { + emsgTrackOutputs = new TrackOutput[2]; + int emsgTrackOutputCount = 0; + if (additionalEmsgTrackOutput != null) { + emsgTrackOutputs[emsgTrackOutputCount++] = additionalEmsgTrackOutput; + } + if ((flags & FLAG_ENABLE_EMSG_TRACK) != 0) { + emsgTrackOutputs[emsgTrackOutputCount++] = + extractorOutput.track(trackBundles.size(), C.TRACK_TYPE_METADATA); + } + emsgTrackOutputs = Arrays.copyOf(emsgTrackOutputs, emsgTrackOutputCount); + + for (TrackOutput eventMessageTrackOutput : emsgTrackOutputs) { + eventMessageTrackOutput.format(EMSG_FORMAT); + } } if (cea608TrackOutputs == null) { cea608TrackOutputs = new TrackOutput[closedCaptionFormats.size()]; @@ -510,29 +546,34 @@ public final class FragmentedMp4Extractor implements Extractor { } /** - * Handles an emsg atom (defined in 23009-1). + * Parses an emsg atom (defined in 23009-1). */ private void onEmsgLeafAtomRead(ParsableByteArray atom) { - if (eventMessageTrackOutput == null) { + if (emsgTrackOutputs.length == 0) { return; } - // Parse the event's presentation time delta. + atom.setPosition(Atom.FULL_HEADER_SIZE); + int sampleSize = atom.bytesLeft(); atom.readNullTerminatedString(); // schemeIdUri atom.readNullTerminatedString(); // value long timescale = atom.readUnsignedInt(); long presentationTimeDeltaUs = Util.scaleLargeTimestamp(atom.readUnsignedInt(), C.MICROS_PER_SECOND, timescale); + // Output the sample data. - atom.setPosition(Atom.FULL_HEADER_SIZE); - int sampleSize = atom.bytesLeft(); - eventMessageTrackOutput.sampleData(atom, sampleSize); + for (TrackOutput emsgTrackOutput : emsgTrackOutputs) { + atom.setPosition(Atom.FULL_HEADER_SIZE); + emsgTrackOutput.sampleData(atom, sampleSize); + } + // Output the sample metadata. if (segmentIndexEarliestPresentationTimeUs != C.TIME_UNSET) { - // We can output the sample metadata immediately. - eventMessageTrackOutput.sampleMetadata( - segmentIndexEarliestPresentationTimeUs + presentationTimeDeltaUs, - C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0 /* offset */, null); + for (TrackOutput emsgTrackOutput : emsgTrackOutputs) { + emsgTrackOutput.sampleMetadata( + segmentIndexEarliestPresentationTimeUs + presentationTimeDeltaUs, + C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0 /* offset */, null); + } } else { // We need the first sample timestamp in the segment before we can output the metadata. pendingMetadataSampleInfos.addLast( @@ -1194,13 +1235,8 @@ public final class FragmentedMp4Extractor implements Extractor { output.sampleMetadata(sampleTimeUs, sampleFlags, sampleSize, 0, cryptoData); - while (!pendingMetadataSampleInfos.isEmpty()) { - MetadataSampleInfo sampleInfo = pendingMetadataSampleInfos.removeFirst(); - pendingMetadataSampleBytes -= sampleInfo.size; - eventMessageTrackOutput.sampleMetadata( - sampleTimeUs + sampleInfo.presentationTimeDeltaUs, - C.BUFFER_FLAG_KEY_FRAME, sampleInfo.size, pendingMetadataSampleBytes, null); - } + // After we have the sampleTimeUs, we can commit all the pending metadata samples + outputPendingMetadataSamples(sampleTimeUs); currentTrackBundle.currentSampleIndex++; currentTrackBundle.currentSampleInTrackRun++; @@ -1214,6 +1250,18 @@ public final class FragmentedMp4Extractor implements Extractor { return true; } + private void outputPendingMetadataSamples(long sampleTimeUs) { + while (!pendingMetadataSampleInfos.isEmpty()) { + MetadataSampleInfo sampleInfo = pendingMetadataSampleInfos.removeFirst(); + pendingMetadataSampleBytes -= sampleInfo.size; + for (TrackOutput emsgTrackOutput : emsgTrackOutputs) { + emsgTrackOutput.sampleMetadata( + sampleTimeUs + sampleInfo.presentationTimeDeltaUs, + C.BUFFER_FLAG_KEY_FRAME, sampleInfo.size, pendingMetadataSampleBytes, null); + } + } + } + /** * Returns the {@link TrackBundle} whose fragment run has the earliest file position out of those * yet to be consumed, or null if all have been consumed. From 079a5b3d8cfce739971ba08dd13ed351020c77ad Mon Sep 17 00:00:00 2001 From: mdoucleff Date: Wed, 29 Nov 2017 16:59:41 -0800 Subject: [PATCH 0806/2472] Add manifestless captions support. This code fits into the pre-existing captions fetcher architecture. 1. ManifestlessCaptionsMetadata Other captions fetchers must first fetch a manifest (HLS or manifest) to discover captions tracks. This process does not exist for manifestless. All we need to do is scan the FormatStream's for the right itag, so this is an all-static class. 2. ManifestlessSubtitleWindowProvider Once a captions track is selected, a subtitles provider is instantiated. This is the main interface used by the player to retrieve captions according to playback position. This class stores fetched captions in a tree index by time for efficient lookups. Background captions fetches are used to populate the tree. 3. ManifestlessCaptionsFetch Captions are fetched one segment at a time. One instance of this object is required per fetch. It performs a blocking fetch on call(), and is intended to be submitted to a background-thread executor. 4. ManifestlessCaptionsFetch.CaptionSegment This is the result of the caption fetch. These values are used to populate the captions tree. Manifestlessness The initial request is always a headm request. There is a separate tree of every segment indexed by start time. This tree is used to improve manifestless sequence number calculation. Once we have data for the current timestamp, we walk forward through the tree to find the next unfetched sequence number, and fetch that. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177385094 --- .../google/android/exoplayer2/text/webvtt/WebvttCssStyle.java | 4 +++- .../com/google/android/exoplayer2/text/webvtt/WebvttCue.java | 4 ++-- .../android/exoplayer2/text/webvtt/WebvttCueParser.java | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/library/core/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 index 10c17e2888..a78c5afa78 100644 --- a/library/core/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 @@ -31,10 +31,11 @@ import java.util.List; * @see W3C specification - Apply * CSS properties */ -/* package */ final class WebvttCssStyle { +public final class WebvttCssStyle { public static final int UNSPECIFIED = -1; + /** Style flag enum */ @Retention(RetentionPolicy.SOURCE) @IntDef(flag = true, value = {UNSPECIFIED, STYLE_NORMAL, STYLE_BOLD, STYLE_ITALIC, STYLE_BOLD_ITALIC}) @@ -44,6 +45,7 @@ import java.util.List; public static final int STYLE_ITALIC = Typeface.ITALIC; public static final int STYLE_BOLD_ITALIC = Typeface.BOLD_ITALIC; + /** Font size unit enum */ @Retention(RetentionPolicy.SOURCE) @IntDef({UNSPECIFIED, FONT_SIZE_UNIT_PIXEL, FONT_SIZE_UNIT_EM, FONT_SIZE_UNIT_PERCENT}) public @interface FontSizeUnit {} diff --git a/library/core/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 index 295fdc656f..e16b231f7e 100644 --- a/library/core/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 @@ -23,7 +23,7 @@ import com.google.android.exoplayer2.text.Cue; /** * A representation of a WebVTT cue. */ -/* package */ final class WebvttCue extends Cue { +public final class WebvttCue extends Cue { public final long startTime; public final long endTime; @@ -59,7 +59,7 @@ import com.google.android.exoplayer2.text.Cue; * Builder for WebVTT cues. */ @SuppressWarnings("hiding") - public static final class Builder { + public static class Builder { private static final String TAG = "WebvttCueBuilder"; diff --git a/library/core/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 index 54af4dbf63..80ebecdc0e 100644 --- a/library/core/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 @@ -45,7 +45,7 @@ import java.util.regex.Pattern; /** * Parser for WebVTT cues. (https://w3c.github.io/webvtt/#cues) */ -/* package */ final class WebvttCueParser { +public final class WebvttCueParser { public static final Pattern CUE_HEADER_PATTERN = Pattern .compile("^(\\S+)\\s+-->\\s+(\\S+)(.*)?$"); @@ -90,7 +90,7 @@ import java.util.regex.Pattern; * @param styles List of styles defined by the CSS style blocks preceeding the cues. * @return Whether a valid Cue was found. */ - /* package */ boolean parseCue(ParsableByteArray webvttData, WebvttCue.Builder builder, + public boolean parseCue(ParsableByteArray webvttData, WebvttCue.Builder builder, List styles) { String firstLine = webvttData.readLine(); if (firstLine == null) { From 882d698d5f64d3cf735f97deec09cd124ed50572 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 30 Nov 2017 00:33:10 -0800 Subject: [PATCH 0807/2472] Log load errors from AdsMediaSource in the demo app ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177419981 --- .../android/exoplayer2/demo/EventLogger.java | 21 ++++++++++++++++++- .../exoplayer2/demo/PlayerActivity.java | 3 ++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java index 473a0d3441..68a10343e6 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java @@ -42,6 +42,7 @@ import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener; import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.trackselection.MappingTrackSelector; import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; import com.google.android.exoplayer2.trackselection.TrackSelection; @@ -57,7 +58,8 @@ import java.util.Locale; */ /* package */ final class EventLogger implements Player.EventListener, MetadataOutput, AudioRendererEventListener, VideoRendererEventListener, AdaptiveMediaSourceEventListener, - ExtractorMediaSource.EventListener, DefaultDrmSessionManager.EventListener { + ExtractorMediaSource.EventListener, AdsMediaSource.AdsListener, + DefaultDrmSessionManager.EventListener { private static final String TAG = "EventLogger"; private static final int MAX_TIMELINE_ITEM_LINES = 3; @@ -371,6 +373,23 @@ import java.util.Locale; // Do nothing. } + // AdsMediaSource.EventListener + + @Override + public void onAdLoadError(IOException error) { + printInternalError("loadError", error); + } + + @Override + public void onAdClicked() { + // Do nothing. + } + + @Override + public void onAdTapped() { + // Do nothing. + } + // Internal methods private void printInternalError(String type, Exception e) { diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index cf0f8b8dc8..7d0975a750 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -471,7 +471,8 @@ public class PlayerActivity extends Activity implements OnClickListener, // The demo app has a non-null overlay frame layout. simpleExoPlayerView.getOverlayFrameLayout().addView(adUiViewGroup); } - return new AdsMediaSource(mediaSource, mediaDataSourceFactory, adsLoader, adUiViewGroup); + return new AdsMediaSource(mediaSource, mediaDataSourceFactory, adsLoader, adUiViewGroup, + mainHandler, eventLogger); } private void releaseAdsLoader() { From 5865f1fe408dcd6df1be6cd72ce88cc7a7b6f7f1 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 30 Nov 2017 01:25:28 -0800 Subject: [PATCH 0808/2472] Use a MediaSource factory internally in AdsMediaSource Support ad MediaSources that aren't prepared immediately by using DeferredMediaPeriod, moved up from DynamicConcatenatingMediaSource. In a later change the new interfaces will be made public so that apps can provide their own MediaSource factories. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177424172 --- .../source/DeferredMediaPeriod.java | 139 +++++++++++++++++ .../DynamicConcatenatingMediaSource.java | 107 ------------- .../exoplayer2/source/ads/AdsMediaSource.java | 143 +++++++++++++++--- 3 files changed, 259 insertions(+), 130 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java new file mode 100644 index 0000000000..bc29b2fdf1 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java @@ -0,0 +1,139 @@ +/* + * 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; + +import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import com.google.android.exoplayer2.trackselection.TrackSelection; +import com.google.android.exoplayer2.upstream.Allocator; +import java.io.IOException; + +/** + * Media period that wraps a media source and defers calling its + * {@link MediaSource#createPeriod(MediaPeriodId, Allocator)} method until {@link #createPeriod()} + * has been called. This is useful if you need to return a media period immediately but the media + * source that should create it is not yet prepared. + */ +public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callback { + + public final MediaSource mediaSource; + + private final MediaPeriodId id; + private final Allocator allocator; + + private MediaPeriod mediaPeriod; + private Callback callback; + private long preparePositionUs; + + public DeferredMediaPeriod(MediaSource mediaSource, MediaPeriodId id, Allocator allocator) { + this.id = id; + this.allocator = allocator; + this.mediaSource = mediaSource; + } + + /** + * Calls {@link MediaSource#createPeriod(MediaPeriodId, Allocator)} on the wrapped source then + * prepares it if {@link #prepare(Callback, long)} has been called. Call {@link #releasePeriod()} + * to release the period. + */ + public void createPeriod() { + mediaPeriod = mediaSource.createPeriod(id, allocator); + if (callback != null) { + mediaPeriod.prepare(this, preparePositionUs); + } + } + + /** + * Releases the period. + */ + public void releasePeriod() { + if (mediaPeriod != null) { + mediaSource.releasePeriod(mediaPeriod); + } + } + + @Override + public void prepare(Callback callback, long preparePositionUs) { + this.callback = callback; + this.preparePositionUs = preparePositionUs; + if (mediaPeriod != null) { + mediaPeriod.prepare(this, preparePositionUs); + } + } + + @Override + public void maybeThrowPrepareError() throws IOException { + if (mediaPeriod != null) { + mediaPeriod.maybeThrowPrepareError(); + } else { + mediaSource.maybeThrowSourceInfoRefreshError(); + } + } + + @Override + public TrackGroupArray getTrackGroups() { + return mediaPeriod.getTrackGroups(); + } + + @Override + public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, + SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { + return mediaPeriod.selectTracks(selections, mayRetainStreamFlags, streams, streamResetFlags, + positionUs); + } + + @Override + public void discardBuffer(long positionUs, boolean toKeyframe) { + mediaPeriod.discardBuffer(positionUs, toKeyframe); + } + + @Override + public long readDiscontinuity() { + return mediaPeriod.readDiscontinuity(); + } + + @Override + public long getBufferedPositionUs() { + return mediaPeriod.getBufferedPositionUs(); + } + + @Override + public long seekToUs(long positionUs) { + return mediaPeriod.seekToUs(positionUs); + } + + @Override + public long getNextLoadPositionUs() { + return mediaPeriod.getNextLoadPositionUs(); + } + + @Override + public boolean continueLoading(long positionUs) { + return mediaPeriod != null && mediaPeriod.continueLoading(positionUs); + } + + @Override + public void onContinueLoadingRequested(MediaPeriod source) { + callback.onContinueLoadingRequested(this); + } + + // MediaPeriod.Callback implementation + + @Override + public void onPrepared(MediaPeriod mediaPeriod) { + callback.onPrepared(this); + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java index 6b5c8b2637..c410456e7b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java @@ -27,7 +27,6 @@ import com.google.android.exoplayer2.ExoPlayer.ExoPlayerComponent; import com.google.android.exoplayer2.ExoPlayer.ExoPlayerMessage; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder; -import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; @@ -758,111 +757,5 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl } - /** - * Media period used for periods created from unprepared media sources exposed through - * {@link DeferredTimeline}. Period preparation is postponed until the actual media source becomes - * available. - */ - private static final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callback { - - public final MediaSource mediaSource; - - private final MediaPeriodId id; - private final Allocator allocator; - - private MediaPeriod mediaPeriod; - private Callback callback; - private long preparePositionUs; - - public DeferredMediaPeriod(MediaSource mediaSource, MediaPeriodId id, Allocator allocator) { - this.id = id; - this.allocator = allocator; - this.mediaSource = mediaSource; - } - - public void createPeriod() { - mediaPeriod = mediaSource.createPeriod(id, allocator); - if (callback != null) { - mediaPeriod.prepare(this, preparePositionUs); - } - } - - public void releasePeriod() { - if (mediaPeriod != null) { - mediaSource.releasePeriod(mediaPeriod); - } - } - - @Override - public void prepare(Callback callback, long preparePositionUs) { - this.callback = callback; - this.preparePositionUs = preparePositionUs; - if (mediaPeriod != null) { - mediaPeriod.prepare(this, preparePositionUs); - } - } - - @Override - public void maybeThrowPrepareError() throws IOException { - if (mediaPeriod != null) { - mediaPeriod.maybeThrowPrepareError(); - } else { - mediaSource.maybeThrowSourceInfoRefreshError(); - } - } - - @Override - public TrackGroupArray getTrackGroups() { - return mediaPeriod.getTrackGroups(); - } - - @Override - public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, - SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { - return mediaPeriod.selectTracks(selections, mayRetainStreamFlags, streams, streamResetFlags, - positionUs); - } - - @Override - public void discardBuffer(long positionUs, boolean toKeyframe) { - mediaPeriod.discardBuffer(positionUs, toKeyframe); - } - - @Override - public long readDiscontinuity() { - return mediaPeriod.readDiscontinuity(); - } - - @Override - public long getBufferedPositionUs() { - return mediaPeriod.getBufferedPositionUs(); - } - - @Override - public long seekToUs(long positionUs) { - return mediaPeriod.seekToUs(positionUs); - } - - @Override - public long getNextLoadPositionUs() { - return mediaPeriod.getNextLoadPositionUs(); - } - - @Override - public boolean continueLoading(long positionUs) { - return mediaPeriod != null && mediaPeriod.continueLoading(positionUs); - } - - @Override - public void onContinueLoadingRequested(MediaPeriod source) { - callback.onContinueLoadingRequested(this); - } - - @Override - public void onPrepared(MediaPeriod mediaPeriod) { - callback.onPrepared(this); - } - } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java index 202e31cba1..47a2540c38 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source.ads; +import android.net.Uri; import android.os.Handler; import android.os.Looper; import android.support.annotation.Nullable; @@ -23,15 +24,19 @@ import android.view.ViewGroup; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.DeferredMediaPeriod; import com.google.android.exoplayer2.source.ExtractorMediaSource; +import com.google.android.exoplayer2.source.ExtractorMediaSource.EventListener; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Map; /** @@ -68,12 +73,12 @@ public final class AdsMediaSource implements MediaSource { private static final String TAG = "AdsMediaSource"; private final MediaSource contentMediaSource; - private final DataSource.Factory dataSourceFactory; private final AdsLoader adsLoader; private final ViewGroup adUiViewGroup; private final Handler mainHandler; private final ComponentListener componentListener; - private final Map adMediaSourceByMediaPeriod; + private final AdMediaSourceFactory adMediaSourceFactory; + private final Map> deferredMediaPeriodByAdMediaSource; private final Timeline.Period period; @Nullable private final Handler eventHandler; @@ -95,6 +100,9 @@ public final class AdsMediaSource implements MediaSource { /** * Constructs a new source that inserts ads linearly with the content specified by * {@code contentMediaSource}. + *

          + * Ad media is loaded using {@link ExtractorMediaSource}. If {@code eventListener} is + * non-{@code null} it will be notified of both ad tag and ad media load errors. * * @param contentMediaSource The {@link MediaSource} providing the content to play. * @param dataSourceFactory Factory for data sources used to load ad media. @@ -109,6 +117,9 @@ public final class AdsMediaSource implements MediaSource { /** * Constructs a new source that inserts ads linearly with the content specified by * {@code contentMediaSource}. + *

          + * Ad media is loaded using {@link ExtractorMediaSource}. If {@code eventListener} is + * non-{@code null} it will be notified of both ad tag and ad media load errors. * * @param contentMediaSource The {@link MediaSource} providing the content to play. * @param dataSourceFactory Factory for data sources used to load ad media. @@ -121,18 +132,18 @@ public final class AdsMediaSource implements MediaSource { AdsLoader adsLoader, ViewGroup adUiViewGroup, @Nullable Handler eventHandler, @Nullable AdsListener eventListener) { this.contentMediaSource = contentMediaSource; - this.dataSourceFactory = dataSourceFactory; this.adsLoader = adsLoader; this.adUiViewGroup = adUiViewGroup; this.eventHandler = eventHandler; this.eventListener = eventListener; mainHandler = new Handler(Looper.getMainLooper()); componentListener = new ComponentListener(); - adMediaSourceByMediaPeriod = new HashMap<>(); + adMediaSourceFactory = new ExtractorAdMediaSourceFactory(dataSourceFactory); + deferredMediaPeriodByAdMediaSource = new HashMap<>(); period = new Timeline.Period(); adGroupMediaSources = new MediaSource[0][]; adDurationsUs = new long[0][]; - adsLoader.setSupportedContentTypes(C.TYPE_OTHER); + adsLoader.setSupportedContentTypes(adMediaSourceFactory.getSupportedTypes()); } @Override @@ -173,10 +184,9 @@ public final class AdsMediaSource implements MediaSource { final int adGroupIndex = id.adGroupIndex; final int adIndexInAdGroup = id.adIndexInAdGroup; if (adGroupMediaSources[adGroupIndex].length <= adIndexInAdGroup) { - MediaSource adMediaSource = new ExtractorMediaSource.Builder( - adPlaybackState.adUris[id.adGroupIndex][id.adIndexInAdGroup], dataSourceFactory) - .setEventListener(mainHandler, componentListener) - .build(); + Uri adUri = adPlaybackState.adUris[id.adGroupIndex][id.adIndexInAdGroup]; + final MediaSource adMediaSource = + adMediaSourceFactory.createAdMediaSource(adUri, mainHandler, componentListener); int oldAdCount = adGroupMediaSources[id.adGroupIndex].length; if (adIndexInAdGroup >= oldAdCount) { int adCount = adIndexInAdGroup + 1; @@ -186,30 +196,37 @@ public final class AdsMediaSource implements MediaSource { Arrays.fill(adDurationsUs[adGroupIndex], oldAdCount, adCount, C.TIME_UNSET); } adGroupMediaSources[adGroupIndex][adIndexInAdGroup] = adMediaSource; - adMediaSource.prepareSource(player, false, new Listener() { + deferredMediaPeriodByAdMediaSource.put(adMediaSource, new ArrayList()); + adMediaSource.prepareSource(player, false, new MediaSource.Listener() { @Override public void onSourceInfoRefreshed(MediaSource source, Timeline timeline, - Object manifest) { - onAdSourceInfoRefreshed(adGroupIndex, adIndexInAdGroup, timeline); + @Nullable Object manifest) { + onAdSourceInfoRefreshed(adMediaSource, adGroupIndex, adIndexInAdGroup, timeline); } }); } MediaSource mediaSource = adGroupMediaSources[adGroupIndex][adIndexInAdGroup]; - MediaPeriod mediaPeriod = mediaSource.createPeriod(new MediaPeriodId(0), allocator); - adMediaSourceByMediaPeriod.put(mediaPeriod, mediaSource); - return mediaPeriod; + DeferredMediaPeriod deferredMediaPeriod = + new DeferredMediaPeriod(mediaSource, new MediaPeriodId(0), allocator); + List mediaPeriods = deferredMediaPeriodByAdMediaSource.get(mediaSource); + if (mediaPeriods == null) { + deferredMediaPeriod.createPeriod(); + } else { + // Keep track of the deferred media period so it can be populated with the real media period + // when the source's info becomes available. + mediaPeriods.add(deferredMediaPeriod); + } + return deferredMediaPeriod; } else { - return contentMediaSource.createPeriod(id, allocator); + DeferredMediaPeriod mediaPeriod = new DeferredMediaPeriod(contentMediaSource, id, allocator); + mediaPeriod.createPeriod(); + return mediaPeriod; } } @Override public void releasePeriod(MediaPeriod mediaPeriod) { - if (adMediaSourceByMediaPeriod.containsKey(mediaPeriod)) { - adMediaSourceByMediaPeriod.remove(mediaPeriod).releasePeriod(mediaPeriod); - } else { - contentMediaSource.releasePeriod(mediaPeriod); - } + ((DeferredMediaPeriod) mediaPeriod).releasePeriod(); } @Override @@ -264,9 +281,17 @@ public final class AdsMediaSource implements MediaSource { maybeUpdateSourceInfo(); } - private void onAdSourceInfoRefreshed(int adGroupIndex, int adIndexInAdGroup, Timeline timeline) { + private void onAdSourceInfoRefreshed(MediaSource mediaSource, int adGroupIndex, + int adIndexInAdGroup, Timeline timeline) { Assertions.checkArgument(timeline.getPeriodCount() == 1); adDurationsUs[adGroupIndex][adIndexInAdGroup] = timeline.getPeriod(0, period).getDurationUs(); + if (deferredMediaPeriodByAdMediaSource.containsKey(mediaSource)) { + List mediaPeriods = deferredMediaPeriodByAdMediaSource.get(mediaSource); + for (int i = 0; i < mediaPeriods.size(); i++) { + mediaPeriods.get(i).createPeriod(); + } + deferredMediaPeriodByAdMediaSource.remove(mediaSource); + } maybeUpdateSourceInfo(); } @@ -285,7 +310,7 @@ public final class AdsMediaSource implements MediaSource { * Listener for component events. All methods are called on the main thread. */ private final class ComponentListener implements AdsLoader.EventListener, - ExtractorMediaSource.EventListener { + AdMediaSourceLoadErrorListener { @Override public void onAdPlaybackState(final AdPlaybackState adPlaybackState) { @@ -349,4 +374,76 @@ public final class AdsMediaSource implements MediaSource { } + /** + * Listener for errors while loading an ad {@link MediaSource}. + */ + private interface AdMediaSourceLoadErrorListener { + + /** + * Called when an error occurs loading media data. + * + * @param error The load error. + */ + void onLoadError(IOException error); + + } + + /** + * Factory for {@link MediaSource}s for loading ad media. + */ + private interface AdMediaSourceFactory { + + /** + * Creates a new {@link MediaSource} for loading the ad media with the specified {@code uri}. + * + * @param uri The URI of the ad. + * @param handler A handler for listener events. + * @param listener A listener for ad load errors. To have ad media source load errors notified + * via the ads media source's listener, call this listener's onLoadError method from your + * new media source's load error listener using the specified {@code handler}. Otherwise, + * this parameter can be ignored. + * @return The new media source. + */ + MediaSource createAdMediaSource(Uri uri, Handler handler, + AdMediaSourceLoadErrorListener listener); + + /** + * Returns the content types supported by media sources created by this factory. Each element + * should be one of {@link C#TYPE_DASH}, {@link C#TYPE_SS}, {@link C#TYPE_HLS} or + * {@link C#TYPE_OTHER}. + * + * @return The content types supported by the factory. + */ + int[] getSupportedTypes(); + + } + + private static final class ExtractorAdMediaSourceFactory implements AdMediaSourceFactory { + + private final DataSource.Factory dataSourceFactory; + + public ExtractorAdMediaSourceFactory(DataSource.Factory dataSourceFactory) { + this.dataSourceFactory = dataSourceFactory; + } + + @Override + public MediaSource createAdMediaSource(Uri uri, Handler handler, + final AdMediaSourceLoadErrorListener listener) { + return new ExtractorMediaSource.Builder(uri, dataSourceFactory).setEventListener(handler, + new EventListener() { + @Override + public void onLoadError(IOException error) { + listener.onLoadError(error); + } + }).build(); + } + + @Override + public int[] getSupportedTypes() { + // Only ExtractorMediaSource is supported. + return new int[] {C.TYPE_OTHER}; + } + + } + } From a9c33590dfa0297a3e281c0d17bacf2d2032b158 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 30 Nov 2017 01:27:07 -0800 Subject: [PATCH 0809/2472] Update getPosition(0) positions for FragmentedMp4Extractor ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177424314 --- .../src/androidTest/assets/mp4/sample_fragmented.mp4.0.dump | 2 +- .../src/androidTest/assets/mp4/sample_fragmented_sei.mp4.0.dump | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/library/core/src/androidTest/assets/mp4/sample_fragmented.mp4.0.dump b/library/core/src/androidTest/assets/mp4/sample_fragmented.mp4.0.dump index bf822d9db4..95f6528fd6 100644 --- a/library/core/src/androidTest/assets/mp4/sample_fragmented.mp4.0.dump +++ b/library/core/src/androidTest/assets/mp4/sample_fragmented.mp4.0.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = false duration = UNSET TIME - getPosition(0) = 0 + getPosition(0) = 1828 numberOfTracks = 2 track 0: format: diff --git a/library/core/src/androidTest/assets/mp4/sample_fragmented_sei.mp4.0.dump b/library/core/src/androidTest/assets/mp4/sample_fragmented_sei.mp4.0.dump index 9d3755b23b..ebd33133e2 100644 --- a/library/core/src/androidTest/assets/mp4/sample_fragmented_sei.mp4.0.dump +++ b/library/core/src/androidTest/assets/mp4/sample_fragmented_sei.mp4.0.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = false duration = UNSET TIME - getPosition(0) = 0 + getPosition(0) = 1828 numberOfTracks = 3 track 0: format: From a367ae0d2bbde0dc9feea9a4295070f653b788fb Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 30 Nov 2017 02:57:07 -0800 Subject: [PATCH 0810/2472] Add a notice that NDK <= version 15c is required for VP9 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177430827 --- extensions/vp9/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/extensions/vp9/README.md b/extensions/vp9/README.md index 941b413c09..649e4a6ee2 100644 --- a/extensions/vp9/README.md +++ b/extensions/vp9/README.md @@ -28,7 +28,8 @@ EXOPLAYER_ROOT="$(pwd)" VP9_EXT_PATH="${EXOPLAYER_ROOT}/extensions/vp9/src/main" ``` -* Download the [Android NDK][] and set its location in an environment variable: +* Download the [Android NDK][] and set its location in an environment variable. +Only versions up to NDK 15c are supported currently (see [#3520][]). ``` NDK_PATH="" @@ -70,6 +71,7 @@ ${NDK_PATH}/ndk-build APP_ABI=all -j4 [top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md [Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html +[#3520]: https://github.com/google/ExoPlayer/issues/3520 ## Notes ## From ef8fa28163e6a72d4519c21542484be7785cff06 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 30 Nov 2017 03:39:17 -0800 Subject: [PATCH 0811/2472] Use VideoRendererEventListener to resolve unknown resolution. Some streams don't have the new video resolution in the primary format. Use the subsequent call to videoListener.onVideoInputFormatChanged to resolve this unknown resolution. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177433618 --- .../testutil/ExoPlayerTestRunner.java | 100 +++++++++++++----- 1 file changed, 76 insertions(+), 24 deletions(-) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java index 62e950091b..fddeb60bf0 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java @@ -102,6 +102,8 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener private RenderersFactory renderersFactory; private ActionSchedule actionSchedule; private Player.EventListener eventListener; + private VideoRendererEventListener videoRendererEventListener; + private AudioRendererEventListener audioRendererEventListener; private Integer expectedPlayerEndedCount; /** @@ -258,6 +260,28 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener return this; } + /** + * Sets a {@link VideoRendererEventListener} to be registered. + * + * @param eventListener A {@link VideoRendererEventListener} to be registered. + * @return This builder. + */ + public Builder setVideoRendererEventListener(VideoRendererEventListener eventListener) { + this.videoRendererEventListener = eventListener; + return this; + } + + /** + * Sets an {@link AudioRendererEventListener} to be registered. + * + * @param eventListener An {@link AudioRendererEventListener} to be registered. + * @return This builder. + */ + public Builder setAudioRendererEventListener(AudioRendererEventListener eventListener) { + this.audioRendererEventListener = eventListener; + return this; + } + /** * Sets the number of times the test runner is expected to reach the {@link Player#STATE_ENDED} * or {@link Player#STATE_IDLE}. The default is 1. This affects how long @@ -319,8 +343,17 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener if (expectedPlayerEndedCount == null) { expectedPlayerEndedCount = 1; } - return new ExoPlayerTestRunner(playerFactory, mediaSource, renderersFactory, trackSelector, - loadControl, actionSchedule, eventListener, expectedPlayerEndedCount); + return new ExoPlayerTestRunner( + playerFactory, + mediaSource, + renderersFactory, + trackSelector, + loadControl, + actionSchedule, + eventListener, + videoRendererEventListener, + audioRendererEventListener, + expectedPlayerEndedCount); } } @@ -331,6 +364,8 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener private final LoadControl loadControl; private final @Nullable ActionSchedule actionSchedule; private final @Nullable Player.EventListener eventListener; + private final @Nullable VideoRendererEventListener videoRendererEventListener; + private final @Nullable AudioRendererEventListener audioRendererEventListener; private final HandlerThread playerThread; private final Handler handler; @@ -347,10 +382,17 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener private TrackGroupArray trackGroups; private boolean playerWasPrepared; - private ExoPlayerTestRunner(PlayerFactory playerFactory, MediaSource mediaSource, - RenderersFactory renderersFactory, MappingTrackSelector trackSelector, - LoadControl loadControl, @Nullable ActionSchedule actionSchedule, - @Nullable Player.EventListener eventListener, int expectedPlayerEndedCount) { + private ExoPlayerTestRunner( + PlayerFactory playerFactory, + MediaSource mediaSource, + RenderersFactory renderersFactory, + MappingTrackSelector trackSelector, + LoadControl loadControl, + @Nullable ActionSchedule actionSchedule, + @Nullable Player.EventListener eventListener, + @Nullable VideoRendererEventListener videoRendererEventListener, + @Nullable AudioRendererEventListener audioRendererEventListener, + int expectedPlayerEndedCount) { this.playerFactory = playerFactory; this.mediaSource = mediaSource; this.renderersFactory = renderersFactory; @@ -358,6 +400,8 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener this.loadControl = loadControl; this.actionSchedule = actionSchedule; this.eventListener = eventListener; + this.videoRendererEventListener = videoRendererEventListener; + this.audioRendererEventListener = audioRendererEventListener; this.timelines = new ArrayList<>(); this.manifests = new ArrayList<>(); this.timelineChangeReasons = new ArrayList<>(); @@ -380,25 +424,33 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener * @return This test runner. */ public ExoPlayerTestRunner start() { - handler.post(new Runnable() { - @Override - public void run() { - try { - player = playerFactory.createExoPlayer(renderersFactory, trackSelector, loadControl); - player.addListener(ExoPlayerTestRunner.this); - if (eventListener != null) { - player.addListener(eventListener); + handler.post( + new Runnable() { + @Override + public void run() { + try { + player = playerFactory.createExoPlayer(renderersFactory, trackSelector, loadControl); + player.addListener(ExoPlayerTestRunner.this); + if (eventListener != null) { + player.addListener(eventListener); + } + if (videoRendererEventListener != null) { + player.addVideoDebugListener(videoRendererEventListener); + } + if (audioRendererEventListener != null) { + player.addAudioDebugListener(audioRendererEventListener); + } + player.setPlayWhenReady(true); + if (actionSchedule != null) { + actionSchedule.start( + player, trackSelector, null, handler, ExoPlayerTestRunner.this); + } + player.prepare(mediaSource); + } catch (Exception e) { + handleException(e); + } } - player.setPlayWhenReady(true); - if (actionSchedule != null) { - actionSchedule.start(player, trackSelector, null, handler, ExoPlayerTestRunner.this); - } - player.prepare(mediaSource); - } catch (Exception e) { - handleException(e); - } - } - }); + }); return this; } From ce8736c71ace969920a49f94ef3e8a88b5f939fd Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 30 Nov 2017 04:12:48 -0800 Subject: [PATCH 0812/2472] Avoid concurrent read/write access to sampleQueues in HlsSampleStreamWrapper ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177435977 --- .../exoplayer2/source/hls/HlsSampleStreamWrapper.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/library/hls/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 index 87585a52da..6cb3f854c8 100644 --- a/library/hls/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 @@ -16,12 +16,15 @@ package com.google.android.exoplayer2.source.hls; import android.os.Handler; +import android.util.Log; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import com.google.android.exoplayer2.extractor.DummyTrackOutput; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.SeekMap; +import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SampleQueue; import com.google.android.exoplayer2.source.SampleQueue.UpstreamFormatChangedListener; @@ -67,6 +70,8 @@ import java.util.Arrays; } + private static final String TAG = "HlsSampleStreamWrapper"; + private static final int PRIMARY_TYPE_NONE = 0; private static final int PRIMARY_TYPE_TEXT = 1; private static final int PRIMARY_TYPE_AUDIO = 2; @@ -588,13 +593,17 @@ import java.util.Arrays; // ExtractorOutput implementation. Called by the loading thread. @Override - public SampleQueue track(int id, int type) { + public TrackOutput track(int id, int type) { int trackCount = sampleQueues.length; for (int i = 0; i < trackCount; i++) { if (sampleQueueTrackIds[i] == id) { return sampleQueues[i]; } } + if (sampleQueuesBuilt) { + Log.w(TAG, "Unmapped track with id " + id + " of type " + type); + return new DummyTrackOutput(); + } SampleQueue trackOutput = new SampleQueue(allocator); trackOutput.setSampleOffsetUs(sampleOffsetUs); trackOutput.setUpstreamFormatChangeListener(this); From 58e60e1f9d4506e5e537b095840c6dcfa678fac9 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 30 Nov 2017 04:48:24 -0800 Subject: [PATCH 0813/2472] Use the Builder pattern for DefaultTrackSelector#Parameters ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177438430 --- RELEASENOTES.md | 1 + .../trackselection/DefaultTrackSelector.java | 487 +++++++++--------- .../DefaultTrackSelectorTest.java | 63 +-- 3 files changed, 279 insertions(+), 272 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 2c07ad6118..90dbdb6b00 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,6 +2,7 @@ ### dev-v2 (not yet released) ### +* Replace `DefaultTrackSelector.Parameters` copy methods with a builder. * Allow more flexible loading strategy when playing media containing multiple sub-streams, by allowing injection of custom `CompositeSequenceableLoader` factories through `DashMediaSource.Builder`, `HlsMediaSource.Builder`, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index 49b8e8964b..b4fa64c8fd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -74,11 +74,252 @@ import java.util.concurrent.atomic.AtomicReference; */ public class DefaultTrackSelector extends MappingTrackSelector { + /** + * A builder for {@link Parameters}. + */ + public static final class ParametersBuilder { + + private String preferredAudioLanguage; + private String preferredTextLanguage; + private boolean selectUndeterminedTextLanguage; + private boolean forceLowestBitrate; + private boolean allowMixedMimeAdaptiveness; + private boolean allowNonSeamlessAdaptiveness; + private int maxVideoWidth; + private int maxVideoHeight; + private int maxVideoBitrate; + private boolean exceedVideoConstraintsIfNecessary; + private boolean exceedRendererCapabilitiesIfNecessary; + private int viewportWidth; + private int viewportHeight; + private boolean viewportOrientationMayChange; + + /** + * Creates a builder obtaining the initial values from {@link Parameters#DEFAULT}. + */ + public ParametersBuilder() { + this(Parameters.DEFAULT); + } + + /** + * @param initialValues The {@link Parameters} from which the initial values of the builder are + * obtained. + */ + private ParametersBuilder(Parameters initialValues) { + preferredAudioLanguage = initialValues.preferredAudioLanguage; + preferredTextLanguage = initialValues.preferredTextLanguage; + selectUndeterminedTextLanguage = initialValues.selectUndeterminedTextLanguage; + forceLowestBitrate = initialValues.forceLowestBitrate; + allowMixedMimeAdaptiveness = initialValues.allowMixedMimeAdaptiveness; + allowNonSeamlessAdaptiveness = initialValues.allowNonSeamlessAdaptiveness; + maxVideoWidth = initialValues.maxVideoWidth; + maxVideoHeight = initialValues.maxVideoHeight; + maxVideoBitrate = initialValues.maxVideoBitrate; + exceedVideoConstraintsIfNecessary = initialValues.exceedVideoConstraintsIfNecessary; + exceedRendererCapabilitiesIfNecessary = initialValues.exceedRendererCapabilitiesIfNecessary; + viewportWidth = initialValues.viewportWidth; + viewportHeight = initialValues.viewportHeight; + viewportOrientationMayChange = initialValues.viewportOrientationMayChange; + } + + /** + * See {@link Parameters#preferredAudioLanguage}. + * + * @return This builder. + */ + public ParametersBuilder setPreferredAudioLanguage(String preferredAudioLanguage) { + this.preferredAudioLanguage = preferredAudioLanguage; + return this; + } + + /** + * See {@link Parameters#preferredTextLanguage}. + * + * @return This builder. + */ + public ParametersBuilder setPreferredTextLanguage(String preferredTextLanguage) { + this.preferredTextLanguage = preferredTextLanguage; + return this; + } + + /** + * See {@link Parameters#selectUndeterminedTextLanguage}. + * + * @return This builder. + */ + public ParametersBuilder setSelectUndeterminedTextLanguage( + boolean selectUndeterminedTextLanguage) { + this.selectUndeterminedTextLanguage = selectUndeterminedTextLanguage; + return this; + } + + /** + * See {@link Parameters#forceLowestBitrate}. + * + * @return This builder. + */ + public ParametersBuilder setForceLowestBitrate(boolean forceLowestBitrate) { + this.forceLowestBitrate = forceLowestBitrate; + return this; + } + + /** + * See {@link Parameters#allowMixedMimeAdaptiveness}. + * + * @return This builder. + */ + public ParametersBuilder setAllowMixedMimeAdaptiveness(boolean allowMixedMimeAdaptiveness) { + this.allowMixedMimeAdaptiveness = allowMixedMimeAdaptiveness; + return this; + } + + /** + * See {@link Parameters#allowNonSeamlessAdaptiveness}. + * + * @return This builder. + */ + public ParametersBuilder setAllowNonSeamlessAdaptiveness(boolean allowNonSeamlessAdaptiveness) { + this.allowNonSeamlessAdaptiveness = allowNonSeamlessAdaptiveness; + return this; + } + + /** + * Equivalent to {@link #setMaxVideoSize setMaxVideoSize(1279, 719)}. + * + * @return This builder. + */ + public ParametersBuilder setMaxVideoSizeSd() { + return setMaxVideoSize(1279, 719); + } + + /** + * Equivalent to {@link #setMaxVideoSize setMaxVideoSize(Integer.MAX_VALUE, Integer.MAX_VALUE)}. + * + * @return This builder. + */ + public ParametersBuilder clearVideoSizeConstraints() { + return setMaxVideoSize(Integer.MAX_VALUE, Integer.MAX_VALUE); + } + + /** + * See {@link Parameters#maxVideoWidth} and {@link Parameters#maxVideoHeight}. + * + * @return This builder. + */ + public ParametersBuilder setMaxVideoSize(int maxVideoWidth, int maxVideoHeight) { + this.maxVideoWidth = maxVideoWidth; + this.maxVideoHeight = maxVideoHeight; + return this; + } + + /** + * See {@link Parameters#maxVideoBitrate}. + * + * @return This builder. + */ + public ParametersBuilder setMaxVideoBitrate(int maxVideoBitrate) { + this.maxVideoBitrate = maxVideoBitrate; + return this; + } + + /** + * See {@link Parameters#exceedVideoConstraintsIfNecessary}. + * + * @return This builder. + */ + public ParametersBuilder setExceedVideoConstraintsIfNecessary( + boolean exceedVideoConstraintsIfNecessary) { + this.exceedVideoConstraintsIfNecessary = exceedVideoConstraintsIfNecessary; + return this; + } + + /** + * See {@link Parameters#exceedRendererCapabilitiesIfNecessary}. + * + * @return This builder. + */ + public ParametersBuilder setExceedRendererCapabilitiesIfNecessary( + boolean exceedRendererCapabilitiesIfNecessary) { + this.exceedRendererCapabilitiesIfNecessary = exceedRendererCapabilitiesIfNecessary; + return this; + } + + /** + * Equivalent to invoking {@link #setViewportSize} with the viewport size values obtained from + * the provided {@link Context}. + * + * @param context The context to obtain the viewport size from. + * @param viewportOrientationMayChange See {@link #viewportOrientationMayChange}. + * @return This builder. + */ + public ParametersBuilder setViewportSizeFromContext(Context context, + boolean viewportOrientationMayChange) { + // Assume the viewport is fullscreen. + Point viewportSize = Util.getPhysicalDisplaySize(context); + return setViewportSize(viewportSize.x, viewportSize.y, viewportOrientationMayChange); + } + + /** + * Equivalent to + * {@link #setViewportSize setViewportSize(Integer.MAX_VALUE, Integer.MAX_VALUE, true)}. + * + * @return This builder. + */ + public ParametersBuilder clearViewportSizeConstraints() { + return setViewportSize(Integer.MAX_VALUE, Integer.MAX_VALUE, true); + } + + /** + * See {@link Parameters#viewportWidth}, {@link Parameters#maxVideoHeight} and + * {@link Parameters#viewportOrientationMayChange}. + * + * @return This builder. + */ + public ParametersBuilder setViewportSize(int viewportWidth, int viewportHeight, + boolean viewportOrientationMayChange) { + this.viewportWidth = viewportWidth; + this.viewportHeight = viewportHeight; + this.viewportOrientationMayChange = viewportOrientationMayChange; + return this; + } + + /** + * Builds a {@link Parameters} instance with the selected values. + */ + public Parameters build() { + return new Parameters(preferredAudioLanguage, preferredTextLanguage, + selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, + allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, + exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, + viewportHeight, viewportOrientationMayChange); + } + + } + /** * Constraint parameters for {@link DefaultTrackSelector}. */ public static final class Parameters { + /** + * An instance with default values: + *

            + *
          • No preferred audio language.
          • + *
          • No preferred text language.
          • + *
          • Text tracks with undetermined language are not selected if no track with + * {@link #preferredTextLanguage} is available.
          • + *
          • Lowest bitrate track selections are not forced.
          • + *
          • Adaptation between different mime types is not allowed.
          • + *
          • Non seamless adaptation is allowed.
          • + *
          • No max limit for video width/height.
          • + *
          • No max video bitrate.
          • + *
          • Video constraints are exceeded if no supported selection can be made otherwise.
          • + *
          • Renderer capabilities are exceeded if no supported selection can be made.
          • + *
          • No viewport constraints.
          • + *
          + */ + public static final Parameters DEFAULT = new Parameters(); + // Audio /** * The preferred language for audio, as well as for forced text tracks, as an ISO 639-2/T tag. @@ -150,52 +391,19 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ public final boolean exceedRendererCapabilitiesIfNecessary; - /** - * Default parameters. The default values are: - *
            - *
          • No preferred audio language is set.
          • - *
          • No preferred text language is set.
          • - *
          • Text tracks with undetermined language are not selected if no track with - * {@link #preferredTextLanguage} is available.
          • - *
          • Lowest bitrate track selections are not forced.
          • - *
          • Adaptation between different mime types is not allowed.
          • - *
          • Non seamless adaptation is allowed.
          • - *
          • No max limit for video width/height.
          • - *
          • No max video bitrate.
          • - *
          • Video constraints are exceeded if no supported selection can be made otherwise.
          • - *
          • Renderer capabilities are exceeded if no supported selection can be made.
          • - *
          • No viewport constraints are set.
          • - *
          - */ - public Parameters() { + private Parameters() { this(null, null, false, false, false, true, Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE, true, true, Integer.MAX_VALUE, Integer.MAX_VALUE, true); } - /** - * @param preferredAudioLanguage See {@link #preferredAudioLanguage} - * @param preferredTextLanguage See {@link #preferredTextLanguage} - * @param selectUndeterminedTextLanguage See {@link #selectUndeterminedTextLanguage}. - * @param forceLowestBitrate See {@link #forceLowestBitrate}. - * @param allowMixedMimeAdaptiveness See {@link #allowMixedMimeAdaptiveness} - * @param allowNonSeamlessAdaptiveness See {@link #allowNonSeamlessAdaptiveness} - * @param maxVideoWidth See {@link #maxVideoWidth} - * @param maxVideoHeight See {@link #maxVideoHeight} - * @param maxVideoBitrate See {@link #maxVideoBitrate} - * @param exceedVideoConstraintsIfNecessary See {@link #exceedVideoConstraintsIfNecessary} - * @param exceedRendererCapabilitiesIfNecessary See {@link #preferredTextLanguage} - * @param viewportWidth See {@link #viewportWidth} - * @param viewportHeight See {@link #viewportHeight} - * @param viewportOrientationMayChange See {@link #viewportOrientationMayChange} - */ - public Parameters(String preferredAudioLanguage, String preferredTextLanguage, + private Parameters(String preferredAudioLanguage, String preferredTextLanguage, boolean selectUndeterminedTextLanguage, boolean forceLowestBitrate, boolean allowMixedMimeAdaptiveness, boolean allowNonSeamlessAdaptiveness, int maxVideoWidth, int maxVideoHeight, int maxVideoBitrate, boolean exceedVideoConstraintsIfNecessary, boolean exceedRendererCapabilitiesIfNecessary, int viewportWidth, int viewportHeight, boolean viewportOrientationMayChange) { - this.preferredAudioLanguage = preferredAudioLanguage; - this.preferredTextLanguage = preferredTextLanguage; + this.preferredAudioLanguage = Util.normalizeLanguageCode(preferredAudioLanguage); + this.preferredTextLanguage = Util.normalizeLanguageCode(preferredTextLanguage); this.selectUndeterminedTextLanguage = selectUndeterminedTextLanguage; this.forceLowestBitrate = forceLowestBitrate; this.allowMixedMimeAdaptiveness = allowMixedMimeAdaptiveness; @@ -211,205 +419,10 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * Returns an instance with the provided {@link #preferredAudioLanguage}. + * Creates a new {@link ParametersBuilder}, copying the initial values from this instance. */ - public Parameters withPreferredAudioLanguage(String preferredAudioLanguage) { - preferredAudioLanguage = Util.normalizeLanguageCode(preferredAudioLanguage); - if (TextUtils.equals(preferredAudioLanguage, this.preferredAudioLanguage)) { - return this; - } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, - selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, - allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, - exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, - viewportHeight, viewportOrientationMayChange); - } - - /** - * Returns an instance with the provided {@link #preferredTextLanguage}. - */ - public Parameters withPreferredTextLanguage(String preferredTextLanguage) { - preferredTextLanguage = Util.normalizeLanguageCode(preferredTextLanguage); - if (TextUtils.equals(preferredTextLanguage, this.preferredTextLanguage)) { - return this; - } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, - selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, - allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, - exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, - viewportHeight, viewportOrientationMayChange); - } - - /** - * Returns an instance with the provided {@link #selectUndeterminedTextLanguage}. - */ - public Parameters withSelectUndeterminedTextLanguage(boolean selectUndeterminedTextLanguage) { - if (selectUndeterminedTextLanguage == this.selectUndeterminedTextLanguage) { - return this; - } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, - selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, - allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, - exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, - viewportHeight, viewportOrientationMayChange); - } - - /** - * Returns an instance with the provided {@link #forceLowestBitrate}. - */ - public Parameters withForceLowestBitrate(boolean forceLowestBitrate) { - if (forceLowestBitrate == this.forceLowestBitrate) { - return this; - } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, - selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, - allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, - exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, - viewportHeight, viewportOrientationMayChange); - } - - /** - * Returns an instance with the provided {@link #allowMixedMimeAdaptiveness}. - */ - public Parameters withAllowMixedMimeAdaptiveness(boolean allowMixedMimeAdaptiveness) { - if (allowMixedMimeAdaptiveness == this.allowMixedMimeAdaptiveness) { - return this; - } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, - selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, - allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, - exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, - viewportHeight, viewportOrientationMayChange); - } - - /** - * Returns an instance with the provided {@link #allowNonSeamlessAdaptiveness}. - */ - public Parameters withAllowNonSeamlessAdaptiveness(boolean allowNonSeamlessAdaptiveness) { - if (allowNonSeamlessAdaptiveness == this.allowNonSeamlessAdaptiveness) { - return this; - } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, - selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, - allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, - exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, - viewportHeight, viewportOrientationMayChange); - } - - /** - * Returns an instance with the provided {@link #maxVideoWidth} and {@link #maxVideoHeight}. - */ - public Parameters withMaxVideoSize(int maxVideoWidth, int maxVideoHeight) { - if (maxVideoWidth == this.maxVideoWidth && maxVideoHeight == this.maxVideoHeight) { - return this; - } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, - selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, - allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, - exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, - viewportHeight, viewportOrientationMayChange); - } - - /** - * Returns an instance with the provided {@link #maxVideoBitrate}. - */ - public Parameters withMaxVideoBitrate(int maxVideoBitrate) { - if (maxVideoBitrate == this.maxVideoBitrate) { - return this; - } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, - selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, - allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, - exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, - viewportHeight, viewportOrientationMayChange); - } - - /** - * Equivalent to {@code withMaxVideoSize(1279, 719)}. - * - * @return An instance with maximum standard definition as maximum video size. - */ - public Parameters withMaxVideoSizeSd() { - return withMaxVideoSize(1279, 719); - } - - /** - * Equivalent to {@code withMaxVideoSize(Integer.MAX_VALUE, Integer.MAX_VALUE)}. - * - * @return An instance without video size constraints. - */ - public Parameters withoutVideoSizeConstraints() { - return withMaxVideoSize(Integer.MAX_VALUE, Integer.MAX_VALUE); - } - - /** - * Returns an instance with the provided {@link #exceedVideoConstraintsIfNecessary}. - */ - public Parameters withExceedVideoConstraintsIfNecessary( - boolean exceedVideoConstraintsIfNecessary) { - if (exceedVideoConstraintsIfNecessary == this.exceedVideoConstraintsIfNecessary) { - return this; - } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, - selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, - allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, - exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, - viewportHeight, viewportOrientationMayChange); - } - - /** - * Returns an instance with the provided {@link #exceedRendererCapabilitiesIfNecessary}. - */ - public Parameters withExceedRendererCapabilitiesIfNecessary( - boolean exceedRendererCapabilitiesIfNecessary) { - if (exceedRendererCapabilitiesIfNecessary == this.exceedRendererCapabilitiesIfNecessary) { - return this; - } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, - selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, - allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, - exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, - viewportHeight, viewportOrientationMayChange); - } - - /** - * Returns an instance with the provided {@link #viewportWidth}, {@link #viewportHeight} and - * {@link #viewportOrientationMayChange}. - */ - public Parameters withViewportSize(int viewportWidth, int viewportHeight, - boolean viewportOrientationMayChange) { - if (viewportWidth == this.viewportWidth && viewportHeight == this.viewportHeight - && viewportOrientationMayChange == this.viewportOrientationMayChange) { - return this; - } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, - selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, - allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, - exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, - viewportHeight, viewportOrientationMayChange); - } - - /** - * Returns an instance where the viewport size is obtained from the provided {@link Context}. - * - * @param context The context to obtain the viewport size from. - * @param viewportOrientationMayChange See {@link #viewportOrientationMayChange}. - * @return An instance where the viewport size is obtained from the provided {@link Context}. - */ - public Parameters withViewportSizeFromContext(Context context, - boolean viewportOrientationMayChange) { - // Assume the viewport is fullscreen. - Point viewportSize = Util.getPhysicalDisplaySize(context); - return withViewportSize(viewportSize.x, viewportSize.y, viewportOrientationMayChange); - } - - /** - * Equivalent to {@code withViewportSize(Integer.MAX_VALUE, Integer.MAX_VALUE, true)}. - * - * @return An instance without viewport size constraints. - */ - public Parameters withoutViewportSizeConstraints() { - return withViewportSize(Integer.MAX_VALUE, Integer.MAX_VALUE, true); + public ParametersBuilder buildUpon() { + return new ParametersBuilder(this); } @Override @@ -492,7 +505,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ public DefaultTrackSelector(TrackSelection.Factory adaptiveTrackSelectionFactory) { this.adaptiveTrackSelectionFactory = adaptiveTrackSelectionFactory; - paramsReference = new AtomicReference<>(new Parameters()); + paramsReference = new AtomicReference<>(Parameters.DEFAULT); } /** @@ -882,7 +895,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { return isSupported(formatSupport, false) && format.channelCount == configuration.channelCount && format.sampleRate == configuration.sampleRate && (configuration.mimeType == null - || TextUtils.equals(configuration.mimeType, format.sampleMimeType)); + || TextUtils.equals(configuration.mimeType, format.sampleMimeType)); } // Text track selection implementation. diff --git a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java index 6b14d139ae..1eff48b730 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java @@ -15,6 +15,7 @@ import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.Parameters; +import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.ParametersBuilder; import com.google.android.exoplayer2.trackselection.TrackSelector.InvalidationListener; import com.google.android.exoplayer2.util.MimeTypes; import java.util.HashMap; @@ -33,7 +34,6 @@ import org.robolectric.annotation.Config; @Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) public final class DefaultTrackSelectorTest { - private static final Parameters DEFAULT_PARAMETERS = new Parameters(); private static final RendererCapabilities ALL_AUDIO_FORMAT_SUPPORTED_RENDERER_CAPABILITIES = new FakeRendererCapabilities(C.TRACK_TYPE_AUDIO); private static final RendererCapabilities ALL_TEXT_FORMAT_SUPPORTED_RENDERER_CAPABILITIES = @@ -61,7 +61,6 @@ public final class DefaultTrackSelectorTest { public void testSetParameterWithDefaultParametersDoesNotNotifyInvalidationListener() throws Exception { trackSelector.init(invalidationListener); - trackSelector.setParameters(DEFAULT_PARAMETERS); verify(invalidationListener, never()).onTrackSelectionsInvalidated(); } @@ -73,7 +72,7 @@ public final class DefaultTrackSelectorTest { @Test public void testSetParameterWithNonDefaultParameterNotifyInvalidationListener() throws Exception { - Parameters parameters = DEFAULT_PARAMETERS.withPreferredAudioLanguage("en"); + Parameters parameters = new ParametersBuilder().setPreferredAudioLanguage("eng").build(); trackSelector.init(invalidationListener); trackSelector.setParameters(parameters); @@ -88,10 +87,10 @@ public final class DefaultTrackSelectorTest { @Test public void testSetParameterWithSameParametersDoesNotNotifyInvalidationListenerAgain() throws Exception { - Parameters parameters = DEFAULT_PARAMETERS.withPreferredAudioLanguage("en"); + ParametersBuilder builder = new ParametersBuilder().setPreferredAudioLanguage("eng"); trackSelector.init(invalidationListener); - trackSelector.setParameters(parameters); - trackSelector.setParameters(parameters); + trackSelector.setParameters(builder.build()); + trackSelector.setParameters(builder.build()); verify(invalidationListener, times(1)).onTrackSelectionsInvalidated(); } @@ -122,15 +121,14 @@ public final class DefaultTrackSelectorTest { @Test public void testSelectTracksSelectPreferredAudioLanguage() throws Exception { - Parameters parameters = DEFAULT_PARAMETERS.withPreferredAudioLanguage("en"); - trackSelector.setParameters(parameters); + trackSelector.setParameters(new ParametersBuilder().setPreferredAudioLanguage("eng").build()); Format frAudioFormat = Format.createAudioSampleFormat("audio", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, - Format.NO_VALUE, 2, 44100, null, null, 0, "fr"); + Format.NO_VALUE, 2, 44100, null, null, 0, "fra"); Format enAudioFormat = Format.createAudioSampleFormat("audio", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, - Format.NO_VALUE, 2, 44100, null, null, 0, "en"); + Format.NO_VALUE, 2, 44100, null, null, 0, "eng"); TrackSelectorResult result = trackSelector.selectTracks( new RendererCapabilities[] {ALL_AUDIO_FORMAT_SUPPORTED_RENDERER_CAPABILITIES}, @@ -146,19 +144,18 @@ public final class DefaultTrackSelectorTest { @Test public void testSelectTracksSelectPreferredAudioLanguageOverSelectionFlag() throws Exception { - Parameters parameters = DEFAULT_PARAMETERS.withPreferredAudioLanguage("en"); - trackSelector.setParameters(parameters); + trackSelector.setParameters(new ParametersBuilder().setPreferredAudioLanguage("eng").build()); Format frAudioFormat = Format.createAudioSampleFormat("audio", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, - Format.NO_VALUE, 2, 44100, null, null, C.SELECTION_FLAG_DEFAULT, "fr"); + Format.NO_VALUE, 2, 44100, null, null, C.SELECTION_FLAG_DEFAULT, "fra"); Format enAudioFormat = Format.createAudioSampleFormat("audio", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, - Format.NO_VALUE, 2, 44100, null, null, 0, "en"); + Format.NO_VALUE, 2, 44100, null, null, 0, "eng"); TrackSelectorResult result = trackSelector.selectTracks( new RendererCapabilities[] {ALL_AUDIO_FORMAT_SUPPORTED_RENDERER_CAPABILITIES}, - singleTrackGroup(frAudioFormat, enAudioFormat)); + wrapFormats(frAudioFormat, enAudioFormat)); assertThat(result.selections.get(0).getSelectedFormat()).isEqualTo(enAudioFormat); } @@ -168,8 +165,7 @@ public final class DefaultTrackSelectorTest { * track that exceed renderer's capabilities. */ @Test - public void testSelectTracksPreferTrackWithinCapabilities() - throws Exception { + public void testSelectTracksPreferTrackWithinCapabilities() throws Exception { Format supportedFormat = Format.createAudioSampleFormat("supportedFormat", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, Format.NO_VALUE, 2, 44100, null, null, 0, null); @@ -197,7 +193,6 @@ public final class DefaultTrackSelectorTest { @Test public void testSelectTracksWithNoTrackWithinCapabilitiesSelectExceededCapabilityTrack() throws Exception { - Format audioFormat = Format.createAudioSampleFormat("audio", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, Format.NO_VALUE, 2, 44100, null, null, 0, null); @@ -216,8 +211,8 @@ public final class DefaultTrackSelectorTest { @Test public void testSelectTracksWithNoTrackWithinCapabilitiesAndSetByParamsReturnNoSelection() throws Exception { - Parameters parameters = DEFAULT_PARAMETERS.withExceedRendererCapabilitiesIfNecessary(false); - trackSelector.setParameters(parameters); + trackSelector.setParameters( + new ParametersBuilder().setExceedRendererCapabilitiesIfNecessary(false).build()); Format audioFormat = Format.createAudioSampleFormat("audio", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, @@ -264,15 +259,14 @@ public final class DefaultTrackSelectorTest { @Test public void testSelectTracksPreferTrackWithinCapabilitiesOverPreferredLanguage() throws Exception { - Parameters parameters = DEFAULT_PARAMETERS.withPreferredAudioLanguage("en"); - trackSelector.setParameters(parameters); + trackSelector.setParameters(new ParametersBuilder().setPreferredAudioLanguage("eng").build()); Format supportedFrFormat = Format.createAudioSampleFormat("supportedFormat", MimeTypes.AUDIO_AAC, null, - Format.NO_VALUE, Format.NO_VALUE, 2, 44100, null, null, 0, "fr"); + Format.NO_VALUE, Format.NO_VALUE, 2, 44100, null, null, 0, "fra"); Format exceededEnFormat = Format.createAudioSampleFormat("exceededFormat", MimeTypes.AUDIO_AAC, null, - Format.NO_VALUE, Format.NO_VALUE, 2, 44100, null, null, 0, "en"); + Format.NO_VALUE, Format.NO_VALUE, 2, 44100, null, null, 0, "eng"); Map mappedCapabilities = new HashMap<>(); mappedCapabilities.put(exceededEnFormat.id, FORMAT_EXCEEDS_CAPABILITIES); @@ -295,15 +289,14 @@ public final class DefaultTrackSelectorTest { @Test public void testSelectTracksPreferTrackWithinCapabilitiesOverSelectionFlagAndPreferredLanguage() throws Exception { - Parameters parameters = DEFAULT_PARAMETERS.withPreferredAudioLanguage("en"); - trackSelector.setParameters(parameters); + trackSelector.setParameters(new ParametersBuilder().setPreferredAudioLanguage("eng").build()); Format supportedFrFormat = Format.createAudioSampleFormat("supportedFormat", MimeTypes.AUDIO_AAC, null, - Format.NO_VALUE, Format.NO_VALUE, 2, 44100, null, null, 0, "fr"); + Format.NO_VALUE, Format.NO_VALUE, 2, 44100, null, null, 0, "fra"); Format exceededDefaultSelectionEnFormat = - Format.createAudioSampleFormat("exceededFormat", MimeTypes.AUDIO_AAC, null, - Format.NO_VALUE, Format.NO_VALUE, 2, 44100, null, null, C.SELECTION_FLAG_DEFAULT, "en"); + Format.createAudioSampleFormat("exceededFormat", MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, + Format.NO_VALUE, 2, 44100, null, null, C.SELECTION_FLAG_DEFAULT, "eng"); Map mappedCapabilities = new HashMap<>(); mappedCapabilities.put(exceededDefaultSelectionEnFormat.id, FORMAT_EXCEEDS_CAPABILITIES); @@ -561,12 +554,14 @@ public final class DefaultTrackSelectorTest { wrapFormats(spanish, german, undeterminedUnd, undeterminedNull)); assertThat(result.selections.get(0)).isNull(); - trackSelector.setParameters(DEFAULT_PARAMETERS.withSelectUndeterminedTextLanguage(true)); + trackSelector.setParameters( + new ParametersBuilder().setSelectUndeterminedTextLanguage(true).build()); result = trackSelector.selectTracks(textRendererCapabilites, wrapFormats(spanish, german, undeterminedUnd, undeterminedNull)); assertThat(result.selections.get(0).getFormat(0)).isSameAs(undeterminedUnd); - trackSelector.setParameters(DEFAULT_PARAMETERS.withPreferredTextLanguage("spa")); + ParametersBuilder builder = new ParametersBuilder().setPreferredTextLanguage("spa"); + trackSelector.setParameters(builder.build()); result = trackSelector.selectTracks(textRendererCapabilites, wrapFormats(spanish, german, undeterminedUnd, undeterminedNull)); assertThat(result.selections.get(0).getFormat(0)).isSameAs(spanish); @@ -575,8 +570,7 @@ public final class DefaultTrackSelectorTest { wrapFormats(german, undeterminedUnd, undeterminedNull)); assertThat(result.selections.get(0)).isNull(); - trackSelector.setParameters( - trackSelector.getParameters().withSelectUndeterminedTextLanguage(true)); + trackSelector.setParameters(builder.setSelectUndeterminedTextLanguage(true).build()); result = trackSelector.selectTracks(textRendererCapabilites, wrapFormats(german, undeterminedUnd, undeterminedNull)); assertThat(result.selections.get(0).getFormat(0)).isSameAs(undeterminedUnd); @@ -596,8 +590,7 @@ public final class DefaultTrackSelectorTest { @Test public void testSelectTracksWithinCapabilitiesAndForceLowestBitrateSelectLowerBitrate() throws Exception { - Parameters parameters = DEFAULT_PARAMETERS.withForceLowestBitrate(true); - trackSelector.setParameters(parameters); + trackSelector.setParameters(new ParametersBuilder().setForceLowestBitrate(true).build()); Format lowerBitrateFormat = Format.createAudioSampleFormat("audioFormat", MimeTypes.AUDIO_AAC, null, 15000, From 21d55d4ebabc11206be1546ea9610d64e656cdf8 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 30 Nov 2017 05:17:49 -0800 Subject: [PATCH 0814/2472] Rename DefaultTrackSelector.ParameterBuilder.setViewportSize{FromContext->ToPhysicalDisplaySize} ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177440699 --- .../exoplayer2/trackselection/DefaultTrackSelector.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index b4fa64c8fd..2f0dc8f04e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -245,14 +245,14 @@ public class DefaultTrackSelector extends MappingTrackSelector { } /** - * Equivalent to invoking {@link #setViewportSize} with the viewport size values obtained from - * the provided {@link Context}. + * Equivalent to invoking {@link #setViewportSize} with the viewport size obtained from + * {@link Util#getPhysicalDisplaySize(Context)}. * * @param context The context to obtain the viewport size from. * @param viewportOrientationMayChange See {@link #viewportOrientationMayChange}. * @return This builder. */ - public ParametersBuilder setViewportSizeFromContext(Context context, + public ParametersBuilder setViewportSizeToPhysicalDisplaySize(Context context, boolean viewportOrientationMayChange) { // Assume the viewport is fullscreen. Point viewportSize = Util.getPhysicalDisplaySize(context); From cc54d4d3e6902acf127a41ab88d724d024b95dab Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 30 Nov 2017 05:34:51 -0800 Subject: [PATCH 0815/2472] Snap to frame boundary in ConstantBitrateSeeker - This change snaps the seek position for constant bitrate MP3s to the nearest frame boundary, avoiding the need to skip one byte at a time to re-synchronize (this may still happen if the MP3 does not really have fixed size frames). - Tweaked both ConstantBitrateSeeker and WavHeader to ensure the returned positions are valid. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177441798 --- .../assets/mp3/play-trimmed.mp3.1.dump | 6 +++- .../assets/mp3/play-trimmed.mp3.2.dump | 6 +++- .../assets/mp3/play-trimmed.mp3.3.dump | 6 +++- .../extractor/mp3/ConstantBitrateSeeker.java | 32 +++++++++++++++---- .../extractor/mp3/Mp3Extractor.java | 4 +-- .../exoplayer2/extractor/wav/WavHeader.java | 11 ++++--- 6 files changed, 50 insertions(+), 15 deletions(-) diff --git a/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.1.dump b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.1.dump index 0b6516ccdb..37a04215ee 100644 --- a/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.1.dump +++ b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.1.dump @@ -25,5 +25,9 @@ track 0: language = null drmInitData = - initializationData: - sample count = 0 + sample count = 1 + sample 0: + time = 0 + flags = 1 + data = length 418, hash B819987 tracksEnded = true diff --git a/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.2.dump b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.2.dump index 0b6516ccdb..37a04215ee 100644 --- a/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.2.dump +++ b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.2.dump @@ -25,5 +25,9 @@ track 0: language = null drmInitData = - initializationData: - sample count = 0 + sample count = 1 + sample 0: + time = 0 + flags = 1 + data = length 418, hash B819987 tracksEnded = true diff --git a/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.3.dump b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.3.dump index 0b6516ccdb..37a04215ee 100644 --- a/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.3.dump +++ b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.3.dump @@ -25,5 +25,9 @@ track 0: language = null drmInitData = - initializationData: - sample count = 0 + sample count = 1 + sample 0: + time = 0 + flags = 1 + data = length 418, hash B819987 tracksEnded = true diff --git a/library/core/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 index 47e12161a8..e02e99e139 100644 --- a/library/core/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 @@ -26,27 +26,47 @@ import com.google.android.exoplayer2.util.Util; private static final int BITS_PER_BYTE = 8; private final long firstFramePosition; + private final long dataSize; + private final int frameSize; private final int bitrate; private final long durationUs; - public ConstantBitrateSeeker(long firstFramePosition, int bitrate, long inputLength) { + /** + * @param firstFramePosition The position (byte offset) of the first frame. + * @param inputLength The length of the stream. + * @param frameSize The size of a single frame in the stream. + * @param bitrate The stream's bitrate. + */ + public ConstantBitrateSeeker(long firstFramePosition, long inputLength, int frameSize, + int bitrate) { this.firstFramePosition = firstFramePosition; + this.frameSize = frameSize; this.bitrate = bitrate; - durationUs = inputLength == C.LENGTH_UNSET ? C.TIME_UNSET : getTimeUs(inputLength); + if (inputLength == C.LENGTH_UNSET) { + dataSize = C.LENGTH_UNSET; + durationUs = C.TIME_UNSET; + } else { + dataSize = inputLength - firstFramePosition; + durationUs = getTimeUs(inputLength); + } } @Override public boolean isSeekable() { - return durationUs != C.TIME_UNSET; + return dataSize != C.LENGTH_UNSET; } @Override public long getPosition(long timeUs) { - if (durationUs == C.TIME_UNSET) { + if (dataSize == C.LENGTH_UNSET) { return firstFramePosition; } - timeUs = Util.constrainValue(timeUs, 0, durationUs); - return firstFramePosition + (timeUs * bitrate) / (C.MICROS_PER_SECOND * BITS_PER_BYTE); + long positionOffset = (timeUs * bitrate) / (C.MICROS_PER_SECOND * BITS_PER_BYTE); + // Constrain to nearest preceding frame offset. + positionOffset = (positionOffset / frameSize) * frameSize; + positionOffset = Util.constrainValue(positionOffset, 0, dataSize - frameSize); + // Add data start position. + return firstFramePosition + positionOffset; } @Override diff --git a/library/core/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 index dc7d21851a..7c579504c3 100644 --- a/library/core/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 @@ -393,8 +393,8 @@ public final class Mp3Extractor implements Extractor { input.peekFully(scratch.data, 0, 4); scratch.setPosition(0); MpegAudioHeader.populateHeader(scratch.readInt(), synchronizedHeader); - return new ConstantBitrateSeeker(input.getPosition(), synchronizedHeader.bitrate, - input.getLength()); + return new ConstantBitrateSeeker(input.getPosition(), input.getLength(), + synchronizedHeader.frameSize, synchronizedHeader.bitrate); } /** diff --git a/library/core/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 index 1c1fc97a22..2cdd31cb6f 100644 --- a/library/core/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 @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.extractor.wav; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.SeekMap; +import com.google.android.exoplayer2.util.Util; /** Header for a WAV file. */ /* package */ final class WavHeader implements SeekMap { @@ -83,10 +84,12 @@ import com.google.android.exoplayer2.extractor.SeekMap; @Override public long getPosition(long timeUs) { - long unroundedPosition = (timeUs * averageBytesPerSecond) / C.MICROS_PER_SECOND; - // Round down to nearest frame. - long position = (unroundedPosition / blockAlignment) * blockAlignment; - return Math.min(position, dataSize - blockAlignment) + dataStartPosition; + long positionOffset = (timeUs * averageBytesPerSecond) / C.MICROS_PER_SECOND; + // Constrain to nearest preceding frame offset. + positionOffset = (positionOffset / blockAlignment) * blockAlignment; + positionOffset = Util.constrainValue(positionOffset, 0, dataSize - blockAlignment); + // Add data start position. + return dataStartPosition + positionOffset; } // Misc getters. From 7b0889981822a2e3f6e8d6145491827a7a4fa1e2 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 30 Nov 2017 05:39:50 -0800 Subject: [PATCH 0816/2472] Move resetting audio processors to initialize() The set of active audio processors was only updated on reconfiguration and when draining playback parameters completed. Draining playback parameters are cleared in reset(), so if parameters were set while paused then the sink was quickly reset, without draining completing, the set of active audio processors wouldn't be updated. This means that a switch to or from speed or pitch = 1 would not be handled correctly if made while paused and followed by a seek. Move resetting active audio processors from configure (where if the active audio processors were reset we'd always initialize a new AudioTrack) to initialize(). ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177442098 --- RELEASENOTES.md | 2 ++ .../google/android/exoplayer2/audio/DefaultAudioSink.java | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 90dbdb6b00..3a42311b26 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -25,6 +25,8 @@ preferred language is not available ([#2980](https://github.com/google/ExoPlayer/issues/2980)). * Add optional parameter to `Player.stop` to reset the player when stopping. +* Fix handling of playback parameters changes while paused when followed by a + seek. ### 2.6.0 ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java index 3b14b69916..ab4564e2c3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java @@ -364,9 +364,6 @@ public final class DefaultAudioSink implements AudioSink { encoding = audioProcessor.getOutputEncoding(); } } - if (flush) { - resetAudioProcessors(); - } } int channelConfig; @@ -492,6 +489,9 @@ public final class DefaultAudioSink implements AudioSink { // The old playback parameters may no longer be applicable so try to reset them now. setPlaybackParameters(playbackParameters); + // Flush and reset active audio processors. + resetAudioProcessors(); + int audioSessionId = audioTrack.getAudioSessionId(); if (enablePreV21AudioSessionWorkaround) { if (Util.SDK_INT < 21) { From e28adb00ff500cdc35e3078f8695b0a46aa0e34c Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 30 Nov 2017 05:53:45 -0800 Subject: [PATCH 0817/2472] Use Handler to post action schedule finished callback. Calling it directly might skip other callbacks. For example: ActionSchedule.Builder().waitForTimelineChanged(...).build(). is currently immediately calling through to callback.onActionScheduleFinished when the timeline changes. Depending on the position of the action schedule listener in the listener set, it may skip other listeners also listening to timeline changes. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177442975 --- .../exoplayer2/testutil/ActionSchedule.java | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java index 7a2ce9270c..477071f91f 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java @@ -41,6 +41,7 @@ import com.google.android.exoplayer2.testutil.Action.WaitForPositionDiscontinuit import com.google.android.exoplayer2.testutil.Action.WaitForSeekProcessed; import com.google.android.exoplayer2.testutil.Action.WaitForTimelineChanged; import com.google.android.exoplayer2.trackselection.MappingTrackSelector; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Clock; /** @@ -487,13 +488,30 @@ public final class ActionSchedule { } @Override - protected void doActionImpl(SimpleExoPlayer player, MappingTrackSelector trackSelector, - Surface surface) { + protected void doActionAndScheduleNextImpl( + SimpleExoPlayer player, + MappingTrackSelector trackSelector, + Surface surface, + Handler handler, + ActionNode nextAction) { + Assertions.checkArgument(nextAction == null); if (callback != null) { - callback.onActionScheduleFinished(); + handler.post( + new Runnable() { + @Override + public void run() { + callback.onActionScheduleFinished(); + } + }); } } + @Override + protected void doActionImpl( + SimpleExoPlayer player, MappingTrackSelector trackSelector, Surface surface) { + // Not triggered. + } + } } From 754260e9441c9dc32a320689503e3adcb08d580c Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 30 Nov 2017 07:20:45 -0800 Subject: [PATCH 0818/2472] Fix VBRI and XING seekers - Remove skipping of the VBRI/XING frame before calculating position offsets. This was incorrect. Instead, a constraint is used to ensure we don't return positions within these frames, the difference being that the constraint adjusts only positions that would fall within the frames, where-as the previous approach shifted positions through the whole stream. - Excluded last entry in the VBRI table because it has an invalid position (the length of the stream). - Give variables in XingSeeker descriptive names. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177451295 --- .../androidTest/assets/mp3/bear.mp3.1.dump | 308 +++++++++--------- .../androidTest/assets/mp3/bear.mp3.2.dump | 76 ++--- .../extractor/mp3/ConstantBitrateSeeker.java | 18 +- .../extractor/mp3/Mp3Extractor.java | 7 +- .../exoplayer2/extractor/mp3/VbriSeeker.java | 33 +- .../exoplayer2/extractor/mp3/XingSeeker.java | 112 ++++--- .../extractor/mp3/XingSeekerTest.java | 26 +- 7 files changed, 298 insertions(+), 282 deletions(-) diff --git a/library/core/src/androidTest/assets/mp3/bear.mp3.1.dump b/library/core/src/androidTest/assets/mp3/bear.mp3.1.dump index 2e0b21050c..7b6fe9db37 100644 --- a/library/core/src/androidTest/assets/mp3/bear.mp3.1.dump +++ b/library/core/src/androidTest/assets/mp3/bear.mp3.1.dump @@ -25,309 +25,313 @@ track 0: language = null drmInitData = - initializationData: - sample count = 76 + sample count = 77 sample 0: - time = 945782 + time = 928567 + flags = 1 + data = length 384, hash F7E344F4 + sample 1: + time = 952567 flags = 1 data = length 384, hash 14EF6AFD - sample 1: - time = 969782 + sample 2: + time = 976567 flags = 1 data = length 384, hash 61C9B92C - sample 2: - time = 993782 + sample 3: + time = 1000567 flags = 1 data = length 384, hash ABE1368 - sample 3: - time = 1017782 + sample 4: + time = 1024567 flags = 1 data = length 384, hash 6A3B8547 - sample 4: - time = 1041782 + sample 5: + time = 1048567 flags = 1 data = length 384, hash 30E905FA - sample 5: - time = 1065782 + sample 6: + time = 1072567 flags = 1 data = length 384, hash 21A267CD - sample 6: - time = 1089782 + sample 7: + time = 1096567 flags = 1 data = length 384, hash D96A2651 - sample 7: - time = 1113782 + sample 8: + time = 1120567 flags = 1 data = length 384, hash 72340177 - sample 8: - time = 1137782 + sample 9: + time = 1144567 flags = 1 data = length 384, hash 9345E744 - sample 9: - time = 1161782 + sample 10: + time = 1168567 flags = 1 data = length 384, hash FDE39E3A - sample 10: - time = 1185782 + sample 11: + time = 1192567 flags = 1 data = length 384, hash F0B7465 - sample 11: - time = 1209782 + sample 12: + time = 1216567 flags = 1 data = length 384, hash 3693AB86 - sample 12: - time = 1233782 + sample 13: + time = 1240567 flags = 1 data = length 384, hash F39719B1 - sample 13: - time = 1257782 + sample 14: + time = 1264567 flags = 1 data = length 384, hash DA3958DC - sample 14: - time = 1281782 + sample 15: + time = 1288567 flags = 1 data = length 384, hash FDC7599F - sample 15: - time = 1305782 + sample 16: + time = 1312567 flags = 1 data = length 384, hash AEFF8471 - sample 16: - time = 1329782 + sample 17: + time = 1336567 flags = 1 data = length 384, hash 89C92C19 - sample 17: - time = 1353782 + sample 18: + time = 1360567 flags = 1 data = length 384, hash 5C786A4B - sample 18: - time = 1377782 + sample 19: + time = 1384567 flags = 1 data = length 384, hash 5ACA8B - sample 19: - time = 1401782 + sample 20: + time = 1408567 flags = 1 data = length 384, hash 7755974C - sample 20: - time = 1425782 + sample 21: + time = 1432567 flags = 1 data = length 384, hash 3934B73C - sample 21: - time = 1449782 + sample 22: + time = 1456567 flags = 1 data = length 384, hash DDD70A2F - sample 22: - time = 1473782 + sample 23: + time = 1480567 flags = 1 data = length 384, hash 8FACE2EF - sample 23: - time = 1497782 + sample 24: + time = 1504567 flags = 1 data = length 384, hash 4A602591 - sample 24: - time = 1521782 + sample 25: + time = 1528567 flags = 1 data = length 384, hash D019AA2D - sample 25: - time = 1545782 + sample 26: + time = 1552567 flags = 1 data = length 384, hash 8A680B9D - sample 26: - time = 1569782 + sample 27: + time = 1576567 flags = 1 data = length 384, hash B655C959 - sample 27: - time = 1593782 + sample 28: + time = 1600567 flags = 1 data = length 384, hash 2168336B - sample 28: - time = 1617782 + sample 29: + time = 1624567 flags = 1 data = length 384, hash D77F6D31 - sample 29: - time = 1641782 + sample 30: + time = 1648567 flags = 1 data = length 384, hash 524B4B2F - sample 30: - time = 1665782 + sample 31: + time = 1672567 flags = 1 data = length 384, hash 4752DDFC - sample 31: - time = 1689782 + sample 32: + time = 1696567 flags = 1 data = length 384, hash E786727F - sample 32: - time = 1713782 + sample 33: + time = 1720567 flags = 1 data = length 384, hash 5DA6FB8C - sample 33: - time = 1737782 + sample 34: + time = 1744567 flags = 1 data = length 384, hash 92F24269 - sample 34: - time = 1761782 + sample 35: + time = 1768567 flags = 1 data = length 384, hash CD0A3BA1 - sample 35: - time = 1785782 + sample 36: + time = 1792567 flags = 1 data = length 384, hash 7D00409F - sample 36: - time = 1809782 + sample 37: + time = 1816567 flags = 1 data = length 384, hash D7ADB5FA - sample 37: - time = 1833782 + sample 38: + time = 1840567 flags = 1 data = length 384, hash 4A140209 - sample 38: - time = 1857782 + sample 39: + time = 1864567 flags = 1 data = length 384, hash E801184A - sample 39: - time = 1881782 + sample 40: + time = 1888567 flags = 1 data = length 384, hash 53C6CF9C - sample 40: - time = 1905782 + sample 41: + time = 1912567 flags = 1 data = length 384, hash 19A8D99F - sample 41: - time = 1929782 + sample 42: + time = 1936567 flags = 1 data = length 384, hash E47EB43F - sample 42: - time = 1953782 + sample 43: + time = 1960567 flags = 1 data = length 384, hash 4EA329E7 - sample 43: - time = 1977782 + sample 44: + time = 1984567 flags = 1 data = length 384, hash 1CCAAE62 - sample 44: - time = 2001782 + sample 45: + time = 2008567 flags = 1 data = length 384, hash ED3F8C66 - sample 45: - time = 2025782 + sample 46: + time = 2032567 flags = 1 data = length 384, hash D3D646B6 - sample 46: - time = 2049782 + sample 47: + time = 2056567 flags = 1 data = length 384, hash 68CD1574 - sample 47: - time = 2073782 + sample 48: + time = 2080567 flags = 1 data = length 384, hash 8CEAB382 - sample 48: - time = 2097782 + sample 49: + time = 2104567 flags = 1 data = length 384, hash D54B1C48 - sample 49: - time = 2121782 + sample 50: + time = 2128567 flags = 1 data = length 384, hash FFE2EE90 - sample 50: - time = 2145782 + sample 51: + time = 2152567 flags = 1 data = length 384, hash BFE8A673 - sample 51: - time = 2169782 + sample 52: + time = 2176567 flags = 1 data = length 384, hash 978B1C92 - sample 52: - time = 2193782 + sample 53: + time = 2200567 flags = 1 data = length 384, hash 810CC71E - sample 53: - time = 2217782 + sample 54: + time = 2224567 flags = 1 data = length 384, hash 44FE42D9 - sample 54: - time = 2241782 + sample 55: + time = 2248567 flags = 1 data = length 384, hash 2F5BB02C - sample 55: - time = 2265782 + sample 56: + time = 2272567 flags = 1 data = length 384, hash 77DDB90 - sample 56: - time = 2289782 + sample 57: + time = 2296567 flags = 1 data = length 384, hash 24FB5EDA - sample 57: - time = 2313782 + sample 58: + time = 2320567 flags = 1 data = length 384, hash E73203C6 - sample 58: - time = 2337782 + sample 59: + time = 2344567 flags = 1 data = length 384, hash 14B525F1 - sample 59: - time = 2361782 + sample 60: + time = 2368567 flags = 1 data = length 384, hash 5E0F4E2E - sample 60: - time = 2385782 + sample 61: + time = 2392567 flags = 1 data = length 384, hash 67EE4E31 - sample 61: - time = 2409782 + sample 62: + time = 2416567 flags = 1 data = length 384, hash 2E04EC4C - sample 62: - time = 2433782 + sample 63: + time = 2440567 flags = 1 data = length 384, hash 852CABA7 - sample 63: - time = 2457782 + sample 64: + time = 2464567 flags = 1 data = length 384, hash 19928903 - sample 64: - time = 2481782 + sample 65: + time = 2488567 flags = 1 data = length 384, hash 5DA42021 - sample 65: - time = 2505782 + sample 66: + time = 2512567 flags = 1 data = length 384, hash 45B20B7C - sample 66: - time = 2529782 + sample 67: + time = 2536567 flags = 1 data = length 384, hash D108A215 - sample 67: - time = 2553782 + sample 68: + time = 2560567 flags = 1 data = length 384, hash BD25DB7C - sample 68: - time = 2577782 + sample 69: + time = 2584567 flags = 1 data = length 384, hash DA7F9861 - sample 69: - time = 2601782 + sample 70: + time = 2608567 flags = 1 data = length 384, hash CCD576F - sample 70: - time = 2625782 + sample 71: + time = 2632567 flags = 1 data = length 384, hash 405C1EB5 - sample 71: - time = 2649782 + sample 72: + time = 2656567 flags = 1 data = length 384, hash 6640B74E - sample 72: - time = 2673782 + sample 73: + time = 2680567 flags = 1 data = length 384, hash B4E5937A - sample 73: - time = 2697782 + sample 74: + time = 2704567 flags = 1 data = length 384, hash CEE17733 - sample 74: - time = 2721782 + sample 75: + time = 2728567 flags = 1 data = length 384, hash 2A0DA733 - sample 75: - time = 2745782 + sample 76: + time = 2752567 flags = 1 data = length 384, hash 97F4129B tracksEnded = true diff --git a/library/core/src/androidTest/assets/mp3/bear.mp3.2.dump b/library/core/src/androidTest/assets/mp3/bear.mp3.2.dump index b3cb117cb2..3f393e768e 100644 --- a/library/core/src/androidTest/assets/mp3/bear.mp3.2.dump +++ b/library/core/src/androidTest/assets/mp3/bear.mp3.2.dump @@ -27,155 +27,155 @@ track 0: initializationData: sample count = 38 sample 0: - time = 1858196 + time = 1871586 flags = 1 data = length 384, hash E801184A sample 1: - time = 1882196 + time = 1895586 flags = 1 data = length 384, hash 53C6CF9C sample 2: - time = 1906196 + time = 1919586 flags = 1 data = length 384, hash 19A8D99F sample 3: - time = 1930196 + time = 1943586 flags = 1 data = length 384, hash E47EB43F sample 4: - time = 1954196 + time = 1967586 flags = 1 data = length 384, hash 4EA329E7 sample 5: - time = 1978196 + time = 1991586 flags = 1 data = length 384, hash 1CCAAE62 sample 6: - time = 2002196 + time = 2015586 flags = 1 data = length 384, hash ED3F8C66 sample 7: - time = 2026196 + time = 2039586 flags = 1 data = length 384, hash D3D646B6 sample 8: - time = 2050196 + time = 2063586 flags = 1 data = length 384, hash 68CD1574 sample 9: - time = 2074196 + time = 2087586 flags = 1 data = length 384, hash 8CEAB382 sample 10: - time = 2098196 + time = 2111586 flags = 1 data = length 384, hash D54B1C48 sample 11: - time = 2122196 + time = 2135586 flags = 1 data = length 384, hash FFE2EE90 sample 12: - time = 2146196 + time = 2159586 flags = 1 data = length 384, hash BFE8A673 sample 13: - time = 2170196 + time = 2183586 flags = 1 data = length 384, hash 978B1C92 sample 14: - time = 2194196 + time = 2207586 flags = 1 data = length 384, hash 810CC71E sample 15: - time = 2218196 + time = 2231586 flags = 1 data = length 384, hash 44FE42D9 sample 16: - time = 2242196 + time = 2255586 flags = 1 data = length 384, hash 2F5BB02C sample 17: - time = 2266196 + time = 2279586 flags = 1 data = length 384, hash 77DDB90 sample 18: - time = 2290196 + time = 2303586 flags = 1 data = length 384, hash 24FB5EDA sample 19: - time = 2314196 + time = 2327586 flags = 1 data = length 384, hash E73203C6 sample 20: - time = 2338196 + time = 2351586 flags = 1 data = length 384, hash 14B525F1 sample 21: - time = 2362196 + time = 2375586 flags = 1 data = length 384, hash 5E0F4E2E sample 22: - time = 2386196 + time = 2399586 flags = 1 data = length 384, hash 67EE4E31 sample 23: - time = 2410196 + time = 2423586 flags = 1 data = length 384, hash 2E04EC4C sample 24: - time = 2434196 + time = 2447586 flags = 1 data = length 384, hash 852CABA7 sample 25: - time = 2458196 + time = 2471586 flags = 1 data = length 384, hash 19928903 sample 26: - time = 2482196 + time = 2495586 flags = 1 data = length 384, hash 5DA42021 sample 27: - time = 2506196 + time = 2519586 flags = 1 data = length 384, hash 45B20B7C sample 28: - time = 2530196 + time = 2543586 flags = 1 data = length 384, hash D108A215 sample 29: - time = 2554196 + time = 2567586 flags = 1 data = length 384, hash BD25DB7C sample 30: - time = 2578196 + time = 2591586 flags = 1 data = length 384, hash DA7F9861 sample 31: - time = 2602196 + time = 2615586 flags = 1 data = length 384, hash CCD576F sample 32: - time = 2626196 + time = 2639586 flags = 1 data = length 384, hash 405C1EB5 sample 33: - time = 2650196 + time = 2663586 flags = 1 data = length 384, hash 6640B74E sample 34: - time = 2674196 + time = 2687586 flags = 1 data = length 384, hash B4E5937A sample 35: - time = 2698196 + time = 2711586 flags = 1 data = length 384, hash CEE17733 sample 36: - time = 2722196 + time = 2735586 flags = 1 data = length 384, hash 2A0DA733 sample 37: - time = 2746196 + time = 2759586 flags = 1 data = length 384, hash 97F4129B tracksEnded = true diff --git a/library/core/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 index e02e99e139..442e62deca 100644 --- a/library/core/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 @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.extractor.mp3; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.extractor.MpegAudioHeader; import com.google.android.exoplayer2.util.Util; /** @@ -26,22 +27,21 @@ import com.google.android.exoplayer2.util.Util; private static final int BITS_PER_BYTE = 8; private final long firstFramePosition; - private final long dataSize; private final int frameSize; + private final long dataSize; private final int bitrate; private final long durationUs; /** - * @param firstFramePosition The position (byte offset) of the first frame. - * @param inputLength The length of the stream. - * @param frameSize The size of a single frame in the stream. - * @param bitrate The stream's bitrate. + * @param inputLength The length of the stream in bytes, or {@link C#LENGTH_UNSET} if unknown. + * @param firstFramePosition The position of the first frame in the stream. + * @param mpegAudioHeader The MPEG audio header associated with the first frame. */ - public ConstantBitrateSeeker(long firstFramePosition, long inputLength, int frameSize, - int bitrate) { + public ConstantBitrateSeeker(long inputLength, long firstFramePosition, + MpegAudioHeader mpegAudioHeader) { this.firstFramePosition = firstFramePosition; - this.frameSize = frameSize; - this.bitrate = bitrate; + this.frameSize = mpegAudioHeader.frameSize; + this.bitrate = mpegAudioHeader.bitrate; if (inputLength == C.LENGTH_UNSET) { dataSize = C.LENGTH_UNSET; durationUs = C.TIME_UNSET; diff --git a/library/core/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 index 7c579504c3..5c56dc460a 100644 --- a/library/core/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 @@ -360,7 +360,7 @@ public final class Mp3Extractor implements Extractor { int seekHeader = getSeekFrameHeader(frame, xingBase); Seeker seeker; if (seekHeader == SEEK_HEADER_XING || seekHeader == SEEK_HEADER_INFO) { - seeker = XingSeeker.create(synchronizedHeader, frame, input.getPosition(), input.getLength()); + seeker = XingSeeker.create(input.getLength(), input.getPosition(), synchronizedHeader, frame); if (seeker != null && !gaplessInfoHolder.hasGaplessInfo()) { // If there is a Xing header, read gapless playback metadata at a fixed offset. input.resetPeekPosition(); @@ -375,7 +375,7 @@ public final class Mp3Extractor implements Extractor { return getConstantBitrateSeeker(input); } } else if (seekHeader == SEEK_HEADER_VBRI) { - seeker = VbriSeeker.create(synchronizedHeader, frame, input.getPosition(), input.getLength()); + seeker = VbriSeeker.create(input.getLength(), input.getPosition(), synchronizedHeader, frame); input.skipFully(synchronizedHeader.frameSize); } else { // seekerHeader == SEEK_HEADER_UNSET // This frame doesn't contain seeking information, so reset the peek position. @@ -393,8 +393,7 @@ public final class Mp3Extractor implements Extractor { input.peekFully(scratch.data, 0, 4); scratch.setPosition(0); MpegAudioHeader.populateHeader(scratch.readInt(), synchronizedHeader); - return new ConstantBitrateSeeker(input.getPosition(), input.getLength(), - synchronizedHeader.frameSize, synchronizedHeader.bitrate); + return new ConstantBitrateSeeker(input.getLength(), input.getPosition(), synchronizedHeader); } /** diff --git a/library/core/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 index c43f065592..cc631d9f7e 100644 --- a/library/core/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 @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor.mp3; +import android.util.Log; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.MpegAudioHeader; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -25,21 +26,23 @@ import com.google.android.exoplayer2.util.Util; */ /* package */ final class VbriSeeker implements Mp3Extractor.Seeker { + private static final String TAG = "VbriSeeker"; + /** * Returns a {@link VbriSeeker} for seeking in the stream, if required information is present. * Returns {@code null} if not. On returning, {@code frame}'s position is not specified so the * caller should reset it. * + * @param inputLength The length of the stream in bytes, or {@link C#LENGTH_UNSET} if unknown. + * @param position The position of the start of this frame in the stream. * @param mpegAudioHeader The MPEG audio header associated with the frame. * @param frame The data in this audio frame, with its position set to immediately after the * 'VBRI' tag. - * @param position The position (byte offset) of the start of this frame in the stream. - * @param inputLength The length of the stream in bytes. * @return A {@link VbriSeeker} for seeking in the stream, or {@code null} if the required * information is not present. */ - public static VbriSeeker create(MpegAudioHeader mpegAudioHeader, ParsableByteArray frame, - long position, long inputLength) { + public static VbriSeeker create(long inputLength, long position, MpegAudioHeader mpegAudioHeader, + ParsableByteArray frame) { frame.skipBytes(10); int numFrames = frame.readInt(); if (numFrames <= 0) { @@ -53,15 +56,15 @@ import com.google.android.exoplayer2.util.Util; int entrySize = frame.readUnsignedShort(); frame.skipBytes(2); - // Skip the frame containing the VBRI header. - position += mpegAudioHeader.frameSize; - + long minPosition = position + mpegAudioHeader.frameSize; // Read table of contents entries. - long[] timesUs = new long[entryCount + 1]; - long[] positions = new long[entryCount + 1]; - timesUs[0] = 0L; - positions[0] = position; - for (int index = 1; index < timesUs.length; index++) { + long[] timesUs = new long[entryCount]; + long[] positions = new long[entryCount]; + for (int index = 0; index < entryCount; index++) { + timesUs[index] = (index * durationUs) / entryCount; + // Ensure positions do not fall within the frame containing the VBRI header. This constraint + // will normally only apply to the first entry in the table. + positions[index] = Math.max(position, minPosition); int segmentSize; switch (entrySize) { case 1: @@ -80,9 +83,9 @@ import com.google.android.exoplayer2.util.Util; return null; } position += segmentSize * scale; - timesUs[index] = index * durationUs / entryCount; - positions[index] = - inputLength == C.LENGTH_UNSET ? position : Math.min(inputLength, position); + } + if (inputLength != C.LENGTH_UNSET && inputLength != position) { + Log.w(TAG, "VBRI data size mismatch: " + inputLength + ", " + position); } return new VbriSeeker(timesUs, positions, durationUs); } diff --git a/library/core/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 index 55888066e7..e532249a64 100644 --- a/library/core/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 @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor.mp3; +import android.util.Log; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.MpegAudioHeader; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -25,24 +26,25 @@ import com.google.android.exoplayer2.util.Util; */ /* package */ final class XingSeeker implements Mp3Extractor.Seeker { + private static final String TAG = "XingSeeker"; + /** * Returns a {@link XingSeeker} for seeking in the stream, if required information is present. * Returns {@code null} if not. On returning, {@code frame}'s position is not specified so the * caller should reset it. * + * @param inputLength The length of the stream in bytes, or {@link C#LENGTH_UNSET} if unknown. + * @param position The position of the start of this frame in the stream. * @param mpegAudioHeader The MPEG audio header associated with the frame. * @param frame The data in this audio frame, with its position set to immediately after the * 'Xing' or 'Info' tag. - * @param position The position (byte offset) of the start of this frame in the stream. - * @param inputLength The length of the stream in bytes. * @return A {@link XingSeeker} for seeking in the stream, or {@code null} if the required * information is not present. */ - public static XingSeeker create(MpegAudioHeader mpegAudioHeader, ParsableByteArray frame, - long position, long inputLength) { + public static XingSeeker create(long inputLength, long position, MpegAudioHeader mpegAudioHeader, + ParsableByteArray frame) { int samplesPerFrame = mpegAudioHeader.samplesPerFrame; int sampleRate = mpegAudioHeader.sampleRate; - long firstFramePosition = position + mpegAudioHeader.frameSize; int flags = frame.readInt(); int frameCount; @@ -54,10 +56,10 @@ import com.google.android.exoplayer2.util.Util; sampleRate); if ((flags & 0x06) != 0x06) { // If the size in bytes or table of contents is missing, the stream is not seekable. - return new XingSeeker(firstFramePosition, durationUs, inputLength); + return new XingSeeker(position, mpegAudioHeader.frameSize, durationUs); } - long sizeBytes = frame.readUnsignedIntToInt(); + long dataSize = frame.readUnsignedIntToInt(); long[] tableOfContents = new long[100]; for (int i = 0; i < 100; i++) { tableOfContents[i] = frame.readUnsignedByte(); @@ -66,32 +68,37 @@ import com.google.android.exoplayer2.util.Util; // TODO: Handle encoder delay and padding in 3 bytes offset by xingBase + 213 bytes: // delay = (frame.readUnsignedByte() << 4) + (frame.readUnsignedByte() >> 4); // padding = ((frame.readUnsignedByte() & 0x0F) << 8) + frame.readUnsignedByte(); - return new XingSeeker(firstFramePosition, durationUs, inputLength, tableOfContents, - sizeBytes, mpegAudioHeader.frameSize); + + if (inputLength != C.LENGTH_UNSET && inputLength != position + dataSize) { + Log.w(TAG, "XING data size mismatch: " + inputLength + ", " + (position + dataSize)); + } + return new XingSeeker(position, mpegAudioHeader.frameSize, durationUs, dataSize, + tableOfContents); } - private final long firstFramePosition; + private final long dataStartPosition; + private final int xingFrameSize; private final long durationUs; - private final long inputLength; + /** + * Data size, including the XING frame. + */ + private final long dataSize; /** * Entries are in the range [0, 255], but are stored as long integers for convenience. */ private final long[] tableOfContents; - private final long sizeBytes; - private final int headerSize; - private XingSeeker(long firstFramePosition, long durationUs, long inputLength) { - this(firstFramePosition, durationUs, inputLength, null, 0, 0); + private XingSeeker(long dataStartPosition, int xingFrameSize, long durationUs) { + this(dataStartPosition, xingFrameSize, durationUs, C.LENGTH_UNSET, null); } - private XingSeeker(long firstFramePosition, long durationUs, long inputLength, - long[] tableOfContents, long sizeBytes, int headerSize) { - this.firstFramePosition = firstFramePosition; + private XingSeeker(long dataStartPosition, int xingFrameSize, long durationUs, long dataSize, + long[] tableOfContents) { + this.dataStartPosition = dataStartPosition; + this.xingFrameSize = xingFrameSize; this.durationUs = durationUs; - this.inputLength = inputLength; + this.dataSize = dataSize; this.tableOfContents = tableOfContents; - this.sizeBytes = sizeBytes; - this.headerSize = headerSize; } @Override @@ -102,44 +109,45 @@ import com.google.android.exoplayer2.util.Util; @Override public long getPosition(long timeUs) { if (!isSeekable()) { - return firstFramePosition; + return dataStartPosition + xingFrameSize; } double percent = (timeUs * 100d) / durationUs; - double fx; + double scaledPosition; if (percent <= 0) { - fx = 0; + scaledPosition = 0; } else if (percent >= 100) { - fx = 256; + scaledPosition = 256; } else { - int a = (int) percent; - float fa = tableOfContents[a]; - float fb = a == 99 ? 256 : tableOfContents[a + 1]; - fx = fa + (fb - fa) * (percent - a); + int prevTableIndex = (int) percent; + double prevScaledPosition = tableOfContents[prevTableIndex]; + double nextScaledPosition = prevTableIndex == 99 ? 256 : tableOfContents[prevTableIndex + 1]; + // Linearly interpolate between the two scaled positions. + double interpolateFraction = percent - prevTableIndex; + scaledPosition = prevScaledPosition + + (interpolateFraction * (nextScaledPosition - prevScaledPosition)); } - - long position = Math.round((fx / 256) * sizeBytes) + firstFramePosition; - long maximumPosition = inputLength != C.LENGTH_UNSET ? inputLength - 1 - : firstFramePosition - headerSize + sizeBytes - 1; - return Math.min(position, maximumPosition); + long positionOffset = Math.round((scaledPosition / 256) * dataSize); + // Ensure returned positions skip the frame containing the XING header. + positionOffset = Util.constrainValue(positionOffset, xingFrameSize, dataSize - 1); + return dataStartPosition + positionOffset; } @Override public long getTimeUs(long position) { - if (!isSeekable() || position < firstFramePosition) { + long positionOffset = position - dataStartPosition; + if (!isSeekable() || positionOffset <= xingFrameSize) { return 0L; } - double offsetByte = (256d * (position - firstFramePosition)) / sizeBytes; - int previousTocPosition = - Util.binarySearchFloor(tableOfContents, (long) offsetByte, true, true); - long previousTime = getTimeUsForTocPosition(previousTocPosition); - - // Linearly interpolate the time taking into account the next entry. - long previousByte = tableOfContents[previousTocPosition]; - long nextByte = previousTocPosition == 99 ? 256 : tableOfContents[previousTocPosition + 1]; - long nextTime = getTimeUsForTocPosition(previousTocPosition + 1); - long timeOffset = nextByte == previousByte ? 0 : (long) ((nextTime - previousTime) - * (offsetByte - previousByte) / (nextByte - previousByte)); - return previousTime + timeOffset; + double scaledPosition = (positionOffset * 256d) / dataSize; + int prevTableIndex = Util.binarySearchFloor(tableOfContents, (long) scaledPosition, true, true); + long prevTimeUs = getTimeUsForTableIndex(prevTableIndex); + long prevScaledPosition = tableOfContents[prevTableIndex]; + long nextTimeUs = getTimeUsForTableIndex(prevTableIndex + 1); + long nextScaledPosition = prevTableIndex == 99 ? 256 : tableOfContents[prevTableIndex + 1]; + // Linearly interpolate between the two table entries. + double interpolateFraction = prevScaledPosition == nextScaledPosition ? 0 + : ((scaledPosition - prevScaledPosition) / (nextScaledPosition - prevScaledPosition)); + return prevTimeUs + Math.round(interpolateFraction * (nextTimeUs - prevTimeUs)); } @Override @@ -148,11 +156,13 @@ import com.google.android.exoplayer2.util.Util; } /** - * Returns the time in microseconds corresponding to a table of contents position, which is - * interpreted as a percentage of the stream's duration between 0 and 100. + * Returns the time in microseconds for a given table index. + * + * @param tableIndex A table index in the range [0, 100]. + * @return The corresponding time in microseconds. */ - private long getTimeUsForTocPosition(int tocPosition) { - return (durationUs * tocPosition) / 100; + private long getTimeUsForTableIndex(int tableIndex) { + return (durationUs * tableIndex) / 100; } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp3/XingSeekerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp3/XingSeekerTest.java index b43949b7c2..e644abc7ef 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp3/XingSeekerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp3/XingSeekerTest.java @@ -43,17 +43,17 @@ public final class XingSeekerTest { private static final int XING_FRAME_POSITION = 157; /** - * Size of the audio stream, encoded in {@link #XING_FRAME_PAYLOAD}. + * Data size, as encoded in {@link #XING_FRAME_PAYLOAD}. */ - private static final int STREAM_SIZE_BYTES = 948505; + private static final int DATA_SIZE_BYTES = 948505; /** * Duration of the audio stream in microseconds, encoded in {@link #XING_FRAME_PAYLOAD}. */ private static final int STREAM_DURATION_US = 59271836; /** - * The length of the file in bytes. + * The length of the stream in bytes. */ - private static final int INPUT_LENGTH = 948662; + private static final int STREAM_LENGTH = XING_FRAME_POSITION + DATA_SIZE_BYTES; private XingSeeker seeker; private XingSeeker seekerWithInputLength; @@ -63,10 +63,10 @@ public final class XingSeekerTest { public void setUp() throws Exception { MpegAudioHeader xingFrameHeader = new MpegAudioHeader(); MpegAudioHeader.populateHeader(XING_FRAME_HEADER_DATA, xingFrameHeader); - seeker = XingSeeker.create(xingFrameHeader, new ParsableByteArray(XING_FRAME_PAYLOAD), - XING_FRAME_POSITION, C.LENGTH_UNSET); - seekerWithInputLength = XingSeeker.create(xingFrameHeader, - new ParsableByteArray(XING_FRAME_PAYLOAD), XING_FRAME_POSITION, INPUT_LENGTH); + seeker = XingSeeker.create(C.LENGTH_UNSET, XING_FRAME_POSITION, xingFrameHeader, + new ParsableByteArray(XING_FRAME_PAYLOAD)); + seekerWithInputLength = XingSeeker.create(STREAM_LENGTH, + XING_FRAME_POSITION, xingFrameHeader, new ParsableByteArray(XING_FRAME_PAYLOAD)); xingFrameSize = xingFrameHeader.frameSize; } @@ -84,10 +84,10 @@ public final class XingSeekerTest { @Test public void testGetTimeUsAtEndOfStream() { - assertThat(seeker.getTimeUs(XING_FRAME_POSITION + xingFrameSize + STREAM_SIZE_BYTES)) + assertThat(seeker.getTimeUs(STREAM_LENGTH)) .isEqualTo(STREAM_DURATION_US); assertThat( - seekerWithInputLength.getTimeUs(XING_FRAME_POSITION + xingFrameSize + STREAM_SIZE_BYTES)) + seekerWithInputLength.getTimeUs(STREAM_LENGTH)) .isEqualTo(STREAM_DURATION_US); } @@ -100,14 +100,14 @@ public final class XingSeekerTest { @Test public void testGetPositionAtEndOfStream() { assertThat(seeker.getPosition(STREAM_DURATION_US)) - .isEqualTo(XING_FRAME_POSITION + STREAM_SIZE_BYTES - 1); + .isEqualTo(STREAM_LENGTH - 1); assertThat(seekerWithInputLength.getPosition(STREAM_DURATION_US)) - .isEqualTo(XING_FRAME_POSITION + STREAM_SIZE_BYTES - 1); + .isEqualTo(STREAM_LENGTH - 1); } @Test public void testGetTimeForAllPositions() { - for (int offset = xingFrameSize; offset < STREAM_SIZE_BYTES; offset++) { + for (int offset = xingFrameSize; offset < DATA_SIZE_BYTES; offset++) { int position = XING_FRAME_POSITION + offset; long timeUs = seeker.getTimeUs(position); assertThat(seeker.getPosition(timeUs)).isEqualTo(position); From 3a6b7a346cbd45bbf499955aa76d633c01edc55f Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 30 Nov 2017 08:36:03 -0800 Subject: [PATCH 0819/2472] Fix mp3 extractor test ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177458840 --- .../androidTest/assets/mp3/bear.mp3.1.dump | 154 +++++++++--------- 1 file changed, 77 insertions(+), 77 deletions(-) diff --git a/library/core/src/androidTest/assets/mp3/bear.mp3.1.dump b/library/core/src/androidTest/assets/mp3/bear.mp3.1.dump index 7b6fe9db37..a57894e81e 100644 --- a/library/core/src/androidTest/assets/mp3/bear.mp3.1.dump +++ b/library/core/src/androidTest/assets/mp3/bear.mp3.1.dump @@ -27,311 +27,311 @@ track 0: initializationData: sample count = 77 sample 0: - time = 928567 + time = 928568 flags = 1 data = length 384, hash F7E344F4 sample 1: - time = 952567 + time = 952568 flags = 1 data = length 384, hash 14EF6AFD sample 2: - time = 976567 + time = 976568 flags = 1 data = length 384, hash 61C9B92C sample 3: - time = 1000567 + time = 1000568 flags = 1 data = length 384, hash ABE1368 sample 4: - time = 1024567 + time = 1024568 flags = 1 data = length 384, hash 6A3B8547 sample 5: - time = 1048567 + time = 1048568 flags = 1 data = length 384, hash 30E905FA sample 6: - time = 1072567 + time = 1072568 flags = 1 data = length 384, hash 21A267CD sample 7: - time = 1096567 + time = 1096568 flags = 1 data = length 384, hash D96A2651 sample 8: - time = 1120567 + time = 1120568 flags = 1 data = length 384, hash 72340177 sample 9: - time = 1144567 + time = 1144568 flags = 1 data = length 384, hash 9345E744 sample 10: - time = 1168567 + time = 1168568 flags = 1 data = length 384, hash FDE39E3A sample 11: - time = 1192567 + time = 1192568 flags = 1 data = length 384, hash F0B7465 sample 12: - time = 1216567 + time = 1216568 flags = 1 data = length 384, hash 3693AB86 sample 13: - time = 1240567 + time = 1240568 flags = 1 data = length 384, hash F39719B1 sample 14: - time = 1264567 + time = 1264568 flags = 1 data = length 384, hash DA3958DC sample 15: - time = 1288567 + time = 1288568 flags = 1 data = length 384, hash FDC7599F sample 16: - time = 1312567 + time = 1312568 flags = 1 data = length 384, hash AEFF8471 sample 17: - time = 1336567 + time = 1336568 flags = 1 data = length 384, hash 89C92C19 sample 18: - time = 1360567 + time = 1360568 flags = 1 data = length 384, hash 5C786A4B sample 19: - time = 1384567 + time = 1384568 flags = 1 data = length 384, hash 5ACA8B sample 20: - time = 1408567 + time = 1408568 flags = 1 data = length 384, hash 7755974C sample 21: - time = 1432567 + time = 1432568 flags = 1 data = length 384, hash 3934B73C sample 22: - time = 1456567 + time = 1456568 flags = 1 data = length 384, hash DDD70A2F sample 23: - time = 1480567 + time = 1480568 flags = 1 data = length 384, hash 8FACE2EF sample 24: - time = 1504567 + time = 1504568 flags = 1 data = length 384, hash 4A602591 sample 25: - time = 1528567 + time = 1528568 flags = 1 data = length 384, hash D019AA2D sample 26: - time = 1552567 + time = 1552568 flags = 1 data = length 384, hash 8A680B9D sample 27: - time = 1576567 + time = 1576568 flags = 1 data = length 384, hash B655C959 sample 28: - time = 1600567 + time = 1600568 flags = 1 data = length 384, hash 2168336B sample 29: - time = 1624567 + time = 1624568 flags = 1 data = length 384, hash D77F6D31 sample 30: - time = 1648567 + time = 1648568 flags = 1 data = length 384, hash 524B4B2F sample 31: - time = 1672567 + time = 1672568 flags = 1 data = length 384, hash 4752DDFC sample 32: - time = 1696567 + time = 1696568 flags = 1 data = length 384, hash E786727F sample 33: - time = 1720567 + time = 1720568 flags = 1 data = length 384, hash 5DA6FB8C sample 34: - time = 1744567 + time = 1744568 flags = 1 data = length 384, hash 92F24269 sample 35: - time = 1768567 + time = 1768568 flags = 1 data = length 384, hash CD0A3BA1 sample 36: - time = 1792567 + time = 1792568 flags = 1 data = length 384, hash 7D00409F sample 37: - time = 1816567 + time = 1816568 flags = 1 data = length 384, hash D7ADB5FA sample 38: - time = 1840567 + time = 1840568 flags = 1 data = length 384, hash 4A140209 sample 39: - time = 1864567 + time = 1864568 flags = 1 data = length 384, hash E801184A sample 40: - time = 1888567 + time = 1888568 flags = 1 data = length 384, hash 53C6CF9C sample 41: - time = 1912567 + time = 1912568 flags = 1 data = length 384, hash 19A8D99F sample 42: - time = 1936567 + time = 1936568 flags = 1 data = length 384, hash E47EB43F sample 43: - time = 1960567 + time = 1960568 flags = 1 data = length 384, hash 4EA329E7 sample 44: - time = 1984567 + time = 1984568 flags = 1 data = length 384, hash 1CCAAE62 sample 45: - time = 2008567 + time = 2008568 flags = 1 data = length 384, hash ED3F8C66 sample 46: - time = 2032567 + time = 2032568 flags = 1 data = length 384, hash D3D646B6 sample 47: - time = 2056567 + time = 2056568 flags = 1 data = length 384, hash 68CD1574 sample 48: - time = 2080567 + time = 2080568 flags = 1 data = length 384, hash 8CEAB382 sample 49: - time = 2104567 + time = 2104568 flags = 1 data = length 384, hash D54B1C48 sample 50: - time = 2128567 + time = 2128568 flags = 1 data = length 384, hash FFE2EE90 sample 51: - time = 2152567 + time = 2152568 flags = 1 data = length 384, hash BFE8A673 sample 52: - time = 2176567 + time = 2176568 flags = 1 data = length 384, hash 978B1C92 sample 53: - time = 2200567 + time = 2200568 flags = 1 data = length 384, hash 810CC71E sample 54: - time = 2224567 + time = 2224568 flags = 1 data = length 384, hash 44FE42D9 sample 55: - time = 2248567 + time = 2248568 flags = 1 data = length 384, hash 2F5BB02C sample 56: - time = 2272567 + time = 2272568 flags = 1 data = length 384, hash 77DDB90 sample 57: - time = 2296567 + time = 2296568 flags = 1 data = length 384, hash 24FB5EDA sample 58: - time = 2320567 + time = 2320568 flags = 1 data = length 384, hash E73203C6 sample 59: - time = 2344567 + time = 2344568 flags = 1 data = length 384, hash 14B525F1 sample 60: - time = 2368567 + time = 2368568 flags = 1 data = length 384, hash 5E0F4E2E sample 61: - time = 2392567 + time = 2392568 flags = 1 data = length 384, hash 67EE4E31 sample 62: - time = 2416567 + time = 2416568 flags = 1 data = length 384, hash 2E04EC4C sample 63: - time = 2440567 + time = 2440568 flags = 1 data = length 384, hash 852CABA7 sample 64: - time = 2464567 + time = 2464568 flags = 1 data = length 384, hash 19928903 sample 65: - time = 2488567 + time = 2488568 flags = 1 data = length 384, hash 5DA42021 sample 66: - time = 2512567 + time = 2512568 flags = 1 data = length 384, hash 45B20B7C sample 67: - time = 2536567 + time = 2536568 flags = 1 data = length 384, hash D108A215 sample 68: - time = 2560567 + time = 2560568 flags = 1 data = length 384, hash BD25DB7C sample 69: - time = 2584567 + time = 2584568 flags = 1 data = length 384, hash DA7F9861 sample 70: - time = 2608567 + time = 2608568 flags = 1 data = length 384, hash CCD576F sample 71: - time = 2632567 + time = 2632568 flags = 1 data = length 384, hash 405C1EB5 sample 72: - time = 2656567 + time = 2656568 flags = 1 data = length 384, hash 6640B74E sample 73: - time = 2680567 + time = 2680568 flags = 1 data = length 384, hash B4E5937A sample 74: - time = 2704567 + time = 2704568 flags = 1 data = length 384, hash CEE17733 sample 75: - time = 2728567 + time = 2728568 flags = 1 data = length 384, hash 2A0DA733 sample 76: - time = 2752567 + time = 2752568 flags = 1 data = length 384, hash 97F4129B tracksEnded = true From 23cc102151fbfd1c18e10e7ec7b6f448a1094a5b Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 30 Nov 2017 09:08:11 -0800 Subject: [PATCH 0820/2472] Move internal HlsSampleStreamWrapper methods under internal methods ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177462449 --- .../source/hls/HlsSampleStreamWrapper.java | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/library/hls/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 index 6cb3f854c8..8f063a38f1 100644 --- a/library/hls/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 @@ -410,24 +410,6 @@ import java.util.Arrays; } } - private boolean finishedReadingChunk(HlsMediaChunk chunk) { - int chunkUid = chunk.uid; - int sampleQueueCount = sampleQueues.length; - for (int i = 0; i < sampleQueueCount; i++) { - if (sampleQueuesEnabledStates[i] && sampleQueues[i].peekSourceId() == chunkUid) { - return false; - } - } - return true; - } - - private void resetSampleQueues() { - for (SampleQueue sampleQueue : sampleQueues) { - sampleQueue.reset(pendingResetUpstreamFormats); - } - pendingResetUpstreamFormats = false; - } - // SequenceableLoader implementation @Override @@ -650,6 +632,24 @@ import java.util.Arrays; // Internal methods. + private boolean finishedReadingChunk(HlsMediaChunk chunk) { + int chunkUid = chunk.uid; + int sampleQueueCount = sampleQueues.length; + for (int i = 0; i < sampleQueueCount; i++) { + if (sampleQueuesEnabledStates[i] && sampleQueues[i].peekSourceId() == chunkUid) { + return false; + } + } + return true; + } + + private void resetSampleQueues() { + for (SampleQueue sampleQueue : sampleQueues) { + sampleQueue.reset(pendingResetUpstreamFormats); + } + pendingResetUpstreamFormats = false; + } + private void maybeFinishPrepare() { if (released || prepared || !sampleQueuesBuilt) { return; From 80fff0b7ceba2835cf103f52c16ee4cbcbf4ba3c Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 1 Dec 2017 03:33:49 -0800 Subject: [PATCH 0821/2472] Discard buffer in FakeExoPlayer. This is in line with a recent change in ExoPlayerImplInternal. Not discarding the buffer causes OOM when running simulated playbacks. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177573930 --- .../exoplayer2/testutil/FakeAdaptiveMediaPeriod.java | 8 ++++++++ .../android/exoplayer2/testutil/FakeSimpleExoPlayer.java | 1 + 2 files changed, 9 insertions(+) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java index 3dcf551943..a4c9abb24e 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java @@ -84,6 +84,14 @@ public class FakeAdaptiveMediaPeriod extends FakeMediaPeriod return returnPositionUs; } + @Override + public void discardBuffer(long positionUs, boolean toKeyframe) { + super.discardBuffer(positionUs, toKeyframe); + for (ChunkSampleStream sampleStream : sampleStreams) { + sampleStream.discardBuffer(positionUs, toKeyframe); + } + } + @Override public long getBufferedPositionUs() { super.getBufferedPositionUs(); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java index 0358e5d980..1e7e0cd933 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java @@ -364,6 +364,7 @@ public class FakeSimpleExoPlayer extends SimpleExoPlayer { public void run() { try { maybeContinueLoading(); + mediaPeriod.discardBuffer(rendererPositionUs, /* toKeyframe= */ false); boolean allRenderersEnded = true; boolean allRenderersReadyOrEnded = true; if (playbackState == Player.STATE_READY) { From 03b0d9d46c48ff4ef03928ca6cb4cc6fe571224d Mon Sep 17 00:00:00 2001 From: tonihei Date: Sun, 3 Dec 2017 11:25:51 -0800 Subject: [PATCH 0822/2472] Fix flaky testEmptyTimeline again. Waiting for the timeline change didn't work correctly because the timeline was already equal to Timeline.EMPTY (due to the masking). Now waiting explicitly for the empty Timeline exposed by the source. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177749292 --- .../java/com/google/android/exoplayer2/ExoPlayerTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index 2443f8b892..714dfff676 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -54,7 +54,7 @@ public final class ExoPlayerTest extends TestCase { * error. */ public void testPlayEmptyTimeline() throws Exception { - Timeline timeline = Timeline.EMPTY; + Timeline timeline = new FakeTimeline(/* windowCount= */ 0); FakeRenderer renderer = new FakeRenderer(); // TODO(b/69665207): Without waiting for the timeline update, this test is flaky as the timeline // update happens after the transition to STATE_ENDED and the test runner may already have been From fbccdf594a589abd01d0b0e6fc5dca7229b66b8f Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 4 Dec 2017 01:18:58 -0800 Subject: [PATCH 0823/2472] Use AdaptiveMediaSourceEventListener for ExtractorMediaSource This is a step towards harmonizing the MediaSource Builders and (potentially) providing MediaSource factories. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177783157 --- RELEASENOTES.md | 2 + .../android/exoplayer2/demo/EventLogger.java | 24 +- .../exoplayer2/ext/ima/ImaAdsMediaSource.java | 14 +- .../AdaptiveMediaSourceEventListener.java | 305 +---------- .../source/ExtractorMediaPeriod.java | 62 ++- .../source/ExtractorMediaSource.java | 159 +++++- .../source/MediaSourceEventListener.java | 487 ++++++++++++++++++ .../exoplayer2/source/ads/AdsMediaSource.java | 81 ++- .../android/exoplayer2/upstream/DataSpec.java | 19 +- 9 files changed, 753 insertions(+), 400 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 3a42311b26..f6adf560b9 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -27,6 +27,8 @@ * Add optional parameter to `Player.stop` to reset the player when stopping. * Fix handling of playback parameters changes while paused when followed by a seek. +* Use the same listener `MediaSourceEventListener` for all MediaSource + implementations. ### 2.6.0 ### diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java index 68a10343e6..0635944640 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java @@ -39,7 +39,6 @@ import com.google.android.exoplayer2.metadata.id3.PrivFrame; import com.google.android.exoplayer2.metadata.id3.TextInformationFrame; import com.google.android.exoplayer2.metadata.id3.UrlLinkFrame; import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener; -import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.ads.AdsMediaSource; @@ -53,13 +52,15 @@ import java.io.IOException; import java.text.NumberFormat; import java.util.Locale; -/** - * Logs player events using {@link Log}. - */ -/* package */ final class EventLogger implements Player.EventListener, MetadataOutput, - AudioRendererEventListener, VideoRendererEventListener, AdaptiveMediaSourceEventListener, - ExtractorMediaSource.EventListener, AdsMediaSource.AdsListener, - DefaultDrmSessionManager.EventListener { +/** Logs player events using {@link Log}. */ +/* package */ final class EventLogger + implements Player.EventListener, + MetadataOutput, + AudioRendererEventListener, + VideoRendererEventListener, + AdaptiveMediaSourceEventListener, + AdsMediaSource.EventListener, + DefaultDrmSessionManager.EventListener { private static final String TAG = "EventLogger"; private static final int MAX_TIMELINE_ITEM_LINES = 3; @@ -324,13 +325,6 @@ import java.util.Locale; Log.d(TAG, "drmKeysLoaded [" + getSessionTimeString() + "]"); } - // ExtractorMediaSource.EventListener - - @Override - public void onLoadError(IOException error) { - printInternalError("loadError", error); - } - // AdaptiveMediaSourceEventListener @Override diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java index 02aa4807a5..cd646daf42 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java @@ -52,8 +52,8 @@ public final class ImaAdsMediaSource implements MediaSource { } /** - * Constructs a new source that inserts ads linearly with the content specified by - * {@code contentMediaSource}. + * Constructs a new source that inserts ads linearly with the content specified by {@code + * contentMediaSource}. * * @param contentMediaSource The {@link MediaSource} providing the content to play. * @param dataSourceFactory Factory for data sources used to load ad media. @@ -62,9 +62,13 @@ public final class ImaAdsMediaSource implements MediaSource { * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. */ - public ImaAdsMediaSource(MediaSource contentMediaSource, DataSource.Factory dataSourceFactory, - ImaAdsLoader imaAdsLoader, ViewGroup adUiViewGroup, @Nullable Handler eventHandler, - @Nullable AdsMediaSource.AdsListener eventListener) { + public ImaAdsMediaSource( + MediaSource contentMediaSource, + DataSource.Factory dataSourceFactory, + ImaAdsLoader imaAdsLoader, + ViewGroup adUiViewGroup, + @Nullable Handler eventHandler, + @Nullable AdsMediaSource.EventListener eventListener) { adsMediaSource = new AdsMediaSource(contentMediaSource, dataSourceFactory, imaAdsLoader, adUiViewGroup, eventHandler, eventListener); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/AdaptiveMediaSourceEventListener.java b/library/core/src/main/java/com/google/android/exoplayer2/source/AdaptiveMediaSourceEventListener.java index be07cbb5dc..2bc9d48726 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/AdaptiveMediaSourceEventListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/AdaptiveMediaSourceEventListener.java @@ -16,306 +16,39 @@ package com.google.android.exoplayer2.source; import android.os.Handler; -import android.os.SystemClock; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.upstream.DataSpec; -import com.google.android.exoplayer2.util.Assertions; -import java.io.IOException; +import android.support.annotation.Nullable; /** - * Interface for callbacks to be notified of adaptive {@link MediaSource} events. + * Interface for callbacks to be notified of {@link MediaSource} events. + * + * @deprecated Use {@link MediaSourceEventListener} */ -public interface AdaptiveMediaSourceEventListener { +@Deprecated +public interface AdaptiveMediaSourceEventListener extends MediaSourceEventListener { - /** - * Called when a load begins. - * - * @param dataSpec Defines the data being loaded. - * @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data - * being loaded. - * @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds - * to media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise. - * @param trackFormat The format of the track to which the data belongs. Null if the data does - * not belong to a track. - * @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the - * data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise. - * @param trackSelectionData Optional data associated with the selection of the track to which the - * data belongs. Null if the data does not belong to a track. - * @param mediaStartTimeMs The start time of the media being loaded, or {@link C#TIME_UNSET} if - * the load is not for media data. - * @param mediaEndTimeMs The end time of the media being loaded, or {@link C#TIME_UNSET} if the - * load is not for media data. - * @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} when the load began. - */ - void onLoadStarted(DataSpec dataSpec, int dataType, int trackType, Format trackFormat, - int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs, - long mediaEndTimeMs, long elapsedRealtimeMs); - - /** - * Called when a load ends. - * - * @param dataSpec Defines the data being loaded. - * @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data - * being loaded. - * @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds - * to media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise. - * @param trackFormat The format of the track to which the data belongs. Null if the data does - * not belong to a track. - * @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the - * data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise. - * @param trackSelectionData Optional data associated with the selection of the track to which the - * data belongs. Null if the data does not belong to a track. - * @param mediaStartTimeMs The start time of the media being loaded, or {@link C#TIME_UNSET} if - * the load is not for media data. - * @param mediaEndTimeMs The end time of the media being loaded, or {@link C#TIME_UNSET} if the - * load is not for media data. - * @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} when the load ended. - * @param loadDurationMs The duration of the load. - * @param bytesLoaded The number of bytes that were loaded. - */ - void onLoadCompleted(DataSpec dataSpec, int dataType, int trackType, Format trackFormat, - int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs, - long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs, long bytesLoaded); - - /** - * Called when a load is canceled. - * - * @param dataSpec Defines the data being loaded. - * @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data - * being loaded. - * @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds - * to media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise. - * @param trackFormat The format of the track to which the data belongs. Null if the data does - * not belong to a track. - * @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the - * data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise. - * @param trackSelectionData Optional data associated with the selection of the track to which the - * data belongs. Null if the data does not belong to a track. - * @param mediaStartTimeMs The start time of the media being loaded, or {@link C#TIME_UNSET} if - * the load is not for media data. - * @param mediaEndTimeMs The end time of the media being loaded, or {@link C#TIME_UNSET} if the - * load is not for media data. - * @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} when the load was - * canceled. - * @param loadDurationMs The duration of the load up to the point at which it was canceled. - * @param bytesLoaded The number of bytes that were loaded prior to cancelation. - */ - void onLoadCanceled(DataSpec dataSpec, int dataType, int trackType, Format trackFormat, - int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs, - long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs, long bytesLoaded); - - /** - * Called when a load error occurs. - *

          - * The error may or may not have resulted in the load being canceled, as indicated by the - * {@code wasCanceled} parameter. If the load was canceled, {@link #onLoadCanceled} will - * not be called in addition to this method. - *

          - * This method being called does not indicate that playback has failed, or that it will fail. The - * player may be able to recover from the error and continue. Hence applications should - * not implement this method to display a user visible error or initiate an application - * level retry ({@link Player.EventListener#onPlayerError} is the appropriate place to implement - * such behavior). This method is called to provide the application with an opportunity to log the - * error if it wishes to do so. - * - * @param dataSpec Defines the data being loaded. - * @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data - * being loaded. - * @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds - * to media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise. - * @param trackFormat The format of the track to which the data belongs. Null if the data does - * not belong to a track. - * @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the - * data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise. - * @param trackSelectionData Optional data associated with the selection of the track to which the - * data belongs. Null if the data does not belong to a track. - * @param mediaStartTimeMs The start time of the media being loaded, or {@link C#TIME_UNSET} if - * the load is not for media data. - * @param mediaEndTimeMs The end time of the media being loaded, or {@link C#TIME_UNSET} if the - * load is not for media data. - * @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} when the error - * occurred. - * @param loadDurationMs The duration of the load up to the point at which the error occurred. - * @param bytesLoaded The number of bytes that were loaded prior to the error. - * @param error The load error. - * @param wasCanceled Whether the load was canceled as a result of the error. - */ - void onLoadError(DataSpec dataSpec, int dataType, int trackType, Format trackFormat, - int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs, - long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs, long bytesLoaded, - IOException error, boolean wasCanceled); - - /** - * Called when data is removed from the back of a media buffer, typically so that it can be - * re-buffered in a different format. - * - * @param trackType The type of the media. One of the {@link C} {@code TRACK_TYPE_*} constants. - * @param mediaStartTimeMs The start time of the media being discarded. - * @param mediaEndTimeMs The end time of the media being discarded. - */ - void onUpstreamDiscarded(int trackType, long mediaStartTimeMs, long mediaEndTimeMs); - - /** - * Called when a downstream format change occurs (i.e. when the format of the media being read - * from one or more {@link SampleStream}s provided by the source changes). - * - * @param trackType The type of the media. One of the {@link C} {@code TRACK_TYPE_*} constants. - * @param trackFormat The format of the track to which the data belongs. Null if the data does - * not belong to a track. - * @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the - * data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise. - * @param trackSelectionData Optional data associated with the selection of the track to which the - * data belongs. Null if the data does not belong to a track. - * @param mediaTimeMs The media time at which the change occurred. - */ - void onDownstreamFormatChanged(int trackType, Format trackFormat, int trackSelectionReason, - Object trackSelectionData, long mediaTimeMs); - - /** - * Dispatches events to a {@link AdaptiveMediaSourceEventListener}. - */ - final class EventDispatcher { + /** Dispatches events to a {@link MediaSourceEventListener}. */ + final class EventDispatcher extends MediaSourceEventListener.EventDispatcher { private final Handler handler; - private final AdaptiveMediaSourceEventListener listener; - private final long mediaTimeOffsetMs; + private final MediaSourceEventListener listener; - public EventDispatcher(Handler handler, AdaptiveMediaSourceEventListener listener) { + public EventDispatcher(@Nullable Handler handler, @Nullable MediaSourceEventListener listener) { this(handler, listener, 0); } - public EventDispatcher(Handler handler, AdaptiveMediaSourceEventListener listener, + public EventDispatcher( + @Nullable Handler handler, + @Nullable MediaSourceEventListener listener, long mediaTimeOffsetMs) { - this.handler = listener != null ? Assertions.checkNotNull(handler) : null; + super(handler, listener, mediaTimeOffsetMs); + this.handler = handler; this.listener = listener; - this.mediaTimeOffsetMs = mediaTimeOffsetMs; } - public EventDispatcher copyWithMediaTimeOffsetMs(long mediaTimeOffsetMs) { - return new EventDispatcher(handler, listener, mediaTimeOffsetMs); - } - - public void loadStarted(DataSpec dataSpec, int dataType, long elapsedRealtimeMs) { - loadStarted(dataSpec, dataType, C.TRACK_TYPE_UNKNOWN, null, C.SELECTION_REASON_UNKNOWN, - null, C.TIME_UNSET, C.TIME_UNSET, elapsedRealtimeMs); - } - - public void loadStarted(final DataSpec dataSpec, final int dataType, final int trackType, - final Format trackFormat, final int trackSelectionReason, final Object trackSelectionData, - final long mediaStartTimeUs, final long mediaEndTimeUs, final long elapsedRealtimeMs) { - if (listener != null) { - handler.post(new Runnable() { - @Override - public void run() { - listener.onLoadStarted(dataSpec, dataType, trackType, trackFormat, trackSelectionReason, - trackSelectionData, adjustMediaTime(mediaStartTimeUs), - adjustMediaTime(mediaEndTimeUs), elapsedRealtimeMs); - } - }); - } - } - - public void loadCompleted(DataSpec dataSpec, int dataType, long elapsedRealtimeMs, - long loadDurationMs, long bytesLoaded) { - loadCompleted(dataSpec, dataType, C.TRACK_TYPE_UNKNOWN, null, C.SELECTION_REASON_UNKNOWN, - null, C.TIME_UNSET, C.TIME_UNSET, elapsedRealtimeMs, loadDurationMs, bytesLoaded); - } - - public void loadCompleted(final DataSpec dataSpec, final int dataType, final int trackType, - final Format trackFormat, final int trackSelectionReason, final Object trackSelectionData, - final long mediaStartTimeUs, final long mediaEndTimeUs, final long elapsedRealtimeMs, - final long loadDurationMs, final long bytesLoaded) { - if (listener != null) { - handler.post(new Runnable() { - @Override - public void run() { - listener.onLoadCompleted(dataSpec, dataType, trackType, trackFormat, - trackSelectionReason, trackSelectionData, adjustMediaTime(mediaStartTimeUs), - adjustMediaTime(mediaEndTimeUs), elapsedRealtimeMs, loadDurationMs, bytesLoaded); - } - }); - } - } - - public void loadCanceled(DataSpec dataSpec, int dataType, long elapsedRealtimeMs, - long loadDurationMs, long bytesLoaded) { - loadCanceled(dataSpec, dataType, C.TRACK_TYPE_UNKNOWN, null, C.SELECTION_REASON_UNKNOWN, - null, C.TIME_UNSET, C.TIME_UNSET, elapsedRealtimeMs, loadDurationMs, bytesLoaded); - } - - public void loadCanceled(final DataSpec dataSpec, final int dataType, final int trackType, - final Format trackFormat, final int trackSelectionReason, final Object trackSelectionData, - final long mediaStartTimeUs, final long mediaEndTimeUs, final long elapsedRealtimeMs, - final long loadDurationMs, final long bytesLoaded) { - if (listener != null) { - handler.post(new Runnable() { - @Override - public void run() { - listener.onLoadCanceled(dataSpec, dataType, trackType, trackFormat, - trackSelectionReason, trackSelectionData, adjustMediaTime(mediaStartTimeUs), - adjustMediaTime(mediaEndTimeUs), elapsedRealtimeMs, loadDurationMs, bytesLoaded); - } - }); - } - } - - public void loadError(DataSpec dataSpec, int dataType, long elapsedRealtimeMs, - long loadDurationMs, long bytesLoaded, IOException error, boolean wasCanceled) { - loadError(dataSpec, dataType, C.TRACK_TYPE_UNKNOWN, null, C.SELECTION_REASON_UNKNOWN, - null, C.TIME_UNSET, C.TIME_UNSET, elapsedRealtimeMs, loadDurationMs, bytesLoaded, - error, wasCanceled); - } - - public void loadError(final DataSpec dataSpec, final int dataType, final int trackType, - final Format trackFormat, final int trackSelectionReason, final Object trackSelectionData, - final long mediaStartTimeUs, final long mediaEndTimeUs, final long elapsedRealtimeMs, - final long loadDurationMs, final long bytesLoaded, final IOException error, - final boolean wasCanceled) { - if (listener != null) { - handler.post(new Runnable() { - @Override - public void run() { - listener.onLoadError(dataSpec, dataType, trackType, trackFormat, trackSelectionReason, - trackSelectionData, adjustMediaTime(mediaStartTimeUs), - adjustMediaTime(mediaEndTimeUs), elapsedRealtimeMs, loadDurationMs, bytesLoaded, - error, wasCanceled); - } - }); - } - } - - public void upstreamDiscarded(final int trackType, final long mediaStartTimeUs, - final long mediaEndTimeUs) { - if (listener != null) { - handler.post(new Runnable() { - @Override - public void run() { - listener.onUpstreamDiscarded(trackType, adjustMediaTime(mediaStartTimeUs), - adjustMediaTime(mediaEndTimeUs)); - } - }); - } - } - - public void downstreamFormatChanged(final int trackType, final Format trackFormat, - final int trackSelectionReason, final Object trackSelectionData, - final long mediaTimeUs) { - if (listener != null) { - handler.post(new Runnable() { - @Override - public void run() { - listener.onDownstreamFormatChanged(trackType, trackFormat, trackSelectionReason, - trackSelectionData, adjustMediaTime(mediaTimeUs)); - } - }); - } - } - - private long adjustMediaTime(long mediaTimeUs) { - long mediaTimeMs = C.usToMs(mediaTimeUs); - return mediaTimeMs == C.TIME_UNSET ? C.TIME_UNSET : mediaTimeOffsetMs + mediaTimeMs; + public AdaptiveMediaSourceEventListener.EventDispatcher copyWithMediaTimeOffsetMs( + long mediaTimeOffsetMs) { + return new AdaptiveMediaSourceEventListener.EventDispatcher( + handler, listener, mediaTimeOffsetMs); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java index 900ba5bd37..c0586b3a28 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source; import android.net.Uri; import android.os.Handler; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; @@ -28,6 +29,7 @@ import com.google.android.exoplayer2.extractor.ExtractorOutput; 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.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SampleQueue.UpstreamFormatChangedListener; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.Allocator; @@ -74,11 +76,10 @@ import java.util.Arrays; private final Uri uri; private final DataSource dataSource; private final int minLoadableRetryCount; - private final Handler eventHandler; - private final ExtractorMediaSource.EventListener eventListener; + private final EventDispatcher eventDispatcher; private final Listener listener; private final Allocator allocator; - private final String customCacheKey; + @Nullable private final String customCacheKey; private final long continueLoadingCheckIntervalBytes; private final Loader loader; private final ExtractorHolder extractorHolder; @@ -117,8 +118,7 @@ import java.util.Arrays; * @param dataSource The data source to read the media. * @param extractors The extractors to use to read the data source. * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. - * @param eventHandler A handler for events. May be null if delivery of events is not required. - * @param eventListener A listener of events. May be null if delivery of events is not required. + * @param eventDispatcher A dispatcher to notify of events. * @param listener A listener to notify when information about the period changes. * @param allocator An {@link Allocator} from which to obtain media buffer allocations. * @param customCacheKey A custom key that uniquely identifies the original stream. Used for cache @@ -126,15 +126,20 @@ import java.util.Arrays; * @param continueLoadingCheckIntervalBytes The number of bytes that should be loaded between each * invocation of {@link Callback#onContinueLoadingRequested(SequenceableLoader)}. */ - public ExtractorMediaPeriod(Uri uri, DataSource dataSource, Extractor[] extractors, - int minLoadableRetryCount, Handler eventHandler, - ExtractorMediaSource.EventListener eventListener, Listener listener, - Allocator allocator, String customCacheKey, int continueLoadingCheckIntervalBytes) { + public ExtractorMediaPeriod( + Uri uri, + DataSource dataSource, + Extractor[] extractors, + int minLoadableRetryCount, + EventDispatcher eventDispatcher, + Listener listener, + Allocator allocator, + @Nullable String customCacheKey, + int continueLoadingCheckIntervalBytes) { this.uri = uri; this.dataSource = dataSource; this.minLoadableRetryCount = minLoadableRetryCount; - this.eventHandler = eventHandler; - this.eventListener = eventListener; + this.eventDispatcher = eventDispatcher; this.listener = listener; this.allocator = allocator; this.customCacheKey = customCacheKey; @@ -430,8 +435,22 @@ import java.util.Arrays; public int onLoadError(ExtractingLoadable loadable, long elapsedRealtimeMs, long loadDurationMs, IOException error) { copyLengthFromLoader(loadable); - notifyLoadError(error); - if (isLoadableExceptionFatal(error)) { + boolean isErrorFatal = isLoadableExceptionFatal(error); + eventDispatcher.loadError( + loadable.dataSpec, + C.DATA_TYPE_MEDIA, + C.TRACK_TYPE_UNKNOWN, + /* trackFormat= */ null, + C.SELECTION_REASON_UNKNOWN, + /* trackSelectionData= */ null, + /* mediaStartTimeUs= */ 0, + durationUs, + elapsedRealtimeMs, + loadDurationMs, + loadable.bytesLoaded, + error, + /* wasCanceled= */ isErrorFatal); + if (isErrorFatal) { return Loader.DONT_RETRY_FATAL; } int extractedSamplesCount = getExtractedSamplesCount(); @@ -606,17 +625,6 @@ import java.util.Arrays; return e instanceof UnrecognizedInputFormatException; } - private void notifyLoadError(final IOException error) { - if (eventHandler != null && eventListener != null) { - eventHandler.post(new Runnable() { - @Override - public void run() { - eventListener.onLoadError(error); - } - }); - } - } - private final class SampleStreamImpl implements SampleStream { private final int track; @@ -663,7 +671,9 @@ import java.util.Arrays; private boolean pendingExtractorSeek; private long seekTimeUs; + private DataSpec dataSpec; private long length; + private long bytesLoaded; public ExtractingLoadable(Uri uri, DataSource dataSource, ExtractorHolder extractorHolder, ConditionVariable loadCondition) { @@ -699,7 +709,8 @@ import java.util.Arrays; ExtractorInput input = null; try { long position = positionHolder.position; - length = dataSource.open(new DataSpec(uri, position, C.LENGTH_UNSET, customCacheKey)); + dataSpec = new DataSpec(uri, position, C.LENGTH_UNSET, customCacheKey); + length = dataSource.open(dataSpec); if (length != C.LENGTH_UNSET) { length += position; } @@ -723,6 +734,7 @@ import java.util.Arrays; result = Extractor.RESULT_CONTINUE; } else if (input != null) { positionHolder.position = input.getPosition(); + bytesLoaded = positionHolder.position - dataSpec.absoluteStreamPosition; } Util.closeQuietly(dataSource); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java index 351416df6a..4ea20e242e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java @@ -17,14 +17,18 @@ package com.google.android.exoplayer2.source; import android.net.Uri; import android.os.Handler; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorsFactory; +import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; @@ -40,10 +44,12 @@ import java.io.IOException; * Note that the built-in extractors for AAC, MPEG PS/TS and FLV streams do not support seeking. */ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPeriod.Listener { - /** * Listener of {@link ExtractorMediaSource} events. + * + * @deprecated Use {@link MediaSourceEventListener}. */ + @Deprecated public interface EventListener { /** @@ -89,8 +95,7 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe private final DataSource.Factory dataSourceFactory; private final ExtractorsFactory extractorsFactory; private final int minLoadableRetryCount; - private final Handler eventHandler; - private final EventListener eventListener; + private final EventDispatcher eventDispatcher; private final String customCacheKey; private final int continueLoadingCheckIntervalBytes; @@ -108,9 +113,9 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe private ExtractorsFactory extractorsFactory; private int minLoadableRetryCount; - private Handler eventHandler; - private EventListener eventListener; - private String customCacheKey; + @Nullable private Handler eventHandler; + @Nullable private MediaSourceEventListener eventListener; + @Nullable private String customCacheKey; private int continueLoadingCheckIntervalBytes; private boolean isBuildCalled; @@ -187,8 +192,24 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe * @param eventHandler A handler for events. * @param eventListener A listener of events. * @return This builder. + * @deprecated Use {@link #setEventListener(Handler, MediaSourceEventListener)}. */ + @Deprecated public Builder setEventListener(Handler eventHandler, EventListener eventListener) { + this.eventHandler = eventHandler; + this.eventListener = eventListener == null ? null : new EventListenerWrapper(eventListener); + return this; + } + + /** + * Sets the listener to respond to {@link ExtractorMediaSource} events and the handler to + * deliver these events. + * + * @param eventHandler A handler for events. + * @param eventListener A listener of events. + * @return This builder. + */ + public Builder setEventListener(Handler eventHandler, MediaSourceEventListener eventListener) { this.eventHandler = eventHandler; this.eventListener = eventListener; return this; @@ -270,12 +291,31 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe public ExtractorMediaSource(Uri uri, DataSource.Factory dataSourceFactory, ExtractorsFactory extractorsFactory, int minLoadableRetryCount, Handler eventHandler, EventListener eventListener, String customCacheKey, int continueLoadingCheckIntervalBytes) { + this( + uri, + dataSourceFactory, + extractorsFactory, + minLoadableRetryCount, + eventHandler, + eventListener == null ? null : new EventListenerWrapper(eventListener), + customCacheKey, + continueLoadingCheckIntervalBytes); + } + + private ExtractorMediaSource( + Uri uri, + DataSource.Factory dataSourceFactory, + ExtractorsFactory extractorsFactory, + int minLoadableRetryCount, + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener, + @Nullable String customCacheKey, + int continueLoadingCheckIntervalBytes) { this.uri = uri; this.dataSourceFactory = dataSourceFactory; this.extractorsFactory = extractorsFactory; this.minLoadableRetryCount = minLoadableRetryCount; - this.eventHandler = eventHandler; - this.eventListener = eventListener; + this.eventDispatcher = new EventDispatcher(eventHandler, eventListener); this.customCacheKey = customCacheKey; this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes; } @@ -294,9 +334,16 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe @Override public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { Assertions.checkArgument(id.periodIndex == 0); - return new ExtractorMediaPeriod(uri, dataSourceFactory.createDataSource(), - extractorsFactory.createExtractors(), minLoadableRetryCount, eventHandler, eventListener, - this, allocator, customCacheKey, continueLoadingCheckIntervalBytes); + return new ExtractorMediaPeriod( + uri, + dataSourceFactory.createDataSource(), + extractorsFactory.createExtractors(), + minLoadableRetryCount, + eventDispatcher, + this, + allocator, + customCacheKey, + continueLoadingCheckIntervalBytes); } @Override @@ -332,4 +379,94 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe new SinglePeriodTimeline(timelineDurationUs, timelineIsSeekable, false), null); } + /** + * Wraps a deprecated {@link EventListener}, invoking its callback from the equivalent callback in + * {@link MediaSourceEventListener}. + */ + private static final class EventListenerWrapper implements MediaSourceEventListener { + private final EventListener eventListener; + + public EventListenerWrapper(EventListener eventListener) { + this.eventListener = Assertions.checkNotNull(eventListener); + } + + @Override + public void onLoadStarted( + DataSpec dataSpec, + int dataType, + int trackType, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + long mediaStartTimeMs, + long mediaEndTimeMs, + long elapsedRealtimeMs) { + // Do nothing. + } + + @Override + public void onLoadCompleted( + DataSpec dataSpec, + int dataType, + int trackType, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + long mediaStartTimeMs, + long mediaEndTimeMs, + long elapsedRealtimeMs, + long loadDurationMs, + long bytesLoaded) { + // Do nothing. + } + + @Override + public void onLoadCanceled( + DataSpec dataSpec, + int dataType, + int trackType, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + long mediaStartTimeMs, + long mediaEndTimeMs, + long elapsedRealtimeMs, + long loadDurationMs, + long bytesLoaded) { + // Do nothing. + } + + @Override + public void onLoadError( + DataSpec dataSpec, + int dataType, + int trackType, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + long mediaStartTimeMs, + long mediaEndTimeMs, + long elapsedRealtimeMs, + long loadDurationMs, + long bytesLoaded, + IOException error, + boolean wasCanceled) { + eventListener.onLoadError(error); + } + + @Override + public void onUpstreamDiscarded(int trackType, long mediaStartTimeMs, long mediaEndTimeMs) { + // Do nothing. + } + + @Override + public void onDownstreamFormatChanged( + int trackType, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + long mediaTimeMs) { + // Do nothing. + } + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java new file mode 100644 index 0000000000..82e8781d70 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java @@ -0,0 +1,487 @@ +/* + * 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; + +import android.os.Handler; +import android.os.SystemClock; +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.util.Assertions; +import java.io.IOException; + +/** Interface for callbacks to be notified of {@link MediaSource} events. */ +public interface MediaSourceEventListener { + /** + * Called when a load begins. + * + * @param dataSpec Defines the data being loaded. + * @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data + * being loaded. + * @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds to + * media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise. + * @param trackFormat The format of the track to which the data belongs. Null if the data does not + * belong to a track. + * @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the + * data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise. + * @param trackSelectionData Optional data associated with the selection of the track to which the + * data belongs. Null if the data does not belong to a track. + * @param mediaStartTimeMs The start time of the media being loaded, or {@link C#TIME_UNSET} if + * the load is not for media data. + * @param mediaEndTimeMs The end time of the media being loaded, or {@link C#TIME_UNSET} if the + * load is not for media data. + * @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} when the load began. + */ + void onLoadStarted( + DataSpec dataSpec, + int dataType, + int trackType, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + long mediaStartTimeMs, + long mediaEndTimeMs, + long elapsedRealtimeMs); + + /** + * Called when a load ends. + * + * @param dataSpec Defines the data being loaded. + * @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data + * being loaded. + * @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds to + * media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise. + * @param trackFormat The format of the track to which the data belongs. Null if the data does not + * belong to a track. + * @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the + * data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise. + * @param trackSelectionData Optional data associated with the selection of the track to which the + * data belongs. Null if the data does not belong to a track. + * @param mediaStartTimeMs The start time of the media being loaded, or {@link C#TIME_UNSET} if + * the load is not for media data. + * @param mediaEndTimeMs The end time of the media being loaded, or {@link C#TIME_UNSET} if the + * load is not for media data. + * @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} when the load ended. + * @param loadDurationMs The duration of the load. + * @param bytesLoaded The number of bytes that were loaded. + */ + void onLoadCompleted( + DataSpec dataSpec, + int dataType, + int trackType, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + long mediaStartTimeMs, + long mediaEndTimeMs, + long elapsedRealtimeMs, + long loadDurationMs, + long bytesLoaded); + + /** + * Called when a load is canceled. + * + * @param dataSpec Defines the data being loaded. + * @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data + * being loaded. + * @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds to + * media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise. + * @param trackFormat The format of the track to which the data belongs. Null if the data does not + * belong to a track. + * @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the + * data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise. + * @param trackSelectionData Optional data associated with the selection of the track to which the + * data belongs. Null if the data does not belong to a track. + * @param mediaStartTimeMs The start time of the media being loaded, or {@link C#TIME_UNSET} if + * the load is not for media data. + * @param mediaEndTimeMs The end time of the media being loaded, or {@link C#TIME_UNSET} if the + * load is not for media data. + * @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} when the load was + * canceled. + * @param loadDurationMs The duration of the load up to the point at which it was canceled. + * @param bytesLoaded The number of bytes that were loaded prior to cancelation. + */ + void onLoadCanceled( + DataSpec dataSpec, + int dataType, + int trackType, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + long mediaStartTimeMs, + long mediaEndTimeMs, + long elapsedRealtimeMs, + long loadDurationMs, + long bytesLoaded); + + /** + * Called when a load error occurs. + * + *

          The error may or may not have resulted in the load being canceled, as indicated by the + * {@code wasCanceled} parameter. If the load was canceled, {@link #onLoadCanceled} will + * not be called in addition to this method. + * + *

          This method being called does not indicate that playback has failed, or that it will fail. + * The player may be able to recover from the error and continue. Hence applications should + * not implement this method to display a user visible error or initiate an application + * level retry ({@link Player.EventListener#onPlayerError} is the appropriate place to implement + * such behavior). This method is called to provide the application with an opportunity to log the + * error if it wishes to do so. + * + * @param dataSpec Defines the data being loaded. + * @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data + * being loaded. + * @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds to + * media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise. + * @param trackFormat The format of the track to which the data belongs. Null if the data does not + * belong to a track. + * @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the + * data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise. + * @param trackSelectionData Optional data associated with the selection of the track to which the + * data belongs. Null if the data does not belong to a track. + * @param mediaStartTimeMs The start time of the media being loaded, or {@link C#TIME_UNSET} if + * the load is not for media data. + * @param mediaEndTimeMs The end time of the media being loaded, or {@link C#TIME_UNSET} if the + * load is not for media data. + * @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} when the error + * occurred. + * @param loadDurationMs The duration of the load up to the point at which the error occurred. + * @param bytesLoaded The number of bytes that were loaded prior to the error. + * @param error The load error. + * @param wasCanceled Whether the load was canceled as a result of the error. + */ + void onLoadError( + DataSpec dataSpec, + int dataType, + int trackType, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + long mediaStartTimeMs, + long mediaEndTimeMs, + long elapsedRealtimeMs, + long loadDurationMs, + long bytesLoaded, + IOException error, + boolean wasCanceled); + + /** + * Called when data is removed from the back of a media buffer, typically so that it can be + * re-buffered in a different format. + * + * @param trackType The type of the media. One of the {@link C} {@code TRACK_TYPE_*} constants. + * @param mediaStartTimeMs The start time of the media being discarded. + * @param mediaEndTimeMs The end time of the media being discarded. + */ + void onUpstreamDiscarded(int trackType, long mediaStartTimeMs, long mediaEndTimeMs); + + /** + * Called when a downstream format change occurs (i.e. when the format of the media being read + * from one or more {@link SampleStream}s provided by the source changes). + * + * @param trackType The type of the media. One of the {@link C} {@code TRACK_TYPE_*} constants. + * @param trackFormat The format of the track to which the data belongs. Null if the data does not + * belong to a track. + * @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the + * data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise. + * @param trackSelectionData Optional data associated with the selection of the track to which the + * data belongs. Null if the data does not belong to a track. + * @param mediaTimeMs The media time at which the change occurred. + */ + void onDownstreamFormatChanged( + int trackType, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + long mediaTimeMs); + + /** Dispatches events to a {@link MediaSourceEventListener}. */ + class EventDispatcher { + + @Nullable private final Handler handler; + @Nullable private final MediaSourceEventListener listener; + private final long mediaTimeOffsetMs; + + public EventDispatcher(@Nullable Handler handler, @Nullable MediaSourceEventListener listener) { + this(handler, listener, 0); + } + + public EventDispatcher( + @Nullable Handler handler, + @Nullable MediaSourceEventListener listener, + long mediaTimeOffsetMs) { + this.handler = listener != null ? Assertions.checkNotNull(handler) : null; + this.listener = listener; + this.mediaTimeOffsetMs = mediaTimeOffsetMs; + } + + public void loadStarted(DataSpec dataSpec, int dataType, long elapsedRealtimeMs) { + loadStarted( + dataSpec, + dataType, + C.TRACK_TYPE_UNKNOWN, + null, + C.SELECTION_REASON_UNKNOWN, + null, + C.TIME_UNSET, + C.TIME_UNSET, + elapsedRealtimeMs); + } + + public void loadStarted( + final DataSpec dataSpec, + final int dataType, + final int trackType, + final Format trackFormat, + final int trackSelectionReason, + final Object trackSelectionData, + final long mediaStartTimeUs, + final long mediaEndTimeUs, + final long elapsedRealtimeMs) { + if (listener != null && handler != null) { + handler.post( + new Runnable() { + @Override + public void run() { + listener.onLoadStarted( + dataSpec, + dataType, + trackType, + trackFormat, + trackSelectionReason, + trackSelectionData, + adjustMediaTime(mediaStartTimeUs), + adjustMediaTime(mediaEndTimeUs), + elapsedRealtimeMs); + } + }); + } + } + + public void loadCompleted( + DataSpec dataSpec, + int dataType, + long elapsedRealtimeMs, + long loadDurationMs, + long bytesLoaded) { + loadCompleted( + dataSpec, + dataType, + C.TRACK_TYPE_UNKNOWN, + null, + C.SELECTION_REASON_UNKNOWN, + null, + C.TIME_UNSET, + C.TIME_UNSET, + elapsedRealtimeMs, + loadDurationMs, + bytesLoaded); + } + + public void loadCompleted( + final DataSpec dataSpec, + final int dataType, + final int trackType, + final Format trackFormat, + final int trackSelectionReason, + final Object trackSelectionData, + final long mediaStartTimeUs, + final long mediaEndTimeUs, + final long elapsedRealtimeMs, + final long loadDurationMs, + final long bytesLoaded) { + if (listener != null && handler != null) { + handler.post( + new Runnable() { + @Override + public void run() { + listener.onLoadCompleted( + dataSpec, + dataType, + trackType, + trackFormat, + trackSelectionReason, + trackSelectionData, + adjustMediaTime(mediaStartTimeUs), + adjustMediaTime(mediaEndTimeUs), + elapsedRealtimeMs, + loadDurationMs, + bytesLoaded); + } + }); + } + } + + public void loadCanceled( + DataSpec dataSpec, + int dataType, + long elapsedRealtimeMs, + long loadDurationMs, + long bytesLoaded) { + loadCanceled( + dataSpec, + dataType, + C.TRACK_TYPE_UNKNOWN, + null, + C.SELECTION_REASON_UNKNOWN, + null, + C.TIME_UNSET, + C.TIME_UNSET, + elapsedRealtimeMs, + loadDurationMs, + bytesLoaded); + } + + public void loadCanceled( + final DataSpec dataSpec, + final int dataType, + final int trackType, + final Format trackFormat, + final int trackSelectionReason, + final Object trackSelectionData, + final long mediaStartTimeUs, + final long mediaEndTimeUs, + final long elapsedRealtimeMs, + final long loadDurationMs, + final long bytesLoaded) { + if (listener != null && handler != null) { + handler.post( + new Runnable() { + @Override + public void run() { + listener.onLoadCanceled( + dataSpec, + dataType, + trackType, + trackFormat, + trackSelectionReason, + trackSelectionData, + adjustMediaTime(mediaStartTimeUs), + adjustMediaTime(mediaEndTimeUs), + elapsedRealtimeMs, + loadDurationMs, + bytesLoaded); + } + }); + } + } + + public void loadError( + DataSpec dataSpec, + int dataType, + long elapsedRealtimeMs, + long loadDurationMs, + long bytesLoaded, + IOException error, + boolean wasCanceled) { + loadError( + dataSpec, + dataType, + C.TRACK_TYPE_UNKNOWN, + null, + C.SELECTION_REASON_UNKNOWN, + null, + C.TIME_UNSET, + C.TIME_UNSET, + elapsedRealtimeMs, + loadDurationMs, + bytesLoaded, + error, + wasCanceled); + } + + public void loadError( + final DataSpec dataSpec, + final int dataType, + final int trackType, + final Format trackFormat, + final int trackSelectionReason, + final Object trackSelectionData, + final long mediaStartTimeUs, + final long mediaEndTimeUs, + final long elapsedRealtimeMs, + final long loadDurationMs, + final long bytesLoaded, + final IOException error, + final boolean wasCanceled) { + if (listener != null && handler != null) { + handler.post( + new Runnable() { + @Override + public void run() { + listener.onLoadError( + dataSpec, + dataType, + trackType, + trackFormat, + trackSelectionReason, + trackSelectionData, + adjustMediaTime(mediaStartTimeUs), + adjustMediaTime(mediaEndTimeUs), + elapsedRealtimeMs, + loadDurationMs, + bytesLoaded, + error, + wasCanceled); + } + }); + } + } + + public void upstreamDiscarded( + final int trackType, final long mediaStartTimeUs, final long mediaEndTimeUs) { + if (listener != null && handler != null) { + handler.post( + new Runnable() { + @Override + public void run() { + listener.onUpstreamDiscarded( + trackType, adjustMediaTime(mediaStartTimeUs), adjustMediaTime(mediaEndTimeUs)); + } + }); + } + } + + public void downstreamFormatChanged( + final int trackType, + final Format trackFormat, + final int trackSelectionReason, + final Object trackSelectionData, + final long mediaTimeUs) { + if (listener != null && handler != null) { + handler.post( + new Runnable() { + @Override + public void run() { + listener.onDownstreamFormatChanged( + trackType, + trackFormat, + trackSelectionReason, + trackSelectionData, + adjustMediaTime(mediaTimeUs)); + } + }); + } + } + + private long adjustMediaTime(long mediaTimeUs) { + long mediaTimeMs = C.usToMs(mediaTimeUs); + return mediaTimeMs == C.TIME_UNSET ? C.TIME_UNSET : mediaTimeOffsetMs + mediaTimeMs; + } + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java index 47a2540c38..54a8fd96ae 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java @@ -26,9 +26,9 @@ import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.DeferredMediaPeriod; import com.google.android.exoplayer2.source.ExtractorMediaSource; -import com.google.android.exoplayer2.source.ExtractorMediaSource.EventListener; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.util.Assertions; @@ -44,10 +44,8 @@ import java.util.Map; */ public final class AdsMediaSource implements MediaSource { - /** - * Listener for events relating to ad loading. - */ - public interface AdsListener { + /** Listener for ads media source events. */ + public interface EventListener extends MediaSourceEventListener { /** * Called if there was an error loading ads. The media source will load the content without ads @@ -75,15 +73,13 @@ public final class AdsMediaSource implements MediaSource { private final MediaSource contentMediaSource; private final AdsLoader adsLoader; private final ViewGroup adUiViewGroup; + @Nullable private final Handler eventHandler; + @Nullable private final EventListener eventListener; private final Handler mainHandler; private final ComponentListener componentListener; private final AdMediaSourceFactory adMediaSourceFactory; private final Map> deferredMediaPeriodByAdMediaSource; private final Timeline.Period period; - @Nullable - private final Handler eventHandler; - @Nullable - private final AdsListener eventListener; private Handler playerHandler; private ExoPlayer player; @@ -115,10 +111,10 @@ public final class AdsMediaSource implements MediaSource { } /** - * Constructs a new source that inserts ads linearly with the content specified by - * {@code contentMediaSource}. - *

          - * Ad media is loaded using {@link ExtractorMediaSource}. If {@code eventListener} is + * Constructs a new source that inserts ads linearly with the content specified by {@code + * contentMediaSource}. + * + *

          Ad media is loaded using {@link ExtractorMediaSource}. If {@code eventListener} is * non-{@code null} it will be notified of both ad tag and ad media load errors. * * @param contentMediaSource The {@link MediaSource} providing the content to play. @@ -128,9 +124,13 @@ public final class AdsMediaSource implements MediaSource { * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. */ - public AdsMediaSource(MediaSource contentMediaSource, DataSource.Factory dataSourceFactory, - AdsLoader adsLoader, ViewGroup adUiViewGroup, @Nullable Handler eventHandler, - @Nullable AdsListener eventListener) { + public AdsMediaSource( + MediaSource contentMediaSource, + DataSource.Factory dataSourceFactory, + AdsLoader adsLoader, + ViewGroup adUiViewGroup, + @Nullable Handler eventHandler, + @Nullable EventListener eventListener) { this.contentMediaSource = contentMediaSource; this.adsLoader = adsLoader; this.adUiViewGroup = adUiViewGroup; @@ -186,7 +186,7 @@ public final class AdsMediaSource implements MediaSource { if (adGroupMediaSources[adGroupIndex].length <= adIndexInAdGroup) { Uri adUri = adPlaybackState.adUris[id.adGroupIndex][id.adIndexInAdGroup]; final MediaSource adMediaSource = - adMediaSourceFactory.createAdMediaSource(adUri, mainHandler, componentListener); + adMediaSourceFactory.createAdMediaSource(adUri, eventHandler, eventListener); int oldAdCount = adGroupMediaSources[id.adGroupIndex].length; if (adIndexInAdGroup >= oldAdCount) { int adCount = adIndexInAdGroup + 1; @@ -306,11 +306,8 @@ public final class AdsMediaSource implements MediaSource { } } - /** - * Listener for component events. All methods are called on the main thread. - */ - private final class ComponentListener implements AdsLoader.EventListener, - AdMediaSourceLoadErrorListener { + /** Listener for component events. All methods are called on the main thread. */ + private final class ComponentListener implements AdsLoader.EventListener { @Override public void onAdPlaybackState(final AdPlaybackState adPlaybackState) { @@ -374,20 +371,6 @@ public final class AdsMediaSource implements MediaSource { } - /** - * Listener for errors while loading an ad {@link MediaSource}. - */ - private interface AdMediaSourceLoadErrorListener { - - /** - * Called when an error occurs loading media data. - * - * @param error The load error. - */ - void onLoadError(IOException error); - - } - /** * Factory for {@link MediaSource}s for loading ad media. */ @@ -397,15 +380,13 @@ public final class AdsMediaSource implements MediaSource { * Creates a new {@link MediaSource} for loading the ad media with the specified {@code uri}. * * @param uri The URI of the ad. - * @param handler A handler for listener events. - * @param listener A listener for ad load errors. To have ad media source load errors notified - * via the ads media source's listener, call this listener's onLoadError method from your - * new media source's load error listener using the specified {@code handler}. Otherwise, - * this parameter can be ignored. + * @param handler A handler for listener events. May be null if delivery of events is not + * required. + * @param listener A listener for events. May be null if delivery of events is not required. * @return The new media source. */ - MediaSource createAdMediaSource(Uri uri, Handler handler, - AdMediaSourceLoadErrorListener listener); + MediaSource createAdMediaSource( + Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener); /** * Returns the content types supported by media sources created by this factory. Each element @@ -427,15 +408,11 @@ public final class AdsMediaSource implements MediaSource { } @Override - public MediaSource createAdMediaSource(Uri uri, Handler handler, - final AdMediaSourceLoadErrorListener listener) { - return new ExtractorMediaSource.Builder(uri, dataSourceFactory).setEventListener(handler, - new EventListener() { - @Override - public void onLoadError(IOException error) { - listener.onLoadError(error); - } - }).build(); + public MediaSource createAdMediaSource( + Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener) { + return new ExtractorMediaSource.Builder(uri, dataSourceFactory) + .setEventListener(handler, listener) + .build(); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java index ab1542c7a6..cbe971bc5d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.upstream; import android.net.Uri; import android.support.annotation.IntDef; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Assertions; import java.lang.annotation.Retention; @@ -79,7 +80,7 @@ public final class DataSpec { * A key that uniquely identifies the original stream. Used for cache indexing. May be null if the * {@link DataSpec} is not intended to be used in conjunction with a cache. */ - public final String key; + @Nullable public final String key; /** * Request flags. Currently {@link #FLAG_ALLOW_GZIP} and * {@link #FLAG_ALLOW_CACHING_UNKNOWN_LENGTH} are the only supported flags. @@ -113,7 +114,7 @@ public final class DataSpec { * @param length {@link #length}. * @param key {@link #key}. */ - public DataSpec(Uri uri, long absoluteStreamPosition, long length, String key) { + public DataSpec(Uri uri, long absoluteStreamPosition, long length, @Nullable String key) { this(uri, absoluteStreamPosition, absoluteStreamPosition, length, key, 0); } @@ -147,8 +148,8 @@ public final class DataSpec { } /** - * Construct a {@link DataSpec} where {@link #position} may differ from - * {@link #absoluteStreamPosition}. + * Construct a {@link DataSpec} where {@link #position} may differ from {@link + * #absoluteStreamPosition}. * * @param uri {@link #uri}. * @param postBody {@link #postBody}. @@ -158,8 +159,14 @@ public final class DataSpec { * @param key {@link #key}. * @param flags {@link #flags}. */ - public DataSpec(Uri uri, byte[] postBody, long absoluteStreamPosition, long position, long length, - String key, @Flags int flags) { + public DataSpec( + Uri uri, + byte[] postBody, + long absoluteStreamPosition, + long position, + long length, + @Nullable String key, + @Flags int flags) { Assertions.checkArgument(absoluteStreamPosition >= 0); Assertions.checkArgument(position >= 0); Assertions.checkArgument(length > 0 || length == C.LENGTH_UNSET); From fd938fb4545dddb9016c80cb126f4fa6f9d51d43 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 4 Dec 2017 02:06:42 -0800 Subject: [PATCH 0824/2472] Update internal usages of deprecated AdaptiveMediaSourceEventListener ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177786580 --- .../android/exoplayer2/demo/EventLogger.java | 21 +++-- .../AdaptiveMediaSourceEventListener.java | 36 +-------- .../source/ExtractorMediaSource.java | 2 +- .../source/MediaSourceEventListener.java | 6 +- .../source/chunk/ChunkSampleStream.java | 3 +- .../source/dash/DashMediaPeriod.java | 2 +- .../source/dash/DashMediaSource.java | 80 ++++++++++++------- .../exoplayer2/source/hls/HlsMediaPeriod.java | 2 +- .../exoplayer2/source/hls/HlsMediaSource.java | 64 +++++++++------ .../source/hls/HlsSampleStreamWrapper.java | 2 +- .../hls/playlist/HlsPlaylistTracker.java | 2 +- .../source/smoothstreaming/SsMediaPeriod.java | 2 +- .../source/smoothstreaming/SsMediaSource.java | 68 ++++++++++------ .../testutil/FakeAdaptiveMediaPeriod.java | 2 +- .../testutil/FakeAdaptiveMediaSource.java | 14 ++-- 15 files changed, 170 insertions(+), 136 deletions(-) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java index 0635944640..fa22130eea 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java @@ -38,7 +38,7 @@ import com.google.android.exoplayer2.metadata.id3.Id3Frame; import com.google.android.exoplayer2.metadata.id3.PrivFrame; import com.google.android.exoplayer2.metadata.id3.TextInformationFrame; import com.google.android.exoplayer2.metadata.id3.UrlLinkFrame; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener; +import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.ads.AdsMediaSource; @@ -58,7 +58,7 @@ import java.util.Locale; MetadataOutput, AudioRendererEventListener, VideoRendererEventListener, - AdaptiveMediaSourceEventListener, + MediaSourceEventListener, AdsMediaSource.EventListener, DefaultDrmSessionManager.EventListener { @@ -325,12 +325,19 @@ import java.util.Locale; Log.d(TAG, "drmKeysLoaded [" + getSessionTimeString() + "]"); } - // AdaptiveMediaSourceEventListener + // MediaSourceEventListener @Override - public void onLoadStarted(DataSpec dataSpec, int dataType, int trackType, Format trackFormat, - int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs, - long mediaEndTimeMs, long elapsedRealtimeMs) { + public void onLoadStarted( + DataSpec dataSpec, + int dataType, + int trackType, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + long mediaStartTimeMs, + long mediaEndTimeMs, + long elapsedRealtimeMs) { // Do nothing. } @@ -371,7 +378,7 @@ import java.util.Locale; @Override public void onAdLoadError(IOException error) { - printInternalError("loadError", error); + printInternalError("adLoadError", error); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/AdaptiveMediaSourceEventListener.java b/library/core/src/main/java/com/google/android/exoplayer2/source/AdaptiveMediaSourceEventListener.java index 2bc9d48726..ccc3beac55 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/AdaptiveMediaSourceEventListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/AdaptiveMediaSourceEventListener.java @@ -15,42 +15,10 @@ */ package com.google.android.exoplayer2.source; -import android.os.Handler; -import android.support.annotation.Nullable; - /** * Interface for callbacks to be notified of {@link MediaSource} events. * - * @deprecated Use {@link MediaSourceEventListener} + * @deprecated Use {@link MediaSourceEventListener}. */ @Deprecated -public interface AdaptiveMediaSourceEventListener extends MediaSourceEventListener { - - /** Dispatches events to a {@link MediaSourceEventListener}. */ - final class EventDispatcher extends MediaSourceEventListener.EventDispatcher { - - private final Handler handler; - private final MediaSourceEventListener listener; - - public EventDispatcher(@Nullable Handler handler, @Nullable MediaSourceEventListener listener) { - this(handler, listener, 0); - } - - public EventDispatcher( - @Nullable Handler handler, - @Nullable MediaSourceEventListener listener, - long mediaTimeOffsetMs) { - super(handler, listener, mediaTimeOffsetMs); - this.handler = handler; - this.listener = listener; - } - - public AdaptiveMediaSourceEventListener.EventDispatcher copyWithMediaTimeOffsetMs( - long mediaTimeOffsetMs) { - return new AdaptiveMediaSourceEventListener.EventDispatcher( - handler, listener, mediaTimeOffsetMs); - } - - } - -} +public interface AdaptiveMediaSourceEventListener extends MediaSourceEventListener {} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java index 4ea20e242e..b97d957ec4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java @@ -25,7 +25,7 @@ import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorsFactory; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java index 82e8781d70..4d500f94bd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java @@ -211,7 +211,7 @@ public interface MediaSourceEventListener { long mediaTimeMs); /** Dispatches events to a {@link MediaSourceEventListener}. */ - class EventDispatcher { + final class EventDispatcher { @Nullable private final Handler handler; @Nullable private final MediaSourceEventListener listener; @@ -230,6 +230,10 @@ public interface MediaSourceEventListener { this.mediaTimeOffsetMs = mediaTimeOffsetMs; } + public EventDispatcher copyWithMediaTimeOffsetMs(long mediaTimeOffsetMs) { + return new EventDispatcher(handler, listener, mediaTimeOffsetMs); + } + public void loadStarted(DataSpec dataSpec, int dataType, long elapsedRealtimeMs) { loadStarted( dataSpec, diff --git a/library/core/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 index e352ba551e..20b56e7807 100644 --- a/library/core/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 @@ -16,12 +16,11 @@ package com.google.android.exoplayer2.source.chunk; import android.util.Log; - import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SampleQueue; import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.source.SequenceableLoader; diff --git a/library/dash/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 index 70fba4dd00..8fe10e94ee 100644 --- a/library/dash/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 @@ -20,10 +20,10 @@ import android.util.Pair; import android.util.SparseIntArray; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.EmptySampleStream; import com.google.android.exoplayer2.source.MediaPeriod; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.source.SequenceableLoader; import com.google.android.exoplayer2.source.TrackGroup; diff --git a/library/dash/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 index 2562b27237..11f2c68698 100644 --- a/library/dash/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 @@ -26,12 +26,12 @@ import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.DefaultCompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSourceEventListener; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SequenceableLoader; import com.google.android.exoplayer2.source.dash.manifest.DashManifest; import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser; @@ -72,7 +72,7 @@ public final class DashMediaSource implements MediaSource { private final DashChunkSource.Factory chunkSourceFactory; private ParsingLoadable.Parser manifestParser; - private AdaptiveMediaSourceEventListener eventListener; + private MediaSourceEventListener eventListener; private Handler eventHandler; private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; @@ -155,8 +155,7 @@ public final class DashMediaSource implements MediaSource { * @param eventListener A listener of events. * @return This builder. */ - public Builder setEventListener(Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { + public Builder setEventListener(Handler eventHandler, MediaSourceEventListener eventListener) { this.eventHandler = eventHandler; this.eventListener = eventListener; return this; @@ -284,8 +283,11 @@ public final class DashMediaSource implements MediaSource { * @deprecated Use {@link Builder} instead. */ @Deprecated - public DashMediaSource(DashManifest manifest, DashChunkSource.Factory chunkSourceFactory, - Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { + public DashMediaSource( + DashManifest manifest, + DashChunkSource.Factory chunkSourceFactory, + Handler eventHandler, + MediaSourceEventListener eventListener) { this(manifest, chunkSourceFactory, DEFAULT_MIN_LOADABLE_RETRY_COUNT, eventHandler, eventListener); } @@ -301,9 +303,12 @@ public final class DashMediaSource implements MediaSource { * @deprecated Use {@link Builder} instead. */ @Deprecated - public DashMediaSource(DashManifest manifest, DashChunkSource.Factory chunkSourceFactory, - int minLoadableRetryCount, Handler eventHandler, AdaptiveMediaSourceEventListener - eventListener) { + public DashMediaSource( + DashManifest manifest, + DashChunkSource.Factory chunkSourceFactory, + int minLoadableRetryCount, + Handler eventHandler, + MediaSourceEventListener eventListener) { this(manifest, null, null, null, chunkSourceFactory, new DefaultCompositeSequenceableLoaderFactory(), minLoadableRetryCount, DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS, eventHandler, eventListener); @@ -322,9 +327,12 @@ public final class DashMediaSource implements MediaSource { * @deprecated Use {@link Builder} instead. */ @Deprecated - public DashMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, - DashChunkSource.Factory chunkSourceFactory, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { + public DashMediaSource( + Uri manifestUri, + DataSource.Factory manifestDataSourceFactory, + DashChunkSource.Factory chunkSourceFactory, + Handler eventHandler, + MediaSourceEventListener eventListener) { this(manifestUri, manifestDataSourceFactory, chunkSourceFactory, DEFAULT_MIN_LOADABLE_RETRY_COUNT, DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS, eventHandler, eventListener); @@ -340,18 +348,22 @@ public final class DashMediaSource implements MediaSource { * @param chunkSourceFactory A factory for {@link DashChunkSource} instances. * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. * @param livePresentationDelayMs For live playbacks, the duration in milliseconds by which the - * default start position should precede the end of the live window. Use - * {@link #DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS} to use the value specified by - * the manifest, if present. + * default start position should precede the end of the live window. Use {@link + * #DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS} to use the value specified by the + * manifest, if present. * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. * @deprecated Use {@link Builder} instead. */ @Deprecated - public DashMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, - DashChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, - long livePresentationDelayMs, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { + public DashMediaSource( + Uri manifestUri, + DataSource.Factory manifestDataSourceFactory, + DashChunkSource.Factory chunkSourceFactory, + int minLoadableRetryCount, + long livePresentationDelayMs, + Handler eventHandler, + MediaSourceEventListener eventListener) { this(manifestUri, manifestDataSourceFactory, new DashManifestParser(), chunkSourceFactory, minLoadableRetryCount, livePresentationDelayMs, eventHandler, eventListener); } @@ -367,31 +379,39 @@ public final class DashMediaSource implements MediaSource { * @param chunkSourceFactory A factory for {@link DashChunkSource} instances. * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. * @param livePresentationDelayMs For live playbacks, the duration in milliseconds by which the - * default start position should precede the end of the live window. Use - * {@link #DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS} to use the value specified by - * the manifest, if present. + * default start position should precede the end of the live window. Use {@link + * #DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS} to use the value specified by the + * manifest, if present. * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. * @deprecated Use {@link Builder} instead. */ @Deprecated - public DashMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, + public DashMediaSource( + Uri manifestUri, + DataSource.Factory manifestDataSourceFactory, ParsingLoadable.Parser manifestParser, - DashChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, - long livePresentationDelayMs, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { + DashChunkSource.Factory chunkSourceFactory, + int minLoadableRetryCount, + long livePresentationDelayMs, + Handler eventHandler, + MediaSourceEventListener eventListener) { this(null, manifestUri, manifestDataSourceFactory, manifestParser, chunkSourceFactory, new DefaultCompositeSequenceableLoaderFactory(), minLoadableRetryCount, livePresentationDelayMs, eventHandler, eventListener); } - private DashMediaSource(DashManifest manifest, Uri manifestUri, + private DashMediaSource( + DashManifest manifest, + Uri manifestUri, DataSource.Factory manifestDataSourceFactory, ParsingLoadable.Parser manifestParser, DashChunkSource.Factory chunkSourceFactory, CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, - int minLoadableRetryCount, long livePresentationDelayMs, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { + int minLoadableRetryCount, + long livePresentationDelayMs, + Handler eventHandler, + MediaSourceEventListener eventListener) { this.manifest = manifest; this.manifestUri = manifestUri; this.manifestDataSourceFactory = manifestDataSourceFactory; diff --git a/library/hls/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 index b6c74d61bb..dd596878d2 100644 --- a/library/hls/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 @@ -19,9 +19,9 @@ import android.os.Handler; import android.text.TextUtils; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.MediaPeriod; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.source.SequenceableLoader; import com.google.android.exoplayer2.source.TrackGroup; diff --git a/library/hls/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 index a412b8c3e9..4e5783698a 100644 --- a/library/hls/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 @@ -21,12 +21,12 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.extractor.Extractor; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.DefaultCompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSourceEventListener; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SequenceableLoader; import com.google.android.exoplayer2.source.SinglePeriodTimeline; import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; @@ -60,7 +60,7 @@ public final class HlsMediaSource implements MediaSource, private HlsExtractorFactory extractorFactory; private ParsingLoadable.Parser playlistParser; - private AdaptiveMediaSourceEventListener eventListener; + private MediaSourceEventListener eventListener; private Handler eventHandler; private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; @@ -136,8 +136,7 @@ public final class HlsMediaSource implements MediaSource, * @param eventListener A listener of events. * @return This builder. */ - public Builder setEventListener(Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { + public Builder setEventListener(Handler eventHandler, MediaSourceEventListener eventListener) { this.eventHandler = eventHandler; this.eventListener = eventListener; return this; @@ -219,13 +218,16 @@ public final class HlsMediaSource implements MediaSource, * @param dataSourceFactory An {@link HlsDataSourceFactory} for {@link DataSource}s for manifests, * segments and keys. * @param eventHandler A handler for events. May be null if delivery of events is not required. - * @param eventListener An {@link AdaptiveMediaSourceEventListener}. May be null if delivery of - * events is not required. + * @param eventListener A {@link MediaSourceEventListener}. May be null if delivery of events is + * not required. * @deprecated Use {@link Builder} instead. */ @Deprecated - public HlsMediaSource(Uri manifestUri, DataSource.Factory dataSourceFactory, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { + public HlsMediaSource( + Uri manifestUri, + DataSource.Factory dataSourceFactory, + Handler eventHandler, + MediaSourceEventListener eventListener) { this(manifestUri, dataSourceFactory, DEFAULT_MIN_LOADABLE_RETRY_COUNT, eventHandler, eventListener); } @@ -234,17 +236,20 @@ public final class HlsMediaSource implements MediaSource, * @param manifestUri The {@link Uri} of the HLS manifest. * @param dataSourceFactory An {@link HlsDataSourceFactory} for {@link DataSource}s for manifests, * segments and keys. - * @param minLoadableRetryCount The minimum number of times loads must be retried before - * errors are propagated. + * @param minLoadableRetryCount The minimum number of times loads must be retried before errors + * are propagated. * @param eventHandler A handler for events. May be null if delivery of events is not required. - * @param eventListener An {@link AdaptiveMediaSourceEventListener}. May be null if delivery of - * events is not required. + * @param eventListener A {@link MediaSourceEventListener}. May be null if delivery of events is + * not required. * @deprecated Use {@link Builder} instead. */ @Deprecated - public HlsMediaSource(Uri manifestUri, DataSource.Factory dataSourceFactory, - int minLoadableRetryCount, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { + public HlsMediaSource( + Uri manifestUri, + DataSource.Factory dataSourceFactory, + int minLoadableRetryCount, + Handler eventHandler, + MediaSourceEventListener eventListener) { this(manifestUri, new DefaultHlsDataSourceFactory(dataSourceFactory), HlsExtractorFactory.DEFAULT, minLoadableRetryCount, eventHandler, eventListener, new HlsPlaylistParser()); @@ -255,29 +260,36 @@ public final class HlsMediaSource implements MediaSource, * @param dataSourceFactory An {@link HlsDataSourceFactory} for {@link DataSource}s for manifests, * segments and keys. * @param extractorFactory An {@link HlsExtractorFactory} for {@link Extractor}s for the segments. - * @param minLoadableRetryCount The minimum number of times loads must be retried before - * errors are propagated. + * @param minLoadableRetryCount The minimum number of times loads must be retried before errors + * are propagated. * @param eventHandler A handler for events. May be null if delivery of events is not required. - * @param eventListener An {@link AdaptiveMediaSourceEventListener}. May be null if delivery of - * events is not required. + * @param eventListener A {@link MediaSourceEventListener}. May be null if delivery of events is + * not required. * @param playlistParser A {@link ParsingLoadable.Parser} for HLS playlists. * @deprecated Use {@link Builder} instead. */ @Deprecated - public HlsMediaSource(Uri manifestUri, HlsDataSourceFactory dataSourceFactory, - HlsExtractorFactory extractorFactory, int minLoadableRetryCount, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener, + public HlsMediaSource( + Uri manifestUri, + HlsDataSourceFactory dataSourceFactory, + HlsExtractorFactory extractorFactory, + int minLoadableRetryCount, + Handler eventHandler, + MediaSourceEventListener eventListener, ParsingLoadable.Parser playlistParser) { this(manifestUri, dataSourceFactory, extractorFactory, new DefaultCompositeSequenceableLoaderFactory(), minLoadableRetryCount, eventHandler, eventListener, playlistParser); } - private HlsMediaSource(Uri manifestUri, HlsDataSourceFactory dataSourceFactory, + private HlsMediaSource( + Uri manifestUri, + HlsDataSourceFactory dataSourceFactory, HlsExtractorFactory extractorFactory, CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, - int minLoadableRetryCount, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener, + int minLoadableRetryCount, + Handler eventHandler, + MediaSourceEventListener eventListener, ParsingLoadable.Parser playlistParser) { this.manifestUri = manifestUri; this.dataSourceFactory = dataSourceFactory; diff --git a/library/hls/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 index 8f063a38f1..f4ba9a6eac 100644 --- a/library/hls/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 @@ -25,7 +25,7 @@ import com.google.android.exoplayer2.extractor.DummyTrackOutput; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.TrackOutput; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SampleQueue; import com.google.android.exoplayer2.source.SampleQueue.UpstreamFormatChangedListener; import com.google.android.exoplayer2.source.SampleStream; diff --git a/library/hls/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 index 355a8575ca..0677ff7ca0 100644 --- a/library/hls/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 @@ -20,7 +20,7 @@ import android.os.Handler; import android.os.SystemClock; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.chunk.ChunkedTrackBlacklistUtil; import com.google.android.exoplayer2.source.hls.HlsDataSourceFactory; import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl; diff --git a/library/smoothstreaming/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 index c079a36d62..d418a21dff 100644 --- a/library/smoothstreaming/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 @@ -18,9 +18,9 @@ package com.google.android.exoplayer2.source.smoothstreaming; import android.util.Base64; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.mp4.TrackEncryptionBox; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.MediaPeriod; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.source.SequenceableLoader; import com.google.android.exoplayer2.source.TrackGroup; diff --git a/library/smoothstreaming/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 index a4b601aafe..10772ba36c 100644 --- a/library/smoothstreaming/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 @@ -24,12 +24,12 @@ import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.DefaultCompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSourceEventListener; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SequenceableLoader; import com.google.android.exoplayer2.source.SinglePeriodTimeline; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest; @@ -66,7 +66,7 @@ public final class SsMediaSource implements MediaSource, private final SsChunkSource.Factory chunkSourceFactory; private ParsingLoadable.Parser manifestParser; - private AdaptiveMediaSourceEventListener eventListener; + private MediaSourceEventListener eventListener; private Handler eventHandler; private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; @@ -147,8 +147,7 @@ public final class SsMediaSource implements MediaSource, * @param eventListener A listener of events. * @return This builder. */ - public Builder setEventListener(Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { + public Builder setEventListener(Handler eventHandler, MediaSourceEventListener eventListener) { this.eventHandler = eventHandler; this.eventListener = eventListener; return this; @@ -256,8 +255,11 @@ public final class SsMediaSource implements MediaSource, * @deprecated Use {@link Builder} instead. */ @Deprecated - public SsMediaSource(SsManifest manifest, SsChunkSource.Factory chunkSourceFactory, - Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { + public SsMediaSource( + SsManifest manifest, + SsChunkSource.Factory chunkSourceFactory, + Handler eventHandler, + MediaSourceEventListener eventListener) { this(manifest, chunkSourceFactory, DEFAULT_MIN_LOADABLE_RETRY_COUNT, eventHandler, eventListener); } @@ -273,9 +275,12 @@ public final class SsMediaSource implements MediaSource, * @deprecated Use {@link Builder} instead. */ @Deprecated - public SsMediaSource(SsManifest manifest, SsChunkSource.Factory chunkSourceFactory, - int minLoadableRetryCount, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { + public SsMediaSource( + SsManifest manifest, + SsChunkSource.Factory chunkSourceFactory, + int minLoadableRetryCount, + Handler eventHandler, + MediaSourceEventListener eventListener) { this(manifest, null, null, null, chunkSourceFactory, new DefaultCompositeSequenceableLoaderFactory(), minLoadableRetryCount, DEFAULT_LIVE_PRESENTATION_DELAY_MS, eventHandler, eventListener); @@ -294,9 +299,12 @@ public final class SsMediaSource implements MediaSource, * @deprecated Use {@link Builder} instead. */ @Deprecated - public SsMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, - SsChunkSource.Factory chunkSourceFactory, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { + public SsMediaSource( + Uri manifestUri, + DataSource.Factory manifestDataSourceFactory, + SsChunkSource.Factory chunkSourceFactory, + Handler eventHandler, + MediaSourceEventListener eventListener) { this(manifestUri, manifestDataSourceFactory, chunkSourceFactory, DEFAULT_MIN_LOADABLE_RETRY_COUNT, DEFAULT_LIVE_PRESENTATION_DELAY_MS, eventHandler, eventListener); @@ -318,10 +326,14 @@ public final class SsMediaSource implements MediaSource, * @deprecated Use {@link Builder} instead. */ @Deprecated - public SsMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, - SsChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, - long livePresentationDelayMs, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { + public SsMediaSource( + Uri manifestUri, + DataSource.Factory manifestDataSourceFactory, + SsChunkSource.Factory chunkSourceFactory, + int minLoadableRetryCount, + long livePresentationDelayMs, + Handler eventHandler, + MediaSourceEventListener eventListener) { this(manifestUri, manifestDataSourceFactory, new SsManifestParser(), chunkSourceFactory, minLoadableRetryCount, livePresentationDelayMs, eventHandler, eventListener); } @@ -343,23 +355,31 @@ public final class SsMediaSource implements MediaSource, * @deprecated Use {@link Builder} instead. */ @Deprecated - public SsMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, + public SsMediaSource( + Uri manifestUri, + DataSource.Factory manifestDataSourceFactory, ParsingLoadable.Parser manifestParser, - SsChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, - long livePresentationDelayMs, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { + SsChunkSource.Factory chunkSourceFactory, + int minLoadableRetryCount, + long livePresentationDelayMs, + Handler eventHandler, + MediaSourceEventListener eventListener) { this(null, manifestUri, manifestDataSourceFactory, manifestParser, chunkSourceFactory, new DefaultCompositeSequenceableLoaderFactory(), minLoadableRetryCount, livePresentationDelayMs, eventHandler, eventListener); } - private SsMediaSource(SsManifest manifest, Uri manifestUri, + private SsMediaSource( + SsManifest manifest, + Uri manifestUri, DataSource.Factory manifestDataSourceFactory, ParsingLoadable.Parser manifestParser, SsChunkSource.Factory chunkSourceFactory, CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, - int minLoadableRetryCount, long livePresentationDelayMs, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { + int minLoadableRetryCount, + long livePresentationDelayMs, + Handler eventHandler, + MediaSourceEventListener eventListener) { Assertions.checkState(manifest == null || !manifest.isLive); this.manifest = manifest; this.manifestUri = manifestUri == null ? null diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java index a4c9abb24e..ff2a9b23cd 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java @@ -15,9 +15,9 @@ */ package com.google.android.exoplayer2.testutil; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.CompositeSequenceableLoader; import com.google.android.exoplayer2.source.MediaPeriod; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.source.SequenceableLoader; import com.google.android.exoplayer2.source.TrackGroupArray; diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaSource.java index 59bcaf3e7c..fbb2a83027 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaSource.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaSource.java @@ -18,9 +18,9 @@ package com.google.android.exoplayer2.testutil; import android.os.Handler; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline.Period; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSourceEventListener; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.upstream.Allocator; @@ -33,9 +33,13 @@ public class FakeAdaptiveMediaSource extends FakeMediaSource { private final EventDispatcher eventDispatcher; private final FakeChunkSource.Factory chunkSourceFactory; - public FakeAdaptiveMediaSource(Timeline timeline, Object manifest, - TrackGroupArray trackGroupArray, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener, FakeChunkSource.Factory chunkSourceFactory) { + public FakeAdaptiveMediaSource( + Timeline timeline, + Object manifest, + TrackGroupArray trackGroupArray, + Handler eventHandler, + MediaSourceEventListener eventListener, + FakeChunkSource.Factory chunkSourceFactory) { super(timeline, manifest, trackGroupArray); this.eventDispatcher = new EventDispatcher(eventHandler, eventListener); this.chunkSourceFactory = chunkSourceFactory; From a9c3ca1cfe0fd9295a6472265a9ac71b7f8c64bd Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 4 Dec 2017 06:08:33 -0800 Subject: [PATCH 0825/2472] Tentative fix for roll-up row count Issue: #3513 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177804505 --- RELEASENOTES.md | 2 + .../exoplayer2/text/cea/Cea608Decoder.java | 40 +++++++++++++------ 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index f6adf560b9..2335f8f15e 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -29,6 +29,8 @@ seek. * Use the same listener `MediaSourceEventListener` for all MediaSource implementations. +* CEA-608: Fix handling of row count changes in roll-up mode + ([#3513](https://github.com/google/ExoPlayer/issues/3513)). ### 2.6.0 ### diff --git a/library/core/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 index e2c592be6b..0483f909b3 100644 --- a/library/core/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 @@ -33,7 +33,6 @@ import com.google.android.exoplayer2.text.SubtitleInputBuffer; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; import java.util.ArrayList; -import java.util.LinkedList; import java.util.List; /** @@ -185,7 +184,7 @@ public final class Cea608Decoder extends CeaDecoder { private final ParsableByteArray ccData; private final int packetLength; private final int selectedField; - private final LinkedList cueBuilders; + private final ArrayList cueBuilders; private CueBuilder currentCueBuilder; private List cues; @@ -200,7 +199,7 @@ public final class Cea608Decoder extends CeaDecoder { public Cea608Decoder(String mimeType, int accessibilityChannel) { ccData = new ParsableByteArray(); - cueBuilders = new LinkedList<>(); + cueBuilders = new ArrayList<>(); currentCueBuilder = new CueBuilder(CC_MODE_UNKNOWN, DEFAULT_CAPTIONS_ROW_COUNT); packetLength = MimeTypes.APPLICATION_MP4CEA608.equals(mimeType) ? 2 : 3; switch (accessibilityChannel) { @@ -230,8 +229,8 @@ public final class Cea608Decoder extends CeaDecoder { cues = null; lastCues = null; setCaptionMode(CC_MODE_UNKNOWN); + setCaptionRowCount(DEFAULT_CAPTIONS_ROW_COUNT); resetCueBuilders(); - captionRowCount = DEFAULT_CAPTIONS_ROW_COUNT; repeatableControlSet = false; repeatableControlCc1 = 0; repeatableControlCc2 = 0; @@ -434,16 +433,16 @@ public final class Cea608Decoder extends CeaDecoder { private void handleMiscCode(byte cc2) { switch (cc2) { case CTRL_ROLL_UP_CAPTIONS_2_ROWS: - captionRowCount = 2; setCaptionMode(CC_MODE_ROLL_UP); + setCaptionRowCount(2); return; case CTRL_ROLL_UP_CAPTIONS_3_ROWS: - captionRowCount = 3; setCaptionMode(CC_MODE_ROLL_UP); + setCaptionRowCount(3); return; case CTRL_ROLL_UP_CAPTIONS_4_ROWS: - captionRowCount = 4; setCaptionMode(CC_MODE_ROLL_UP); + setCaptionRowCount(4); return; case CTRL_RESUME_CAPTION_LOADING: setCaptionMode(CC_MODE_POP_ON); @@ -451,6 +450,9 @@ public final class Cea608Decoder extends CeaDecoder { case CTRL_RESUME_DIRECT_CAPTIONING: setCaptionMode(CC_MODE_PAINT_ON); return; + default: + // Fall through. + break; } if (captionMode == CC_MODE_UNKNOWN) { @@ -484,6 +486,9 @@ public final class Cea608Decoder extends CeaDecoder { case CTRL_DELETE_TO_END_OF_ROW: // TODO: implement break; + default: + // Fall through. + break; } } @@ -515,8 +520,13 @@ public final class Cea608Decoder extends CeaDecoder { } } + private void setCaptionRowCount(int captionRowCount) { + this.captionRowCount = captionRowCount; + currentCueBuilder.setCaptionRowCount(captionRowCount); + } + private void resetCueBuilders() { - currentCueBuilder.reset(captionMode, captionRowCount); + currentCueBuilder.reset(captionMode); cueBuilders.clear(); cueBuilders.add(currentCueBuilder); } @@ -594,12 +604,14 @@ public final class Cea608Decoder extends CeaDecoder { public CueBuilder(int captionMode, int captionRowCount) { preambleStyles = new ArrayList<>(); midrowStyles = new ArrayList<>(); - rolledUpCaptions = new LinkedList<>(); + rolledUpCaptions = new ArrayList<>(); captionStringBuilder = new SpannableStringBuilder(); - reset(captionMode, captionRowCount); + reset(captionMode); + setCaptionRowCount(captionRowCount); } - public void reset(int captionMode, int captionRowCount) { + public void reset(int captionMode) { + this.captionMode = captionMode; preambleStyles.clear(); midrowStyles.clear(); rolledUpCaptions.clear(); @@ -607,11 +619,13 @@ public final class Cea608Decoder extends CeaDecoder { row = BASE_ROW; indent = 0; tabOffset = 0; - this.captionMode = captionMode; - this.captionRowCount = captionRowCount; underlineStartPosition = POSITION_UNSET; } + public void setCaptionRowCount(int captionRowCount) { + this.captionRowCount = captionRowCount; + } + public boolean isEmpty() { return preambleStyles.isEmpty() && midrowStyles.isEmpty() && rolledUpCaptions.isEmpty() && captionStringBuilder.length() == 0; From 9c63d377917b45ca9f37f0c41516dbf9f7fffde8 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 4 Dec 2017 06:56:04 -0800 Subject: [PATCH 0826/2472] Support timezone offsets in ISO8601 timestamps Issue: #3524 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177808106 --- RELEASENOTES.md | 2 + .../source/dash/DashMediaSource.java | 56 ++++++++++--------- 2 files changed, 32 insertions(+), 26 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 2335f8f15e..0372191683 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -11,6 +11,8 @@ `DashMediaSource`, `SingleSampleMediaSource`. * DASH: * Support in-MPD EventStream. + * Support time zone designators in ISO8601 UTCTiming elements + ([#3524](https://github.com/google/ExoPlayer/issues/3524)). * Allow a back-buffer of media to be retained behind the current playback position, for fast backward seeking. The back-buffer can be configured by custom `LoadControl` implementations. diff --git a/library/dash/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 index 11f2c68698..af1a445b9f 100644 --- a/library/dash/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 @@ -19,6 +19,7 @@ import android.net.Uri; import android.os.Handler; import android.os.SystemClock; import android.support.annotation.Nullable; +import android.text.TextUtils; import android.util.Log; import android.util.SparseArray; import com.google.android.exoplayer2.C; @@ -51,6 +52,8 @@ import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Locale; import java.util.TimeZone; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * A DASH {@link MediaSource}. @@ -979,41 +982,42 @@ public final class DashMediaSource implements MediaSource { } - private static final class Iso8601Parser implements ParsingLoadable.Parser { + /* package */ static final class Iso8601Parser implements ParsingLoadable.Parser { - private static final String ISO_8601_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'"; - private static final String ISO_8601_WITH_OFFSET_FORMAT = "yyyy-MM-dd'T'HH:mm:ssZ"; - private static final String ISO_8601_WITH_OFFSET_FORMAT_REGEX_PATTERN = ".*[+\\-]\\d{2}:\\d{2}$"; - private static final String ISO_8601_WITH_OFFSET_FORMAT_REGEX_PATTERN_2 = ".*[+\\-]\\d{4}$"; + private static final Pattern TIMESTAMP_WITH_TIMEZONE_PATTERN = + Pattern.compile("(.+?)(Z|((\\+|-|−)(\\d\\d)(:?(\\d\\d))?))"); @Override public Long parse(Uri uri, InputStream inputStream) throws IOException { String firstLine = new BufferedReader(new InputStreamReader(inputStream)).readLine(); - - if (firstLine != null) { - //determine format pattern - String formatPattern; - if (firstLine.matches(ISO_8601_WITH_OFFSET_FORMAT_REGEX_PATTERN)) { - formatPattern = ISO_8601_WITH_OFFSET_FORMAT; - } else if (firstLine.matches(ISO_8601_WITH_OFFSET_FORMAT_REGEX_PATTERN_2)) { - formatPattern = ISO_8601_WITH_OFFSET_FORMAT; + try { + Matcher matcher = TIMESTAMP_WITH_TIMEZONE_PATTERN.matcher(firstLine); + if (!matcher.matches()) { + throw new ParserException("Couldn't parse timestamp: " + firstLine); + } + // Parse the timestamp. + String timestampWithoutTimezone = matcher.group(1); + SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US); + format.setTimeZone(TimeZone.getTimeZone("UTC")); + long timestampMs = format.parse(timestampWithoutTimezone).getTime(); + // Parse the timezone. + String timezone = matcher.group(2); + if ("Z".equals(timezone)) { + // UTC (no offset). } else { - formatPattern = ISO_8601_FORMAT; + long sign = "+".equals(matcher.group(4)) ? 1 : -1; + long hours = Long.parseLong(matcher.group(5)); + String minutesString = matcher.group(7); + long minutes = TextUtils.isEmpty(minutesString) ? 0 : Long.parseLong(minutesString); + long timestampOffsetMs = sign * (((hours * 60) + minutes) * 60 * 1000); + timestampMs -= timestampOffsetMs; } - //parse - try { - SimpleDateFormat format = new SimpleDateFormat(formatPattern, Locale.US); - format.setTimeZone(TimeZone.getTimeZone("UTC")); - return format.parse(firstLine).getTime(); - } catch (ParseException e) { - throw new ParserException(e); - } - - } else { - throw new ParserException("Unable to parse ISO 8601. Input value is null"); + return timestampMs; + } catch (ParseException e) { + throw new ParserException(e); } } } - + } From 7792f667d4474da3c13f4cf29baf69655d22ff90 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 4 Dec 2017 07:29:56 -0800 Subject: [PATCH 0827/2472] Fix setting supported ad MIME types without preloading ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177810991 --- .../exoplayer2/ext/ima/ImaAdsLoader.java | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index 4bf88fe18f..743a428020 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -315,20 +315,13 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A this.adsManager = adsManager; adsManager.addAdErrorListener(this); adsManager.addAdEventListener(this); - if (ENABLE_PRELOADING) { - ImaSdkFactory imaSdkFactory = ImaSdkFactory.getInstance(); - AdsRenderingSettings adsRenderingSettings = imaSdkFactory.createAdsRenderingSettings(); - adsRenderingSettings.setEnablePreloading(true); - adsRenderingSettings.setMimeTypes(supportedMimeTypes); - adsManager.init(adsRenderingSettings); - if (DEBUG) { - Log.d(TAG, "Initialized with preloading"); - } - } else { - adsManager.init(); - if (DEBUG) { - Log.d(TAG, "Initialized without preloading"); - } + ImaSdkFactory imaSdkFactory = ImaSdkFactory.getInstance(); + AdsRenderingSettings adsRenderingSettings = imaSdkFactory.createAdsRenderingSettings(); + adsRenderingSettings.setEnablePreloading(ENABLE_PRELOADING); + adsRenderingSettings.setMimeTypes(supportedMimeTypes); + adsManager.init(adsRenderingSettings); + if (DEBUG) { + Log.d(TAG, "Initialized with ads rendering settings: " + adsRenderingSettings); } long[] adGroupTimesUs = getAdGroupTimesUs(adsManager.getAdCuePoints()); adPlaybackState = new AdPlaybackState(adGroupTimesUs); From bb0fae3ee8243ecd4633b07e5469ad47f251e5c6 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 4 Dec 2017 07:35:50 -0800 Subject: [PATCH 0828/2472] Fix playback of FLV live streams with no audio track Issue: #3188 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177811487 --- RELEASENOTES.md | 2 ++ .../exoplayer2/extractor/flv/FlvExtractor.java | 17 ++++++++++++----- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 0372191683..9d98f2aae0 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -29,6 +29,8 @@ * Add optional parameter to `Player.stop` to reset the player when stopping. * Fix handling of playback parameters changes while paused when followed by a seek. +* Fix playback of live FLV streams that do not contain an audio track + ([#3188](https://github.com/google/ExoPlayer/issues/3188)). * Use the same listener `MediaSourceEventListener` for all MediaSource implementations. * CEA-608: Fix handling of row count changes in roll-up mode diff --git a/library/core/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 index 2da075ff53..d908f28945 100644 --- a/library/core/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 @@ -78,6 +78,7 @@ public final class FlvExtractor implements Extractor { private ExtractorOutput extractorOutput; private @States int state; + private long mediaTagTimestampOffsetUs; private int bytesToNextTagHeader; private int tagType; private int tagDataSize; @@ -93,6 +94,7 @@ public final class FlvExtractor implements Extractor { tagData = new ParsableByteArray(); metadataReader = new ScriptTagPayloadReader(); state = STATE_READING_FLV_HEADER; + mediaTagTimestampOffsetUs = C.TIME_UNSET; } @Override @@ -134,6 +136,7 @@ public final class FlvExtractor implements Extractor { @Override public void seek(long position, long timeUs) { state = STATE_READING_FLV_HEADER; + mediaTagTimestampOffsetUs = C.TIME_UNSET; bytesToNextTagHeader = 0; } @@ -255,11 +258,11 @@ public final class FlvExtractor implements Extractor { private boolean readTagData(ExtractorInput input) throws IOException, InterruptedException { boolean wasConsumed = true; if (tagType == TAG_TYPE_AUDIO && audioReader != null) { - ensureOutputSeekMap(); - audioReader.consume(prepareTagData(input), tagTimestampUs); + ensureReadyForMediaOutput(); + audioReader.consume(prepareTagData(input), mediaTagTimestampOffsetUs + tagTimestampUs); } else if (tagType == TAG_TYPE_VIDEO && videoReader != null) { - ensureOutputSeekMap(); - videoReader.consume(prepareTagData(input), tagTimestampUs); + ensureReadyForMediaOutput(); + videoReader.consume(prepareTagData(input), mediaTagTimestampOffsetUs + tagTimestampUs); } else if (tagType == TAG_TYPE_SCRIPT_DATA && !outputSeekMap) { metadataReader.consume(prepareTagData(input), tagTimestampUs); long durationUs = metadataReader.getDurationUs(); @@ -288,11 +291,15 @@ public final class FlvExtractor implements Extractor { return tagData; } - private void ensureOutputSeekMap() { + private void ensureReadyForMediaOutput() { if (!outputSeekMap) { extractorOutput.seekMap(new SeekMap.Unseekable(C.TIME_UNSET)); outputSeekMap = true; } + if (mediaTagTimestampOffsetUs == C.TIME_UNSET) { + mediaTagTimestampOffsetUs = + metadataReader.getDurationUs() == C.TIME_UNSET ? -tagTimestampUs : 0; + } } } From fbfa43f5a3ef11f5da115c439092b521564fb4fa Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 4 Dec 2017 08:12:35 -0800 Subject: [PATCH 0829/2472] Enhance SeekMaps to return SeekPoints Issue: #2882 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177814974 --- .../src/androidTest/assets/bear.flac.0.dump | 2 +- .../src/androidTest/assets/bear.flac.1.dump | 2 +- .../src/androidTest/assets/bear.flac.2.dump | 2 +- .../src/androidTest/assets/bear.flac.3.dump | 2 +- .../exoplayer2/ext/flac/FlacExtractor.java | 52 ++++--- .../androidTest/assets/flv/sample.flv.0.dump | 2 +- .../androidTest/assets/mkv/sample.mkv.0.dump | 2 +- .../androidTest/assets/mkv/sample.mkv.1.dump | 2 +- .../androidTest/assets/mkv/sample.mkv.2.dump | 2 +- .../androidTest/assets/mkv/sample.mkv.3.dump | 2 +- .../subsample_encrypted_altref.webm.0.dump | 2 +- .../subsample_encrypted_noaltref.webm.0.dump | 2 +- .../androidTest/assets/mp3/bear.mp3.0.dump | 2 +- .../androidTest/assets/mp3/bear.mp3.1.dump | 2 +- .../androidTest/assets/mp3/bear.mp3.2.dump | 2 +- .../androidTest/assets/mp3/bear.mp3.3.dump | 2 +- .../assets/mp3/play-trimmed.mp3.0.dump | 2 +- .../assets/mp3/play-trimmed.mp3.1.dump | 2 +- .../assets/mp3/play-trimmed.mp3.2.dump | 2 +- .../assets/mp3/play-trimmed.mp3.3.dump | 2 +- .../assets/mp3/play-trimmed.mp3.unklen.dump | 2 +- .../androidTest/assets/mp4/sample.mp4.0.dump | 2 +- .../androidTest/assets/mp4/sample.mp4.1.dump | 2 +- .../androidTest/assets/mp4/sample.mp4.2.dump | 2 +- .../androidTest/assets/mp4/sample.mp4.3.dump | 2 +- .../assets/mp4/sample_fragmented.mp4.0.dump | 2 +- .../mp4/sample_fragmented_sei.mp4.0.dump | 2 +- .../androidTest/assets/ogg/bear.opus.0.dump | 2 +- .../androidTest/assets/ogg/bear.opus.1.dump | 2 +- .../androidTest/assets/ogg/bear.opus.2.dump | 2 +- .../androidTest/assets/ogg/bear.opus.3.dump | 2 +- .../assets/ogg/bear.opus.unklen.dump | 2 +- .../assets/ogg/bear_flac.ogg.0.dump | 2 +- .../assets/ogg/bear_flac.ogg.1.dump | 2 +- .../assets/ogg/bear_flac.ogg.2.dump | 2 +- .../assets/ogg/bear_flac.ogg.3.dump | 2 +- .../assets/ogg/bear_flac.ogg.unklen.dump | 2 +- .../ogg/bear_flac_noseektable.ogg.0.dump | 2 +- .../ogg/bear_flac_noseektable.ogg.1.dump | 2 +- .../ogg/bear_flac_noseektable.ogg.2.dump | 2 +- .../ogg/bear_flac_noseektable.ogg.3.dump | 2 +- .../ogg/bear_flac_noseektable.ogg.unklen.dump | 2 +- .../assets/ogg/bear_vorbis.ogg.0.dump | 2 +- .../assets/ogg/bear_vorbis.ogg.1.dump | 2 +- .../assets/ogg/bear_vorbis.ogg.2.dump | 2 +- .../assets/ogg/bear_vorbis.ogg.3.dump | 2 +- .../assets/ogg/bear_vorbis.ogg.unklen.dump | 2 +- .../assets/rawcc/sample.rawcc.0.dump | 2 +- .../androidTest/assets/ts/sample.ac3.0.dump | 2 +- .../androidTest/assets/ts/sample.adts.0.dump | 2 +- .../androidTest/assets/ts/sample.ps.0.dump | 2 +- .../androidTest/assets/ts/sample.ts.0.dump | 2 +- .../androidTest/assets/wav/sample.wav.0.dump | 8 +- .../androidTest/assets/wav/sample.wav.1.dump | 6 +- .../androidTest/assets/wav/sample.wav.2.dump | 4 +- .../androidTest/assets/wav/sample.wav.3.dump | 4 +- .../exoplayer2/extractor/ChunkIndex.java | 11 +- .../android/exoplayer2/extractor/SeekMap.java | 91 ++++++++--- .../exoplayer2/extractor/SeekPoint.java | 62 ++++++++ .../extractor/mp3/ConstantBitrateSeeker.java | 18 ++- .../exoplayer2/extractor/mp3/VbriSeeker.java | 12 +- .../exoplayer2/extractor/mp3/XingSeeker.java | 8 +- .../extractor/mp4/Mp4Extractor.java | 146 ++++++++++++++---- .../extractor/ogg/DefaultOggSeeker.java | 8 +- .../exoplayer2/extractor/ogg/FlacReader.java | 15 +- .../exoplayer2/extractor/wav/WavHeader.java | 19 ++- .../source/ExtractorMediaPeriod.java | 3 +- .../extractor/mp3/XingSeekerTest.java | 34 ++-- .../exoplayer2/testutil/ExtractorAsserts.java | 2 +- .../testutil/FakeExtractorOutput.java | 7 +- 70 files changed, 439 insertions(+), 173 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekPoint.java diff --git a/extensions/flac/src/androidTest/assets/bear.flac.0.dump b/extensions/flac/src/androidTest/assets/bear.flac.0.dump index b03636f2bb..6908f5cc93 100644 --- a/extensions/flac/src/androidTest/assets/bear.flac.0.dump +++ b/extensions/flac/src/androidTest/assets/bear.flac.0.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 2741000 - getPosition(0) = 8880 + getPosition(0) = [[timeUs=0, position=8880]] numberOfTracks = 1 track 0: format: diff --git a/extensions/flac/src/androidTest/assets/bear.flac.1.dump b/extensions/flac/src/androidTest/assets/bear.flac.1.dump index 4e8388dba8..1414443187 100644 --- a/extensions/flac/src/androidTest/assets/bear.flac.1.dump +++ b/extensions/flac/src/androidTest/assets/bear.flac.1.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 2741000 - getPosition(0) = 8880 + getPosition(0) = [[timeUs=0, position=8880]] numberOfTracks = 1 track 0: format: diff --git a/extensions/flac/src/androidTest/assets/bear.flac.2.dump b/extensions/flac/src/androidTest/assets/bear.flac.2.dump index 0860c36cef..e343241650 100644 --- a/extensions/flac/src/androidTest/assets/bear.flac.2.dump +++ b/extensions/flac/src/androidTest/assets/bear.flac.2.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 2741000 - getPosition(0) = 8880 + getPosition(0) = [[timeUs=0, position=8880]] numberOfTracks = 1 track 0: format: diff --git a/extensions/flac/src/androidTest/assets/bear.flac.3.dump b/extensions/flac/src/androidTest/assets/bear.flac.3.dump index 6f7f72b806..95ab255bd0 100644 --- a/extensions/flac/src/androidTest/assets/bear.flac.3.dump +++ b/extensions/flac/src/androidTest/assets/bear.flac.3.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 2741000 - getPosition(0) = 8880 + getPosition(0) = [[timeUs=0, position=8880]] numberOfTracks = 1 track 0: format: diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java index a2f141a712..b630298c6e 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacExtractor.java @@ -25,6 +25,7 @@ import com.google.android.exoplayer2.extractor.ExtractorOutput; 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.SeekPoint; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.util.FlacStreamInfo; import com.google.android.exoplayer2.util.MimeTypes; @@ -104,26 +105,11 @@ public final class FlacExtractor implements Extractor { } metadataParsed = true; - extractorOutput.seekMap(new SeekMap() { - final boolean isSeekable = decoderJni.getSeekPosition(0) != -1; - final long durationUs = streamInfo.durationUs(); - - @Override - public boolean isSeekable() { - return isSeekable; - } - - @Override - public long getPosition(long timeUs) { - return isSeekable ? decoderJni.getSeekPosition(timeUs) : 0; - } - - @Override - public long getDurationUs() { - return durationUs; - } - - }); + boolean isSeekable = decoderJni.getSeekPosition(0) != -1; + extractorOutput.seekMap( + isSeekable + ? new FlacSeekMap(streamInfo.durationUs(), decoderJni) + : new SeekMap.Unseekable(streamInfo.durationUs(), 0)); Format mediaFormat = Format.createAudioSampleFormat( null, @@ -184,4 +170,30 @@ public final class FlacExtractor implements Extractor { } } + private static final class FlacSeekMap implements SeekMap { + + private final long durationUs; + private final FlacDecoderJni decoderJni; + + public FlacSeekMap(long durationUs, FlacDecoderJni decoderJni) { + this.durationUs = durationUs; + this.decoderJni = decoderJni; + } + + @Override + public boolean isSeekable() { + return true; + } + + @Override + public SeekPoints getSeekPoints(long timeUs) { + // TODO: Access the seek table via JNI to return two seek points when appropriate. + return new SeekPoints(new SeekPoint(timeUs, decoderJni.getSeekPosition(timeUs))); + } + + @Override + public long getDurationUs() { + return durationUs; + } + } } diff --git a/library/core/src/androidTest/assets/flv/sample.flv.0.dump b/library/core/src/androidTest/assets/flv/sample.flv.0.dump index b4129ecb88..7a4a74770c 100644 --- a/library/core/src/androidTest/assets/flv/sample.flv.0.dump +++ b/library/core/src/androidTest/assets/flv/sample.flv.0.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = false duration = 1136000 - getPosition(0) = 0 + getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 2 track 8: format: diff --git a/library/core/src/androidTest/assets/mkv/sample.mkv.0.dump b/library/core/src/androidTest/assets/mkv/sample.mkv.0.dump index 34bad9b82a..0f005ee5a9 100644 --- a/library/core/src/androidTest/assets/mkv/sample.mkv.0.dump +++ b/library/core/src/androidTest/assets/mkv/sample.mkv.0.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 1072000 - getPosition(0) = 5576 + getPosition(0) = [[timeUs=67000, position=5576]] numberOfTracks = 2 track 1: format: diff --git a/library/core/src/androidTest/assets/mkv/sample.mkv.1.dump b/library/core/src/androidTest/assets/mkv/sample.mkv.1.dump index 546c934eff..378f5d7f2a 100644 --- a/library/core/src/androidTest/assets/mkv/sample.mkv.1.dump +++ b/library/core/src/androidTest/assets/mkv/sample.mkv.1.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 1072000 - getPosition(0) = 5576 + getPosition(0) = [[timeUs=67000, position=5576]] numberOfTracks = 2 track 1: format: diff --git a/library/core/src/androidTest/assets/mkv/sample.mkv.2.dump b/library/core/src/androidTest/assets/mkv/sample.mkv.2.dump index ec84908172..80caf24a93 100644 --- a/library/core/src/androidTest/assets/mkv/sample.mkv.2.dump +++ b/library/core/src/androidTest/assets/mkv/sample.mkv.2.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 1072000 - getPosition(0) = 5576 + getPosition(0) = [[timeUs=67000, position=5576]] numberOfTracks = 2 track 1: format: diff --git a/library/core/src/androidTest/assets/mkv/sample.mkv.3.dump b/library/core/src/androidTest/assets/mkv/sample.mkv.3.dump index ac8d9a2c1c..c9672ba9c4 100644 --- a/library/core/src/androidTest/assets/mkv/sample.mkv.3.dump +++ b/library/core/src/androidTest/assets/mkv/sample.mkv.3.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 1072000 - getPosition(0) = 5576 + getPosition(0) = [[timeUs=67000, position=5576]] numberOfTracks = 2 track 1: format: diff --git a/library/core/src/androidTest/assets/mkv/subsample_encrypted_altref.webm.0.dump b/library/core/src/androidTest/assets/mkv/subsample_encrypted_altref.webm.0.dump index f533e14c3f..abc07dc503 100644 --- a/library/core/src/androidTest/assets/mkv/subsample_encrypted_altref.webm.0.dump +++ b/library/core/src/androidTest/assets/mkv/subsample_encrypted_altref.webm.0.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = false duration = 1000 - getPosition(0) = 0 + getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 1 track 1: format: diff --git a/library/core/src/androidTest/assets/mkv/subsample_encrypted_noaltref.webm.0.dump b/library/core/src/androidTest/assets/mkv/subsample_encrypted_noaltref.webm.0.dump index d84c549dea..c43a43b576 100644 --- a/library/core/src/androidTest/assets/mkv/subsample_encrypted_noaltref.webm.0.dump +++ b/library/core/src/androidTest/assets/mkv/subsample_encrypted_noaltref.webm.0.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = false duration = 1000 - getPosition(0) = 0 + getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 1 track 1: format: diff --git a/library/core/src/androidTest/assets/mp3/bear.mp3.0.dump b/library/core/src/androidTest/assets/mp3/bear.mp3.0.dump index b66d263c84..eca3a6687d 100644 --- a/library/core/src/androidTest/assets/mp3/bear.mp3.0.dump +++ b/library/core/src/androidTest/assets/mp3/bear.mp3.0.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 2784000 - getPosition(0) = 201 + getPosition(0) = [[timeUs=0, position=201]] numberOfTracks = 1 track 0: format: diff --git a/library/core/src/androidTest/assets/mp3/bear.mp3.1.dump b/library/core/src/androidTest/assets/mp3/bear.mp3.1.dump index a57894e81e..12abf149c4 100644 --- a/library/core/src/androidTest/assets/mp3/bear.mp3.1.dump +++ b/library/core/src/androidTest/assets/mp3/bear.mp3.1.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 2784000 - getPosition(0) = 201 + getPosition(0) = [[timeUs=0, position=201]] numberOfTracks = 1 track 0: format: diff --git a/library/core/src/androidTest/assets/mp3/bear.mp3.2.dump b/library/core/src/androidTest/assets/mp3/bear.mp3.2.dump index 3f393e768e..3568616e76 100644 --- a/library/core/src/androidTest/assets/mp3/bear.mp3.2.dump +++ b/library/core/src/androidTest/assets/mp3/bear.mp3.2.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 2784000 - getPosition(0) = 201 + getPosition(0) = [[timeUs=0, position=201]] numberOfTracks = 1 track 0: format: diff --git a/library/core/src/androidTest/assets/mp3/bear.mp3.3.dump b/library/core/src/androidTest/assets/mp3/bear.mp3.3.dump index a2387eb887..8a31fe5e7d 100644 --- a/library/core/src/androidTest/assets/mp3/bear.mp3.3.dump +++ b/library/core/src/androidTest/assets/mp3/bear.mp3.3.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 2784000 - getPosition(0) = 201 + getPosition(0) = [[timeUs=0, position=201]] numberOfTracks = 1 track 0: format: diff --git a/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.0.dump b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.0.dump index 37a04215ee..88601665b0 100644 --- a/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.0.dump +++ b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.0.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 26125 - getPosition(0) = 0 + getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 1 track 0: format: diff --git a/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.1.dump b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.1.dump index 37a04215ee..88601665b0 100644 --- a/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.1.dump +++ b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.1.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 26125 - getPosition(0) = 0 + getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 1 track 0: format: diff --git a/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.2.dump b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.2.dump index 37a04215ee..88601665b0 100644 --- a/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.2.dump +++ b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.2.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 26125 - getPosition(0) = 0 + getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 1 track 0: format: diff --git a/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.3.dump b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.3.dump index 37a04215ee..88601665b0 100644 --- a/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.3.dump +++ b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.3.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 26125 - getPosition(0) = 0 + getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 1 track 0: format: diff --git a/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.unklen.dump b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.unklen.dump index b75aefd91b..2c0ac67561 100644 --- a/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.unklen.dump +++ b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.unklen.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = false duration = UNSET TIME - getPosition(0) = 0 + getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 1 track 0: format: diff --git a/library/core/src/androidTest/assets/mp4/sample.mp4.0.dump b/library/core/src/androidTest/assets/mp4/sample.mp4.0.dump index be0a16681c..7cd3486505 100644 --- a/library/core/src/androidTest/assets/mp4/sample.mp4.0.dump +++ b/library/core/src/androidTest/assets/mp4/sample.mp4.0.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 1024000 - getPosition(0) = 48 + getPosition(0) = [[timeUs=0, position=48]] numberOfTracks = 2 track 0: format: diff --git a/library/core/src/androidTest/assets/mp4/sample.mp4.1.dump b/library/core/src/androidTest/assets/mp4/sample.mp4.1.dump index a759e4250a..fcf9402cba 100644 --- a/library/core/src/androidTest/assets/mp4/sample.mp4.1.dump +++ b/library/core/src/androidTest/assets/mp4/sample.mp4.1.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 1024000 - getPosition(0) = 48 + getPosition(0) = [[timeUs=0, position=48]] numberOfTracks = 2 track 0: format: diff --git a/library/core/src/androidTest/assets/mp4/sample.mp4.2.dump b/library/core/src/androidTest/assets/mp4/sample.mp4.2.dump index 59ee715255..5dbb6e1561 100644 --- a/library/core/src/androidTest/assets/mp4/sample.mp4.2.dump +++ b/library/core/src/androidTest/assets/mp4/sample.mp4.2.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 1024000 - getPosition(0) = 48 + getPosition(0) = [[timeUs=0, position=48]] numberOfTracks = 2 track 0: format: diff --git a/library/core/src/androidTest/assets/mp4/sample.mp4.3.dump b/library/core/src/androidTest/assets/mp4/sample.mp4.3.dump index a81a4189d9..bac707446d 100644 --- a/library/core/src/androidTest/assets/mp4/sample.mp4.3.dump +++ b/library/core/src/androidTest/assets/mp4/sample.mp4.3.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 1024000 - getPosition(0) = 48 + getPosition(0) = [[timeUs=0, position=48]] numberOfTracks = 2 track 0: format: diff --git a/library/core/src/androidTest/assets/mp4/sample_fragmented.mp4.0.dump b/library/core/src/androidTest/assets/mp4/sample_fragmented.mp4.0.dump index 95f6528fd6..736e57693c 100644 --- a/library/core/src/androidTest/assets/mp4/sample_fragmented.mp4.0.dump +++ b/library/core/src/androidTest/assets/mp4/sample_fragmented.mp4.0.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = false duration = UNSET TIME - getPosition(0) = 1828 + getPosition(0) = [[timeUs=0, position=1828]] numberOfTracks = 2 track 0: format: diff --git a/library/core/src/androidTest/assets/mp4/sample_fragmented_sei.mp4.0.dump b/library/core/src/androidTest/assets/mp4/sample_fragmented_sei.mp4.0.dump index ebd33133e2..8186a2b9ce 100644 --- a/library/core/src/androidTest/assets/mp4/sample_fragmented_sei.mp4.0.dump +++ b/library/core/src/androidTest/assets/mp4/sample_fragmented_sei.mp4.0.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = false duration = UNSET TIME - getPosition(0) = 1828 + getPosition(0) = [[timeUs=0, position=1828]] numberOfTracks = 3 track 0: format: diff --git a/library/core/src/androidTest/assets/ogg/bear.opus.0.dump b/library/core/src/androidTest/assets/ogg/bear.opus.0.dump index 8033ce8089..4d09067f3b 100644 --- a/library/core/src/androidTest/assets/ogg/bear.opus.0.dump +++ b/library/core/src/androidTest/assets/ogg/bear.opus.0.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 2747500 - getPosition(0) = 125 + getPosition(0) = [[timeUs=0, position=125]] numberOfTracks = 1 track 0: format: diff --git a/library/core/src/androidTest/assets/ogg/bear.opus.1.dump b/library/core/src/androidTest/assets/ogg/bear.opus.1.dump index f9aceae68a..821351e989 100644 --- a/library/core/src/androidTest/assets/ogg/bear.opus.1.dump +++ b/library/core/src/androidTest/assets/ogg/bear.opus.1.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 2747500 - getPosition(0) = 125 + getPosition(0) = [[timeUs=0, position=125]] numberOfTracks = 1 track 0: format: diff --git a/library/core/src/androidTest/assets/ogg/bear.opus.2.dump b/library/core/src/androidTest/assets/ogg/bear.opus.2.dump index f2f07f3e2f..3aea1e8d74 100644 --- a/library/core/src/androidTest/assets/ogg/bear.opus.2.dump +++ b/library/core/src/androidTest/assets/ogg/bear.opus.2.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 2747500 - getPosition(0) = 125 + getPosition(0) = [[timeUs=0, position=125]] numberOfTracks = 1 track 0: format: diff --git a/library/core/src/androidTest/assets/ogg/bear.opus.3.dump b/library/core/src/androidTest/assets/ogg/bear.opus.3.dump index 905055797c..b49af29f2c 100644 --- a/library/core/src/androidTest/assets/ogg/bear.opus.3.dump +++ b/library/core/src/androidTest/assets/ogg/bear.opus.3.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 2747500 - getPosition(0) = 125 + getPosition(0) = [[timeUs=0, position=125]] numberOfTracks = 1 track 0: format: diff --git a/library/core/src/androidTest/assets/ogg/bear.opus.unklen.dump b/library/core/src/androidTest/assets/ogg/bear.opus.unklen.dump index cd29da3e27..b2d5a9f3d2 100644 --- a/library/core/src/androidTest/assets/ogg/bear.opus.unklen.dump +++ b/library/core/src/androidTest/assets/ogg/bear.opus.unklen.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = false duration = UNSET TIME - getPosition(0) = 0 + getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 1 track 0: format: diff --git a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.0.dump b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.0.dump index 5ba8cc29ae..572d1da891 100644 --- a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.0.dump +++ b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.0.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 2741000 - getPosition(0) = 8457 + getPosition(0) = [[timeUs=0, position=8457]] numberOfTracks = 1 track 0: format: diff --git a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.1.dump b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.1.dump index f698fd28cf..d53f257fd2 100644 --- a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.1.dump +++ b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.1.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 2741000 - getPosition(0) = 8457 + getPosition(0) = [[timeUs=0, position=8457]] numberOfTracks = 1 track 0: format: diff --git a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.2.dump b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.2.dump index 8d803d0bac..cdfd6efab8 100644 --- a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.2.dump +++ b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.2.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 2741000 - getPosition(0) = 8457 + getPosition(0) = [[timeUs=0, position=8457]] numberOfTracks = 1 track 0: format: diff --git a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.3.dump b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.3.dump index 09f6267270..9b029d3301 100644 --- a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.3.dump +++ b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.3.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 2741000 - getPosition(0) = 8457 + getPosition(0) = [[timeUs=0, position=8457]] numberOfTracks = 1 track 0: format: diff --git a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.unklen.dump b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.unklen.dump index 5ba8cc29ae..572d1da891 100644 --- a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.unklen.dump +++ b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.unklen.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 2741000 - getPosition(0) = 8457 + getPosition(0) = [[timeUs=0, position=8457]] numberOfTracks = 1 track 0: format: diff --git a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.0.dump b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.0.dump index 73e537f8c8..1c02c1bbef 100644 --- a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.0.dump +++ b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.0.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 2741000 - getPosition(0) = 8407 + getPosition(0) = [[timeUs=0, position=8407]] numberOfTracks = 1 track 0: format: diff --git a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.1.dump b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.1.dump index 3b7dc3fd1e..81d79b8674 100644 --- a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.1.dump +++ b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.1.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 2741000 - getPosition(0) = 8407 + getPosition(0) = [[timeUs=0, position=8407]] numberOfTracks = 1 track 0: format: diff --git a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.2.dump b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.2.dump index b6a6741fcc..f8b00bcb3a 100644 --- a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.2.dump +++ b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.2.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 2741000 - getPosition(0) = 8407 + getPosition(0) = [[timeUs=0, position=8407]] numberOfTracks = 1 track 0: format: diff --git a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.3.dump b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.3.dump index 738002f7ef..b020618488 100644 --- a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.3.dump +++ b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.3.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 2741000 - getPosition(0) = 8407 + getPosition(0) = [[timeUs=0, position=8407]] numberOfTracks = 1 track 0: format: diff --git a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.unklen.dump b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.unklen.dump index a237fd0dfc..bf135434f4 100644 --- a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.unklen.dump +++ b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.unklen.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = false duration = UNSET TIME - getPosition(0) = 0 + getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 1 track 0: format: diff --git a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.0.dump b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.0.dump index 8e2c5125a3..860e8a3b5b 100644 --- a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.0.dump +++ b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.0.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 2741000 - getPosition(0) = 3995 + getPosition(0) = [[timeUs=0, position=3995]] numberOfTracks = 1 track 0: format: diff --git a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.1.dump b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.1.dump index aa25303ac3..11afeb9665 100644 --- a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.1.dump +++ b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.1.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 2741000 - getPosition(0) = 3995 + getPosition(0) = [[timeUs=0, position=3995]] numberOfTracks = 1 track 0: format: diff --git a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.2.dump b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.2.dump index 58969058fa..f2f97ebcfa 100644 --- a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.2.dump +++ b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.2.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 2741000 - getPosition(0) = 3995 + getPosition(0) = [[timeUs=0, position=3995]] numberOfTracks = 1 track 0: format: diff --git a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.3.dump b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.3.dump index 4c789a8431..5d5f284cf2 100644 --- a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.3.dump +++ b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.3.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 2741000 - getPosition(0) = 3995 + getPosition(0) = [[timeUs=0, position=3995]] numberOfTracks = 1 track 0: format: diff --git a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.unklen.dump b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.unklen.dump index 2f163572bf..ee1176773e 100644 --- a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.unklen.dump +++ b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.unklen.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = false duration = UNSET TIME - getPosition(0) = 0 + getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 1 track 0: format: diff --git a/library/core/src/androidTest/assets/rawcc/sample.rawcc.0.dump b/library/core/src/androidTest/assets/rawcc/sample.rawcc.0.dump index 3e84813162..d430d1d8d4 100644 --- a/library/core/src/androidTest/assets/rawcc/sample.rawcc.0.dump +++ b/library/core/src/androidTest/assets/rawcc/sample.rawcc.0.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = false duration = UNSET TIME - getPosition(0) = 0 + getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 1 track 0: format: diff --git a/library/core/src/androidTest/assets/ts/sample.ac3.0.dump b/library/core/src/androidTest/assets/ts/sample.ac3.0.dump index 1b6c77efb6..bedffcf198 100644 --- a/library/core/src/androidTest/assets/ts/sample.ac3.0.dump +++ b/library/core/src/androidTest/assets/ts/sample.ac3.0.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = false duration = UNSET TIME - getPosition(0) = 0 + getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 1 track 0: format: diff --git a/library/core/src/androidTest/assets/ts/sample.adts.0.dump b/library/core/src/androidTest/assets/ts/sample.adts.0.dump index 0a7427d3f1..a97cf860d1 100644 --- a/library/core/src/androidTest/assets/ts/sample.adts.0.dump +++ b/library/core/src/androidTest/assets/ts/sample.adts.0.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = false duration = UNSET TIME - getPosition(0) = 0 + getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 2 track 0: format: diff --git a/library/core/src/androidTest/assets/ts/sample.ps.0.dump b/library/core/src/androidTest/assets/ts/sample.ps.0.dump index 98f3c6a85a..41db704d56 100644 --- a/library/core/src/androidTest/assets/ts/sample.ps.0.dump +++ b/library/core/src/androidTest/assets/ts/sample.ps.0.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = false duration = UNSET TIME - getPosition(0) = 0 + getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 2 track 192: format: diff --git a/library/core/src/androidTest/assets/ts/sample.ts.0.dump b/library/core/src/androidTest/assets/ts/sample.ts.0.dump index 91e48b1722..e900b94673 100644 --- a/library/core/src/androidTest/assets/ts/sample.ts.0.dump +++ b/library/core/src/androidTest/assets/ts/sample.ts.0.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = false duration = UNSET TIME - getPosition(0) = 0 + getPosition(0) = [[timeUs=0, position=0]] numberOfTracks = 2 track 256: format: diff --git a/library/core/src/androidTest/assets/wav/sample.wav.0.dump b/library/core/src/androidTest/assets/wav/sample.wav.0.dump index 9ad01284b7..5d0f4d77f0 100644 --- a/library/core/src/androidTest/assets/wav/sample.wav.0.dump +++ b/library/core/src/androidTest/assets/wav/sample.wav.0.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 1000000 - getPosition(0) = 78 + getPosition(0) = [[timeUs=0, position=78]] numberOfTracks = 1 track 0: format: @@ -27,15 +27,15 @@ track 0: initializationData: sample count = 3 sample 0: - time = 884 + time = 0 flags = 1 data = length 32768, hash 9A8CEEBA sample 1: - time = 372403 + time = 371519 flags = 1 data = length 32768, hash C1717317 sample 2: - time = 743922 + time = 743038 flags = 1 data = length 22664, hash 819F5F62 tracksEnded = true diff --git a/library/core/src/androidTest/assets/wav/sample.wav.1.dump b/library/core/src/androidTest/assets/wav/sample.wav.1.dump index ca98cc5cf5..e59239bff8 100644 --- a/library/core/src/androidTest/assets/wav/sample.wav.1.dump +++ b/library/core/src/androidTest/assets/wav/sample.wav.1.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 1000000 - getPosition(0) = 78 + getPosition(0) = [[timeUs=0, position=78]] numberOfTracks = 1 track 0: format: @@ -27,11 +27,11 @@ track 0: initializationData: sample count = 2 sample 0: - time = 334195 + time = 333310 flags = 1 data = length 32768, hash 42D6E860 sample 1: - time = 705714 + time = 704829 flags = 1 data = length 26034, hash 62692C38 tracksEnded = true diff --git a/library/core/src/androidTest/assets/wav/sample.wav.2.dump b/library/core/src/androidTest/assets/wav/sample.wav.2.dump index da212b220a..c80a260385 100644 --- a/library/core/src/androidTest/assets/wav/sample.wav.2.dump +++ b/library/core/src/androidTest/assets/wav/sample.wav.2.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 1000000 - getPosition(0) = 78 + getPosition(0) = [[timeUs=0, position=78]] numberOfTracks = 1 track 0: format: @@ -27,7 +27,7 @@ track 0: initializationData: sample count = 1 sample 0: - time = 667528 + time = 666643 flags = 1 data = length 29402, hash 4241604E tracksEnded = true diff --git a/library/core/src/androidTest/assets/wav/sample.wav.3.dump b/library/core/src/androidTest/assets/wav/sample.wav.3.dump index 3275ba6ef5..9f25028923 100644 --- a/library/core/src/androidTest/assets/wav/sample.wav.3.dump +++ b/library/core/src/androidTest/assets/wav/sample.wav.3.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = true duration = 1000000 - getPosition(0) = 78 + getPosition(0) = [[timeUs=0, position=78]] numberOfTracks = 1 track 0: format: @@ -27,7 +27,7 @@ track 0: initializationData: sample count = 1 sample 0: - time = 1000861 + time = 999977 flags = 1 data = length 2, hash 116 tracksEnded = true diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ChunkIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ChunkIndex.java index baa5589f4b..d0c66f930a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ChunkIndex.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ChunkIndex.java @@ -91,8 +91,15 @@ public final class ChunkIndex implements SeekMap { } @Override - public long getPosition(long timeUs) { - return offsets[getChunkIndex(timeUs)]; + public SeekPoints getSeekPoints(long timeUs) { + int chunkIndex = getChunkIndex(timeUs); + SeekPoint seekPoint = new SeekPoint(timesUs[chunkIndex], offsets[chunkIndex]); + if (seekPoint.timeUs >= timeUs || chunkIndex == length - 1) { + return new SeekPoints(seekPoint); + } else { + SeekPoint nextSeekPoint = new SeekPoint(timesUs[chunkIndex + 1], offsets[chunkIndex + 1]); + return new SeekPoints(seekPoint, nextSeekPoint); + } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekMap.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekMap.java index 964c43a45a..aa718c23e5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekMap.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekMap.java @@ -16,36 +16,36 @@ package com.google.android.exoplayer2.extractor; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.util.Assertions; /** * Maps seek positions (in microseconds) to corresponding positions (byte offsets) in the stream. */ public interface SeekMap { - /** - * A {@link SeekMap} that does not support seeking. - */ + /** A {@link SeekMap} that does not support seeking. */ final class Unseekable implements SeekMap { private final long durationUs; - private final long startPosition; + private final SeekPoints startSeekPoints; /** - * @param durationUs The duration of the stream in microseconds, or {@link C#TIME_UNSET} if - * the duration is unknown. + * @param durationUs The duration of the stream in microseconds, or {@link C#TIME_UNSET} if the + * duration is unknown. */ public Unseekable(long durationUs) { this(durationUs, 0); } /** - * @param durationUs The duration of the stream in microseconds, or {@link C#TIME_UNSET} if - * the duration is unknown. + * @param durationUs The duration of the stream in microseconds, or {@link C#TIME_UNSET} if the + * duration is unknown. * @param startPosition The position (byte offset) of the start of the media. */ public Unseekable(long durationUs, long startPosition) { this.durationUs = durationUs; - this.startPosition = startPosition; + startSeekPoints = + new SeekPoints(startPosition == 0 ? SeekPoint.START : new SeekPoint(0, startPosition)); } @Override @@ -59,17 +59,58 @@ public interface SeekMap { } @Override - public long getPosition(long timeUs) { - return startPosition; + public SeekPoints getSeekPoints(long timeUs) { + return startSeekPoints; + } + } + + /** Contains one or two {@link SeekPoint}s. */ + final class SeekPoints { + + /** The first seek point. */ + public final SeekPoint first; + /** The second seek point, or {@link #first} if there's only one seek point. */ + public final SeekPoint second; + + /** @param point The single seek point. */ + public SeekPoints(SeekPoint point) { + this(point, point); } + /** + * @param first The first seek point. + * @param second The second seek point. + */ + public SeekPoints(SeekPoint first, SeekPoint second) { + this.first = Assertions.checkNotNull(first); + this.second = Assertions.checkNotNull(second); + } + + @Override + public String toString() { + return "[" + first + (first.equals(second) ? "" : (", " + second)) + "]"; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + SeekPoints other = (SeekPoints) obj; + return first.equals(other.first) && second.equals(other.second); + } + + @Override + public int hashCode() { + return (31 * first.hashCode()) + second.hashCode(); + } } /** * Returns whether seeking is supported. - *

          - * If seeking is not supported then the only valid seek position is the start of the file, and so - * {@link #getPosition(long)} will return 0 for all input values. * * @return Whether seeking is supported. */ @@ -78,20 +119,22 @@ public interface SeekMap { /** * Returns the duration of the stream in microseconds. * - * @return The duration of the stream in microseconds, or {@link C#TIME_UNSET} if the - * duration is unknown. + * @return The duration of the stream in microseconds, or {@link C#TIME_UNSET} if the duration is + * unknown. */ long getDurationUs(); /** - * Maps a seek position in microseconds to a corresponding position (byte offset) in the stream - * from which data can be provided to the extractor. + * Obtains seek points for the specified seek time in microseconds. The returned {@link + * SeekPoints} will contain one or two distinct seek points. * - * @param timeUs A seek position in microseconds. - * @return The corresponding position (byte offset) in the stream from which data can be provided - * to the extractor. If {@link #isSeekable()} returns false then the returned value will be - * independent of {@code timeUs}, and will indicate the start of the media in the stream. + *

          Two seek points [A, B] are returned in the case that seeking can only be performed to + * discrete points in time, there does not exist a seek point at exactly the requested time, and + * there exist seek points on both sides of it. In this case A and B are the closest seek points + * before and after the requested time. A single seek point is returned in all other cases. + * + * @param timeUs A seek time in microseconds. + * @return The corresponding seek points. */ - long getPosition(long timeUs); - + SeekPoints getSeekPoints(long timeUs); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekPoint.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekPoint.java new file mode 100644 index 0000000000..93cfbd9200 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekPoint.java @@ -0,0 +1,62 @@ +/* + * 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; + +/** Defines a seek point in a media stream. */ +public final class SeekPoint { + + /** A {@link SeekPoint} whose time and byte offset are both set to 0. */ + public static final SeekPoint START = new SeekPoint(0, 0); + + /** The time of the seek point, in microseconds. */ + public final long timeUs; + + /** The byte offset of the seek point. */ + public final long position; + + /** + * @param timeUs The time of the seek point, in microseconds. + * @param position The byte offset of the seek point. + */ + public SeekPoint(long timeUs, long position) { + this.timeUs = timeUs; + this.position = position; + } + + @Override + public String toString() { + return "[timeUs=" + timeUs + ", position=" + position + "]"; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + SeekPoint other = (SeekPoint) obj; + return timeUs == other.timeUs && position == other.position; + } + + @Override + public int hashCode() { + int result = (int) timeUs; + result = 31 * result + (int) position; + return result; + } +} diff --git a/library/core/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 index 442e62deca..d358c0cae1 100644 --- a/library/core/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 @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.extractor.mp3; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.MpegAudioHeader; +import com.google.android.exoplayer2.extractor.SeekPoint; import com.google.android.exoplayer2.util.Util; /** @@ -57,16 +58,25 @@ import com.google.android.exoplayer2.util.Util; } @Override - public long getPosition(long timeUs) { + public SeekPoints getSeekPoints(long timeUs) { if (dataSize == C.LENGTH_UNSET) { - return firstFramePosition; + return new SeekPoints(new SeekPoint(0, firstFramePosition)); } long positionOffset = (timeUs * bitrate) / (C.MICROS_PER_SECOND * BITS_PER_BYTE); // Constrain to nearest preceding frame offset. positionOffset = (positionOffset / frameSize) * frameSize; positionOffset = Util.constrainValue(positionOffset, 0, dataSize - frameSize); - // Add data start position. - return firstFramePosition + positionOffset; + long seekPosition = firstFramePosition + positionOffset; + long seekTimeUs = getTimeUs(seekPosition); + SeekPoint seekPoint = new SeekPoint(seekTimeUs, seekPosition); + if (seekTimeUs >= timeUs || positionOffset == dataSize - frameSize) { + return new SeekPoints(seekPoint); + } else { + long secondSeekPosition = seekPosition + frameSize; + long secondSeekTimeUs = getTimeUs(secondSeekPosition); + SeekPoint secondSeekPoint = new SeekPoint(secondSeekTimeUs, secondSeekPosition); + return new SeekPoints(seekPoint, secondSeekPoint); + } } @Override diff --git a/library/core/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 index cc631d9f7e..f918b5c43d 100644 --- a/library/core/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 @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor.mp3; import android.util.Log; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.MpegAudioHeader; +import com.google.android.exoplayer2.extractor.SeekPoint; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; @@ -106,8 +107,15 @@ import com.google.android.exoplayer2.util.Util; } @Override - public long getPosition(long timeUs) { - return positions[Util.binarySearchFloor(timesUs, timeUs, true, true)]; + public SeekPoints getSeekPoints(long timeUs) { + int tableIndex = Util.binarySearchFloor(timesUs, timeUs, true, true); + SeekPoint seekPoint = new SeekPoint(timesUs[tableIndex], positions[tableIndex]); + if (seekPoint.timeUs >= timeUs || tableIndex == timesUs.length - 1) { + return new SeekPoints(seekPoint); + } else { + SeekPoint nextSeekPoint = new SeekPoint(timesUs[tableIndex + 1], positions[tableIndex + 1]); + return new SeekPoints(seekPoint, nextSeekPoint); + } } @Override diff --git a/library/core/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 index e532249a64..a3bd5a2da2 100644 --- a/library/core/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 @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor.mp3; import android.util.Log; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.MpegAudioHeader; +import com.google.android.exoplayer2.extractor.SeekPoint; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; @@ -107,10 +108,11 @@ import com.google.android.exoplayer2.util.Util; } @Override - public long getPosition(long timeUs) { + public SeekPoints getSeekPoints(long timeUs) { if (!isSeekable()) { - return dataStartPosition + xingFrameSize; + return new SeekPoints(new SeekPoint(0, dataStartPosition + xingFrameSize)); } + timeUs = Util.constrainValue(timeUs, 0, durationUs); double percent = (timeUs * 100d) / durationUs; double scaledPosition; if (percent <= 0) { @@ -129,7 +131,7 @@ import com.google.android.exoplayer2.util.Util; long positionOffset = Math.round((scaledPosition / 256) * dataSize); // Ensure returned positions skip the frame containing the XING header. positionOffset = Util.constrainValue(positionOffset, xingFrameSize, dataSize - 1); - return dataStartPosition + positionOffset; + return new SeekPoints(new SeekPoint(timeUs, dataStartPosition + positionOffset)); } @Override diff --git a/library/core/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 index f2412bf4ba..50fc0aec80 100644 --- a/library/core/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 @@ -26,6 +26,7 @@ import com.google.android.exoplayer2.extractor.ExtractorsFactory; import com.google.android.exoplayer2.extractor.GaplessInfoHolder; import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; +import com.google.android.exoplayer2.extractor.SeekPoint; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.mp4.Atom.ContainerAtom; import com.google.android.exoplayer2.metadata.Metadata; @@ -108,6 +109,7 @@ public final class Mp4Extractor implements Extractor, SeekMap { // Extractor outputs. private ExtractorOutput extractorOutput; private Mp4Track[] tracks; + private int firstVideoTrackIndex; private long durationUs; private boolean isQuickTime; @@ -196,21 +198,56 @@ public final class Mp4Extractor implements Extractor, SeekMap { } @Override - public long getPosition(long timeUs) { - long earliestSamplePosition = Long.MAX_VALUE; - for (Mp4Track track : tracks) { - TrackSampleTable sampleTable = track.sampleTable; - int sampleIndex = sampleTable.getIndexOfEarlierOrEqualSynchronizationSample(timeUs); + public SeekPoints getSeekPoints(long timeUs) { + if (tracks.length == 0) { + return new SeekPoints(SeekPoint.START); + } + + long firstTimeUs; + long firstOffset; + long secondTimeUs = C.TIME_UNSET; + long secondOffset = C.POSITION_UNSET; + + // If we have a video track, use it to establish one or two seek points. + if (firstVideoTrackIndex != C.INDEX_UNSET) { + TrackSampleTable sampleTable = tracks[firstVideoTrackIndex].sampleTable; + int sampleIndex = getSynchronizationSampleIndex(sampleTable, timeUs); if (sampleIndex == C.INDEX_UNSET) { - // Handle the case where the requested time is before the first synchronization sample. - sampleIndex = sampleTable.getIndexOfLaterOrEqualSynchronizationSample(timeUs); + return new SeekPoints(SeekPoint.START); } - long offset = sampleTable.offsets[sampleIndex]; - if (offset < earliestSamplePosition) { - earliestSamplePosition = offset; + long sampleTimeUs = sampleTable.timestampsUs[sampleIndex]; + firstTimeUs = sampleTimeUs; + firstOffset = sampleTable.offsets[sampleIndex]; + if (sampleTimeUs < timeUs && sampleIndex < sampleTable.sampleCount - 1) { + int secondSampleIndex = sampleTable.getIndexOfLaterOrEqualSynchronizationSample(timeUs); + if (secondSampleIndex != C.INDEX_UNSET && secondSampleIndex != sampleIndex) { + secondTimeUs = sampleTable.timestampsUs[secondSampleIndex]; + secondOffset = sampleTable.offsets[secondSampleIndex]; + } + } + } else { + firstTimeUs = timeUs; + firstOffset = Long.MAX_VALUE; + } + + // Take into account other tracks. + for (int i = 0; i < tracks.length; i++) { + if (i != firstVideoTrackIndex) { + TrackSampleTable sampleTable = tracks[i].sampleTable; + firstOffset = maybeAdjustSeekOffset(sampleTable, firstTimeUs, firstOffset); + if (secondTimeUs != C.TIME_UNSET) { + secondOffset = maybeAdjustSeekOffset(sampleTable, secondTimeUs, secondOffset); + } } } - return earliestSamplePosition; + + SeekPoint firstSeekPoint = new SeekPoint(firstTimeUs, firstOffset); + if (secondTimeUs == C.TIME_UNSET) { + return new SeekPoints(firstSeekPoint); + } else { + SeekPoint secondSeekPoint = new SeekPoint(secondTimeUs, secondOffset); + return new SeekPoints(firstSeekPoint, secondSeekPoint); + } } // Private methods. @@ -326,31 +363,11 @@ public final class Mp4Extractor implements Extractor, SeekMap { } } - /** - * Process an ftyp atom to determine whether the media is QuickTime. - * - * @param atomData The ftyp atom data. - * @return Whether the media is QuickTime. - */ - private static boolean processFtypAtom(ParsableByteArray atomData) { - atomData.setPosition(Atom.HEADER_SIZE); - int majorBrand = atomData.readInt(); - if (majorBrand == BRAND_QUICKTIME) { - return true; - } - atomData.skipBytes(4); // minor_version - while (atomData.bytesLeft() > 0) { - if (atomData.readInt() == BRAND_QUICKTIME) { - return true; - } - } - return false; - } - /** * Updates the stored track metadata to reflect the contents of the specified moov atom. */ private void processMoovAtom(ContainerAtom moov) throws ParserException { + int firstVideoTrackIndex = C.INDEX_UNSET; long durationUs = C.TIME_UNSET; List tracks = new ArrayList<>(); long earliestSampleOffset = Long.MAX_VALUE; @@ -402,6 +419,9 @@ public final class Mp4Extractor implements Extractor, SeekMap { mp4Track.trackOutput.format(format); durationUs = Math.max(durationUs, track.durationUs); + if (track.type == C.TRACK_TYPE_VIDEO && firstVideoTrackIndex == C.INDEX_UNSET) { + firstVideoTrackIndex = tracks.size(); + } tracks.add(mp4Track); long firstSampleOffset = trackSampleTable.offsets[0]; @@ -409,8 +429,10 @@ public final class Mp4Extractor implements Extractor, SeekMap { earliestSampleOffset = firstSampleOffset; } } + this.firstVideoTrackIndex = firstVideoTrackIndex; this.durationUs = durationUs; this.tracks = tracks.toArray(new Mp4Track[tracks.size()]); + extractorOutput.endTracks(); extractorOutput.seekMap(this); } @@ -538,6 +560,66 @@ public final class Mp4Extractor implements Extractor, SeekMap { } } + /** + * Adjusts a seek point offset to take into account the track with the given {@code sampleTable}, + * for a given {@code seekTimeUs}. + * + * @param sampleTable The sample table to use. + * @param seekTimeUs The seek time in microseconds. + * @param offset The current offset. + * @return The adjusted offset. + */ + private static long maybeAdjustSeekOffset( + TrackSampleTable sampleTable, long seekTimeUs, long offset) { + int sampleIndex = getSynchronizationSampleIndex(sampleTable, seekTimeUs); + if (sampleIndex == C.INDEX_UNSET) { + return offset; + } + long sampleOffset = sampleTable.offsets[sampleIndex]; + return Math.min(sampleOffset, offset); + } + + /** + * Returns the index of the synchronization sample before or at {@code timeUs}, or the index of + * the first synchronization sample if located after {@code timeUs}, or {@link C#INDEX_UNSET} if + * there are no synchronization samples in the table. + * + * @param sampleTable The sample table in which to locate a synchronization sample. + * @param timeUs A time in microseconds. + * @return The index of the synchronization sample before or at {@code timeUs}, or the index of + * the first synchronization sample if located after {@code timeUs}, or {@link C#INDEX_UNSET} + * if there are no synchronization samples in the table. + */ + private static int getSynchronizationSampleIndex(TrackSampleTable sampleTable, long timeUs) { + int sampleIndex = sampleTable.getIndexOfEarlierOrEqualSynchronizationSample(timeUs); + if (sampleIndex == C.INDEX_UNSET) { + // Handle the case where the requested time is before the first synchronization sample. + sampleIndex = sampleTable.getIndexOfLaterOrEqualSynchronizationSample(timeUs); + } + return sampleIndex; + } + + /** + * Process an ftyp atom to determine whether the media is QuickTime. + * + * @param atomData The ftyp atom data. + * @return Whether the media is QuickTime. + */ + private static boolean processFtypAtom(ParsableByteArray atomData) { + atomData.setPosition(Atom.HEADER_SIZE); + int majorBrand = atomData.readInt(); + if (majorBrand == BRAND_QUICKTIME) { + return true; + } + atomData.skipBytes(4); // minor_version + while (atomData.bytesLeft() > 0) { + if (atomData.readInt() == BRAND_QUICKTIME) { + return true; + } + } + return false; + } + /** * Returns whether the extractor should decode a leaf atom with type {@code atom}. */ diff --git a/library/core/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 index 77def57275..042ab681f9 100644 --- a/library/core/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 @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor.ogg; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.SeekMap; +import com.google.android.exoplayer2.extractor.SeekPoint; import com.google.android.exoplayer2.util.Assertions; import java.io.EOFException; import java.io.IOException; @@ -219,12 +220,13 @@ import java.io.IOException; } @Override - public long getPosition(long timeUs) { + public SeekPoints getSeekPoints(long timeUs) { if (timeUs == 0) { - return startPosition; + return new SeekPoints(new SeekPoint(0, startPosition)); } long granule = streamReader.convertTimeToGranule(timeUs); - return getEstimatedPosition(startPosition, granule, DEFAULT_OFFSET); + long estimatedPosition = getEstimatedPosition(startPosition, granule, DEFAULT_OFFSET); + return new SeekPoints(new SeekPoint(timeUs, estimatedPosition)); } @Override diff --git a/library/core/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 index 304fb3dd96..5eb0727908 100644 --- a/library/core/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 @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor.ogg; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.SeekMap; +import com.google.android.exoplayer2.extractor.SeekPoint; import com.google.android.exoplayer2.util.FlacStreamInfo; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -192,10 +193,20 @@ import java.util.List; } @Override - public long getPosition(long timeUs) { + public SeekPoints getSeekPoints(long timeUs) { long granule = convertTimeToGranule(timeUs); int index = Util.binarySearchFloor(seekPointGranules, granule, true, true); - return firstFrameOffset + seekPointOffsets[index]; + long seekTimeUs = convertGranuleToTime(seekPointGranules[index]); + long seekPosition = firstFrameOffset + seekPointOffsets[index]; + SeekPoint seekPoint = new SeekPoint(seekTimeUs, seekPosition); + if (seekTimeUs >= timeUs || index == seekPointGranules.length - 1) { + return new SeekPoints(seekPoint); + } else { + long secondSeekTimeUs = convertGranuleToTime(seekPointGranules[index + 1]); + long secondSeekPosition = firstFrameOffset + seekPointOffsets[index + 1]; + SeekPoint secondSeekPoint = new SeekPoint(secondSeekTimeUs, secondSeekPosition); + return new SeekPoints(seekPoint, secondSeekPoint); + } } @Override diff --git a/library/core/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 index 2cdd31cb6f..33db6c1e6c 100644 --- a/library/core/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 @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.extractor.wav; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.SeekMap; +import com.google.android.exoplayer2.extractor.SeekPoint; import com.google.android.exoplayer2.util.Util; /** Header for a WAV file. */ @@ -83,13 +84,22 @@ import com.google.android.exoplayer2.util.Util; } @Override - public long getPosition(long timeUs) { + public SeekPoints getSeekPoints(long timeUs) { long positionOffset = (timeUs * averageBytesPerSecond) / C.MICROS_PER_SECOND; // Constrain to nearest preceding frame offset. positionOffset = (positionOffset / blockAlignment) * blockAlignment; positionOffset = Util.constrainValue(positionOffset, 0, dataSize - blockAlignment); - // Add data start position. - return dataStartPosition + positionOffset; + long seekPosition = dataStartPosition + positionOffset; + long seekTimeUs = getTimeUs(seekPosition); + SeekPoint seekPoint = new SeekPoint(seekTimeUs, seekPosition); + if (seekTimeUs >= timeUs || positionOffset == dataSize - blockAlignment) { + return new SeekPoints(seekPoint); + } else { + long secondSeekPosition = seekPosition + blockAlignment; + long secondSeekTimeUs = getTimeUs(secondSeekPosition); + SeekPoint secondSeekPoint = new SeekPoint(secondSeekTimeUs, secondSeekPosition); + return new SeekPoints(seekPoint, secondSeekPoint); + } } // Misc getters. @@ -100,7 +110,8 @@ import com.google.android.exoplayer2.util.Util; * @param position The position in bytes. */ public long getTimeUs(long position) { - return position * C.MICROS_PER_SECOND / averageBytesPerSecond; + long positionOffset = Math.max(0, position - dataStartPosition); + return (positionOffset * C.MICROS_PER_SECOND) / averageBytesPerSecond; } /** Returns the bytes per frame of this WAV. */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java index c0586b3a28..344286ed3d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -549,7 +549,8 @@ import java.util.Arrays; pendingResetPositionUs = C.TIME_UNSET; return; } - loadable.setLoadPosition(seekMap.getPosition(pendingResetPositionUs), pendingResetPositionUs); + loadable.setLoadPosition( + seekMap.getSeekPoints(pendingResetPositionUs).first.position, pendingResetPositionUs); pendingResetPositionUs = C.TIME_UNSET; } extractedSamplesCountAtStartOfLoad = getExtractedSamplesCount(); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp3/XingSeekerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp3/XingSeekerTest.java index e644abc7ef..46cd7a2451 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp3/XingSeekerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp3/XingSeekerTest.java @@ -19,6 +19,8 @@ import static com.google.common.truth.Truth.assertThat; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.MpegAudioHeader; +import com.google.android.exoplayer2.extractor.SeekMap.SeekPoints; +import com.google.android.exoplayer2.extractor.SeekPoint; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; import org.junit.Before; @@ -92,27 +94,39 @@ public final class XingSeekerTest { } @Test - public void testGetPositionAtStartOfStream() { - assertThat(seeker.getPosition(0)).isEqualTo(XING_FRAME_POSITION + xingFrameSize); - assertThat(seekerWithInputLength.getPosition(0)).isEqualTo(XING_FRAME_POSITION + xingFrameSize); + public void testGetSeekPointsAtStartOfStream() { + SeekPoints seekPoints = seeker.getSeekPoints(0); + SeekPoint seekPoint = seekPoints.first; + assertThat(seekPoint).isEqualTo(seekPoints.second); + assertThat(seekPoint.timeUs).isEqualTo(0); + assertThat(seekPoint.position).isEqualTo(XING_FRAME_POSITION + xingFrameSize); } @Test - public void testGetPositionAtEndOfStream() { - assertThat(seeker.getPosition(STREAM_DURATION_US)) - .isEqualTo(STREAM_LENGTH - 1); - assertThat(seekerWithInputLength.getPosition(STREAM_DURATION_US)) - .isEqualTo(STREAM_LENGTH - 1); + public void testGetSeekPointsAtEndOfStream() { + SeekPoints seekPoints = seeker.getSeekPoints(STREAM_DURATION_US); + SeekPoint seekPoint = seekPoints.first; + assertThat(seekPoint).isEqualTo(seekPoints.second); + assertThat(seekPoint.timeUs).isEqualTo(STREAM_DURATION_US); + assertThat(seekPoint.position).isEqualTo(STREAM_LENGTH - 1); } @Test public void testGetTimeForAllPositions() { for (int offset = xingFrameSize; offset < DATA_SIZE_BYTES; offset++) { int position = XING_FRAME_POSITION + offset; + // Test seeker. long timeUs = seeker.getTimeUs(position); - assertThat(seeker.getPosition(timeUs)).isEqualTo(position); + SeekPoints seekPoints = seeker.getSeekPoints(timeUs); + SeekPoint seekPoint = seekPoints.first; + assertThat(seekPoint).isEqualTo(seekPoints.second); + assertThat(seekPoint.position).isEqualTo(position); + // Test seekerWithInputLength. timeUs = seekerWithInputLength.getTimeUs(position); - assertThat(seekerWithInputLength.getPosition(timeUs)).isEqualTo(position); + seekPoints = seekerWithInputLength.getSeekPoints(timeUs); + seekPoint = seekPoints.first; + assertThat(seekPoint).isEqualTo(seekPoints.second); + assertThat(seekPoint.position).isEqualTo(position); } } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExtractorAsserts.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExtractorAsserts.java index db63662c45..8c419ce1a0 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExtractorAsserts.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExtractorAsserts.java @@ -143,7 +143,7 @@ public final class ExtractorAsserts { long durationUs = seekMap.getDurationUs(); for (int j = 0; j < 4; j++) { long timeUs = (durationUs * j) / 3; - long position = seekMap.getPosition(timeUs); + long position = seekMap.getSeekPoints(timeUs).first.position; input.setPosition((int) position); for (int i = 0; i < extractorOutput.numberOfTracks; i++) { extractorOutput.trackOutputs.valueAt(i).clear(); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorOutput.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorOutput.java index ee8927ea21..3f9a7c542f 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorOutput.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorOutput.java @@ -78,7 +78,7 @@ public final class FakeExtractorOutput implements ExtractorOutput, Dumper.Dumpab Assert.assertNotNull(seekMap); Assert.assertEquals(expected.seekMap.getClass(), seekMap.getClass()); Assert.assertEquals(expected.seekMap.isSeekable(), seekMap.isSeekable()); - Assert.assertEquals(expected.seekMap.getPosition(0), seekMap.getPosition(0)); + Assert.assertEquals(expected.seekMap.getSeekPoints(0), seekMap.getSeekPoints(0)); } for (int i = 0; i < numberOfTracks; i++) { Assert.assertEquals(expected.trackOutputs.keyAt(i), trackOutputs.keyAt(i)); @@ -114,10 +114,11 @@ public final class FakeExtractorOutput implements ExtractorOutput, Dumper.Dumpab @Override public void dump(Dumper dumper) { if (seekMap != null) { - dumper.startBlock("seekMap") + dumper + .startBlock("seekMap") .add("isSeekable", seekMap.isSeekable()) .addTime("duration", seekMap.getDurationUs()) - .add("getPosition(0)", seekMap.getPosition(0)) + .add("getPosition(0)", seekMap.getSeekPoints(0)) .endBlock(); } dumper.add("numberOfTracks", numberOfTracks); From 002df729a558773ca1dc347ad1a05c4cedd449d5 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Mon, 4 Dec 2017 10:50:33 -0800 Subject: [PATCH 0830/2472] Allow late HLS sample queue building Issue:#3149 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177836048 --- ISSUE_TEMPLATE | 2 + .../source/hls/HlsSampleStream.java | 38 ++++- .../source/hls/HlsSampleStreamWrapper.java | 140 ++++++++++++------ .../hls/SampleQueueMappingException.java | 29 ++++ 4 files changed, 156 insertions(+), 53 deletions(-) create mode 100644 library/hls/src/main/java/com/google/android/exoplayer2/source/hls/SampleQueueMappingException.java diff --git a/ISSUE_TEMPLATE b/ISSUE_TEMPLATE index e85c0c28c7..1b912312d1 100644 --- a/ISSUE_TEMPLATE +++ b/ISSUE_TEMPLATE @@ -1,3 +1,5 @@ +*** ISSUES THAT IGNORE THIS TEMPLATE WILL BE CLOSED WITHOUT INVESTIGATION *** + Before filing an issue: ----------------------- - Search existing issues, including issues that are closed. diff --git a/library/hls/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 index 0388f354ce..d53db1feaf 100644 --- a/library/hls/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 @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source.hls; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.source.SampleStream; @@ -25,33 +26,60 @@ import java.io.IOException; */ /* package */ final class HlsSampleStream implements SampleStream { - public final int sampleQueueIndex; - + private final int trackGroupIndex; private final HlsSampleStreamWrapper sampleStreamWrapper; + private int sampleQueueIndex; - public HlsSampleStream(HlsSampleStreamWrapper sampleStreamWrapper, int sampleQueueIndex) { + public HlsSampleStream(HlsSampleStreamWrapper sampleStreamWrapper, int trackGroupIndex) { this.sampleStreamWrapper = sampleStreamWrapper; - this.sampleQueueIndex = sampleQueueIndex; + this.trackGroupIndex = trackGroupIndex; } + public void unbindSampleQueue() { + if (sampleQueueIndex != C.INDEX_UNSET) { + sampleStreamWrapper.unbindSampleQueue(trackGroupIndex); + } + } + + // SampleStream implementation. + @Override public boolean isReady() { - return sampleStreamWrapper.isReady(sampleQueueIndex); + return ensureBoundSampleQueue() && sampleStreamWrapper.isReady(sampleQueueIndex); } @Override public void maybeThrowError() throws IOException { + if (!ensureBoundSampleQueue()) { + throw new SampleQueueMappingException( + sampleStreamWrapper.getTrackGroups().get(trackGroupIndex).getFormat(0).sampleMimeType); + } sampleStreamWrapper.maybeThrowError(); } @Override public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean requireFormat) { + if (!ensureBoundSampleQueue()) { + return C.RESULT_NOTHING_READ; + } return sampleStreamWrapper.readData(sampleQueueIndex, formatHolder, buffer, requireFormat); } @Override public int skipData(long positionUs) { + if (!ensureBoundSampleQueue()) { + return 0; + } return sampleStreamWrapper.skipData(sampleQueueIndex, positionUs); } + // Internal methods. + + private boolean ensureBoundSampleQueue() { + if (sampleQueueIndex != C.INDEX_UNSET) { + return true; + } + sampleQueueIndex = sampleStreamWrapper.bindSampleQueueToSampleStream(trackGroupIndex); + return sampleQueueIndex != C.INDEX_UNSET; + } } diff --git a/library/hls/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 index f4ba9a6eac..dbb71329c5 100644 --- a/library/hls/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 @@ -88,13 +88,14 @@ import java.util.Arrays; private final HlsChunkSource.HlsChunkHolder nextChunkHolder; private final ArrayList mediaChunks; private final Runnable maybeFinishPrepareRunnable; + private final Runnable onTracksEndedRunnable; private final Handler handler; private SampleQueue[] sampleQueues; private int[] sampleQueueTrackIds; private boolean sampleQueuesBuilt; private boolean prepared; - private int enabledSampleQueueCount; + private int enabledTrackGroupCount; private Format downstreamTrackFormat; private boolean released; @@ -108,13 +109,16 @@ import java.util.Arrays; private boolean[] sampleQueuesEnabledStates; private boolean[] sampleQueueIsAudioVideoFlags; - private long sampleOffsetUs; private long lastSeekPositionUs; private long pendingResetPositionUs; private boolean pendingResetUpstreamFormats; private boolean seenFirstTrackSelection; private boolean loadingFinished; + // Accessed only by the loading thread. + private boolean tracksEnded; + private long sampleOffsetUs; + /** * @param trackType The type of the track. One of the {@link C} {@code TRACK_TYPE_*} constants. * @param callback A callback for the wrapper. @@ -143,12 +147,20 @@ import java.util.Arrays; sampleQueueIsAudioVideoFlags = new boolean[0]; sampleQueuesEnabledStates = new boolean[0]; mediaChunks = new ArrayList<>(); - maybeFinishPrepareRunnable = new Runnable() { - @Override - public void run() { - maybeFinishPrepare(); - } - }; + maybeFinishPrepareRunnable = + new Runnable() { + @Override + public void run() { + maybeFinishPrepare(); + } + }; + onTracksEndedRunnable = + new Runnable() { + @Override + public void run() { + onTracksEnded(); + } + }; handler = new Handler(); lastSeekPositionUs = positionUs; pendingResetPositionUs = positionUs; @@ -166,8 +178,8 @@ import java.util.Arrays; */ public void prepareSingleTrack(Format format) { track(0, C.TRACK_TYPE_UNKNOWN).format(format); - sampleQueuesBuilt = true; - maybeFinishPrepare(); + tracksEnded = true; + onTracksEnded(); } public void maybeThrowPrepareError() throws IOException { @@ -178,6 +190,19 @@ import java.util.Arrays; return trackGroups; } + public int bindSampleQueueToSampleStream(int trackGroupIndex) { + int sampleQueueIndex = trackGroupToSampleQueueIndex[trackGroupIndex]; + if (sampleQueueIndex == C.INDEX_UNSET) { + return C.INDEX_UNSET; + } + setSampleQueueEnabledState(sampleQueueIndex, true); + return sampleQueueIndex; + } + + public void unbindSampleQueue(int trackGroupIndex) { + setSampleQueueEnabledState(trackGroupToSampleQueueIndex[trackGroupIndex], false); + } + /** * Called by the parent {@link HlsMediaPeriod} when a track selection occurs. * @@ -198,20 +223,23 @@ import java.util.Arrays; public boolean selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, SampleStream[] streams, boolean[] streamResetFlags, long positionUs, boolean forceReset) { Assertions.checkState(prepared); - int oldEnabledSampleQueueCount = enabledSampleQueueCount; + int oldEnabledTrackGroupCount = enabledTrackGroupCount; // Deselect old tracks. for (int i = 0; i < selections.length; i++) { if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) { - setSampleQueueEnabledState(((HlsSampleStream) streams[i]).sampleQueueIndex, false); + enabledTrackGroupCount--; + ((HlsSampleStream) streams[i]).unbindSampleQueue(); streams[i] = null; } } // We'll always need to seek if we're being forced to reset, or if this is a first selection to // a position other than the one we started preparing with, or if we're making a selection // having previously disabled all tracks. - boolean seekRequired = forceReset - || (seenFirstTrackSelection ? oldEnabledSampleQueueCount == 0 - : positionUs != lastSeekPositionUs); + boolean seekRequired = + forceReset + || (seenFirstTrackSelection + ? oldEnabledTrackGroupCount == 0 + : positionUs != lastSeekPositionUs); // Get the old (i.e. current before the loop below executes) primary track selection. The new // primary selection will equal the old one unless it's changed in the loop. TrackSelection oldPrimaryTrackSelection = chunkSource.getTrackSelection(); @@ -219,19 +247,18 @@ import java.util.Arrays; // Select new tracks. for (int i = 0; i < selections.length; i++) { if (streams[i] == null && selections[i] != null) { + enabledTrackGroupCount++; TrackSelection selection = selections[i]; int trackGroupIndex = trackGroups.indexOf(selection.getTrackGroup()); - int sampleQueueIndex = trackGroupToSampleQueueIndex[trackGroupIndex]; - setSampleQueueEnabledState(sampleQueueIndex, true); if (trackGroupIndex == primaryTrackGroupIndex) { primaryTrackSelection = selection; chunkSource.selectTracks(selection); } - streams[i] = new HlsSampleStream(this, sampleQueueIndex); + streams[i] = new HlsSampleStream(this, trackGroupIndex); streamResetFlags[i] = true; // If there's still a chance of avoiding a seek, try and seek within the sample queue. - if (!seekRequired) { - SampleQueue sampleQueue = sampleQueues[sampleQueueIndex]; + if (sampleQueuesBuilt && !seekRequired) { + SampleQueue sampleQueue = sampleQueues[trackGroupToSampleQueueIndex[trackGroupIndex]]; sampleQueue.rewind(); // A seek can be avoided if we're able to advance to the current playback position in the // sample queue, or if we haven't read anything from the queue since the previous seek @@ -243,14 +270,16 @@ import java.util.Arrays; } } - if (enabledSampleQueueCount == 0) { + if (enabledTrackGroupCount == 0) { chunkSource.reset(); downstreamTrackFormat = null; mediaChunks.clear(); if (loader.isLoading()) { - // Discard as much as we can synchronously. - for (SampleQueue sampleQueue : sampleQueues) { - sampleQueue.discardToEnd(); + if (sampleQueuesBuilt) { + // Discard as much as we can synchronously. + for (SampleQueue sampleQueue : sampleQueues) { + sampleQueue.discardToEnd(); + } } loader.cancelLoading(); } else { @@ -297,6 +326,9 @@ import java.util.Arrays; } public void discardBuffer(long positionUs, boolean toKeyframe) { + if (!sampleQueuesBuilt) { + return; + } int sampleQueueCount = sampleQueues.length; for (int i = 0; i < sampleQueueCount; i++) { sampleQueues[i].discardTo(positionUs, toKeyframe, sampleQueuesEnabledStates[i]); @@ -314,7 +346,7 @@ import java.util.Arrays; public boolean seekToUs(long positionUs, boolean forceReset) { lastSeekPositionUs = positionUs; // If we're not forced to reset nor have a pending reset, see if we can seek within the buffer. - if (!forceReset && !isPendingReset() && seekInsideBufferUs(positionUs)) { + if (sampleQueuesBuilt && !forceReset && !isPendingReset() && seekInsideBufferUs(positionUs)) { return false; } // We were unable to seek within the buffer, so need to reset. @@ -426,9 +458,11 @@ import java.util.Arrays; if (lastCompletedMediaChunk != null) { bufferedPositionUs = Math.max(bufferedPositionUs, lastCompletedMediaChunk.endTimeUs); } - for (SampleQueue sampleQueue : sampleQueues) { - bufferedPositionUs = Math.max(bufferedPositionUs, - sampleQueue.getLargestQueuedTimestampUs()); + if (sampleQueuesBuilt) { + for (SampleQueue sampleQueue : sampleQueues) { + bufferedPositionUs = + Math.max(bufferedPositionUs, sampleQueue.getLargestQueuedTimestampUs()); + } } return bufferedPositionUs; } @@ -513,7 +547,7 @@ import java.util.Arrays; loadable.endTimeUs, elapsedRealtimeMs, loadDurationMs, loadable.bytesLoaded()); if (!released) { resetSampleQueues(); - if (enabledSampleQueueCount > 0) { + if (enabledTrackGroupCount > 0) { callback.onContinueLoadingRequested(this); } } @@ -582,7 +616,7 @@ import java.util.Arrays; return sampleQueues[i]; } } - if (sampleQueuesBuilt) { + if (tracksEnded) { Log.w(TAG, "Unmapped track with id " + id + " of type " + type); return new DummyTrackOutput(); } @@ -603,8 +637,8 @@ import java.util.Arrays; @Override public void endTracks() { - sampleQueuesBuilt = true; - handler.post(maybeFinishPrepareRunnable); + tracksEnded = true; + handler.post(onTracksEndedRunnable); } @Override @@ -616,9 +650,7 @@ import java.util.Arrays; @Override public void onUpstreamFormatChanged(Format format) { - if (!prepared) { - handler.post(maybeFinishPrepareRunnable); - } + handler.post(maybeFinishPrepareRunnable); } // Called by the loading thread. @@ -650,6 +682,11 @@ import java.util.Arrays; pendingResetUpstreamFormats = false; } + private void onTracksEnded() { + sampleQueuesBuilt = true; + maybeFinishPrepare(); + } + private void maybeFinishPrepare() { if (released || prepared || !sampleQueuesBuilt) { return; @@ -739,14 +776,14 @@ import java.util.Arrays; if (i == primaryExtractorTrackIndex) { Format[] formats = new Format[chunkSourceTrackCount]; for (int j = 0; j < chunkSourceTrackCount; j++) { - formats[j] = deriveFormat(chunkSourceTrackGroup.getFormat(j), sampleFormat); + formats[j] = deriveFormat(chunkSourceTrackGroup.getFormat(j), sampleFormat, true); } trackGroups[i] = new TrackGroup(formats); primaryTrackGroupIndex = i; } else { Format trackFormat = primaryExtractorTrackType == PRIMARY_TYPE_VIDEO && MimeTypes.isAudio(sampleFormat.sampleMimeType) ? muxedAudioFormat : null; - trackGroups[i] = new TrackGroup(deriveFormat(trackFormat, sampleFormat)); + trackGroups[i] = new TrackGroup(deriveFormat(trackFormat, sampleFormat, false)); } } this.trackGroups = new TrackGroupArray(trackGroups); @@ -761,7 +798,6 @@ import java.util.Arrays; private void setSampleQueueEnabledState(int sampleQueueIndex, boolean enabledState) { Assertions.checkState(sampleQueuesEnabledStates[sampleQueueIndex] != enabledState); sampleQueuesEnabledStates[sampleQueueIndex] = enabledState; - enabledSampleQueueCount = enabledSampleQueueCount + (enabledState ? 1 : -1); } private HlsMediaChunk getLastMediaChunk() { @@ -797,22 +833,30 @@ import java.util.Arrays; } /** - * Derives a track format corresponding to a given container format, by combining it with sample - * level information obtained from the samples. + * Derives a track format using master playlist and sample format information. * - * @param containerFormat The container format for which the track format should be derived. - * @param sampleFormat A sample format from which to obtain sample level information. + * @param playlistFormat The format information obtained from the master playlist. + * @param sampleFormat The format information obtained from the samples. + * @param propagateBitrate Whether the bitrate from the playlist format should be included in the + * derived format. * @return The derived track format. */ - private static Format deriveFormat(Format containerFormat, Format sampleFormat) { - if (containerFormat == null) { + private static Format deriveFormat( + Format playlistFormat, Format sampleFormat, boolean propagateBitrate) { + if (playlistFormat == null) { return sampleFormat; } + int bitrate = propagateBitrate ? playlistFormat.bitrate : Format.NO_VALUE; int sampleTrackType = MimeTypes.getTrackType(sampleFormat.sampleMimeType); - String codecs = Util.getCodecsOfType(containerFormat.codecs, sampleTrackType); - return sampleFormat.copyWithContainerInfo(containerFormat.id, codecs, containerFormat.bitrate, - containerFormat.width, containerFormat.height, containerFormat.selectionFlags, - containerFormat.language); + String codecs = Util.getCodecsOfType(playlistFormat.codecs, sampleTrackType); + return sampleFormat.copyWithContainerInfo( + playlistFormat.id, + codecs, + bitrate, + playlistFormat.width, + playlistFormat.height, + playlistFormat.selectionFlags, + playlistFormat.language); } private static boolean isMediaChunk(Chunk chunk) { diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/SampleQueueMappingException.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/SampleQueueMappingException.java new file mode 100644 index 0000000000..2d430d2c79 --- /dev/null +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/SampleQueueMappingException.java @@ -0,0 +1,29 @@ +/* + * 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.hls; + +import com.google.android.exoplayer2.source.SampleQueue; +import com.google.android.exoplayer2.source.TrackGroup; +import java.io.IOException; + +/** Thrown when it is not possible to map a {@link TrackGroup} to a {@link SampleQueue}. */ +public final class SampleQueueMappingException extends IOException { + + /** @param mimeType The mime type of the track group whose mapping failed. */ + public SampleQueueMappingException(String mimeType) { + super("Unable to bind a sample queue to TrackGroup with mime type " + mimeType + "."); + } +} From b3ebdaaed332d2c1f4fa24ee011bbbd23c743deb Mon Sep 17 00:00:00 2001 From: amesbah Date: Mon, 4 Dec 2017 10:50:51 -0800 Subject: [PATCH 0831/2472] Add @SuppressWarnings("ComparableType") for instances of a class implementing 'Comparable' where T is not compatible with the type of the class. In order to facilitate enabling a compile-time error check, we are suppressing these existing instances. Once the compile-time error is enabled, we will file bugs to clean up any unfixed instances in []. Note that this CL should result in no effective changes to the code, but the code as currently-written might contain a real bug. If you'd prefer to fix the bug now, please either reply with edits, or accept this CL then follow up with a change that fixes the underlying issue. Tested: tap_presubmit: [] Some tests failed; test failures are believed to be unrelated to this CL ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177836122 --- .../exoplayer2/source/hls/playlist/HlsMediaPlaylist.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/library/hls/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 index b21ecb02d5..1f44607f98 100644 --- a/library/hls/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 @@ -29,9 +29,8 @@ import java.util.List; */ public final class HlsMediaPlaylist extends HlsPlaylist { - /** - * Media segment reference. - */ + /** Media segment reference. */ + @SuppressWarnings("ComparableType") public static final class Segment implements Comparable { /** From 59f6b059b0c44c8dcc572ade7fa9f6d815ea0d80 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 5 Dec 2017 00:29:56 -0800 Subject: [PATCH 0832/2472] Make one ad request in ImaAdsLoader This fixes an issue where quickly detaching and reattaching the player might cause ads to be requested multiple times with both responses handled. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177922167 --- .../exoplayer2/ext/ima/ImaAdsLoader.java | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index 743a428020..0eba9db2ed 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -50,6 +50,7 @@ import com.google.android.exoplayer2.source.ads.AdPlaybackState; import com.google.android.exoplayer2.source.ads.AdsLoader; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -120,6 +121,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A private final AdDisplayContainer adDisplayContainer; private final com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader; + private Object pendingAdRequestContext; private List supportedMimeTypes; private EventListener eventListener; private Player player; @@ -183,10 +185,6 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A * Whether {@link #getContentProgress()} has sent {@link #pendingContentPositionMs} to IMA. */ private boolean sentPendingContentPositionMs; - /** - * Whether {@link #release()} has been called. - */ - private boolean released; /** * Creates a new IMA ads loader. @@ -296,7 +294,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A @Override public void release() { - released = true; + pendingAdRequestContext = null; if (adsManager != null) { adsManager.destroy(); adsManager = null; @@ -308,10 +306,11 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A @Override public void onAdsManagerLoaded(AdsManagerLoadedEvent adsManagerLoadedEvent) { AdsManager adsManager = adsManagerLoadedEvent.getAdsManager(); - if (released) { + if (!Util.areEqual(pendingAdRequestContext, adsManagerLoadedEvent.getUserRequestContext())) { adsManager.destroy(); return; } + pendingAdRequestContext = null; this.adsManager = adsManager; adsManager.addAdErrorListener(this); adsManager.addAdEventListener(this); @@ -403,6 +402,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A Log.d(TAG, "onAdError " + adErrorEvent); } if (adsManager == null) { + pendingAdRequestContext = null; adPlaybackState = new AdPlaybackState(new long[0]); updateAdPlaybackState(); } @@ -623,10 +623,16 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A // Internal methods. private void requestAds() { + if (pendingAdRequestContext != null) { + // Ad request already in flight. + return; + } + pendingAdRequestContext = new Object(); AdsRequest request = imaSdkFactory.createAdsRequest(); request.setAdTagUrl(adTagUri.toString()); request.setAdDisplayContainer(adDisplayContainer); request.setContentProgressProvider(this); + request.setUserRequestContext(pendingAdRequestContext); adsLoader.requestAds(request); } From f0e9dbf148fb6c7090fe6c69eae4beaf89fe6e89 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 5 Dec 2017 00:42:28 -0800 Subject: [PATCH 0833/2472] Fix HLS (broken on [] ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177922948 --- .../google/android/exoplayer2/source/hls/HlsSampleStream.java | 1 + 1 file changed, 1 insertion(+) diff --git a/library/hls/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 index d53db1feaf..d180039b30 100644 --- a/library/hls/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 @@ -33,6 +33,7 @@ import java.io.IOException; public HlsSampleStream(HlsSampleStreamWrapper sampleStreamWrapper, int trackGroupIndex) { this.sampleStreamWrapper = sampleStreamWrapper; this.trackGroupIndex = trackGroupIndex; + sampleQueueIndex = C.INDEX_UNSET; } public void unbindSampleQueue() { From f2f767bc1229cc982d3011b29e90cc0239d1f45a Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 5 Dec 2017 03:53:01 -0800 Subject: [PATCH 0834/2472] Invoke onLoadCanceled/Completed for ExtractorMediaSource ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177936271 --- .../source/ExtractorMediaPeriod.java | 30 +++++++++++++++++-- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java index 344286ed3d..17a6c3bcb8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -405,14 +405,26 @@ import java.util.Arrays; @Override public void onLoadCompleted(ExtractingLoadable loadable, long elapsedRealtimeMs, long loadDurationMs) { - copyLengthFromLoader(loadable); - loadingFinished = true; if (durationUs == C.TIME_UNSET) { long largestQueuedTimestampUs = getLargestQueuedTimestampUs(); durationUs = largestQueuedTimestampUs == Long.MIN_VALUE ? 0 : largestQueuedTimestampUs + DEFAULT_LAST_SAMPLE_DURATION_US; listener.onSourceInfoRefreshed(durationUs, seekMap.isSeekable()); } + eventDispatcher.loadCompleted( + loadable.dataSpec, + C.DATA_TYPE_MEDIA, + C.TRACK_TYPE_UNKNOWN, + /* trackFormat= */ null, + C.SELECTION_REASON_UNKNOWN, + /* trackSelectionData= */ null, + /* mediaStartTimeUs= */ 0, + durationUs, + elapsedRealtimeMs, + loadDurationMs, + loadable.bytesLoaded); + copyLengthFromLoader(loadable); + loadingFinished = true; callback.onContinueLoadingRequested(this); } @@ -422,6 +434,18 @@ import java.util.Arrays; if (released) { return; } + eventDispatcher.loadCanceled( + loadable.dataSpec, + C.DATA_TYPE_MEDIA, + C.TRACK_TYPE_UNKNOWN, + /* trackFormat= */ null, + C.SELECTION_REASON_UNKNOWN, + /* trackSelectionData= */ null, + /* mediaStartTimeUs= */ 0, + durationUs, + elapsedRealtimeMs, + loadDurationMs, + loadable.bytesLoaded); copyLengthFromLoader(loadable); for (SampleQueue sampleQueue : sampleQueues) { sampleQueue.reset(); @@ -434,7 +458,6 @@ import java.util.Arrays; @Override public int onLoadError(ExtractingLoadable loadable, long elapsedRealtimeMs, long loadDurationMs, IOException error) { - copyLengthFromLoader(loadable); boolean isErrorFatal = isLoadableExceptionFatal(error); eventDispatcher.loadError( loadable.dataSpec, @@ -450,6 +473,7 @@ import java.util.Arrays; loadable.bytesLoaded, error, /* wasCanceled= */ isErrorFatal); + copyLengthFromLoader(loadable); if (isErrorFatal) { return Loader.DONT_RETRY_FATAL; } From 2b317234346bacfe8d77e53cfb3ad6b9760db5e1 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 5 Dec 2017 04:21:18 -0800 Subject: [PATCH 0835/2472] Remove self @link ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177938212 --- .../java/com/google/android/exoplayer2/SimpleExoPlayer.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 909a5d0fd5..17cf118ea3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -873,15 +873,15 @@ public class SimpleExoPlayer implements ExoPlayer { // Internal methods. /** - * Creates the ExoPlayer implementation used by this {@link SimpleExoPlayer}. + * Creates the {@link ExoPlayer} implementation used by this instance. * * @param renderers The {@link Renderer}s that will 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. * @return A new {@link ExoPlayer} instance. */ - protected ExoPlayer createExoPlayerImpl(Renderer[] renderers, TrackSelector trackSelector, - LoadControl loadControl) { + protected ExoPlayer createExoPlayerImpl( + Renderer[] renderers, TrackSelector trackSelector, LoadControl loadControl) { return new ExoPlayerImpl(renderers, trackSelector, loadControl); } From a155edc5685d8ebe6c15724dfe843a0652693d80 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 5 Dec 2017 05:19:48 -0800 Subject: [PATCH 0836/2472] Hide subtitles when switching player in SimpleExoPlayerView ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177941993 --- .../google/android/exoplayer2/ui/SimpleExoPlayerView.java | 3 +++ .../java/com/google/android/exoplayer2/ui/SubtitleView.java | 5 +++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index b09e80c591..dcc1c62569 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -425,6 +425,9 @@ public final class SimpleExoPlayerView extends FrameLayout { if (shutterView != null) { shutterView.setVisibility(VISIBLE); } + if (subtitleView != null) { + subtitleView.setCues(null); + } if (player != null) { if (surfaceView instanceof TextureView) { player.setVideoTextureView((TextureView) surfaceView); diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java index 618f2fa336..d89f82b7c4 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java @@ -19,6 +19,7 @@ import android.annotation.TargetApi; import android.content.Context; import android.content.res.Resources; import android.graphics.Canvas; +import android.support.annotation.Nullable; import android.util.AttributeSet; import android.util.TypedValue; import android.view.View; @@ -87,9 +88,9 @@ public final class SubtitleView extends View implements TextOutput { /** * Sets the cues to be displayed by the view. * - * @param cues The cues to display. + * @param cues The cues to display, or null to clear the cues. */ - public void setCues(List cues) { + public void setCues(@Nullable List cues) { if (this.cues == cues) { return; } From aebc7da82b38ccb0f796ee50c610c5fc0aecc3f4 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 5 Dec 2017 06:57:27 -0800 Subject: [PATCH 0837/2472] Immediately release temp buffer memory in FakeRenderer. FakeRenderer only needs to allocate real memory because it extends BaseRenderer which uses the actual SampleStream implementation. Immediately release the memory after using it to prevent excessive memory usage when running fast simulations. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177949628 --- .../com/google/android/exoplayer2/testutil/FakeRenderer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeRenderer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeRenderer.java index c4270eb9c4..75adcf9018 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeRenderer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeRenderer.java @@ -59,10 +59,10 @@ public class FakeRenderer extends BaseRenderer { @Override public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { if (!isEnded) { - buffer.clear(); // Verify the format matches the expected format. FormatHolder formatHolder = new FormatHolder(); int result = readSource(formatHolder, buffer, false); + buffer.data = null; if (result == C.RESULT_FORMAT_READ) { formatReadCount++; Assert.assertTrue(expectedFormats.contains(formatHolder.format)); From 6606d73b29c3e27184ff59aa94a5d68908bbc30c Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 5 Dec 2017 07:12:10 -0800 Subject: [PATCH 0838/2472] Unset sample queue index in HlsSampleStream#unbindSampleQueue ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177950960 --- .../google/android/exoplayer2/source/hls/HlsSampleStream.java | 1 + 1 file changed, 1 insertion(+) diff --git a/library/hls/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 index d180039b30..301cd2920b 100644 --- a/library/hls/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 @@ -39,6 +39,7 @@ import java.io.IOException; public void unbindSampleQueue() { if (sampleQueueIndex != C.INDEX_UNSET) { sampleStreamWrapper.unbindSampleQueue(trackGroupIndex); + sampleQueueIndex = C.INDEX_UNSET; } } From 88dea59cd225e424dc360e3fd7f412441ec3dc21 Mon Sep 17 00:00:00 2001 From: hoangtc Date: Tue, 5 Dec 2017 08:31:17 -0800 Subject: [PATCH 0839/2472] Add ability for media period to discard buffered media at the back of the queue In some occasions, we may want to discard a part of the buffered media to improve playback quality. This CL adds this functionality by allowing the loading media period to re-evaluate its buffer periodically (every 2s) and discard chunks as it needs. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177958910 --- RELEASENOTES.md | 2 + .../exoplayer2/ExoPlayerImplInternal.java | 13 +- .../source/ClippingMediaPeriod.java | 5 + .../source/CompositeSequenceableLoader.java | 7 + .../source/DeferredMediaPeriod.java | 5 + .../source/ExtractorMediaPeriod.java | 5 + .../exoplayer2/source/MediaPeriod.java | 106 +++-- .../exoplayer2/source/MergingMediaPeriod.java | 5 + .../exoplayer2/source/SequenceableLoader.java | 11 + .../source/SingleSampleMediaPeriod.java | 5 + .../source/chunk/ChunkSampleStream.java | 90 ++-- .../AdaptiveTrackSelection.java | 176 ++++--- .../CompositeSequenceableLoaderTest.java | 5 + .../AdaptiveTrackSelectionTest.java | 428 ++++++++++++++++++ .../source/dash/DashMediaPeriod.java | 5 + .../exoplayer2/source/hls/HlsMediaPeriod.java | 5 + .../source/hls/HlsSampleStreamWrapper.java | 5 + .../source/smoothstreaming/SsMediaPeriod.java | 5 + .../smoothstreaming/manifest/SsManifest.java | 16 +- .../exoplayer2/testutil/FakeMediaPeriod.java | 5 + 20 files changed, 765 insertions(+), 139 deletions(-) create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelectionTest.java diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 9d98f2aae0..80c55c4706 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,6 +2,8 @@ ### dev-v2 (not yet released) ### +* Add ability for `SequenceableLoader` to reevaluate its buffer and discard + buffered media so that it can be re-buffered in a different quality. * Replace `DefaultTrackSelector.Parameters` copy methods with a builder. * Allow more flexible loading strategy when playing media containing multiple sub-streams, by allowing injection of custom `CompositeSequenceableLoader` diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index b0ef675e71..83e7858eaa 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -1283,6 +1283,7 @@ import java.io.IOException; // Update the loading period if required. maybeUpdateLoadingPeriod(); + if (loadingPeriodHolder == null || loadingPeriodHolder.isFullyBuffered()) { setIsLoading(false); } else if (loadingPeriodHolder != null && !isLoading) { @@ -1386,6 +1387,7 @@ import java.io.IOException; if (loadingPeriodHolder == null) { info = mediaPeriodInfoSequence.getFirstMediaPeriodInfo(playbackInfo); } else { + loadingPeriodHolder.reevaluateBuffer(rendererPositionUs); if (loadingPeriodHolder.info.isFinal || !loadingPeriodHolder.isFullyBuffered() || loadingPeriodHolder.info.durationUs == C.TIME_UNSET) { return; @@ -1440,6 +1442,7 @@ import java.io.IOException; // Stale event. return; } + loadingPeriodHolder.reevaluateBuffer(rendererPositionUs); maybeContinueLoading(); } @@ -1628,13 +1631,18 @@ import java.io.IOException; info = info.copyWithStartPositionUs(newStartPositionUs); } + public void reevaluateBuffer(long rendererPositionUs) { + if (prepared) { + mediaPeriod.reevaluateBuffer(toPeriodTime(rendererPositionUs)); + } + } + public boolean shouldContinueLoading(long rendererPositionUs, float playbackSpeed) { long nextLoadPositionUs = !prepared ? 0 : mediaPeriod.getNextLoadPositionUs(); if (nextLoadPositionUs == C.TIME_END_OF_SOURCE) { return false; } else { - long loadingPeriodPositionUs = toPeriodTime(rendererPositionUs); - long bufferedDurationUs = nextLoadPositionUs - loadingPeriodPositionUs; + long bufferedDurationUs = nextLoadPositionUs - toPeriodTime(rendererPositionUs); return loadControl.shouldContinueLoading(bufferedDurationUs, playbackSpeed); } } @@ -1694,7 +1702,6 @@ import java.io.IOException; Assertions.checkState(trackSelections.get(i) == null); } } - // The track selection has changed. loadControl.onTracksSelected(renderers, trackSelectorResult.groups, trackSelections); return positionUs; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java index 36e8e51ffb..539c4841e9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java @@ -123,6 +123,11 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb mediaPeriod.discardBuffer(positionUs + startUs, toKeyframe); } + @Override + public void reevaluateBuffer(long positionUs) { + mediaPeriod.reevaluateBuffer(positionUs + startUs); + } + @Override public long readDiscontinuity() { if (isPendingInitialDiscontinuity()) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java b/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java index e9a187a747..c41933b48b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/CompositeSequenceableLoader.java @@ -52,6 +52,13 @@ public class CompositeSequenceableLoader implements SequenceableLoader { return nextLoadPositionUs == Long.MAX_VALUE ? C.TIME_END_OF_SOURCE : nextLoadPositionUs; } + @Override + public final void reevaluateBuffer(long positionUs) { + for (SequenceableLoader loader : loaders) { + loader.reevaluateBuffer(positionUs); + } + } + @Override public boolean continueLoading(long positionUs) { boolean madeProgress = false; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java index bc29b2fdf1..32a180b956 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java @@ -119,6 +119,11 @@ public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callb return mediaPeriod.getNextLoadPositionUs(); } + @Override + public void reevaluateBuffer(long positionUs) { + mediaPeriod.reevaluateBuffer(positionUs); + } + @Override public boolean continueLoading(long positionUs) { return mediaPeriod != null && mediaPeriod.continueLoading(positionUs); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java index 17a6c3bcb8..6b9aeb39da 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -288,6 +288,11 @@ import java.util.Arrays; } } + @Override + public void reevaluateBuffer(long positionUs) { + // Do nothing. + } + @Override public boolean continueLoading(long playbackPositionUs) { if (loadingFinished || (prepared && enabledTrackCount == 0)) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java index 439562e0ab..54b34bc531 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java @@ -35,27 +35,25 @@ public interface MediaPeriod extends SequenceableLoader { /** * Called when preparation completes. - *

          - * Called on the playback thread. After invoking this method, the {@link MediaPeriod} can expect - * for {@link #selectTracks(TrackSelection[], boolean[], SampleStream[], boolean[], long)} to be - * called with the initial track selection. + * + *

          Called on the playback thread. After invoking this method, the {@link MediaPeriod} can + * expect for {@link #selectTracks(TrackSelection[], boolean[], SampleStream[], boolean[], + * long)} to be called with the initial track selection. * * @param mediaPeriod The prepared {@link MediaPeriod}. */ void onPrepared(MediaPeriod mediaPeriod); - } /** * Prepares this media period asynchronously. - *

          - * {@code callback.onPrepared} is called when preparation completes. If preparation fails, + * + *

          {@code callback.onPrepared} is called when preparation completes. If preparation fails, * {@link #maybeThrowPrepareError()} will throw an {@link IOException}. - *

          - * If preparation succeeds and results in a source timeline change (e.g. the period duration - * becoming known), - * {@link MediaSource.Listener#onSourceInfoRefreshed(MediaSource, Timeline, Object)} will be - * called before {@code callback.onPrepared}. + * + *

          If preparation succeeds and results in a source timeline change (e.g. the period duration + * becoming known), {@link MediaSource.Listener#onSourceInfoRefreshed(MediaSource, Timeline, + * Object)} will be called before {@code callback.onPrepared}. * * @param callback Callback to receive updates from this period, including being notified when * preparation completes. @@ -66,8 +64,8 @@ public interface MediaPeriod extends SequenceableLoader { /** * Throws an error that's preventing the period from becoming prepared. Does nothing if no such * error exists. - *

          - * This method should only be called before the period has completed preparation. + * + *

          This method should only be called before the period has completed preparation. * * @throws IOException The underlying error. */ @@ -75,8 +73,8 @@ public interface MediaPeriod extends SequenceableLoader { /** * Returns the {@link TrackGroup}s exposed by the period. - *

          - * This method should only be called after the period has been prepared. + * + *

          This method should only be called after the period has been prepared. * * @return The {@link TrackGroup}s. */ @@ -84,16 +82,16 @@ public interface MediaPeriod extends SequenceableLoader { /** * Performs a track selection. - *

          - * The call receives track {@code selections} for each renderer, {@code mayRetainStreamFlags} + * + *

          The call receives track {@code selections} for each renderer, {@code mayRetainStreamFlags} * indicating whether the existing {@code SampleStream} can be retained for each selection, and * the existing {@code stream}s themselves. The call will update {@code streams} to reflect the * provided selections, clearing, setting and replacing entries as required. If an existing sample * stream is retained but with the requirement that the consuming renderer be reset, then the * corresponding flag in {@code streamResetFlags} will be set to true. This flag will also be set * if a new sample stream is created. - *

          - * This method should only be called after the period has been prepared. + * + *

          This method should only be called after the period has been prepared. * * @param selections The renderer track selections. * @param mayRetainStreamFlags Flags indicating whether the existing sample stream can be retained @@ -104,16 +102,20 @@ public interface MediaPeriod extends SequenceableLoader { * @param streamResetFlags Will be updated to indicate new sample streams, and sample streams that * have been retained but with the requirement that the consuming renderer be reset. * @param positionUs The current playback position in microseconds. If playback of this period has - * not yet started, the value will be the starting position. + * not yet started, the value will be the starting position. * @return The actual position at which the tracks were enabled, in microseconds. */ - long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, - SampleStream[] streams, boolean[] streamResetFlags, long positionUs); + long selectTracks( + TrackSelection[] selections, + boolean[] mayRetainStreamFlags, + SampleStream[] streams, + boolean[] streamResetFlags, + long positionUs); /** * Discards buffered media up to the specified position. - *

          - * This method should only be called after the period has been prepared. + * + *

          This method should only be called after the period has been prepared. * * @param positionUs The position in microseconds. * @param toKeyframe If true then for each track discards samples up to the keyframe before or at @@ -123,11 +125,11 @@ public interface MediaPeriod extends SequenceableLoader { /** * Attempts to read a discontinuity. - *

          - * After this method has returned a value other than {@link C#TIME_UNSET}, all - * {@link SampleStream}s provided by the period are guaranteed to start from a key frame. - *

          - * This method should only be called after the period has been prepared. + * + *

          After this method has returned a value other than {@link C#TIME_UNSET}, all {@link + * SampleStream}s provided by the period are guaranteed to start from a key frame. + * + *

          This method should only be called after the period has been prepared. * * @return If a discontinuity was read then the playback position in microseconds after the * discontinuity. Else {@link C#TIME_UNSET}. @@ -136,11 +138,11 @@ public interface MediaPeriod extends SequenceableLoader { /** * Attempts to seek to the specified position in microseconds. - *

          - * After this method has been called, all {@link SampleStream}s provided by the period are + * + *

          After this method has been called, all {@link SampleStream}s provided by the period are * guaranteed to start from a key frame. - *

          - * This method should only be called when at least one track is selected. + * + *

          This method should only be called when at least one track is selected. * * @param positionUs The seek position in microseconds. * @return The actual position to which the period was seeked, in microseconds. @@ -151,8 +153,8 @@ public interface MediaPeriod extends SequenceableLoader { /** * Returns an estimate of the position up to which data is buffered for the enabled tracks. - *

          - * This method should only be called when at least one track is selected. + * + *

          This method should only be called when at least one track is selected. * * @return An estimate of the absolute position in microseconds up to which data is buffered, or * {@link C#TIME_END_OF_SOURCE} if the track is fully buffered. @@ -162,19 +164,19 @@ public interface MediaPeriod extends SequenceableLoader { /** * Returns the next load time, or {@link C#TIME_END_OF_SOURCE} if loading has finished. - *

          - * This method should only be called after the period has been prepared. It may be called when no - * tracks are selected. + * + *

          This method should only be called after the period has been prepared. It may be called when + * no tracks are selected. */ @Override long getNextLoadPositionUs(); /** * Attempts to continue loading. - *

          - * This method may be called both during and after the period has been prepared. - *

          - * A period may call {@link Callback#onContinueLoadingRequested(SequenceableLoader)} on the + * + *

          This method may be called both during and after the period has been prepared. + * + *

          A period may call {@link Callback#onContinueLoadingRequested(SequenceableLoader)} on the * {@link Callback} passed to {@link #prepare(Callback, long)} to request that this method be * called when the period is permitted to continue loading data. A period may do this both during * and after preparation. @@ -182,10 +184,24 @@ public interface MediaPeriod extends SequenceableLoader { * @param positionUs The current playback position in microseconds. If playback of this period has * not yet started, the value will be the starting position in this period minus the duration * of any media in previous periods still to be played. - * @return True if progress was made, meaning that {@link #getNextLoadPositionUs()} will return - * a different value than prior to the call. False otherwise. + * @return True if progress was made, meaning that {@link #getNextLoadPositionUs()} will return a + * different value than prior to the call. False otherwise. */ @Override boolean continueLoading(long positionUs); + /** + * Re-evaluates the buffer given the playback position. + * + *

          This method should only be called after the period has been prepared. + * + *

          A period may choose to discard buffered media so that it can be re-buffered in a different + * quality. + * + * @param positionUs The current playback position in microseconds. If playback of this period has + * not yet started, the value will be the starting position in this period minus the duration + * of any media in previous periods still to be played. + */ + @Override + void reevaluateBuffer(long positionUs); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java index bd37b5efec..5ac9fc8d97 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java @@ -139,6 +139,11 @@ import java.util.IdentityHashMap; } } + @Override + public void reevaluateBuffer(long positionUs) { + compositeSequenceableLoader.reevaluateBuffer(positionUs); + } + @Override public boolean continueLoading(long positionUs) { return compositeSequenceableLoader.continueLoading(positionUs); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SequenceableLoader.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SequenceableLoader.java index 6daa1e847a..182f0f17cc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SequenceableLoader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SequenceableLoader.java @@ -60,4 +60,15 @@ public interface SequenceableLoader { */ boolean continueLoading(long positionUs); + /** + * Re-evaluates the buffer given the playback position. + * + *

          Re-evaluation may discard buffered media so that it can be re-buffered in a different + * quality. + * + * @param positionUs The current playback position in microseconds. If playback of this period has + * not yet started, the value will be the starting position in this period minus the duration + * of any media in previous periods still to be played. + */ + void reevaluateBuffer(long positionUs); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java index 0cea0fad66..7b8b54eedc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java @@ -120,6 +120,11 @@ import java.util.Arrays; // Do nothing. } + @Override + public void reevaluateBuffer(long positionUs) { + // Do nothing. + } + @Override public boolean continueLoading(long positionUs) { if (loadingFinished || loader.isLoading()) { diff --git a/library/core/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 index 20b56e7807..85c4b12241 100644 --- a/library/core/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 @@ -319,7 +319,9 @@ public class ChunkSampleStream implements SampleStream, S IOException error) { long bytesLoaded = loadable.bytesLoaded(); boolean isMediaChunk = isMediaChunk(loadable); - boolean cancelable = bytesLoaded == 0 || !isMediaChunk || !haveReadFromLastMediaChunk(); + int lastChunkIndex = mediaChunks.size() - 1; + boolean cancelable = + bytesLoaded == 0 || !isMediaChunk || !haveReadFromMediaChunk(lastChunkIndex); boolean canceled = false; if (chunkSource.onChunkLoadError(loadable, cancelable, error)) { if (!cancelable) { @@ -327,12 +329,8 @@ public class ChunkSampleStream implements SampleStream, S } else { canceled = true; if (isMediaChunk) { - BaseMediaChunk removed = mediaChunks.remove(mediaChunks.size() - 1); + BaseMediaChunk removed = discardUpstreamMediaChunksFromIndex(lastChunkIndex); Assertions.checkState(removed == loadable); - primarySampleQueue.discardUpstreamSamples(removed.getFirstSampleIndex(0)); - for (int i = 0; i < embeddedSampleQueues.length; i++) { - embeddedSampleQueues[i].discardUpstreamSamples(removed.getFirstSampleIndex(i + 1)); - } if (mediaChunks.isEmpty()) { pendingResetPositionUs = lastSeekPositionUs; } @@ -405,35 +403,29 @@ public class ChunkSampleStream implements SampleStream, S } } - // Internal methods - - // TODO[REFACTOR]: Call maybeDiscardUpstream for DASH and SmoothStreaming. - /** - * Discards media chunks from the back of the buffer if conditions have changed such that it's - * preferable to re-buffer the media at a different quality. - * - * @param positionUs The current playback position in microseconds. - */ - @SuppressWarnings("unused") - private void maybeDiscardUpstream(long positionUs) { + @Override + public void reevaluateBuffer(long positionUs) { + if (loader.isLoading() || isPendingReset()) { + return; + } int queueSize = chunkSource.getPreferredQueueSize(positionUs, readOnlyMediaChunks); - discardUpstreamMediaChunks(Math.max(1, queueSize)); + discardUpstreamMediaChunks(queueSize); } + // Internal methods + private boolean isMediaChunk(Chunk chunk) { return chunk instanceof BaseMediaChunk; } - /** - * Returns whether samples have been read from {@code mediaChunks.getLast()}. - */ - private boolean haveReadFromLastMediaChunk() { - BaseMediaChunk lastChunk = getLastMediaChunk(); - if (primarySampleQueue.getReadIndex() > lastChunk.getFirstSampleIndex(0)) { + /** Returns whether samples have been read from media chunk at given index. */ + private boolean haveReadFromMediaChunk(int mediaChunkIndex) { + BaseMediaChunk mediaChunk = mediaChunks.get(mediaChunkIndex); + if (primarySampleQueue.getReadIndex() > mediaChunk.getFirstSampleIndex(0)) { return true; } for (int i = 0; i < embeddedSampleQueues.length; i++) { - if (embeddedSampleQueues[i].getReadIndex() > lastChunk.getFirstSampleIndex(i + 1)) { + if (embeddedSampleQueues[i].getReadIndex() > mediaChunk.getFirstSampleIndex(i + 1)) { return true; } } @@ -492,27 +484,51 @@ public class ChunkSampleStream implements SampleStream, S } /** - * Discard upstream media chunks until the queue length is equal to the length specified. + * Discard upstream media chunks until the queue length is equal to the length specified, but + * avoid discarding any chunk whose samples have been read by either primary sample stream or + * embedded sample streams. * - * @param queueLength The desired length of the queue. - * @return Whether chunks were discarded. + * @param desiredQueueSize The desired length of the queue. The final queue size after discarding + * maybe larger than this if there are chunks after the specified position that have been read + * by either primary sample stream or embedded sample streams. */ - private boolean discardUpstreamMediaChunks(int queueLength) { - if (mediaChunks.size() <= queueLength) { - return false; + private void discardUpstreamMediaChunks(int desiredQueueSize) { + if (mediaChunks.size() <= desiredQueueSize) { + return; } + int firstIndexToRemove = desiredQueueSize; + for (int i = firstIndexToRemove; i < mediaChunks.size(); i++) { + if (!haveReadFromMediaChunk(i)) { + firstIndexToRemove = i; + break; + } + } + + if (firstIndexToRemove == mediaChunks.size()) { + return; + } long endTimeUs = getLastMediaChunk().endTimeUs; - BaseMediaChunk firstRemovedChunk = mediaChunks.get(queueLength); - long startTimeUs = firstRemovedChunk.startTimeUs; - Util.removeRange(mediaChunks, /* fromIndex= */ queueLength, /* toIndex= */ mediaChunks.size()); + BaseMediaChunk firstRemovedChunk = discardUpstreamMediaChunksFromIndex(firstIndexToRemove); + loadingFinished = false; + eventDispatcher.upstreamDiscarded(primaryTrackType, firstRemovedChunk.startTimeUs, endTimeUs); + } + + /** + * Discard upstream media chunks from {@code chunkIndex} and corresponding samples from sample + * queues. + * + * @param chunkIndex The index of the first chunk to discard. + * @return The chunk at given index. + */ + private BaseMediaChunk discardUpstreamMediaChunksFromIndex(int chunkIndex) { + BaseMediaChunk firstRemovedChunk = mediaChunks.get(chunkIndex); + Util.removeRange(mediaChunks, /* fromIndex= */ chunkIndex, /* toIndex= */ mediaChunks.size()); primarySampleQueue.discardUpstreamSamples(firstRemovedChunk.getFirstSampleIndex(0)); for (int i = 0; i < embeddedSampleQueues.length; i++) { embeddedSampleQueues[i].discardUpstreamSamples(firstRemovedChunk.getFirstSampleIndex(i + 1)); } - loadingFinished = false; - eventDispatcher.upstreamDiscarded(primaryTrackType, startTimeUs, endTimeUs); - return true; + return firstRemovedChunk; } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java index ba45b2b186..973155c2e3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java @@ -15,12 +15,12 @@ */ package com.google.android.exoplayer2.trackselection; -import android.os.SystemClock; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.chunk.MediaChunk; import com.google.android.exoplayer2.upstream.BandwidthMeter; +import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.Util; import java.util.List; @@ -42,17 +42,23 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { private final int minDurationToRetainAfterDiscardMs; private final float bandwidthFraction; private final float bufferedFractionToLiveEdgeForQualityIncrease; + private final long minTimeBetweenBufferReevaluationMs; + private final Clock clock; /** * @param bandwidthMeter Provides an estimate of the currently available bandwidth. */ public Factory(BandwidthMeter bandwidthMeter) { - this (bandwidthMeter, DEFAULT_MAX_INITIAL_BITRATE, + this( + bandwidthMeter, + DEFAULT_MAX_INITIAL_BITRATE, DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS, DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS, DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, DEFAULT_BANDWIDTH_FRACTION, - DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE); + DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE, + DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS, + Clock.DEFAULT); } /** @@ -74,37 +80,55 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { public Factory(BandwidthMeter bandwidthMeter, int maxInitialBitrate, int minDurationForQualityIncreaseMs, int maxDurationForQualityDecreaseMs, int minDurationToRetainAfterDiscardMs, float bandwidthFraction) { - this (bandwidthMeter, maxInitialBitrate, minDurationForQualityIncreaseMs, - maxDurationForQualityDecreaseMs, minDurationToRetainAfterDiscardMs, - bandwidthFraction, DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE); + this( + bandwidthMeter, + maxInitialBitrate, + minDurationForQualityIncreaseMs, + maxDurationForQualityDecreaseMs, + minDurationToRetainAfterDiscardMs, + bandwidthFraction, + DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE, + DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS, + Clock.DEFAULT); } /** * @param bandwidthMeter Provides an estimate of the currently available bandwidth. - * @param maxInitialBitrate The maximum bitrate in bits per second that should be assumed - * when a bandwidth estimate is unavailable. - * @param minDurationForQualityIncreaseMs The minimum duration of buffered data required for - * the selected track to switch to one of higher quality. - * @param maxDurationForQualityDecreaseMs The maximum duration of buffered data required for - * the selected track to switch to one of lower quality. + * @param maxInitialBitrate The maximum bitrate in bits per second that should be assumed when a + * bandwidth estimate is unavailable. + * @param minDurationForQualityIncreaseMs The minimum duration of buffered data required for the + * selected track to switch to one of higher quality. + * @param maxDurationForQualityDecreaseMs The maximum duration of buffered data required for the + * selected track to switch to one of lower quality. * @param minDurationToRetainAfterDiscardMs When switching to a track of significantly higher * quality, the selection may indicate that media already buffered at the lower quality can * be discarded to speed up the switch. This is the minimum duration of media that must be * retained at the lower quality. * @param bandwidthFraction The fraction of the available bandwidth that the selection should - * consider available for use. Setting to a value less than 1 is recommended to account - * for inaccuracies in the bandwidth estimator. - * @param bufferedFractionToLiveEdgeForQualityIncrease For live streaming, the fraction of - * the duration from current playback position to the live edge that has to be buffered - * before the selected track can be switched to one of higher quality. This parameter is - * only applied when the playback position is closer to the live edge than - * {@code minDurationForQualityIncreaseMs}, which would otherwise prevent switching to a - * higher quality from happening. + * consider available for use. Setting to a value less than 1 is recommended to account for + * inaccuracies in the bandwidth estimator. + * @param bufferedFractionToLiveEdgeForQualityIncrease For live streaming, the fraction of the + * duration from current playback position to the live edge that has to be buffered before + * the selected track can be switched to one of higher quality. This parameter is only + * applied when the playback position is closer to the live edge than {@code + * minDurationForQualityIncreaseMs}, which would otherwise prevent switching to a higher + * quality from happening. + * @param minTimeBetweenBufferReevaluationMs The track selection may periodically reevaluate its + * buffer and discard some chunks of lower quality to improve the playback quality if + * network conditions have changed. This is the minimum duration between 2 consecutive + * buffer reevaluation calls. + * @param clock A {@link Clock}. */ - public Factory(BandwidthMeter bandwidthMeter, int maxInitialBitrate, - int minDurationForQualityIncreaseMs, int maxDurationForQualityDecreaseMs, - int minDurationToRetainAfterDiscardMs, float bandwidthFraction, - float bufferedFractionToLiveEdgeForQualityIncrease) { + public Factory( + BandwidthMeter bandwidthMeter, + int maxInitialBitrate, + int minDurationForQualityIncreaseMs, + int maxDurationForQualityDecreaseMs, + int minDurationToRetainAfterDiscardMs, + float bandwidthFraction, + float bufferedFractionToLiveEdgeForQualityIncrease, + long minTimeBetweenBufferReevaluationMs, + Clock clock) { this.bandwidthMeter = bandwidthMeter; this.maxInitialBitrate = maxInitialBitrate; this.minDurationForQualityIncreaseMs = minDurationForQualityIncreaseMs; @@ -113,14 +137,24 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { this.bandwidthFraction = bandwidthFraction; this.bufferedFractionToLiveEdgeForQualityIncrease = bufferedFractionToLiveEdgeForQualityIncrease; + this.minTimeBetweenBufferReevaluationMs = minTimeBetweenBufferReevaluationMs; + this.clock = clock; } @Override public AdaptiveTrackSelection createTrackSelection(TrackGroup group, int... tracks) { - return new AdaptiveTrackSelection(group, tracks, bandwidthMeter, maxInitialBitrate, - minDurationForQualityIncreaseMs, maxDurationForQualityDecreaseMs, - minDurationToRetainAfterDiscardMs, bandwidthFraction, - bufferedFractionToLiveEdgeForQualityIncrease); + return new AdaptiveTrackSelection( + group, + tracks, + bandwidthMeter, + maxInitialBitrate, + minDurationForQualityIncreaseMs, + maxDurationForQualityDecreaseMs, + minDurationToRetainAfterDiscardMs, + bandwidthFraction, + bufferedFractionToLiveEdgeForQualityIncrease, + minTimeBetweenBufferReevaluationMs, + clock); } } @@ -131,6 +165,7 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { public static final int DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS = 25000; public static final float DEFAULT_BANDWIDTH_FRACTION = 0.75f; public static final float DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE = 0.75f; + public static final long DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS = 2000; private final BandwidthMeter bandwidthMeter; private final int maxInitialBitrate; @@ -139,10 +174,13 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { private final long minDurationToRetainAfterDiscardUs; private final float bandwidthFraction; private final float bufferedFractionToLiveEdgeForQualityIncrease; + private final long minTimeBetweenBufferReevaluationMs; + private final Clock clock; private float playbackSpeed; private int selectedIndex; private int reason; + private long lastBufferEvaluationMs; /** * @param group The {@link TrackGroup}. @@ -152,12 +190,18 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { */ public AdaptiveTrackSelection(TrackGroup group, int[] tracks, BandwidthMeter bandwidthMeter) { - this (group, tracks, bandwidthMeter, DEFAULT_MAX_INITIAL_BITRATE, + this( + group, + tracks, + bandwidthMeter, + DEFAULT_MAX_INITIAL_BITRATE, DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS, DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS, DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, DEFAULT_BANDWIDTH_FRACTION, - DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE); + DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE, + DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS, + Clock.DEFAULT); } /** @@ -172,23 +216,35 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { * @param maxDurationForQualityDecreaseMs The maximum duration of buffered data required for the * selected track to switch to one of lower quality. * @param minDurationToRetainAfterDiscardMs When switching to a track of significantly higher - * quality, the selection may indicate that media already buffered at the lower quality can - * be discarded to speed up the switch. This is the minimum duration of media that must be + * quality, the selection may indicate that media already buffered at the lower quality can be + * discarded to speed up the switch. This is the minimum duration of media that must be * retained at the lower quality. * @param bandwidthFraction The fraction of the available bandwidth that the selection should - * consider available for use. Setting to a value less than 1 is recommended to account - * for inaccuracies in the bandwidth estimator. - * @param bufferedFractionToLiveEdgeForQualityIncrease For live streaming, the fraction of - * the duration from current playback position to the live edge that has to be buffered - * before the selected track can be switched to one of higher quality. This parameter is - * only applied when the playback position is closer to the live edge than - * {@code minDurationForQualityIncreaseMs}, which would otherwise prevent switching to a - * higher quality from happening. + * consider available for use. Setting to a value less than 1 is recommended to account for + * inaccuracies in the bandwidth estimator. + * @param bufferedFractionToLiveEdgeForQualityIncrease For live streaming, the fraction of the + * duration from current playback position to the live edge that has to be buffered before the + * selected track can be switched to one of higher quality. This parameter is only applied + * when the playback position is closer to the live edge than {@code + * minDurationForQualityIncreaseMs}, which would otherwise prevent switching to a higher + * quality from happening. + * @param minTimeBetweenBufferReevaluationMs The track selection may periodically reevaluate its + * buffer and discard some chunks of lower quality to improve the playback quality if network + * condition has changed. This is the minimum duration between 2 consecutive buffer + * reevaluation calls. */ - public AdaptiveTrackSelection(TrackGroup group, int[] tracks, BandwidthMeter bandwidthMeter, - int maxInitialBitrate, long minDurationForQualityIncreaseMs, - long maxDurationForQualityDecreaseMs, long minDurationToRetainAfterDiscardMs, - float bandwidthFraction, float bufferedFractionToLiveEdgeForQualityIncrease) { + public AdaptiveTrackSelection( + TrackGroup group, + int[] tracks, + BandwidthMeter bandwidthMeter, + int maxInitialBitrate, + long minDurationForQualityIncreaseMs, + long maxDurationForQualityDecreaseMs, + long minDurationToRetainAfterDiscardMs, + float bandwidthFraction, + float bufferedFractionToLiveEdgeForQualityIncrease, + long minTimeBetweenBufferReevaluationMs, + Clock clock) { super(group, tracks); this.bandwidthMeter = bandwidthMeter; this.maxInitialBitrate = maxInitialBitrate; @@ -198,9 +254,17 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { this.bandwidthFraction = bandwidthFraction; this.bufferedFractionToLiveEdgeForQualityIncrease = bufferedFractionToLiveEdgeForQualityIncrease; + this.minTimeBetweenBufferReevaluationMs = minTimeBetweenBufferReevaluationMs; + this.clock = clock; playbackSpeed = 1f; selectedIndex = determineIdealSelectedIndex(Long.MIN_VALUE); reason = C.SELECTION_REASON_INITIAL; + lastBufferEvaluationMs = C.TIME_UNSET; + } + + @Override + public void enable() { + lastBufferEvaluationMs = C.TIME_UNSET; } @Override @@ -211,7 +275,7 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { @Override public void updateSelectedTrack(long playbackPositionUs, long bufferedDurationUs, long availableDurationUs) { - long nowMs = SystemClock.elapsedRealtime(); + long nowMs = clock.elapsedRealtime(); // Stash the current selection, then make a new one. int currentSelectedIndex = selectedIndex; selectedIndex = determineIdealSelectedIndex(nowMs); @@ -258,17 +322,25 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { @Override public int evaluateQueueSize(long playbackPositionUs, List queue) { + long nowMs = clock.elapsedRealtime(); + if (lastBufferEvaluationMs != C.TIME_UNSET + && nowMs - lastBufferEvaluationMs < minTimeBetweenBufferReevaluationMs) { + return queue.size(); + } + lastBufferEvaluationMs = nowMs; if (queue.isEmpty()) { return 0; } + int queueSize = queue.size(); - long mediaBufferedDurationUs = queue.get(queueSize - 1).endTimeUs - playbackPositionUs; - long playoutBufferedDurationUs = - Util.getPlayoutDurationForMediaDuration(mediaBufferedDurationUs, playbackSpeed); - if (playoutBufferedDurationUs < minDurationToRetainAfterDiscardUs) { + MediaChunk lastChunk = queue.get(queueSize - 1); + long playoutBufferedDurationBeforeLastChunkUs = + Util.getPlayoutDurationForMediaDuration( + lastChunk.startTimeUs - playbackPositionUs, playbackSpeed); + if (playoutBufferedDurationBeforeLastChunkUs < minDurationToRetainAfterDiscardUs) { return queueSize; } - int idealSelectedIndex = determineIdealSelectedIndex(SystemClock.elapsedRealtime()); + int idealSelectedIndex = determineIdealSelectedIndex(nowMs); Format idealFormat = getFormat(idealSelectedIndex); // If the chunks contain video, discard from the first SD chunk beyond // minDurationToRetainAfterDiscardUs whose resolution and bitrate are both lower than the ideal @@ -293,8 +365,8 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { /** * Computes the ideal selected index ignoring buffer health. * - * @param nowMs The current time in the timebase of {@link SystemClock#elapsedRealtime()}, or - * {@link Long#MIN_VALUE} to ignore blacklisting. + * @param nowMs The current time in the timebase of {@link Clock#elapsedRealtime()}, or {@link + * Long#MIN_VALUE} to ignore blacklisting. */ private int determineIdealSelectedIndex(long nowMs) { long bitrateEstimate = bandwidthMeter.getBitrateEstimate(); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/CompositeSequenceableLoaderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/CompositeSequenceableLoaderTest.java index e3ac104754..f7e29d2b06 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/CompositeSequenceableLoaderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/CompositeSequenceableLoaderTest.java @@ -265,6 +265,11 @@ public final class CompositeSequenceableLoaderTest { return loaded; } + @Override + public void reevaluateBuffer(long positionUs) { + // Do nothing. + } + private void setNextChunkDurationUs(int nextChunkDurationUs) { this.nextChunkDurationUs = nextChunkDurationUs; } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelectionTest.java b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelectionTest.java new file mode 100644 index 0000000000..ea19c72826 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelectionTest.java @@ -0,0 +1,428 @@ +/* + * 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.trackselection; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.initMocks; + +import android.net.Uri; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.source.TrackGroup; +import com.google.android.exoplayer2.source.chunk.MediaChunk; +import com.google.android.exoplayer2.testutil.FakeClock; +import com.google.android.exoplayer2.upstream.BandwidthMeter; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.upstream.DefaultHttpDataSource; +import com.google.android.exoplayer2.util.MimeTypes; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** Unit test for {@link AdaptiveTrackSelection}. */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) +public final class AdaptiveTrackSelectionTest { + + @Mock private BandwidthMeter mockBandwidthMeter; + private FakeClock fakeClock; + + private AdaptiveTrackSelection adaptiveTrackSelection; + + @Before + public void setUp() { + initMocks(this); + fakeClock = new FakeClock(0); + when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(BandwidthMeter.NO_ESTIMATE); + } + + @Test + public void testSelectInitialIndexUseMaxInitialBitrateIfNoBandwidthEstimate() { + Format format1 = videoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240); + Format format2 = videoFormat(/* bitrate= */ 1000, /* width= */ 640, /* height= */ 480); + Format format3 = videoFormat(/* bitrate= */ 2000, /* width= */ 960, /* height= */ 720); + TrackGroup trackGroup = new TrackGroup(format1, format2, format3); + + adaptiveTrackSelection = adaptiveTrackSelection(trackGroup, /* initialBitrate= */ 1000); + + assertThat(adaptiveTrackSelection.getSelectedFormat()).isEqualTo(format2); + assertThat(adaptiveTrackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_INITIAL); + } + + @Test + public void testSelectInitialIndexUseBandwidthEstimateIfAvailable() { + Format format1 = videoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240); + Format format2 = videoFormat(/* bitrate= */ 1000, /* width= */ 640, /* height= */ 480); + Format format3 = videoFormat(/* bitrate= */ 2000, /* width= */ 960, /* height= */ 720); + TrackGroup trackGroup = new TrackGroup(format1, format2, format3); + + when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(500L); + + adaptiveTrackSelection = adaptiveTrackSelection(trackGroup, /* initialBitrate= */ 1000); + + assertThat(adaptiveTrackSelection.getSelectedFormat()).isEqualTo(format1); + assertThat(adaptiveTrackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_INITIAL); + } + + @Test + public void testUpdateSelectedTrackDoNotSwitchUpIfNotBufferedEnough() { + Format format1 = videoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240); + Format format2 = videoFormat(/* bitrate= */ 1000, /* width= */ 640, /* height= */ 480); + Format format3 = videoFormat(/* bitrate= */ 2000, /* width= */ 960, /* height= */ 720); + TrackGroup trackGroup = new TrackGroup(format1, format2, format3); + + // initially bandwidth meter does not have any estimation. The second measurement onward returns + // 2000L, which prompts the track selection to switch up if possible. + when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(BandwidthMeter.NO_ESTIMATE, 2000L); + + adaptiveTrackSelection = + adaptiveTrackSelectionWithMinDurationForQualityIncreaseMs( + trackGroup, /* initialBitrate= */ 1000, /* minDurationForQualityIncreaseMs= */ 10_000); + + adaptiveTrackSelection.updateSelectedTrack( + /* playbackPositionUs= */ 0, + /* bufferedDurationUs= */ 9_999_000, + /* availableDurationUs= */ C.TIME_UNSET); + + // When bandwidth estimation is updated to 2000L, we can switch up to use a higher bitrate + // format. However, since we only buffered 9_999_000 us, which is smaller than + // minDurationForQualityIncreaseMs, we should defer switch up. + assertThat(adaptiveTrackSelection.getSelectedFormat()).isEqualTo(format2); + assertThat(adaptiveTrackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_INITIAL); + } + + @Test + public void testUpdateSelectedTrackSwitchUpIfBufferedEnough() { + Format format1 = videoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240); + Format format2 = videoFormat(/* bitrate= */ 1000, /* width= */ 640, /* height= */ 480); + Format format3 = videoFormat(/* bitrate= */ 2000, /* width= */ 960, /* height= */ 720); + TrackGroup trackGroup = new TrackGroup(format1, format2, format3); + + // initially bandwidth meter does not have any estimation. The second measurement onward returns + // 2000L, which prompts the track selection to switch up if possible. + when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(BandwidthMeter.NO_ESTIMATE, 2000L); + + adaptiveTrackSelection = + adaptiveTrackSelectionWithMinDurationForQualityIncreaseMs( + trackGroup, /* initialBitrate= */ 1000, /* minDurationForQualityIncreaseMs= */ 10_000); + + adaptiveTrackSelection.updateSelectedTrack( + /* playbackPositionUs= */ 0, + /* bufferedDurationUs= */ 10_000_000, + /* availableDurationUs= */ C.TIME_UNSET); + + // When bandwidth estimation is updated to 2000L, we can switch up to use a higher bitrate + // format. When we have buffered enough (10_000_000 us, which is equal to + // minDurationForQualityIncreaseMs), we should switch up now. + assertThat(adaptiveTrackSelection.getSelectedFormat()).isEqualTo(format3); + assertThat(adaptiveTrackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_ADAPTIVE); + } + + @Test + public void testUpdateSelectedTrackDoNotSwitchDownIfBufferedEnough() { + Format format1 = videoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240); + Format format2 = videoFormat(/* bitrate= */ 1000, /* width= */ 640, /* height= */ 480); + Format format3 = videoFormat(/* bitrate= */ 2000, /* width= */ 960, /* height= */ 720); + TrackGroup trackGroup = new TrackGroup(format1, format2, format3); + + // initially bandwidth meter does not have any estimation. The second measurement onward returns + // 500L, which prompts the track selection to switch down if necessary. + when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(BandwidthMeter.NO_ESTIMATE, 500L); + + adaptiveTrackSelection = + adaptiveTrackSelectionWithMaxDurationForQualityDecreaseMs( + trackGroup, /* initialBitrate= */ 1000, /* maxDurationForQualityDecreaseMs= */ 25_000); + + adaptiveTrackSelection.updateSelectedTrack( + /* playbackPositionUs= */ 0, + /* bufferedDurationUs= */ 25_000_000, + /* availableDurationUs= */ C.TIME_UNSET); + + // When bandwidth estimation is updated to 500L, we should switch down to use a lower bitrate + // format. However, since we have enough buffer at higher quality (25_000_000 us, which is equal + // to maxDurationForQualityDecreaseMs), we should defer switch down. + assertThat(adaptiveTrackSelection.getSelectedFormat()).isEqualTo(format2); + assertThat(adaptiveTrackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_INITIAL); + } + + @Test + public void testUpdateSelectedTrackSwitchDownIfNotBufferedEnough() { + Format format1 = videoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240); + Format format2 = videoFormat(/* bitrate= */ 1000, /* width= */ 640, /* height= */ 480); + Format format3 = videoFormat(/* bitrate= */ 2000, /* width= */ 960, /* height= */ 720); + TrackGroup trackGroup = new TrackGroup(format1, format2, format3); + + // initially bandwidth meter does not have any estimation. The second measurement onward returns + // 500L, which prompts the track selection to switch down if necessary. + when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(BandwidthMeter.NO_ESTIMATE, 500L); + + adaptiveTrackSelection = + adaptiveTrackSelectionWithMaxDurationForQualityDecreaseMs( + trackGroup, /* initialBitrate= */ 1000, /* maxDurationForQualityDecreaseMs= */ 25_000); + + adaptiveTrackSelection.updateSelectedTrack( + /* playbackPositionUs= */ 0, + /* bufferedDurationUs= */ 24_999_000, + /* availableDurationUs= */ C.TIME_UNSET); + + // When bandwidth estimation is updated to 500L, we should switch down to use a lower bitrate + // format. When we don't have enough buffer at higher quality (24_999_000 us is smaller than + // maxDurationForQualityDecreaseMs), we should switch down now. + assertThat(adaptiveTrackSelection.getSelectedFormat()).isEqualTo(format1); + assertThat(adaptiveTrackSelection.getSelectionReason()).isEqualTo(C.SELECTION_REASON_ADAPTIVE); + } + + @Test + public void testEvaluateQueueSizeReturnQueueSizeIfBandwidthIsNotImproved() { + Format format1 = videoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240); + Format format2 = videoFormat(/* bitrate= */ 1000, /* width= */ 640, /* height= */ 480); + Format format3 = videoFormat(/* bitrate= */ 2000, /* width= */ 960, /* height= */ 720); + TrackGroup trackGroup = new TrackGroup(format1, format2, format3); + + FakeMediaChunk chunk1 = + new FakeMediaChunk(format1, /* startTimeUs= */ 0, /* endTimeUs= */ 10_000_000); + FakeMediaChunk chunk2 = + new FakeMediaChunk(format1, /* startTimeUs= */ 10_000_000, /* endTimeUs= */ 20_000_000); + FakeMediaChunk chunk3 = + new FakeMediaChunk(format1, /* startTimeUs= */ 20_000_000, /* endTimeUs= */ 30_000_000); + List queue = new ArrayList<>(); + queue.add(chunk1); + queue.add(chunk2); + queue.add(chunk3); + + when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(500L); + adaptiveTrackSelection = adaptiveTrackSelection(trackGroup, /* initialBitrate= */ 1000); + + int size = adaptiveTrackSelection.evaluateQueueSize(0, queue); + assertThat(size).isEqualTo(3); + } + + @Test + public void testEvaluateQueueSizeDoNotReevaluateUntilAfterMinTimeBetweenBufferReevaluation() { + Format format1 = videoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240); + Format format2 = videoFormat(/* bitrate= */ 1000, /* width= */ 640, /* height= */ 480); + Format format3 = videoFormat(/* bitrate= */ 2000, /* width= */ 960, /* height= */ 720); + TrackGroup trackGroup = new TrackGroup(format1, format2, format3); + + FakeMediaChunk chunk1 = + new FakeMediaChunk(format1, /* startTimeUs= */ 0, /* endTimeUs= */ 10_000_000); + FakeMediaChunk chunk2 = + new FakeMediaChunk(format1, /* startTimeUs= */ 10_000_000, /* endTimeUs= */ 20_000_000); + FakeMediaChunk chunk3 = + new FakeMediaChunk(format1, /* startTimeUs= */ 20_000_000, /* endTimeUs= */ 30_000_000); + List queue = new ArrayList<>(); + queue.add(chunk1); + queue.add(chunk2); + queue.add(chunk3); + + when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(500L); + adaptiveTrackSelection = + adaptiveTrackSelectionWithMinTimeBetweenBufferReevaluationMs( + trackGroup, + /* initialBitrate= */ 1000, + /* durationToRetainAfterDiscardMs= */ 15_000, + /* minTimeBetweenBufferReevaluationMs= */ 2000); + + int initialQueueSize = adaptiveTrackSelection.evaluateQueueSize(0, queue); + + fakeClock.advanceTime(1999); + when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(1000L); + + // When bandwidth estimation is updated, we can discard chunks at the end of the queue now. + // However, since min duration between buffer reevaluation = 2000, we will not reevaluate + // queue size if time now is only 1999 ms after last buffer reevaluation. + int newSize = adaptiveTrackSelection.evaluateQueueSize(0, queue); + assertThat(newSize).isEqualTo(initialQueueSize); + } + + @Test + public void testEvaluateQueueSizeRetainMoreThanMinimumDurationAfterDiscard() { + Format format1 = videoFormat(/* bitrate= */ 500, /* width= */ 320, /* height= */ 240); + Format format2 = videoFormat(/* bitrate= */ 1000, /* width= */ 640, /* height= */ 480); + Format format3 = videoFormat(/* bitrate= */ 2000, /* width= */ 960, /* height= */ 720); + TrackGroup trackGroup = new TrackGroup(format1, format2, format3); + + FakeMediaChunk chunk1 = + new FakeMediaChunk(format1, /* startTimeUs= */ 0, /* endTimeUs= */ 10_000_000); + FakeMediaChunk chunk2 = + new FakeMediaChunk(format1, /* startTimeUs= */ 10_000_000, /* endTimeUs= */ 20_000_000); + FakeMediaChunk chunk3 = + new FakeMediaChunk(format1, /* startTimeUs= */ 20_000_000, /* endTimeUs= */ 30_000_000); + List queue = new ArrayList<>(); + queue.add(chunk1); + queue.add(chunk2); + queue.add(chunk3); + + when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(500L); + adaptiveTrackSelection = + adaptiveTrackSelectionWithMinTimeBetweenBufferReevaluationMs( + trackGroup, + /* initialBitrate= */ 1000, + /* durationToRetainAfterDiscardMs= */ 15_000, + /* minTimeBetweenBufferReevaluationMs= */ 2000); + + int initialQueueSize = adaptiveTrackSelection.evaluateQueueSize(0, queue); + assertThat(initialQueueSize).isEqualTo(3); + + fakeClock.advanceTime(2000); + when(mockBandwidthMeter.getBitrateEstimate()).thenReturn(1000L); + + // When bandwidth estimation is updated and time has advanced enough, we can discard chunks at + // the end of the queue now. + // However, since duration to retain after discard = 15 000 ms, we need to retain at least the + // first 2 chunks + int newSize = adaptiveTrackSelection.evaluateQueueSize(0, queue); + assertThat(newSize).isEqualTo(2); + } + + private AdaptiveTrackSelection adaptiveTrackSelection(TrackGroup trackGroup, int initialBitrate) { + return new AdaptiveTrackSelection( + trackGroup, + selectedAllTracksInGroup(trackGroup), + mockBandwidthMeter, + initialBitrate, + AdaptiveTrackSelection.DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS, + AdaptiveTrackSelection.DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS, + AdaptiveTrackSelection.DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, + /* bandwidthFraction= */ 1.0f, + AdaptiveTrackSelection.DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE, + AdaptiveTrackSelection.DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS, + fakeClock); + } + + private AdaptiveTrackSelection adaptiveTrackSelectionWithMinDurationForQualityIncreaseMs( + TrackGroup trackGroup, int initialBitrate, long minDurationForQualityIncreaseMs) { + return new AdaptiveTrackSelection( + trackGroup, + selectedAllTracksInGroup(trackGroup), + mockBandwidthMeter, + initialBitrate, + minDurationForQualityIncreaseMs, + AdaptiveTrackSelection.DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS, + AdaptiveTrackSelection.DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, + /* bandwidthFraction= */ 1.0f, + AdaptiveTrackSelection.DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE, + AdaptiveTrackSelection.DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS, + fakeClock); + } + + private AdaptiveTrackSelection adaptiveTrackSelectionWithMaxDurationForQualityDecreaseMs( + TrackGroup trackGroup, int initialBitrate, long maxDurationForQualityDecreaseMs) { + return new AdaptiveTrackSelection( + trackGroup, + selectedAllTracksInGroup(trackGroup), + mockBandwidthMeter, + initialBitrate, + AdaptiveTrackSelection.DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS, + maxDurationForQualityDecreaseMs, + AdaptiveTrackSelection.DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, + /* bandwidthFraction= */ 1.0f, + AdaptiveTrackSelection.DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE, + AdaptiveTrackSelection.DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS, + fakeClock); + } + + private AdaptiveTrackSelection adaptiveTrackSelectionWithMinTimeBetweenBufferReevaluationMs( + TrackGroup trackGroup, + int initialBitrate, + long durationToRetainAfterDiscardMs, + long minTimeBetweenBufferReevaluationMs) { + return new AdaptiveTrackSelection( + trackGroup, + selectedAllTracksInGroup(trackGroup), + mockBandwidthMeter, + initialBitrate, + AdaptiveTrackSelection.DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS, + AdaptiveTrackSelection.DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS, + durationToRetainAfterDiscardMs, + /* bandwidth fraction= */ 1.0f, + AdaptiveTrackSelection.DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE, + minTimeBetweenBufferReevaluationMs, + fakeClock); + } + + private int[] selectedAllTracksInGroup(TrackGroup trackGroup) { + int[] listIndices = new int[trackGroup.length]; + for (int i = 0; i < trackGroup.length; i++) { + listIndices[i] = i; + } + return listIndices; + } + + private static Format videoFormat(int bitrate, int width, int height) { + return Format.createVideoSampleFormat( + /* id= */ null, + /* sampleMimeType= */ MimeTypes.VIDEO_H264, + /* codecs= */ null, + /* bitrate= */ bitrate, + /* maxInputSize= */ Format.NO_VALUE, + /* width= */ width, + /* height= */ height, + /* frameRate= */ Format.NO_VALUE, + /* initializationData= */ null, + /* drmInitData= */ null); + } + + private static final class FakeMediaChunk extends MediaChunk { + + private static final DataSource DATA_SOURCE = new DefaultHttpDataSource("TEST_AGENT", null); + + public FakeMediaChunk(Format trackFormat, long startTimeUs, long endTimeUs) { + super( + DATA_SOURCE, + new DataSpec(Uri.EMPTY), + trackFormat, + C.SELECTION_REASON_ADAPTIVE, + null, + startTimeUs, + endTimeUs, + 0); + } + + @Override + public void cancelLoad() { + // Do nothing. + } + + @Override + public boolean isLoadCanceled() { + return false; + } + + @Override + public void load() throws IOException, InterruptedException { + // Do nothing. + } + + @Override + public boolean isLoadCompleted() { + return true; + } + + @Override + public long bytesLoaded() { + return 0; + } + } +} diff --git a/library/dash/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 index 8fe10e94ee..f320ad2844 100644 --- a/library/dash/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 @@ -270,6 +270,11 @@ import java.util.Map; } } + @Override + public void reevaluateBuffer(long positionUs) { + compositeSequenceableLoader.reevaluateBuffer(positionUs); + } + @Override public boolean continueLoading(long positionUs) { return compositeSequenceableLoader.continueLoading(positionUs); diff --git a/library/hls/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 index dd596878d2..fd8f2bdbe9 100644 --- a/library/hls/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 @@ -195,6 +195,11 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper } } + @Override + public void reevaluateBuffer(long positionUs) { + compositeSequenceableLoader.reevaluateBuffer(positionUs); + } + @Override public boolean continueLoading(long positionUs) { return compositeSequenceableLoader.continueLoading(positionUs); diff --git a/library/hls/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 index dbb71329c5..2e69e41d30 100644 --- a/library/hls/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 @@ -524,6 +524,11 @@ import java.util.Arrays; return true; } + @Override + public void reevaluateBuffer(long positionUs) { + // Do nothing. + } + // Loader.Callback implementation. @Override diff --git a/library/smoothstreaming/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 index d418a21dff..564993befe 100644 --- a/library/smoothstreaming/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 @@ -149,6 +149,11 @@ import java.util.ArrayList; } } + @Override + public void reevaluateBuffer(long positionUs) { + compositeSequenceableLoader.reevaluateBuffer(positionUs); + } + @Override public boolean continueLoading(long positionUs) { return compositeSequenceableLoader.continueLoading(positionUs); diff --git a/library/smoothstreaming/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 index fbc3726a0e..0df180a5a6 100644 --- a/library/smoothstreaming/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 @@ -203,8 +203,20 @@ public class SsManifest { long timescale, String name, int maxWidth, int maxHeight, int displayWidth, int displayHeight, String language, Format[] formats, List chunkStartTimes, long lastChunkDuration) { - this (baseUri, chunkTemplate, type, subType, timescale, name, maxWidth, maxHeight, - displayWidth, displayHeight, language, formats, chunkStartTimes, + this( + baseUri, + chunkTemplate, + type, + subType, + timescale, + name, + maxWidth, + maxHeight, + displayWidth, + displayHeight, + language, + formats, + chunkStartTimes, Util.scaleLargeTimestamps(chunkStartTimes, C.MICROS_PER_SECOND, timescale), Util.scaleLargeTimestamp(lastChunkDuration, C.MICROS_PER_SECOND, timescale)); } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java index c1be199b1e..d34c1d1c0c 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java @@ -151,6 +151,11 @@ public class FakeMediaPeriod implements MediaPeriod { // Do nothing. } + @Override + public void reevaluateBuffer(long positionUs) { + // Do nothing. + } + @Override public long readDiscontinuity() { Assert.assertTrue(prepared); From 8a0a8339e84262e72594cfa192f190bbf60ffc10 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 5 Dec 2017 09:10:05 -0800 Subject: [PATCH 0840/2472] Change handling of renderer position offset for first media period. This should be a no-op change. And it eliminates the need to use the index variable which will be removed once the MediaPeriodHolderQueue is implemented. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177963360 --- .../android/exoplayer2/ExoPlayerImplInternal.java | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 83e7858eaa..dd3ce136d6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -1407,9 +1407,10 @@ import java.io.IOException; return; } - long rendererPositionOffsetUs = loadingPeriodHolder == null - ? RENDERER_TIMESTAMP_OFFSET_US - : (loadingPeriodHolder.getRendererOffset() + loadingPeriodHolder.info.durationUs); + long rendererPositionOffsetUs = + loadingPeriodHolder == null + ? (info.startPositionUs + RENDERER_TIMESTAMP_OFFSET_US) + : (loadingPeriodHolder.getRendererOffset() + loadingPeriodHolder.info.durationUs); int holderIndex = loadingPeriodHolder == null ? 0 : loadingPeriodHolder.index + 1; Object uid = playbackInfo.timeline.getPeriod(info.id.periodIndex, period, true).uid; MediaPeriodHolder newPeriodHolder = new MediaPeriodHolder(renderers, rendererCapabilities, @@ -1553,8 +1554,8 @@ import java.io.IOException; public final int index; public final SampleStream[] sampleStreams; public final boolean[] mayRetainStreamFlags; - public final long rendererPositionOffsetUs; + public long rendererPositionOffsetUs; public MediaPeriodInfo info; public boolean prepared; public boolean hasEnabledTracks; @@ -1574,7 +1575,7 @@ import java.io.IOException; MediaSource mediaSource, Object periodUid, int index, MediaPeriodInfo info) { this.renderers = renderers; this.rendererCapabilities = rendererCapabilities; - this.rendererPositionOffsetUs = rendererPositionOffsetUs; + this.rendererPositionOffsetUs = rendererPositionOffsetUs - info.startPositionUs; this.trackSelector = trackSelector; this.loadControl = loadControl; this.mediaSource = mediaSource; @@ -1601,8 +1602,7 @@ import java.io.IOException; } public long getRendererOffset() { - return index == 0 ? rendererPositionOffsetUs - : (rendererPositionOffsetUs - info.startPositionUs); + return rendererPositionOffsetUs; } public boolean isFullyBuffered() { @@ -1628,6 +1628,7 @@ import java.io.IOException; prepared = true; selectTracks(playbackSpeed); long newStartPositionUs = updatePeriodTrackSelection(info.startPositionUs, false); + rendererPositionOffsetUs += info.startPositionUs - newStartPositionUs; info = info.copyWithStartPositionUs(newStartPositionUs); } From 586e657bd7fe9aeb194e40f2bff9dc3d3bbaff8e Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 6 Dec 2017 08:06:51 -0800 Subject: [PATCH 0841/2472] Allow opt-in HLS chunkless preparation If allowed, the media period will try to finish preparation without downloading chunks (similar to what DashMediaPeriod does). To create track groups, HlsMediaPeriod will try to obtain as much information as possible from the master playlist. If any vital information is missing for specific urls, traditional preparation will take place instead. This version does not support tracks with DrmInitData info. This affects tracks with CDM DRM (e.g: Widevine, Clearkey, etc). AES_128 encryption is not affected. This information needs to be obtained from media playlists, and this version only takes the master playlist into account for preparation. Issue:#3149 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178098759 --- RELEASENOTES.md | 4 + .../exoplayer2/source/hls/HlsMediaPeriod.java | 229 ++++++++++++++---- .../exoplayer2/source/hls/HlsMediaSource.java | 60 ++++- .../source/hls/HlsSampleStream.java | 2 +- .../source/hls/HlsSampleStreamWrapper.java | 89 +++++-- 5 files changed, 298 insertions(+), 86 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 80c55c4706..920d80ee48 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,6 +2,10 @@ ### dev-v2 (not yet released) ### +* Add initial support for chunkless preparation in HLS. This allows an HLS media + source to finish preparation without donwloading any chunks, which might + considerably reduce the initial buffering time + ([#3149](https://github.com/google/ExoPlayer/issues/2980)). * Add ability for `SequenceableLoader` to reevaluate its buffer and discard buffered media so that it can be re-buffered in a different quality. * Replace `DefaultTrackSelector.Parameters` copy methods with a builder. diff --git a/library/hls/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 index fd8f2bdbe9..24acf0f84d 100644 --- a/library/hls/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 @@ -16,7 +16,6 @@ package com.google.android.exoplayer2.source.hls; import android.os.Handler; -import android.text.TextUtils; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; @@ -32,6 +31,8 @@ import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistTracker; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; @@ -55,6 +56,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper private final TimestampAdjusterProvider timestampAdjusterProvider; private final Handler continueLoadingHandler; private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; + private final boolean allowChunklessPreparation; private Callback callback; private int pendingPrepareCount; @@ -63,10 +65,15 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper private HlsSampleStreamWrapper[] enabledSampleStreamWrappers; private SequenceableLoader compositeSequenceableLoader; - public HlsMediaPeriod(HlsExtractorFactory extractorFactory, HlsPlaylistTracker playlistTracker, - HlsDataSourceFactory dataSourceFactory, int minLoadableRetryCount, - EventDispatcher eventDispatcher, Allocator allocator, - CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory) { + public HlsMediaPeriod( + HlsExtractorFactory extractorFactory, + HlsPlaylistTracker playlistTracker, + HlsDataSourceFactory dataSourceFactory, + int minLoadableRetryCount, + EventDispatcher eventDispatcher, + Allocator allocator, + CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, + boolean allowChunklessPreparation) { this.extractorFactory = extractorFactory; this.playlistTracker = playlistTracker; this.dataSourceFactory = dataSourceFactory; @@ -74,6 +81,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper this.eventDispatcher = eventDispatcher; this.allocator = allocator; this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; + this.allowChunklessPreparation = allowChunklessPreparation; streamWrapperIndices = new IdentityHashMap<>(); timestampAdjusterProvider = new TimestampAdjusterProvider(); continueLoadingHandler = new Handler(); @@ -293,15 +301,92 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper private void buildAndPrepareSampleStreamWrappers(long positionUs) { HlsMasterPlaylist masterPlaylist = playlistTracker.getMasterPlaylist(); - // Build the default stream wrapper. + List audioRenditions = masterPlaylist.audios; + List subtitleRenditions = masterPlaylist.subtitles; + + int wrapperCount = 1 /* variants */ + audioRenditions.size() + subtitleRenditions.size(); + sampleStreamWrappers = new HlsSampleStreamWrapper[wrapperCount]; + pendingPrepareCount = wrapperCount; + + buildAndPrepareMainSampleStreamWrapper(masterPlaylist, positionUs); + int currentWrapperIndex = 1; + + // TODO: Build video stream wrappers here. + + // Audio sample stream wrappers. + for (int i = 0; i < audioRenditions.size(); i++) { + HlsUrl audioRendition = audioRenditions.get(i); + HlsSampleStreamWrapper sampleStreamWrapper = + buildSampleStreamWrapper( + C.TRACK_TYPE_AUDIO, + new HlsUrl[] {audioRendition}, + null, + Collections.emptyList(), + positionUs); + sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper; + Format renditionFormat = audioRendition.format; + if (allowChunklessPreparation && renditionFormat.codecs != null) { + sampleStreamWrapper.prepareWithMasterPlaylistInfo( + new TrackGroupArray(new TrackGroup(audioRendition.format)), 0); + } else { + sampleStreamWrapper.continuePreparing(); + } + } + + // Subtitle stream wrappers. We can always use master playlist information to prepare these. + for (int i = 0; i < subtitleRenditions.size(); i++) { + HlsUrl url = subtitleRenditions.get(i); + HlsSampleStreamWrapper sampleStreamWrapper = + buildSampleStreamWrapper( + C.TRACK_TYPE_TEXT, + new HlsUrl[] {url}, + null, + Collections.emptyList(), + positionUs); + sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper; + sampleStreamWrapper.prepareWithMasterPlaylistInfo( + new TrackGroupArray(new TrackGroup(url.format)), 0); + } + + // All wrappers are enabled during preparation. + enabledSampleStreamWrappers = sampleStreamWrappers; + } + + /** + * This method creates and starts preparation of the main {@link HlsSampleStreamWrapper}. + * + *

          The main sample stream wrapper is the first element of {@link #sampleStreamWrappers}. It + * provides {@link SampleStream}s for the variant urls in the master playlist. It may be adaptive + * and may contain multiple muxed tracks. + * + *

          If chunkless preparation is allowed, the media period will try preparation without segment + * downloads. This is only possible if variants contain the CODECS attribute. If not, traditional + * preparation with segment downloads will take place. The following points apply to chunkless + * preparation: + * + *

            + *
          • A muxed audio track will be exposed if the codecs list contain an audio entry and the + * master playlist either contains an EXT-X-MEDIA tag without the URI attribute or does not + * contain any EXT-X-MEDIA tag. + *
          • Closed captions will only be exposed if they are declared by the master playlist. + *
          • ID3 tracks are not exposed. + *
          + * + * @param masterPlaylist The HLS master playlist. + * @param positionUs If preparation requires any chunk downloads, the position in microseconds at + * which downloading should start. Ignored otherwise. + */ + private void buildAndPrepareMainSampleStreamWrapper( + HlsMasterPlaylist masterPlaylist, long positionUs) { List selectedVariants = new ArrayList<>(masterPlaylist.variants); ArrayList definiteVideoVariants = new ArrayList<>(); ArrayList definiteAudioOnlyVariants = new ArrayList<>(); for (int i = 0; i < selectedVariants.size(); i++) { HlsUrl variant = selectedVariants.get(i); - if (variant.format.height > 0 || variantHasExplicitCodecWithPrefix(variant, "avc")) { + Format format = variant.format; + if (format.height > 0 || Util.getCodecsOfType(format.codecs, C.TRACK_TYPE_VIDEO) != null) { definiteVideoVariants.add(variant); - } else if (variantHasExplicitCodecWithPrefix(variant, "mp4a")) { + } else if (Util.getCodecsOfType(format.codecs, C.TRACK_TYPE_AUDIO) != null) { definiteAudioOnlyVariants.add(variant); } } @@ -317,43 +402,56 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper } else { // Leave the enabled variants unchanged. They're likely either all video or all audio. } - List audioRenditions = masterPlaylist.audios; - List subtitleRenditions = masterPlaylist.subtitles; - sampleStreamWrappers = new HlsSampleStreamWrapper[1 /* variants */ + audioRenditions.size() - + subtitleRenditions.size()]; - int currentWrapperIndex = 0; - pendingPrepareCount = sampleStreamWrappers.length; - Assertions.checkArgument(!selectedVariants.isEmpty()); - HlsUrl[] variants = new HlsMasterPlaylist.HlsUrl[selectedVariants.size()]; - selectedVariants.toArray(variants); + HlsUrl[] variants = selectedVariants.toArray(new HlsUrl[0]); + String codecs = variants[0].format.codecs; HlsSampleStreamWrapper sampleStreamWrapper = buildSampleStreamWrapper(C.TRACK_TYPE_DEFAULT, variants, masterPlaylist.muxedAudioFormat, masterPlaylist.muxedCaptionFormats, positionUs); - sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper; - sampleStreamWrapper.setIsTimestampMaster(true); - sampleStreamWrapper.continuePreparing(); + sampleStreamWrappers[0] = sampleStreamWrapper; + if (allowChunklessPreparation && codecs != null) { + boolean variantsContainVideoCodecs = Util.getCodecsOfType(codecs, C.TRACK_TYPE_VIDEO) != null; + boolean variantsContainAudioCodecs = Util.getCodecsOfType(codecs, C.TRACK_TYPE_AUDIO) != null; + List muxedTrackGroups = new ArrayList<>(); + if (variantsContainVideoCodecs) { + Format[] videoFormats = new Format[selectedVariants.size()]; + for (int i = 0; i < videoFormats.length; i++) { + videoFormats[i] = deriveVideoFormat(variants[i].format); + } + muxedTrackGroups.add(new TrackGroup(videoFormats)); - // TODO: Build video stream wrappers here. - - // Build audio stream wrappers. - for (int i = 0; i < audioRenditions.size(); i++) { - sampleStreamWrapper = buildSampleStreamWrapper(C.TRACK_TYPE_AUDIO, - new HlsUrl[] {audioRenditions.get(i)}, null, Collections.emptyList(), positionUs); - sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper; + if (variantsContainAudioCodecs + && (masterPlaylist.muxedAudioFormat != null || masterPlaylist.audios.isEmpty())) { + muxedTrackGroups.add( + new TrackGroup( + deriveMuxedAudioFormat( + variants[0].format, masterPlaylist.muxedAudioFormat, Format.NO_VALUE))); + } + List ccFormats = masterPlaylist.muxedCaptionFormats; + if (ccFormats != null) { + for (int i = 0; i < ccFormats.size(); i++) { + muxedTrackGroups.add(new TrackGroup(ccFormats.get(i))); + } + } + } else if (variantsContainAudioCodecs) { + // Variants only contain audio. + Format[] audioFormats = new Format[selectedVariants.size()]; + for (int i = 0; i < audioFormats.length; i++) { + Format variantFormat = variants[i].format; + audioFormats[i] = + deriveMuxedAudioFormat( + variantFormat, masterPlaylist.muxedAudioFormat, variantFormat.bitrate); + } + muxedTrackGroups.add(new TrackGroup(audioFormats)); + } else { + // Variants contain codecs but no video or audio entries could be identified. + throw new IllegalArgumentException("Unexpected codecs attribute: " + codecs); + } + sampleStreamWrapper.prepareWithMasterPlaylistInfo( + new TrackGroupArray(muxedTrackGroups.toArray(new TrackGroup[0])), 0); + } else { + sampleStreamWrapper.setIsTimestampMaster(true); sampleStreamWrapper.continuePreparing(); } - - // Build subtitle stream wrappers. - for (int i = 0; i < subtitleRenditions.size(); i++) { - HlsUrl url = subtitleRenditions.get(i); - sampleStreamWrapper = buildSampleStreamWrapper(C.TRACK_TYPE_TEXT, new HlsUrl[] {url}, null, - Collections.emptyList(), positionUs); - sampleStreamWrapper.prepareSingleTrack(url.format); - sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper; - } - - // All wrappers are enabled during preparation. - enabledSampleStreamWrappers = sampleStreamWrappers; } private HlsSampleStreamWrapper buildSampleStreamWrapper(int trackType, HlsUrl[] variants, @@ -375,18 +473,49 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper } } - private static boolean variantHasExplicitCodecWithPrefix(HlsUrl variant, String prefix) { - String codecs = variant.format.codecs; - if (TextUtils.isEmpty(codecs)) { - return false; + private static Format deriveVideoFormat(Format variantFormat) { + String codecs = Util.getCodecsOfType(variantFormat.codecs, C.TRACK_TYPE_VIDEO); + String mimeType = MimeTypes.getMediaMimeType(codecs); + return Format.createVideoSampleFormat( + variantFormat.id, + mimeType, + codecs, + variantFormat.bitrate, + Format.NO_VALUE, + variantFormat.width, + variantFormat.height, + variantFormat.frameRate, + null, + null); + } + + private static Format deriveMuxedAudioFormat( + Format variantFormat, Format mediaTagFormat, int bitrate) { + String codecs; + int channelCount = Format.NO_VALUE; + int selectionFlags = 0; + String language = null; + if (mediaTagFormat != null) { + codecs = mediaTagFormat.codecs; + channelCount = mediaTagFormat.channelCount; + selectionFlags = mediaTagFormat.selectionFlags; + language = mediaTagFormat.language; + } else { + codecs = Util.getCodecsOfType(variantFormat.codecs, C.TRACK_TYPE_AUDIO); } - String[] codecArray = codecs.split("(\\s*,\\s*)|(\\s*$)"); - for (String codec : codecArray) { - if (codec.startsWith(prefix)) { - return true; - } - } - return false; + String mimeType = MimeTypes.getMediaMimeType(codecs); + return Format.createAudioSampleFormat( + variantFormat.id, + mimeType, + codecs, + bitrate, + Format.NO_VALUE, + channelCount, + Format.NO_VALUE, + null, + null, + selectionFlags, + language); } } diff --git a/library/hls/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 index 4e5783698a..1cddf6e94e 100644 --- a/library/hls/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 @@ -63,8 +63,9 @@ public final class HlsMediaSource implements MediaSource, private MediaSourceEventListener eventListener; private Handler eventHandler; private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; - private int minLoadableRetryCount; + private boolean allowChunklessPreparation; + private boolean isBuildCalled; /** @@ -98,7 +99,6 @@ public final class HlsMediaSource implements MediaSource, private Builder(Uri manifestUri, HlsDataSourceFactory hlsDataSourceFactory) { this.manifestUri = manifestUri; this.hlsDataSourceFactory = hlsDataSourceFactory; - minLoadableRetryCount = DEFAULT_MIN_LOADABLE_RETRY_COUNT; } @@ -170,6 +170,18 @@ public final class HlsMediaSource implements MediaSource, return this; } + /** + * Sets whether chunkless preparation is allowed. If true, preparation without chunk downloads + * will be enabled for streams that provide sufficient information in their master playlist. + * + * @param allowChunklessPreparation Whether chunkless preparation is allowed. + * @return This builder. + */ + public Builder setAllowChunklessPreparation(boolean allowChunklessPreparation) { + this.allowChunklessPreparation = allowChunklessPreparation; + return this; + } + /** * Builds a new {@link HlsMediaSource} using the current parameters. *

          @@ -190,9 +202,16 @@ public final class HlsMediaSource implements MediaSource, if (compositeSequenceableLoaderFactory == null) { compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory(); } - return new HlsMediaSource(manifestUri, hlsDataSourceFactory, extractorFactory, - compositeSequenceableLoaderFactory, minLoadableRetryCount, eventHandler, eventListener, - playlistParser); + return new HlsMediaSource( + manifestUri, + hlsDataSourceFactory, + extractorFactory, + compositeSequenceableLoaderFactory, + minLoadableRetryCount, + eventHandler, + eventListener, + playlistParser, + allowChunklessPreparation); } } @@ -209,6 +228,7 @@ public final class HlsMediaSource implements MediaSource, private final int minLoadableRetryCount; private final EventDispatcher eventDispatcher; private final ParsingLoadable.Parser playlistParser; + private final boolean allowChunklessPreparation; private HlsPlaylistTracker playlistTracker; private Listener sourceListener; @@ -277,9 +297,16 @@ public final class HlsMediaSource implements MediaSource, Handler eventHandler, MediaSourceEventListener eventListener, ParsingLoadable.Parser playlistParser) { - this(manifestUri, dataSourceFactory, extractorFactory, - new DefaultCompositeSequenceableLoaderFactory(), minLoadableRetryCount, eventHandler, - eventListener, playlistParser); + this( + manifestUri, + dataSourceFactory, + extractorFactory, + new DefaultCompositeSequenceableLoaderFactory(), + minLoadableRetryCount, + eventHandler, + eventListener, + playlistParser, + false); } private HlsMediaSource( @@ -290,13 +317,15 @@ public final class HlsMediaSource implements MediaSource, int minLoadableRetryCount, Handler eventHandler, MediaSourceEventListener eventListener, - ParsingLoadable.Parser playlistParser) { + ParsingLoadable.Parser playlistParser, + boolean allowChunklessPreparation) { this.manifestUri = manifestUri; this.dataSourceFactory = dataSourceFactory; this.extractorFactory = extractorFactory; + this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; this.minLoadableRetryCount = minLoadableRetryCount; this.playlistParser = playlistParser; - this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; + this.allowChunklessPreparation = allowChunklessPreparation; eventDispatcher = new EventDispatcher(eventHandler, eventListener); } @@ -317,8 +346,15 @@ public final class HlsMediaSource implements MediaSource, @Override public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { Assertions.checkArgument(id.periodIndex == 0); - return new HlsMediaPeriod(extractorFactory, playlistTracker, dataSourceFactory, - minLoadableRetryCount, eventDispatcher, allocator, compositeSequenceableLoaderFactory); + return new HlsMediaPeriod( + extractorFactory, + playlistTracker, + dataSourceFactory, + minLoadableRetryCount, + eventDispatcher, + allocator, + compositeSequenceableLoaderFactory, + allowChunklessPreparation); } @Override diff --git a/library/hls/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 index 301cd2920b..6563a5fba0 100644 --- a/library/hls/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 @@ -52,7 +52,7 @@ import java.io.IOException; @Override public void maybeThrowError() throws IOException { - if (!ensureBoundSampleQueue()) { + if (!ensureBoundSampleQueue() && sampleStreamWrapper.isMappingFinished()) { throw new SampleQueueMappingException( sampleStreamWrapper.getTrackGroups().get(trackGroupIndex).getFormat(0).sampleMimeType); } diff --git a/library/hls/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 index 2e69e41d30..eba4596b7f 100644 --- a/library/hls/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 @@ -173,13 +173,17 @@ import java.util.Arrays; } /** - * Prepares a sample stream wrapper for which the master playlist provides enough information to - * prepare. + * Prepares the sample stream wrapper with master playlist information. + * + * @param trackGroups This {@link TrackGroupArray} to expose. + * @param primaryTrackGroupIndex The index of the adaptive track group. */ - public void prepareSingleTrack(Format format) { - track(0, C.TRACK_TYPE_UNKNOWN).format(format); - tracksEnded = true; - onTracksEnded(); + public void prepareWithMasterPlaylistInfo( + TrackGroupArray trackGroups, int primaryTrackGroupIndex) { + prepared = true; + this.trackGroups = trackGroups; + this.primaryTrackGroupIndex = primaryTrackGroupIndex; + callback.onPrepared(); } public void maybeThrowPrepareError() throws IOException { @@ -190,17 +194,30 @@ import java.util.Arrays; return trackGroups; } + public boolean isMappingFinished() { + return trackGroupToSampleQueueIndex != null; + } + public int bindSampleQueueToSampleStream(int trackGroupIndex) { + if (!isMappingFinished()) { + return C.INDEX_UNSET; + } int sampleQueueIndex = trackGroupToSampleQueueIndex[trackGroupIndex]; if (sampleQueueIndex == C.INDEX_UNSET) { return C.INDEX_UNSET; } - setSampleQueueEnabledState(sampleQueueIndex, true); + if (sampleQueuesEnabledStates[sampleQueueIndex]) { + // This sample queue is already bound to a different sample stream. + return C.INDEX_UNSET; + } + sampleQueuesEnabledStates[sampleQueueIndex] = true; return sampleQueueIndex; } public void unbindSampleQueue(int trackGroupIndex) { - setSampleQueueEnabledState(trackGroupToSampleQueueIndex[trackGroupIndex], false); + int sampleQueueIndex = trackGroupToSampleQueueIndex[trackGroupIndex]; + Assertions.checkState(sampleQueuesEnabledStates[sampleQueueIndex]); + sampleQueuesEnabledStates[sampleQueueIndex] = false; } /** @@ -693,7 +710,7 @@ import java.util.Arrays; } private void maybeFinishPrepare() { - if (released || prepared || !sampleQueuesBuilt) { + if (released || trackGroupToSampleQueueIndex != null || !sampleQueuesBuilt) { return; } for (SampleQueue sampleQueue : sampleQueues) { @@ -701,9 +718,31 @@ import java.util.Arrays; return; } } - buildTracks(); - prepared = true; - callback.onPrepared(); + if (trackGroups != null) { + // The track groups were created with master playlist information. They only need to be mapped + // to a sample queue. + mapSampleQueuesToMatchTrackGroups(); + } else { + // Tracks are created using media segment information. + buildTracks(); + prepared = true; + callback.onPrepared(); + } + } + + private void mapSampleQueuesToMatchTrackGroups() { + int trackGroupCount = trackGroups.length; + trackGroupToSampleQueueIndex = new int[trackGroupCount]; + Arrays.fill(trackGroupToSampleQueueIndex, C.INDEX_UNSET); + for (int i = 0; i < trackGroupCount; i++) { + for (int queueIndex = 0; queueIndex < sampleQueues.length; queueIndex++) { + SampleQueue sampleQueue = sampleQueues[queueIndex]; + if (formatsMatch(sampleQueue.getUpstreamFormat(), trackGroups.get(i).getFormat(0))) { + trackGroupToSampleQueueIndex[i] = queueIndex; + break; + } + } + } } /** @@ -794,17 +833,6 @@ import java.util.Arrays; this.trackGroups = new TrackGroupArray(trackGroups); } - /** - * Enables or disables a specified sample queue. - * - * @param sampleQueueIndex The index of the sample queue. - * @param enabledState True if the sample queue is being enabled, or false if it's being disabled. - */ - private void setSampleQueueEnabledState(int sampleQueueIndex, boolean enabledState) { - Assertions.checkState(sampleQueuesEnabledStates[sampleQueueIndex] != enabledState); - sampleQueuesEnabledStates[sampleQueueIndex] = enabledState; - } - private HlsMediaChunk getLastMediaChunk() { return mediaChunks.get(mediaChunks.size() - 1); } @@ -868,4 +896,19 @@ import java.util.Arrays; return chunk instanceof HlsMediaChunk; } + private static boolean formatsMatch(Format manifestFormat, Format sampleFormat) { + String manifestFormatMimeType = manifestFormat.sampleMimeType; + String sampleFormatMimeType = sampleFormat.sampleMimeType; + int manifestFormatTrackType = MimeTypes.getTrackType(manifestFormatMimeType); + if (manifestFormatTrackType != C.TRACK_TYPE_TEXT) { + return manifestFormatTrackType == MimeTypes.getTrackType(sampleFormatMimeType); + } else if (!Util.areEqual(manifestFormatMimeType, sampleFormatMimeType)) { + return false; + } + if (MimeTypes.APPLICATION_CEA608.equals(manifestFormatMimeType) + || MimeTypes.APPLICATION_CEA708.equals(manifestFormatMimeType)) { + return manifestFormat.accessibilityChannel == sampleFormat.accessibilityChannel; + } + return true; + } } From a7b11ecb17acb3dbd28b9158a13c9cab0f95efb1 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 6 Dec 2017 10:33:23 -0800 Subject: [PATCH 0842/2472] Add missing Nullable annotation ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178117289 --- .../java/com/google/android/exoplayer2/SimpleExoPlayer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 17cf118ea3..338c708864 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -729,7 +729,7 @@ public class SimpleExoPlayer implements ExoPlayer { } @Override - public void setPlaybackParameters(PlaybackParameters playbackParameters) { + public void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters) { player.setPlaybackParameters(playbackParameters); } From 2f6a497d4418fa1c949104d309894648a53e66eb Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 7 Dec 2017 02:45:16 -0800 Subject: [PATCH 0843/2472] Use mappedTrackInfo local ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178216750 --- .../com/google/android/exoplayer2/demo/PlayerActivity.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index 7d0975a750..0623f48a51 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -233,8 +233,8 @@ public class PlayerActivity extends Activity implements OnClickListener, } else if (view.getParent() == debugRootView) { MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo(); if (mappedTrackInfo != null) { - trackSelectionHelper.showSelectionDialog(this, ((Button) view).getText(), - trackSelector.getCurrentMappedTrackInfo(), (int) view.getTag()); + trackSelectionHelper.showSelectionDialog( + this, ((Button) view).getText(), mappedTrackInfo, (int) view.getTag()); } } } From 8b4b01c7f67e7d28d7125c7848a7236f0463586b Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 7 Dec 2017 03:05:22 -0800 Subject: [PATCH 0844/2472] Skip ads before the initial player position Issue: #3527 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178218391 --- RELEASENOTES.md | 3 + .../exoplayer2/ext/ima/ImaAdsLoader.java | 57 +++++++++++++++++-- 2 files changed, 54 insertions(+), 6 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 920d80ee48..3e70286a5a 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -41,6 +41,9 @@ implementations. * CEA-608: Fix handling of row count changes in roll-up mode ([#3513](https://github.com/google/ExoPlayer/issues/3513)). +* IMA extension: + * Skip ads before the ad preceding the player's initial seek position + ([#3527](https://github.com/google/ExoPlayer/issues/3527)). ### 2.6.0 ### diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index 0eba9db2ed..91e419fd48 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -65,7 +65,6 @@ import java.util.Map; */ public final class ImaAdsLoader extends Player.DefaultEventListener implements AdsLoader, VideoAdPlayer, ContentProgressProvider, AdErrorListener, AdsLoadedListener, AdEventListener { - static { ExoPlayerLibraryInfo.registerModule("goog.exo.ima"); } @@ -132,6 +131,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A private AdsManager adsManager; private Timeline timeline; private long contentDurationMs; + private int podIndexOffset; private AdPlaybackState adPlaybackState; // Fields tracking IMA's state. @@ -274,6 +274,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A adsManager.resume(); } } else { + pendingContentPositionMs = player.getCurrentPosition(); requestAds(); } } @@ -311,19 +312,45 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A return; } pendingAdRequestContext = null; + + long[] adGroupTimesUs = getAdGroupTimesUs(adsManager.getAdCuePoints()); + adPlaybackState = new AdPlaybackState(adGroupTimesUs); + this.adsManager = adsManager; adsManager.addAdErrorListener(this); adsManager.addAdEventListener(this); + ImaSdkFactory imaSdkFactory = ImaSdkFactory.getInstance(); AdsRenderingSettings adsRenderingSettings = imaSdkFactory.createAdsRenderingSettings(); adsRenderingSettings.setEnablePreloading(ENABLE_PRELOADING); adsRenderingSettings.setMimeTypes(supportedMimeTypes); + int adGroupIndexForPosition = + getAdGroupIndexForPosition(adGroupTimesUs, C.msToUs(pendingContentPositionMs)); + if (adGroupIndexForPosition == C.INDEX_UNSET) { + pendingContentPositionMs = C.TIME_UNSET; + } else if (adGroupIndexForPosition > 0) { + // Skip ad groups before the one at or immediately before the playback position. + for (int i = 0; i < adGroupIndexForPosition; i++) { + adPlaybackState.playedAdGroup(i); + } + // Play ads after the midpoint between the ad to play and the one before it, to avoid issues + // with rounding one of the two ad times. + long adGroupForPositionTimeUs = adGroupTimesUs[adGroupIndexForPosition]; + long adGroupBeforeTimeUs = adGroupTimesUs[adGroupIndexForPosition - 1]; + double midpointTimeUs = (adGroupForPositionTimeUs + adGroupBeforeTimeUs) / 2d; + adsRenderingSettings.setPlayAdsAfterTime(midpointTimeUs / C.MICROS_PER_SECOND); + + // We're removing one or more ads, which means that the earliest ad (if any) will be a + // midroll/postroll. According to the AdPodInfo documentation, midroll pod indices always + // start at 1, so take this into account when offsetting the pod index for the skipped ads. + podIndexOffset = adGroupIndexForPosition - 1; + } + adsManager.init(adsRenderingSettings); if (DEBUG) { Log.d(TAG, "Initialized with ads rendering settings: " + adsRenderingSettings); } - long[] adGroupTimesUs = getAdGroupTimesUs(adsManager.getAdCuePoints()); - adPlaybackState = new AdPlaybackState(adGroupTimesUs); + updateAdPlaybackState(); } @@ -351,13 +378,15 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A // The ad position is not always accurate when using preloading. See [Internal: b/62613240]. AdPodInfo adPodInfo = ad.getAdPodInfo(); int podIndex = adPodInfo.getPodIndex(); - adGroupIndex = podIndex == -1 ? adPlaybackState.adGroupCount - 1 : podIndex; + adGroupIndex = + podIndex == -1 ? (adPlaybackState.adGroupCount - 1) : (podIndex + podIndexOffset); int adPosition = adPodInfo.getAdPosition(); int adCountInAdGroup = adPodInfo.getTotalAds(); adsManager.start(); if (DEBUG) { - Log.d(TAG, "Loaded ad " + adPosition + " of " + adCountInAdGroup + " in ad group " - + adGroupIndex); + Log.d( + TAG, + "Loaded ad " + adPosition + " of " + adCountInAdGroup + " in group " + adGroupIndex); } adPlaybackState.setAdCount(adGroupIndex, adCountInAdGroup); updateAdPlaybackState(); @@ -741,4 +770,20 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A return adGroupTimesUs; } + /** + * Returns the index of the ad group that should be played before playing the content at {@code + * playbackPositionUs} when starting playback for the first time. This is the latest ad group at + * or before the specified playback position. If the first ad is after the playback position, + * returns {@link C#INDEX_UNSET}. + */ + private int getAdGroupIndexForPosition(long[] adGroupTimesUs, long playbackPositionUs) { + for (int i = 0; i < adGroupTimesUs.length; i++) { + long adGroupTimeUs = adGroupTimesUs[i]; + // A postroll ad is after any position in the content. + if (adGroupTimeUs == C.TIME_END_OF_SOURCE || playbackPositionUs < adGroupTimeUs) { + return i == 0 ? C.INDEX_UNSET : (i - 1); + } + } + return adGroupTimesUs.length == 0 ? C.INDEX_UNSET : (adGroupTimesUs.length - 1); + } } From 439c3022d9453691432d7b4c05957e2b5c1521fc Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 7 Dec 2017 03:06:54 -0800 Subject: [PATCH 0845/2472] Blacklist Moto Z from using secure DummySurface. Issue: #3215 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178218535 --- .../com/google/android/exoplayer2/video/DummySurface.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java index 2d7a9dfd33..cc50443296 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java @@ -150,14 +150,17 @@ public final class DummySurface extends Surface { */ @TargetApi(24) private static boolean enableSecureDummySurfaceV24(Context context) { - if (Util.SDK_INT < 26 && "samsung".equals(Util.MANUFACTURER)) { + if (Util.SDK_INT < 26 && ("samsung".equals(Util.MANUFACTURER) || "XT1650".equals(Util.MODEL))) { // Samsung devices running Nougat are known to be broken. See // https://github.com/google/ExoPlayer/issues/3373 and [Internal: b/37197802]. + // Moto Z XT1650 is also affected. See + // https://github.com/google/ExoPlayer/issues/3215. return false; } if (Util.SDK_INT < 26 && !context.getPackageManager().hasSystemFeature( PackageManager.FEATURE_VR_MODE_HIGH_PERFORMANCE)) { - // Pre API level 26 devices were not well tested unless they supported VR mode. + // Pre API level 26 devices were not well tested unless they supported VR mode. See + // https://github.com/google/ExoPlayer/issues/3215. return false; } EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY); From 44a8245a1a6a4ca575e2ac808a549fb1d9cbd926 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 7 Dec 2017 06:38:39 -0800 Subject: [PATCH 0846/2472] Fix ad loading when there is no preroll ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178234009 --- RELEASENOTES.md | 1 + .../android/exoplayer2/ext/ima/ImaAdsLoader.java | 11 +++++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 3e70286a5a..0caad888d9 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -44,6 +44,7 @@ * IMA extension: * Skip ads before the ad preceding the player's initial seek position ([#3527](https://github.com/google/ExoPlayer/issues/3527)). + * Fix ad loading when there is no preroll. ### 2.6.0 ### diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index 91e419fd48..19dfa1e83f 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -326,9 +326,13 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A adsRenderingSettings.setMimeTypes(supportedMimeTypes); int adGroupIndexForPosition = getAdGroupIndexForPosition(adGroupTimesUs, C.msToUs(pendingContentPositionMs)); - if (adGroupIndexForPosition == C.INDEX_UNSET) { + if (adGroupIndexForPosition == 0) { + podIndexOffset = 0; + } else if (adGroupIndexForPosition == C.INDEX_UNSET) { pendingContentPositionMs = C.TIME_UNSET; - } else if (adGroupIndexForPosition > 0) { + // There is no preroll and midroll pod indices start at 1. + podIndexOffset = -1; + } else /* adGroupIndexForPosition > 0 */ { // Skip ad groups before the one at or immediately before the playback position. for (int i = 0; i < adGroupIndexForPosition; i++) { adPlaybackState.playedAdGroup(i); @@ -341,8 +345,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A adsRenderingSettings.setPlayAdsAfterTime(midpointTimeUs / C.MICROS_PER_SECOND); // We're removing one or more ads, which means that the earliest ad (if any) will be a - // midroll/postroll. According to the AdPodInfo documentation, midroll pod indices always - // start at 1, so take this into account when offsetting the pod index for the skipped ads. + // midroll/postroll. Midroll pod indices start at 1. podIndexOffset = adGroupIndexForPosition - 1; } From fede9c39c6c3ec4eaea64e13f149da4abac16e97 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 8 Dec 2017 04:44:55 -0800 Subject: [PATCH 0847/2472] Treat captions that are wider than expected as middle aligned Issue: #3534 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178364353 --- .../google/android/exoplayer2/text/cea/Cea608Decoder.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/library/core/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 index 0483f909b3..f018e055fb 100644 --- a/library/core/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 @@ -740,8 +740,10 @@ public final class Cea608Decoder extends CeaDecoder { // The number of empty columns after the end of the text, in the same range. int endPadding = SCREEN_CHARWIDTH - startPadding - cueString.length(); int startEndPaddingDelta = startPadding - endPadding; - if (captionMode == CC_MODE_POP_ON && Math.abs(startEndPaddingDelta) < 3) { - // Treat approximately centered pop-on captions are middle aligned. + if (captionMode == CC_MODE_POP_ON && (Math.abs(startEndPaddingDelta) < 3 || endPadding < 0)) { + // Treat approximately centered pop-on captions as middle aligned. We also treat captions + // that are wider than they should be in this way. See + // https://github.com/google/ExoPlayer/issues/3534. position = 0.5f; positionAnchor = Cue.ANCHOR_TYPE_MIDDLE; } else if (captionMode == CC_MODE_POP_ON && startEndPaddingDelta > 0) { From 2b0b39ca3898ec71a8aa7b29386f789b3fff26fb Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 8 Dec 2017 06:14:14 -0800 Subject: [PATCH 0848/2472] Public API for setting seek parameters Issue: #2882 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178370038 --- .../google/android/exoplayer2/ExoPlayer.java | 7 ++ .../android/exoplayer2/ExoPlayerImpl.java | 8 ++ .../exoplayer2/ExoPlayerImplInternal.java | 76 ++++++++++--------- .../android/exoplayer2/SeekParameters.java | 72 ++++++++++++++++++ .../android/exoplayer2/SimpleExoPlayer.java | 5 ++ .../exoplayer2/testutil/StubExoPlayer.java | 6 ++ 6 files changed, 137 insertions(+), 37 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/SeekParameters.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java index 915a083657..cc767752be 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2; import android.os.Looper; +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.ClippingMediaSource; @@ -251,4 +252,10 @@ public interface ExoPlayer extends Player { */ void blockingSendMessages(ExoPlayerMessage... messages); + /** + * Sets the parameters that control how seek operations are performed. + * + * @param seekParameters The seek parameters, or {@code null} to use the defaults. + */ + void setSeekParameters(@Nullable SeekParameters seekParameters); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index ee96cb0c47..3fe6cc6eed 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -258,6 +258,14 @@ import java.util.concurrent.CopyOnWriteArraySet; return playbackParameters; } + @Override + public void setSeekParameters(@Nullable SeekParameters seekParameters) { + if (seekParameters == null) { + seekParameters = SeekParameters.DEFAULT; + } + internalPlayer.setSeekParameters(seekParameters); + } + @Override public void stop() { stop(/* reset= */ false); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index dd3ce136d6..8f59451c48 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -66,15 +66,16 @@ import java.io.IOException; private static final int MSG_DO_SOME_WORK = 2; private static final int MSG_SEEK_TO = 3; 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 MSG_SET_REPEAT_MODE = 12; - private static final int MSG_SET_SHUFFLE_ENABLED = 13; + private static final int MSG_SET_SEEK_PARAMETERS = 5; + private static final int MSG_STOP = 6; + private static final int MSG_RELEASE = 7; + private static final int MSG_REFRESH_SOURCE_INFO = 8; + private static final int MSG_PERIOD_PREPARED = 9; + private static final int MSG_SOURCE_CONTINUE_LOADING_REQUESTED = 10; + private static final int MSG_TRACK_SELECTION_INVALIDATED = 11; + private static final int MSG_CUSTOM = 12; + private static final int MSG_SET_REPEAT_MODE = 13; + private static final int MSG_SET_SHUFFLE_ENABLED = 14; private static final int PREPARING_SOURCE_INTERVAL_MS = 10; private static final int RENDERING_INTERVAL_MS = 10; @@ -110,6 +111,9 @@ import java.io.IOException; private final boolean retainBackBufferFromKeyframe; private final DefaultMediaClock mediaClock; + @SuppressWarnings("unused") + private SeekParameters seekParameters; + private PlaybackInfo playbackInfo; private MediaSource mediaSource; private Renderer[] enabledRenderers; @@ -148,6 +152,7 @@ import java.io.IOException; backBufferDurationUs = loadControl.getBackBufferDurationUs(); retainBackBufferFromKeyframe = loadControl.retainBackBufferFromKeyframe(); + seekParameters = SeekParameters.DEFAULT; playbackInfo = new PlaybackInfo(null, null, 0, C.TIME_UNSET); rendererCapabilities = new RendererCapabilities[renderers.length]; for (int i = 0; i < renderers.length; i++) { @@ -195,6 +200,10 @@ import java.io.IOException; handler.obtainMessage(MSG_SET_PLAYBACK_PARAMETERS, playbackParameters).sendToTarget(); } + public void setSeekParameters(SeekParameters seekParameters) { + handler.obtainMessage(MSG_SET_SEEK_PARAMETERS, seekParameters).sendToTarget(); + } + public void stop(boolean reset) { handler.obtainMessage(MSG_STOP, reset ? 1 : 0, 0).sendToTarget(); } @@ -294,62 +303,51 @@ import java.io.IOException; public boolean handleMessage(Message msg) { try { switch (msg.what) { - case MSG_PREPARE: { + case MSG_PREPARE: prepareInternal((MediaSource) msg.obj, msg.arg1 != 0); return true; - } - case MSG_SET_PLAY_WHEN_READY: { + case MSG_SET_PLAY_WHEN_READY: setPlayWhenReadyInternal(msg.arg1 != 0); return true; - } - case MSG_SET_REPEAT_MODE: { + case MSG_SET_REPEAT_MODE: setRepeatModeInternal(msg.arg1); return true; - } - case MSG_SET_SHUFFLE_ENABLED: { + case MSG_SET_SHUFFLE_ENABLED: setShuffleModeEnabledInternal(msg.arg1 != 0); return true; - } - case MSG_DO_SOME_WORK: { + case MSG_DO_SOME_WORK: doSomeWork(); return true; - } - case MSG_SEEK_TO: { + case MSG_SEEK_TO: seekToInternal((SeekPosition) msg.obj); return true; - } - case MSG_SET_PLAYBACK_PARAMETERS: { + case MSG_SET_PLAYBACK_PARAMETERS: setPlaybackParametersInternal((PlaybackParameters) msg.obj); return true; - } - case MSG_STOP: { + case MSG_SET_SEEK_PARAMETERS: + setSeekParametersInternal((SeekParameters) msg.obj); + return true; + case MSG_STOP: stopInternal(/* reset= */ msg.arg1 != 0); return true; - } - case MSG_RELEASE: { + case MSG_RELEASE: releaseInternal(); return true; - } - case MSG_PERIOD_PREPARED: { + case MSG_PERIOD_PREPARED: handlePeriodPrepared((MediaPeriod) msg.obj); return true; - } - case MSG_REFRESH_SOURCE_INFO: { + case MSG_REFRESH_SOURCE_INFO: handleSourceInfoRefreshed((MediaSourceRefreshInfo) msg.obj); return true; - } - case MSG_SOURCE_CONTINUE_LOADING_REQUESTED: { + case MSG_SOURCE_CONTINUE_LOADING_REQUESTED: handleContinueLoadingRequested((MediaPeriod) msg.obj); return true; - } - case MSG_TRACK_SELECTION_INVALIDATED: { + case MSG_TRACK_SELECTION_INVALIDATED: reselectTracksInternal(); return true; - } - case MSG_CUSTOM: { + case MSG_CUSTOM: sendMessagesInternal((ExoPlayerMessage[]) msg.obj); return true; - } default: return false; } @@ -765,6 +763,10 @@ import java.io.IOException; mediaClock.setPlaybackParameters(playbackParameters); } + private void setSeekParametersInternal(SeekParameters seekParameters) { + this.seekParameters = seekParameters; + } + private void stopInternal(boolean reset) { resetInternal( /* releaseMediaSource= */ true, /* resetPosition= */ reset, /* resetState= */ reset); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SeekParameters.java b/library/core/src/main/java/com/google/android/exoplayer2/SeekParameters.java new file mode 100644 index 0000000000..8643b3999e --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/SeekParameters.java @@ -0,0 +1,72 @@ +/* + * 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 com.google.android.exoplayer2.util.Assertions; + +/** + * Parameters that apply to seeking. + * + *

          The predefined {@link #EXACT}, {@link #CLOSEST_SYNC}, {@link #PREVIOUS_SYNC} and {@link + * #NEXT_SYNC} parameters are suitable for most use cases. Seeking to sync points is typically + * faster but less accurate than exact seeking. + * + *

          In the general case, an instance specifies a maximum tolerance before ({@link + * #toleranceBeforeUs}) and after ({@link #toleranceAfterUs}) a requested seek position ({@code x}). + * If one or more sync points falls within the window {@code [x - toleranceBeforeUs, x + + * toleranceAfterUs]} then the seek will be performed to the sync point within the window that's + * closest to {@code x}. If no sync point falls within the window then the seek will be performed to + * {@code x - toleranceBeforeUs}. Internally the player may need to seek to an earlier sync point + * and discard media until this position is reached. + */ +public final class SeekParameters { + + /** Parameters for exact seeking. */ + public static final SeekParameters EXACT = new SeekParameters(0, 0); + /** Parameters for seeking to the closest sync point. */ + public static final SeekParameters CLOSEST_SYNC = + new SeekParameters(Long.MAX_VALUE, Long.MAX_VALUE); + /** Parameters for seeking to the sync point immediately before a requested seek position. */ + public static final SeekParameters PREVIOUS_SYNC = new SeekParameters(Long.MAX_VALUE, 0); + /** Parameters for seeking to the sync point immediately after a requested seek position. */ + public static final SeekParameters NEXT_SYNC = new SeekParameters(0, Long.MAX_VALUE); + /** Default parameters. */ + public static final SeekParameters DEFAULT = EXACT; + + /** + * The maximum time that the actual position seeked to may precede the requested seek position, in + * microseconds. + */ + public final long toleranceBeforeUs; + /** + * The maximum time that the actual position seeked to may exceed the requested seek position, in + * microseconds. + */ + public final long toleranceAfterUs; + + /** + * @param toleranceBeforeUs The maximum time that the actual position seeked to may precede the + * requested seek position, in microseconds. Must be non-negative. + * @param toleranceAfterUs The maximum time that the actual position seeked to may exceed the + * requested seek position, in microseconds. Must be non-negative. + */ + public SeekParameters(long toleranceBeforeUs, long toleranceAfterUs) { + Assertions.checkArgument(toleranceBeforeUs >= 0); + Assertions.checkArgument(toleranceAfterUs >= 0); + this.toleranceBeforeUs = toleranceBeforeUs; + this.toleranceAfterUs = toleranceAfterUs; + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 338c708864..69369d4229 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -738,6 +738,11 @@ public class SimpleExoPlayer implements ExoPlayer { return player.getPlaybackParameters(); } + @Override + public void setSeekParameters(@Nullable SeekParameters seekParameters) { + player.setSeekParameters(seekParameters); + } + @Override public void stop() { player.stop(); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java index 0d94b8fa03..1ea83bf1ec 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java @@ -19,6 +19,7 @@ import android.os.Looper; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.TrackGroupArray; @@ -125,6 +126,11 @@ public abstract class StubExoPlayer implements ExoPlayer { throw new UnsupportedOperationException(); } + @Override + public void setSeekParameters(SeekParameters seekParameters) { + throw new UnsupportedOperationException(); + } + @Override public void stop() { throw new UnsupportedOperationException(); From aa01fb85dc816e035ab8ab2f75a30ca256953b8b Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 8 Dec 2017 06:51:51 -0800 Subject: [PATCH 0849/2472] Add an option to turn off hiding controls during ads Issue: #3532 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178372763 --- RELEASENOTES.md | 2 + .../exoplayer2/ui/SimpleExoPlayerView.java | 122 ++++++++++-------- library/ui/src/main/res/values/attrs.xml | 1 + 3 files changed, 70 insertions(+), 55 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 0caad888d9..a464b7e826 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -45,6 +45,8 @@ * Skip ads before the ad preceding the player's initial seek position ([#3527](https://github.com/google/ExoPlayer/issues/3527)). * Fix ad loading when there is no preroll. + * Add an option to turn off hiding controls during ad playback + ([#3532](https://github.com/google/ExoPlayer/issues/3532)). ### 2.6.0 ### diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index dcc1c62569..1f67b83ba0 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -56,146 +56,144 @@ import java.util.List; /** * A high level view for {@link SimpleExoPlayer} media playbacks. It displays video, subtitles and * album art during playback, and displays playback controls using a {@link PlaybackControlView}. - *

          - * A SimpleExoPlayerView can be customized by setting attributes (or calling corresponding methods), - * overriding the view's layout file or by specifying a custom view layout file, as outlined below. + * + *

          A SimpleExoPlayerView can be customized by setting attributes (or calling corresponding + * methods), overriding the view's layout file or by specifying a custom view layout file, as + * outlined below. * *

          Attributes

          + * * The following attributes can be set on a SimpleExoPlayerView when used in a layout XML file: + * *

          + * *

            *
          • {@code use_artwork} - Whether artwork is used if available in audio streams. *
              - *
            • Corresponding method: {@link #setUseArtwork(boolean)}
            • - *
            • Default: {@code true}
            • + *
            • Corresponding method: {@link #setUseArtwork(boolean)} + *
            • Default: {@code true} *
            - *
          • *
          • {@code default_artwork} - Default artwork to use if no artwork available in audio * streams. *
              - *
            • Corresponding method: {@link #setDefaultArtwork(Bitmap)}
            • - *
            • Default: {@code null}
            • + *
            • Corresponding method: {@link #setDefaultArtwork(Bitmap)} + *
            • Default: {@code null} *
            - *
          • *
          • {@code use_controller} - Whether the playback controls can be shown. *
              - *
            • Corresponding method: {@link #setUseController(boolean)}
            • - *
            • Default: {@code true}
            • + *
            • 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}
            • + *
            • Corresponding method: {@link #setControllerHideOnTouch(boolean)} + *
            • Default: {@code true} *
            - *
          • *
          • {@code auto_show} - Whether the playback controls are automatically shown when * playback starts, pauses, ends, or fails. If set to false, the playback controls can be * manually operated with {@link #showController()} and {@link #hideController()}. *
              - *
            • Corresponding method: {@link #setControllerAutoShow(boolean)}
            • - *
            • Default: {@code true}
            • + *
            • Corresponding method: {@link #setControllerAutoShow(boolean)} + *
            • Default: {@code true} + *
            + *
          • {@code hide_during_ads} - Whether the playback controls are hidden during ads. + * Controls are always shown during ads if they are enabled and the player is paused. + *
              + *
            • Corresponding method: {@link #setControllerHideDuringAds(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}. *
              - *
            • Corresponding method: {@link #setResizeMode(int)}
            • - *
            • Default: {@code fit}
            • + *
            • Corresponding method: {@link #setResizeMode(int)} + *
            • Default: {@code fit} *
            - *
          • *
          • {@code surface_type} - The type of surface view used for video playbacks. Valid * values are {@code surface_view}, {@code texture_view} and {@code none}. Using {@code none} * is recommended for audio only applications, since creating the surface can be expensive. * Using {@code surface_view} is recommended for video applications. *
              - *
            • Corresponding method: None
            • - *
            • Default: {@code surface_view}
            • + *
            • Corresponding method: None + *
            • Default: {@code surface_view} *
            - *
          • *
          • {@code shutter_background_color} - The background color of the {@code exo_shutter} * view. *
              - *
            • Corresponding method: {@link #setShutterBackgroundColor(int)}
            • - *
            • Default: {@code unset}
            • + *
            • Corresponding method: {@link #setShutterBackgroundColor(int)} + *
            • Default: {@code unset} *
            - *
          • *
          • {@code player_layout_id} - Specifies the id of the layout to be inflated. See below * for more details. *
              - *
            • Corresponding method: None
            • - *
            • Default: {@code R.id.exo_simple_player_view}
            • + *
            • Corresponding method: None + *
            • Default: {@code R.id.exo_simple_player_view} *
            *
          • {@code controller_layout_id} - Specifies the id of the layout resource to be * inflated by the child {@link PlaybackControlView}. See below for more details. *
              - *
            • Corresponding method: None
            • - *
            • Default: {@code R.id.exo_playback_control_view}
            • + *
            • Corresponding method: None + *
            • Default: {@code R.id.exo_playback_control_view} *
            *
          • All attributes that can be set on a {@link PlaybackControlView} can also be set on a * SimpleExoPlayerView, and will be propagated to the inflated {@link PlaybackControlView} * unless the layout is overridden to specify a custom {@code exo_controller} (see below). - *
          • *
          * *

          Overriding the layout file

          + * * To customize the layout of SimpleExoPlayerView throughout your app, or just for certain * configurations, you can define {@code exo_simple_player_view.xml} layout files in your * application {@code res/layout*} directories. These layouts will override the one provided by the * ExoPlayer library, and will be inflated for use by SimpleExoPlayerView. The view identifies and * binds its children by looking for the following ids: + * *

          + * *

            *
          • {@code exo_content_frame} - A frame whose aspect ratio is resized based on the video * or album art of the media being played, and the configured {@code resize_mode}. The video * surface view is inflated into this frame as its first child. *
              - *
            • Type: {@link AspectRatioFrameLayout}
            • + *
            • Type: {@link AspectRatioFrameLayout} *
            - *
          • *
          • {@code exo_shutter} - A view that's made visible when video should be hidden. This * view is typically an opaque view that covers the video surface view, thereby obscuring it * when visible. *
              - *
            • Type: {@link View}
            • + *
            • Type: {@link View} *
            - *
          • *
          • {@code exo_subtitles} - Displays subtitles. *
              - *
            • Type: {@link SubtitleView}
            • + *
            • Type: {@link SubtitleView} *
            - *
          • *
          • {@code exo_artwork} - Displays album art. *
              - *
            • Type: {@link ImageView}
            • + *
            • Type: {@link ImageView} *
            - *
          • *
          • {@code exo_controller_placeholder} - A placeholder that's replaced with the inflated * {@link PlaybackControlView}. Ignored if an {@code exo_controller} view exists. *
              - *
            • Type: {@link View}
            • + *
            • Type: {@link View} *
            - *
          • *
          • {@code exo_controller} - An already inflated {@link PlaybackControlView}. Allows use - * of a custom extension of {@link PlaybackControlView}. Note that attributes such as - * {@code rewind_increment} will not be automatically propagated through to this instance. If - * a view exists with this id, any {@code exo_controller_placeholder} view will be ignored. + * of a custom extension of {@link PlaybackControlView}. Note that attributes such as {@code + * rewind_increment} will not be automatically propagated through to this instance. If a view + * exists with this id, any {@code exo_controller_placeholder} view will be ignored. *
              - *
            • Type: {@link PlaybackControlView}
            • + *
            • Type: {@link PlaybackControlView} *
            - *
          • *
          • {@code exo_overlay} - A {@link FrameLayout} positioned on top of the player which * the app can access via {@link #getOverlayFrameLayout()}, provided for convenience. *
              - *
            • Type: {@link FrameLayout}
            • + *
            • Type: {@link FrameLayout} *
            - *
          • *
          - *

          - * All child views are optional and so can be omitted if not required, however where defined they + * + *

          All child views are optional and so can be omitted if not required, however where defined they * must be of the expected type. * *

          Specifying a custom layout file

          + * * Defining your own {@code exo_simple_player_view.xml} is useful to customize the layout of * SimpleExoPlayerView throughout your application. It's also possible to customize the layout for a * single instance in a layout file. This is achieved by setting the {@code player_layout_id} @@ -224,6 +222,7 @@ public final class SimpleExoPlayerView extends FrameLayout { private Bitmap defaultArtwork; private int controllerShowTimeoutMs; private boolean controllerAutoShow; + private boolean controllerHideDuringAds; private boolean controllerHideOnTouch; public SimpleExoPlayerView(Context context) { @@ -267,6 +266,7 @@ public final class SimpleExoPlayerView extends FrameLayout { int controllerShowTimeoutMs = PlaybackControlView.DEFAULT_SHOW_TIMEOUT_MS; boolean controllerHideOnTouch = true; boolean controllerAutoShow = true; + boolean controllerHideDuringAds = true; if (attrs != null) { TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.SimpleExoPlayerView, 0, 0); @@ -288,6 +288,8 @@ public final class SimpleExoPlayerView extends FrameLayout { controllerHideOnTouch); controllerAutoShow = a.getBoolean(R.styleable.SimpleExoPlayerView_auto_show, controllerAutoShow); + controllerHideDuringAds = + a.getBoolean(R.styleable.SimpleExoPlayerView_hide_during_ads, controllerHideDuringAds); } finally { a.recycle(); } @@ -358,6 +360,7 @@ public final class SimpleExoPlayerView extends FrameLayout { this.controllerShowTimeoutMs = controller != null ? controllerShowTimeoutMs : 0; this.controllerHideOnTouch = controllerHideOnTouch; this.controllerAutoShow = controllerAutoShow; + this.controllerHideDuringAds = controllerHideDuringAds; this.useController = useController && controller != null; hideController(); } @@ -649,6 +652,16 @@ public final class SimpleExoPlayerView extends FrameLayout { this.controllerAutoShow = controllerAutoShow; } + /** + * Sets whether the playback controls are hidden when ads are playing. Controls are always shown + * during ads if they are enabled and the player is paused. + * + * @param controllerHideDuringAds Whether the playback controls are hidden when ads are playing. + */ + public void setControllerHideDuringAds(boolean controllerHideDuringAds) { + this.controllerHideDuringAds = controllerHideDuringAds; + } + /** * Set the {@link PlaybackControlView.VisibilityListener}. * @@ -784,8 +797,7 @@ public final class SimpleExoPlayerView extends FrameLayout { * Shows the playback controls, but only if forced or shown indefinitely. */ private void maybeShowController(boolean isForced) { - if (isPlayingAd()) { - // Never show the controller if an ad is currently playing. + if (isPlayingAd() && controllerHideDuringAds) { return; } if (useController) { @@ -956,7 +968,7 @@ public final class SimpleExoPlayerView extends FrameLayout { @Override public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { - if (isPlayingAd()) { + if (isPlayingAd() && controllerHideDuringAds) { hideController(); } else { maybeShowController(false); @@ -965,7 +977,7 @@ public final class SimpleExoPlayerView extends FrameLayout { @Override public void onPositionDiscontinuity(@DiscontinuityReason int reason) { - if (isPlayingAd()) { + if (isPlayingAd() && controllerHideDuringAds) { hideController(); } } diff --git a/library/ui/src/main/res/values/attrs.xml b/library/ui/src/main/res/values/attrs.xml index 525f95768c..1ab3854d21 100644 --- a/library/ui/src/main/res/values/attrs.xml +++ b/library/ui/src/main/res/values/attrs.xml @@ -47,6 +47,7 @@ + From e419154b86b0aad7ccd0cdcdda8db25d935fedb8 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 8 Dec 2017 07:57:06 -0800 Subject: [PATCH 0850/2472] Make DashMediaSource.Builder a factory for DashMediaSources This is in preparation for supporting non-extractor MediaSources for ads in AdsMediaSource. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178377627 --- .../exoplayer2/castdemo/PlayerManager.java | 7 +- .../exoplayer2/demo/PlayerActivity.java | 9 +- .../exoplayer2/source/ads/AdsMediaSource.java | 65 +++--- .../source/dash/DashMediaSource.java | 219 ++++++++++-------- .../playbacktests/gts/DashTestRunner.java | 5 +- 5 files changed, 159 insertions(+), 146 deletions(-) diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java index 0f4adfae99..7d63150201 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java @@ -393,10 +393,9 @@ import java.util.ArrayList; new DefaultSsChunkSource.Factory(DATA_SOURCE_FACTORY)) .build(); case DemoUtil.MIME_TYPE_DASH: - return DashMediaSource.Builder - .forManifestUri(uri, DATA_SOURCE_FACTORY, - new DefaultDashChunkSource.Factory(DATA_SOURCE_FACTORY)) - .build(); + return new DashMediaSource.Factory( + new DefaultDashChunkSource.Factory(DATA_SOURCE_FACTORY), DATA_SOURCE_FACTORY) + .createMediaSource(uri); case DemoUtil.MIME_TYPE_HLS: return HlsMediaSource.Builder .forDataSource(uri, DATA_SOURCE_FACTORY) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index 0623f48a51..1be6df8437 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -371,11 +371,10 @@ public class PlayerActivity extends Activity implements OnClickListener, .setEventListener(mainHandler, eventLogger) .build(); case C.TYPE_DASH: - return DashMediaSource.Builder - .forManifestUri(uri, buildDataSourceFactory(false), - new DefaultDashChunkSource.Factory(mediaDataSourceFactory)) - .setEventListener(mainHandler, eventLogger) - .build(); + return new DashMediaSource.Factory( + new DefaultDashChunkSource.Factory(mediaDataSourceFactory), + buildDataSourceFactory(false)) + .createMediaSource(uri, mainHandler, eventLogger); case C.TYPE_HLS: return HlsMediaSource.Builder .forDataSource(uri, mediaDataSourceFactory) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java index 54a8fd96ae..c701d6ca64 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java @@ -44,6 +44,31 @@ import java.util.Map; */ public final class AdsMediaSource implements MediaSource { + /** Factory for creating {@link MediaSource}s to play ad media. */ + public interface MediaSourceFactory { + + /** + * Creates a new {@link MediaSource} for loading the ad media with the specified {@code uri}. + * + * @param uri The URI of the media or manifest to play. + * @param handler A handler for listener events. May be null if delivery of events is not + * required. + * @param listener A listener for events. May be null if delivery of events is not required. + * @return The new media source. + */ + MediaSource createMediaSource( + Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener); + + /** + * Returns the content types supported by media sources created by this factory. Each element + * should be one of {@link C#TYPE_DASH}, {@link C#TYPE_SS}, {@link C#TYPE_HLS} or {@link + * C#TYPE_OTHER}. + * + * @return The content types supported by media sources created by this factory. + */ + int[] getSupportedTypes(); + } + /** Listener for ads media source events. */ public interface EventListener extends MediaSourceEventListener { @@ -77,7 +102,7 @@ public final class AdsMediaSource implements MediaSource { @Nullable private final EventListener eventListener; private final Handler mainHandler; private final ComponentListener componentListener; - private final AdMediaSourceFactory adMediaSourceFactory; + private final MediaSourceFactory adMediaSourceFactory; private final Map> deferredMediaPeriodByAdMediaSource; private final Timeline.Period period; @@ -138,7 +163,7 @@ public final class AdsMediaSource implements MediaSource { this.eventListener = eventListener; mainHandler = new Handler(Looper.getMainLooper()); componentListener = new ComponentListener(); - adMediaSourceFactory = new ExtractorAdMediaSourceFactory(dataSourceFactory); + adMediaSourceFactory = new ExtractorMediaSourceFactory(dataSourceFactory); deferredMediaPeriodByAdMediaSource = new HashMap<>(); period = new Timeline.Period(); adGroupMediaSources = new MediaSource[0][]; @@ -186,7 +211,7 @@ public final class AdsMediaSource implements MediaSource { if (adGroupMediaSources[adGroupIndex].length <= adIndexInAdGroup) { Uri adUri = adPlaybackState.adUris[id.adGroupIndex][id.adIndexInAdGroup]; final MediaSource adMediaSource = - adMediaSourceFactory.createAdMediaSource(adUri, eventHandler, eventListener); + adMediaSourceFactory.createMediaSource(adUri, eventHandler, eventListener); int oldAdCount = adGroupMediaSources[id.adGroupIndex].length; if (adIndexInAdGroup >= oldAdCount) { int adCount = adIndexInAdGroup + 1; @@ -371,44 +396,16 @@ public final class AdsMediaSource implements MediaSource { } - /** - * Factory for {@link MediaSource}s for loading ad media. - */ - private interface AdMediaSourceFactory { - - /** - * Creates a new {@link MediaSource} for loading the ad media with the specified {@code uri}. - * - * @param uri The URI of the ad. - * @param handler A handler for listener events. May be null if delivery of events is not - * required. - * @param listener A listener for events. May be null if delivery of events is not required. - * @return The new media source. - */ - MediaSource createAdMediaSource( - Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener); - - /** - * Returns the content types supported by media sources created by this factory. Each element - * should be one of {@link C#TYPE_DASH}, {@link C#TYPE_SS}, {@link C#TYPE_HLS} or - * {@link C#TYPE_OTHER}. - * - * @return The content types supported by the factory. - */ - int[] getSupportedTypes(); - - } - - private static final class ExtractorAdMediaSourceFactory implements AdMediaSourceFactory { + private static final class ExtractorMediaSourceFactory implements MediaSourceFactory { private final DataSource.Factory dataSourceFactory; - public ExtractorAdMediaSourceFactory(DataSource.Factory dataSourceFactory) { + public ExtractorMediaSourceFactory(DataSource.Factory dataSourceFactory) { this.dataSourceFactory = dataSourceFactory; } @Override - public MediaSource createAdMediaSource( + public MediaSource createMediaSource( Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener) { return new ExtractorMediaSource.Builder(uri, dataSourceFactory) .setEventListener(handler, listener) diff --git a/library/dash/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 index af1a445b9f..9c0c58c87b 100644 --- a/library/dash/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 @@ -34,6 +34,7 @@ import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SequenceableLoader; +import com.google.android.exoplayer2.source.ads.AdsMediaSource; 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.UtcTimingElement; @@ -64,62 +65,35 @@ public final class DashMediaSource implements MediaSource { ExoPlayerLibraryInfo.registerModule("goog.exo.dash"); } - /** - * Builder for {@link DashMediaSource}. Each builder instance can only be used once. - */ - public static final class Builder { + /** Factory for {@link DashMediaSource}s. */ + public static final class Factory implements AdsMediaSource.MediaSourceFactory { - private final DashManifest manifest; - private final Uri manifestUri; - private final DataSource.Factory manifestDataSourceFactory; private final DashChunkSource.Factory chunkSourceFactory; + private final @Nullable DataSource.Factory manifestDataSourceFactory; - private ParsingLoadable.Parser manifestParser; - private MediaSourceEventListener eventListener; - private Handler eventHandler; + private @Nullable ParsingLoadable.Parser manifestParser; private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; - private int minLoadableRetryCount; private long livePresentationDelayMs; - private boolean isBuildCalled; + private boolean isCreateCalled; /** - * Creates a {@link Builder} for a {@link DashMediaSource} with a side-loaded manifest. + * Creates a new factory for {@link DashMediaSource}s. * - * @param manifest The manifest. {@link DashManifest#dynamic} must be false. * @param chunkSourceFactory A factory for {@link DashChunkSource} instances. - * @return A new builder. - */ - public static Builder forSideloadedManifest(DashManifest manifest, - DashChunkSource.Factory chunkSourceFactory) { - Assertions.checkArgument(!manifest.dynamic); - return new Builder(manifest, null, null, chunkSourceFactory); - } - - /** - * Creates a {@link Builder} for a {@link DashMediaSource} with a loadable manifest Uri. - * - * @param manifestUri The manifest {@link Uri}. * @param manifestDataSourceFactory A factory for {@link DataSource} instances that will be used - * to load (and refresh) the manifest. - * @param chunkSourceFactory A factory for {@link DashChunkSource} instances. - * @return A new builder. + * to load (and refresh) the manifest. May be {@code null} if the factory will only ever be + * used to create create media sources with sideloaded manifests via {@link + * #createMediaSource(DashManifest, Handler, MediaSourceEventListener)}. */ - public static Builder forManifestUri(Uri manifestUri, - DataSource.Factory manifestDataSourceFactory, DashChunkSource.Factory chunkSourceFactory) { - return new Builder(null, manifestUri, manifestDataSourceFactory, chunkSourceFactory); - } - - private Builder(@Nullable DashManifest manifest, @Nullable Uri manifestUri, - @Nullable DataSource.Factory manifestDataSourceFactory, - DashChunkSource.Factory chunkSourceFactory) { - this.manifest = manifest; - this.manifestUri = manifestUri; + public Factory( + DashChunkSource.Factory chunkSourceFactory, + @Nullable DataSource.Factory manifestDataSourceFactory) { + this.chunkSourceFactory = Assertions.checkNotNull(chunkSourceFactory); this.manifestDataSourceFactory = manifestDataSourceFactory; - this.chunkSourceFactory = chunkSourceFactory; - minLoadableRetryCount = DEFAULT_MIN_LOADABLE_RETRY_COUNT; livePresentationDelayMs = DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS; + compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory(); } /** @@ -127,95 +101,140 @@ public final class DashMediaSource implements MediaSource { * {@link #DEFAULT_MIN_LOADABLE_RETRY_COUNT}. * * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. - * @return This builder. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Builder setMinLoadableRetryCount(int minLoadableRetryCount) { + public Factory setMinLoadableRetryCount(int minLoadableRetryCount) { + Assertions.checkState(!isCreateCalled); this.minLoadableRetryCount = minLoadableRetryCount; return this; } /** * Sets the duration in milliseconds by which the default start position should precede the end - * of the live window for live playbacks. The default value is - * {@link #DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS}. + * of the live window for live playbacks. The default value is {@link + * #DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS}. * * @param livePresentationDelayMs For live playbacks, the duration in milliseconds by which the - * default start position should precede the end of the live window. Use - * {@link #DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS} to use the value specified by - * the manifest, if present. - * @return This builder. + * default start position should precede the end of the live window. Use {@link + * #DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS} to use the value specified by the + * manifest, if present. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Builder setLivePresentationDelayMs(long livePresentationDelayMs) { + public Factory setLivePresentationDelayMs(long livePresentationDelayMs) { + Assertions.checkState(!isCreateCalled); this.livePresentationDelayMs = livePresentationDelayMs; return this; } /** - * Sets the listener to respond to adaptive {@link MediaSource} events and the handler to - * deliver these events. - * - * @param eventHandler A handler for events. - * @param eventListener A listener of events. - * @return This builder. - */ - public Builder setEventListener(Handler eventHandler, MediaSourceEventListener eventListener) { - this.eventHandler = eventHandler; - this.eventListener = eventListener; - return this; - } - - /** - * Sets the manifest parser to parse loaded manifest data. The default is - * {@link DashManifestParser}, or {@code null} if the manifest is sideloaded. + * Sets the manifest parser to parse loaded manifest data when loading a manifest URI. * * @param manifestParser A parser for loaded manifest data. - * @return This builder. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Builder setManifestParser( + public Factory setManifestParser( ParsingLoadable.Parser manifestParser) { - this.manifestParser = manifestParser; + Assertions.checkState(!isCreateCalled); + this.manifestParser = Assertions.checkNotNull(manifestParser); return this; } /** * Sets the factory to create composite {@link SequenceableLoader}s for when this media source - * loads data from multiple streams (video, audio etc...). The default is an instance of - * {@link DefaultCompositeSequenceableLoaderFactory}. + * loads data from multiple streams (video, audio etc...). The default is an instance of {@link + * DefaultCompositeSequenceableLoaderFactory}. * - * @param compositeSequenceableLoaderFactory A factory to create composite - * {@link SequenceableLoader}s for when this media source loads data from multiple streams - * (video, audio etc...). - * @return This builder. + * @param compositeSequenceableLoaderFactory A factory to create composite {@link + * SequenceableLoader}s for when this media source loads data from multiple streams (video, + * audio etc...). + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Builder setCompositeSequenceableLoaderFactory( + public Factory setCompositeSequenceableLoaderFactory( CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory) { - this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; + Assertions.checkState(!isCreateCalled); + this.compositeSequenceableLoaderFactory = + Assertions.checkNotNull(compositeSequenceableLoaderFactory); return this; } /** - * Builds a new {@link DashMediaSource} using the current parameters. - *

          - * After this call, the builder should not be re-used. + * Returns a new {@link DashMediaSource} using the current parameters and the specified + * sideloaded manifest. * - * @return The newly built {@link DashMediaSource}. + * @param manifest The manifest. {@link DashManifest#dynamic} must be false. + * @param eventHandler A handler for events. + * @param eventListener A listener of events. + * @return The new {@link DashMediaSource}. + * @throws IllegalArgumentException If {@link DashManifest#dynamic} is true. */ - public DashMediaSource build() { - Assertions.checkArgument((eventListener == null) == (eventHandler == null)); - Assertions.checkState(!isBuildCalled); - isBuildCalled = true; - boolean loadableManifestUri = manifestUri != null; - if (loadableManifestUri && manifestParser == null) { - manifestParser = new DashManifestParser(); - } - if (compositeSequenceableLoaderFactory == null) { - compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory(); - } - return new DashMediaSource(manifest, manifestUri, manifestDataSourceFactory, manifestParser, - chunkSourceFactory, compositeSequenceableLoaderFactory, minLoadableRetryCount, - livePresentationDelayMs, eventHandler, eventListener); + public DashMediaSource createMediaSource( + DashManifest manifest, + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { + Assertions.checkArgument(!manifest.dynamic); + isCreateCalled = true; + return new DashMediaSource( + manifest, + null, + null, + null, + chunkSourceFactory, + compositeSequenceableLoaderFactory, + minLoadableRetryCount, + livePresentationDelayMs, + eventHandler, + eventListener); } + /** + * Returns a new {@link DashMediaSource} using the current parameters. Media source events will + * not be delivered. + * + * @param manifestUri The manifest {@link Uri}. + * @return The new {@link DashMediaSource}. + */ + public DashMediaSource createMediaSource(Uri manifestUri) { + return createMediaSource(manifestUri, null, null); + } + + /** + * Returns a new {@link DashMediaSource} using the current parameters. + * + * @param manifestUri The manifest {@link Uri}. + * @param eventHandler A handler for events. + * @param eventListener A listener of events. + * @return The new {@link DashMediaSource}. + */ + @Override + public DashMediaSource createMediaSource( + Uri manifestUri, + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { + isCreateCalled = true; + if (manifestParser == null) { + manifestParser = new DashManifestParser(); + } + return new DashMediaSource( + null, + Assertions.checkNotNull(manifestUri), + manifestDataSourceFactory, + manifestParser, + chunkSourceFactory, + compositeSequenceableLoaderFactory, + minLoadableRetryCount, + livePresentationDelayMs, + eventHandler, + eventListener); + } + + @Override + public int[] getSupportedTypes() { + return new int[] {C.TYPE_DASH}; + } } /** @@ -283,7 +302,7 @@ public final class DashMediaSource implements MediaSource { * @param chunkSourceFactory A factory for {@link DashChunkSource} instances. * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated public DashMediaSource( @@ -303,7 +322,7 @@ public final class DashMediaSource implements MediaSource { * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated public DashMediaSource( @@ -327,7 +346,7 @@ public final class DashMediaSource implements MediaSource { * @param chunkSourceFactory A factory for {@link DashChunkSource} instances. * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated public DashMediaSource( @@ -356,7 +375,7 @@ public final class DashMediaSource implements MediaSource { * manifest, if present. * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated public DashMediaSource( @@ -387,7 +406,7 @@ public final class DashMediaSource implements MediaSource { * manifest, if present. * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated public DashMediaSource( diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java index 215d8a0518..8973853245 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java @@ -316,11 +316,10 @@ public final class DashTestRunner { Uri manifestUri = Uri.parse(manifestUrl); DefaultDashChunkSource.Factory chunkSourceFactory = new DefaultDashChunkSource.Factory( mediaDataSourceFactory); - return DashMediaSource.Builder - .forManifestUri(manifestUri, manifestDataSourceFactory, chunkSourceFactory) + return new DashMediaSource.Factory(chunkSourceFactory, manifestDataSourceFactory) .setMinLoadableRetryCount(MIN_LOADABLE_RETRY_COUNT) .setLivePresentationDelayMs(0) - .build(); + .createMediaSource(manifestUri); } @Override From 8947950b5277fbeb01cf4cafa070bb7db1da094e Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 8 Dec 2017 08:32:46 -0800 Subject: [PATCH 0851/2472] Make SsMediaSource.Builder a factory for SsMediaSources ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178380856 --- .../exoplayer2/castdemo/PlayerManager.java | 7 +- .../exoplayer2/demo/PlayerActivity.java | 9 +- .../source/smoothstreaming/SsMediaSource.java | 210 ++++++++++-------- 3 files changed, 122 insertions(+), 104 deletions(-) diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java index 7d63150201..20ef72f9f2 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java @@ -388,10 +388,9 @@ import java.util.ArrayList; Uri uri = Uri.parse(sample.uri); switch (sample.mimeType) { case DemoUtil.MIME_TYPE_SS: - return SsMediaSource.Builder - .forManifestUri(uri, DATA_SOURCE_FACTORY, - new DefaultSsChunkSource.Factory(DATA_SOURCE_FACTORY)) - .build(); + return new SsMediaSource.Factory( + new DefaultSsChunkSource.Factory(DATA_SOURCE_FACTORY), DATA_SOURCE_FACTORY) + .createMediaSource(uri); case DemoUtil.MIME_TYPE_DASH: return new DashMediaSource.Factory( new DefaultDashChunkSource.Factory(DATA_SOURCE_FACTORY), DATA_SOURCE_FACTORY) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index 1be6df8437..38938bd367 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -365,11 +365,10 @@ public class PlayerActivity extends Activity implements OnClickListener, : Util.inferContentType("." + overrideExtension); switch (type) { case C.TYPE_SS: - return SsMediaSource.Builder - .forManifestUri(uri, buildDataSourceFactory(false), - new DefaultSsChunkSource.Factory(mediaDataSourceFactory)) - .setEventListener(mainHandler, eventLogger) - .build(); + return new SsMediaSource.Factory( + new DefaultSsChunkSource.Factory(mediaDataSourceFactory), + buildDataSourceFactory(false)) + .createMediaSource(uri, mainHandler, eventLogger); case C.TYPE_DASH: return new DashMediaSource.Factory( new DefaultDashChunkSource.Factory(mediaDataSourceFactory), diff --git a/library/smoothstreaming/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 index 10772ba36c..9932db7869 100644 --- a/library/smoothstreaming/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 @@ -32,6 +32,7 @@ import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SequenceableLoader; import com.google.android.exoplayer2.source.SinglePeriodTimeline; +import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest.StreamElement; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifestParser; @@ -55,62 +56,35 @@ public final class SsMediaSource implements MediaSource, ExoPlayerLibraryInfo.registerModule("goog.exo.smoothstreaming"); } - /** - * Builder for {@link SsMediaSource}. Each builder instance can only be used once. - */ - public static final class Builder { + /** Factory for {@link SsMediaSource}. */ + public static final class Factory implements AdsMediaSource.MediaSourceFactory { - private final SsManifest manifest; - private final Uri manifestUri; - private final DataSource.Factory manifestDataSourceFactory; private final SsChunkSource.Factory chunkSourceFactory; + private final @Nullable DataSource.Factory manifestDataSourceFactory; - private ParsingLoadable.Parser manifestParser; - private MediaSourceEventListener eventListener; - private Handler eventHandler; + private @Nullable ParsingLoadable.Parser manifestParser; private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; - private int minLoadableRetryCount; private long livePresentationDelayMs; - private boolean isBuildCalled; + private boolean isCreateCalled; /** - * Creates a {@link Builder} for a {@link SsMediaSource} with a side-loaded manifest. + * Creates a new factory for {@link SsMediaSource}s. * - * @param manifest The manifest. {@link SsManifest#isLive} must be false. * @param chunkSourceFactory A factory for {@link SsChunkSource} instances. - * @return A new builder. - */ - public static Builder forSideLoadedManifest(SsManifest manifest, - SsChunkSource.Factory chunkSourceFactory) { - Assertions.checkArgument(!manifest.isLive); - return new Builder(manifest, null, null, chunkSourceFactory); - } - - /** - * Creates a {@link Builder} for a {@link SsMediaSource} with a loadable manifest Uri. - * - * @param manifestUri The manifest {@link Uri}. * @param manifestDataSourceFactory A factory for {@link DataSource} instances that will be used - * to load (and refresh) the manifest. - * @param chunkSourceFactory A factory for {@link SsChunkSource} instances. - * @return A new builder. + * to load (and refresh) the manifest. May be {@code null} if the factory will only ever be + * used to create create media sources with sideloaded manifests via {@link + * #createMediaSource(SsManifest, Handler, MediaSourceEventListener)}. */ - public static Builder forManifestUri(Uri manifestUri, - DataSource.Factory manifestDataSourceFactory, SsChunkSource.Factory chunkSourceFactory) { - return new Builder(null, manifestUri, manifestDataSourceFactory, chunkSourceFactory); - } - - private Builder(@Nullable SsManifest manifest, @Nullable Uri manifestUri, - @Nullable DataSource.Factory manifestDataSourceFactory, - SsChunkSource.Factory chunkSourceFactory) { - this.manifest = manifest; - this.manifestUri = manifestUri; + public Factory( + SsChunkSource.Factory chunkSourceFactory, + @Nullable DataSource.Factory manifestDataSourceFactory) { + this.chunkSourceFactory = Assertions.checkNotNull(chunkSourceFactory); this.manifestDataSourceFactory = manifestDataSourceFactory; - this.chunkSourceFactory = chunkSourceFactory; - minLoadableRetryCount = DEFAULT_MIN_LOADABLE_RETRY_COUNT; livePresentationDelayMs = DEFAULT_LIVE_PRESENTATION_DELAY_MS; + compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory(); } /** @@ -118,90 +92,136 @@ public final class SsMediaSource implements MediaSource, * {@link #DEFAULT_MIN_LOADABLE_RETRY_COUNT}. * * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. - * @return This builder. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Builder setMinLoadableRetryCount(int minLoadableRetryCount) { + public Factory setMinLoadableRetryCount(int minLoadableRetryCount) { + Assertions.checkState(!isCreateCalled); this.minLoadableRetryCount = minLoadableRetryCount; return this; } /** * Sets the duration in milliseconds by which the default start position should precede the end - * of the live window for live playbacks. The default value is - * {@link #DEFAULT_LIVE_PRESENTATION_DELAY_MS}. + * of the live window for live playbacks. The default value is {@link + * #DEFAULT_LIVE_PRESENTATION_DELAY_MS}. * * @param livePresentationDelayMs For live playbacks, the duration in milliseconds by which the * default start position should precede the end of the live window. - * @return This builder. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Builder setLivePresentationDelayMs(long livePresentationDelayMs) { + public Factory setLivePresentationDelayMs(long livePresentationDelayMs) { + Assertions.checkState(!isCreateCalled); this.livePresentationDelayMs = livePresentationDelayMs; return this; } /** - * Sets the listener to respond to adaptive {@link MediaSource} events and the handler to - * deliver these events. - * - * @param eventHandler A handler for events. - * @param eventListener A listener of events. - * @return This builder. - */ - public Builder setEventListener(Handler eventHandler, MediaSourceEventListener eventListener) { - this.eventHandler = eventHandler; - this.eventListener = eventListener; - return this; - } - - /** - * Sets the manifest parser to parse loaded manifest data. The default is an instance of - * {@link SsManifestParser}, or {@code null} if the manifest is sideloaded. + * Sets the manifest parser to parse loaded manifest data when loading a manifest URI. * * @param manifestParser A parser for loaded manifest data. - * @return This builder. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Builder setManifestParser(ParsingLoadable.Parser manifestParser) { - this.manifestParser = manifestParser; + public Factory setManifestParser(ParsingLoadable.Parser manifestParser) { + Assertions.checkState(!isCreateCalled); + this.manifestParser = Assertions.checkNotNull(manifestParser); return this; } /** * Sets the factory to create composite {@link SequenceableLoader}s for when this media source - * loads data from multiple streams (video, audio etc...). The default is an instance of - * {@link DefaultCompositeSequenceableLoaderFactory}. + * loads data from multiple streams (video, audio etc.). The default is an instance of {@link + * DefaultCompositeSequenceableLoaderFactory}. * - * @param compositeSequenceableLoaderFactory A factory to create composite - * {@link SequenceableLoader}s for when this media source loads data from multiple streams - * (video, audio etc...). - * @return This builder. + * @param compositeSequenceableLoaderFactory A factory to create composite {@link + * SequenceableLoader}s for when this media source loads data from multiple streams (video, + * audio etc.). + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Builder setCompositeSequenceableLoaderFactory( + public Factory setCompositeSequenceableLoaderFactory( CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory) { - this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; + Assertions.checkState(!isCreateCalled); + this.compositeSequenceableLoaderFactory = + Assertions.checkNotNull(compositeSequenceableLoaderFactory); return this; } /** - * Builds a new {@link SsMediaSource} using the current parameters. - *

          - * After this call, the builder should not be re-used. + * Returns a new {@link SsMediaSource} using the current parameters and the specified sideloaded + * manifest. * - * @return The newly built {@link SsMediaSource}. + * @param manifest The manifest. {@link SsManifest#isLive} must be false. + * @param eventHandler A handler for events. + * @param eventListener A listener of events. + * @return The new {@link SsMediaSource}. + * @throws IllegalArgumentException If {@link SsManifest#isLive} is true. */ - public SsMediaSource build() { - Assertions.checkArgument((eventListener == null) == (eventHandler == null)); - Assertions.checkState(!isBuildCalled); - isBuildCalled = true; - boolean loadableManifestUri = manifestUri != null; - if (loadableManifestUri && manifestParser == null) { + public SsMediaSource createMediaSource( + SsManifest manifest, + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { + Assertions.checkArgument(!manifest.isLive); + isCreateCalled = true; + return new SsMediaSource( + manifest, + null, + null, + null, + chunkSourceFactory, + compositeSequenceableLoaderFactory, + minLoadableRetryCount, + livePresentationDelayMs, + eventHandler, + eventListener); + } + + /** + * Returns a new {@link SsMediaSource} using the current parameters. Media source events will + * not be delivered. + * + * @param manifestUri The manifest {@link Uri}. + * @return The new {@link SsMediaSource}. + */ + public SsMediaSource createMediaSource(Uri manifestUri) { + return createMediaSource(manifestUri, null, null); + } + + /** + * Returns a new {@link SsMediaSource} using the current parameters. + * + * @param manifestUri The manifest {@link Uri}. + * @param eventHandler A handler for events. + * @param eventListener A listener of events. + * @return The new {@link SsMediaSource}. + */ + @Override + public SsMediaSource createMediaSource( + Uri manifestUri, + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { + isCreateCalled = true; + if (manifestParser == null) { manifestParser = new SsManifestParser(); } - if (compositeSequenceableLoaderFactory == null) { - compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory(); - } - return new SsMediaSource(manifest, manifestUri, manifestDataSourceFactory, manifestParser, - chunkSourceFactory, compositeSequenceableLoaderFactory, minLoadableRetryCount, - livePresentationDelayMs, eventHandler, eventListener); + return new SsMediaSource( + null, + Assertions.checkNotNull(manifestUri), + manifestDataSourceFactory, + manifestParser, + chunkSourceFactory, + compositeSequenceableLoaderFactory, + minLoadableRetryCount, + livePresentationDelayMs, + eventHandler, + eventListener); + } + + @Override + public int[] getSupportedTypes() { + return new int[] {C.TYPE_SS}; } } @@ -252,7 +272,7 @@ public final class SsMediaSource implements MediaSource, * @param chunkSourceFactory A factory for {@link SsChunkSource} instances. * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated public SsMediaSource( @@ -272,7 +292,7 @@ public final class SsMediaSource implements MediaSource, * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated public SsMediaSource( @@ -296,7 +316,7 @@ public final class SsMediaSource implements MediaSource, * @param chunkSourceFactory A factory for {@link SsChunkSource} instances. * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated public SsMediaSource( @@ -323,7 +343,7 @@ public final class SsMediaSource implements MediaSource, * default start position should precede the end of the live window. * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated public SsMediaSource( @@ -352,7 +372,7 @@ public final class SsMediaSource implements MediaSource, * default start position should precede the end of the live window. * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated public SsMediaSource( From ba32d95dc476c452dd1e4e97229e04d0cd1c2632 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 8 Dec 2017 09:07:55 -0800 Subject: [PATCH 0852/2472] Make HlsMediaSource.Builder a factory for HlsMediaSources ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178384204 --- .../exoplayer2/castdemo/PlayerManager.java | 4 +- .../exoplayer2/demo/PlayerActivity.java | 6 +- .../exoplayer2/source/hls/HlsMediaSource.java | 171 +++++++++--------- 3 files changed, 85 insertions(+), 96 deletions(-) diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java index 20ef72f9f2..aec53c8e8a 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java @@ -396,9 +396,7 @@ import java.util.ArrayList; new DefaultDashChunkSource.Factory(DATA_SOURCE_FACTORY), DATA_SOURCE_FACTORY) .createMediaSource(uri); case DemoUtil.MIME_TYPE_HLS: - return HlsMediaSource.Builder - .forDataSource(uri, DATA_SOURCE_FACTORY) - .build(); + return new HlsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); case DemoUtil.MIME_TYPE_VIDEO_MP4: return new ExtractorMediaSource.Builder(uri, DATA_SOURCE_FACTORY).build(); default: { diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index 38938bd367..215c4708e8 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -375,10 +375,8 @@ public class PlayerActivity extends Activity implements OnClickListener, buildDataSourceFactory(false)) .createMediaSource(uri, mainHandler, eventLogger); case C.TYPE_HLS: - return HlsMediaSource.Builder - .forDataSource(uri, mediaDataSourceFactory) - .setEventListener(mainHandler, eventLogger) - .build(); + return new HlsMediaSource.Factory(mediaDataSourceFactory) + .createMediaSource(uri, mainHandler, eventLogger); case C.TYPE_OTHER: return new ExtractorMediaSource.Builder(uri, mediaDataSourceFactory) .setEventListener(mainHandler, eventLogger) diff --git a/library/hls/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 index 1cddf6e94e..4c14d2029e 100644 --- a/library/hls/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 @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source.hls; import android.net.Uri; import android.os.Handler; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; @@ -29,6 +30,7 @@ import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SequenceableLoader; import com.google.android.exoplayer2.source.SinglePeriodTimeline; +import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylist; import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser; @@ -50,68 +52,54 @@ public final class HlsMediaSource implements MediaSource, ExoPlayerLibraryInfo.registerModule("goog.exo.hls"); } - /** - * Builder for {@link HlsMediaSource}. Each builder instance can only be used once. - */ - public static final class Builder { + /** Factory for {@link HlsMediaSource}s. */ + public static final class Factory implements AdsMediaSource.MediaSourceFactory { - private final Uri manifestUri; private final HlsDataSourceFactory hlsDataSourceFactory; private HlsExtractorFactory extractorFactory; - private ParsingLoadable.Parser playlistParser; - private MediaSourceEventListener eventListener; - private Handler eventHandler; + private @Nullable ParsingLoadable.Parser playlistParser; private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private int minLoadableRetryCount; private boolean allowChunklessPreparation; - - private boolean isBuildCalled; + private boolean isCreateCalled; /** - * Creates a {@link Builder} for a {@link HlsMediaSource} with a loadable manifest Uri and - * a {@link DataSource.Factory}. + * Creates a new factory for {@link HlsMediaSource}s. * - * @param manifestUri The {@link Uri} of the HLS manifest. - * @param dataSourceFactory A data source factory that will be wrapped by a - * {@link DefaultHlsDataSourceFactory} to build {@link DataSource}s for manifests, - * segments and keys. - * @return A new builder. + * @param dataSourceFactory A data source factory that will be wrapped by a {@link + * DefaultHlsDataSourceFactory} to create {@link DataSource}s for manifests, segments and + * keys. */ - public static Builder forDataSource(Uri manifestUri, DataSource.Factory dataSourceFactory) { - return new Builder(manifestUri, new DefaultHlsDataSourceFactory(dataSourceFactory)); + public Factory(DataSource.Factory dataSourceFactory) { + this(new DefaultHlsDataSourceFactory(dataSourceFactory)); } /** - * Creates a {@link Builder} for a {@link HlsMediaSource} with a loadable manifest Uri and - * a {@link HlsDataSourceFactory}. + * Creates a new factory for {@link HlsMediaSource}s. * - * @param manifestUri The {@link Uri} of the HLS manifest. - * @param dataSourceFactory An {@link HlsDataSourceFactory} for {@link DataSource}s for + * @param hlsDataSourceFactory An {@link HlsDataSourceFactory} for {@link DataSource}s for * manifests, segments and keys. - * @return A new builder. */ - public static Builder forHlsDataSource(Uri manifestUri, - HlsDataSourceFactory dataSourceFactory) { - return new Builder(manifestUri, dataSourceFactory); - } - - private Builder(Uri manifestUri, HlsDataSourceFactory hlsDataSourceFactory) { - this.manifestUri = manifestUri; - this.hlsDataSourceFactory = hlsDataSourceFactory; + public Factory(HlsDataSourceFactory hlsDataSourceFactory) { + this.hlsDataSourceFactory = Assertions.checkNotNull(hlsDataSourceFactory); + extractorFactory = HlsExtractorFactory.DEFAULT; minLoadableRetryCount = DEFAULT_MIN_LOADABLE_RETRY_COUNT; + compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory(); } /** - * Sets the factory for {@link Extractor}s for the segments. Default value is - * {@link HlsExtractorFactory#DEFAULT}. + * Sets the factory for {@link Extractor}s for the segments. The default value is {@link + * HlsExtractorFactory#DEFAULT}. * * @param extractorFactory An {@link HlsExtractorFactory} for {@link Extractor}s for the - * segments. - * @return This builder. + * segments. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Builder setExtractorFactory(HlsExtractorFactory extractorFactory) { - this.extractorFactory = extractorFactory; + public Factory setExtractorFactory(HlsExtractorFactory extractorFactory) { + Assertions.checkState(!isCreateCalled); + this.extractorFactory = Assertions.checkNotNull(extractorFactory); return this; } @@ -119,54 +107,46 @@ public final class HlsMediaSource implements MediaSource, * Sets the minimum number of times to retry if a loading error occurs. The default value is * {@link #DEFAULT_MIN_LOADABLE_RETRY_COUNT}. * - * @param minLoadableRetryCount The minimum number of times loads must be retried before - * errors are propagated. - * @return This builder. + * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Builder setMinLoadableRetryCount(int minLoadableRetryCount) { + public Factory setMinLoadableRetryCount(int minLoadableRetryCount) { + Assertions.checkState(!isCreateCalled); this.minLoadableRetryCount = minLoadableRetryCount; return this; } /** - * Sets the listener to respond to adaptive {@link MediaSource} events and the handler to - * deliver these events. - * - * @param eventHandler A handler for events. - * @param eventListener A listener of events. - * @return This builder. - */ - public Builder setEventListener(Handler eventHandler, MediaSourceEventListener eventListener) { - this.eventHandler = eventHandler; - this.eventListener = eventListener; - return this; - } - - /** - * Sets the parser to parse HLS playlists. The default is an instance of - * {@link HlsPlaylistParser}. + * Sets the parser to parse HLS playlists. The default is an instance of {@link + * HlsPlaylistParser}. * * @param playlistParser A {@link ParsingLoadable.Parser} for HLS playlists. - * @return This builder. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Builder setPlaylistParser(ParsingLoadable.Parser playlistParser) { - this.playlistParser = playlistParser; + public Factory setPlaylistParser(ParsingLoadable.Parser playlistParser) { + Assertions.checkState(!isCreateCalled); + this.playlistParser = Assertions.checkNotNull(playlistParser); return this; } /** * Sets the factory to create composite {@link SequenceableLoader}s for when this media source - * loads data from multiple streams (video, audio etc...). The default is an instance of - * {@link DefaultCompositeSequenceableLoaderFactory}. + * loads data from multiple streams (video, audio etc...). The default is an instance of {@link + * DefaultCompositeSequenceableLoaderFactory}. * - * @param compositeSequenceableLoaderFactory A factory to create composite - * {@link SequenceableLoader}s for when this media source loads data from multiple streams - * (video, audio etc...). - * @return This builder. + * @param compositeSequenceableLoaderFactory A factory to create composite {@link + * SequenceableLoader}s for when this media source loads data from multiple streams (video, + * audio etc...). + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Builder setCompositeSequenceableLoaderFactory( + public Factory setCompositeSequenceableLoaderFactory( CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory) { - this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; + Assertions.checkState(!isCreateCalled); + this.compositeSequenceableLoaderFactory = + Assertions.checkNotNull(compositeSequenceableLoaderFactory); return this; } @@ -175,35 +155,44 @@ public final class HlsMediaSource implements MediaSource, * will be enabled for streams that provide sufficient information in their master playlist. * * @param allowChunklessPreparation Whether chunkless preparation is allowed. - * @return This builder. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Builder setAllowChunklessPreparation(boolean allowChunklessPreparation) { + public Factory setAllowChunklessPreparation(boolean allowChunklessPreparation) { + Assertions.checkState(!isCreateCalled); this.allowChunklessPreparation = allowChunklessPreparation; return this; } /** - * Builds a new {@link HlsMediaSource} using the current parameters. - *

          - * After this call, the builder should not be re-used. + * Returns a new {@link HlsMediaSource} using the current parameters. Media source events will + * not be delivered. * - * @return The newly built {@link HlsMediaSource}. + * @return The new {@link HlsMediaSource}. */ - public HlsMediaSource build() { - Assertions.checkArgument((eventListener == null) == (eventHandler == null)); - Assertions.checkState(!isBuildCalled); - isBuildCalled = true; - if (extractorFactory == null) { - extractorFactory = HlsExtractorFactory.DEFAULT; - } + public MediaSource createMediaSource(Uri playlistUri) { + return createMediaSource(playlistUri, null, null); + } + + /** + * Returns a new {@link HlsMediaSource} using the current parameters. + * + * @param playlistUri The playlist {@link Uri}. + * @param eventHandler A handler for events. + * @param eventListener A listener of events. + * @return The new {@link HlsMediaSource}. + */ + @Override + public MediaSource createMediaSource( + Uri playlistUri, + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { + isCreateCalled = true; if (playlistParser == null) { playlistParser = new HlsPlaylistParser(); } - if (compositeSequenceableLoaderFactory == null) { - compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory(); - } return new HlsMediaSource( - manifestUri, + playlistUri, hlsDataSourceFactory, extractorFactory, compositeSequenceableLoaderFactory, @@ -214,6 +203,10 @@ public final class HlsMediaSource implements MediaSource, allowChunklessPreparation); } + @Override + public int[] getSupportedTypes() { + return new int[] {C.TYPE_HLS}; + } } /** @@ -240,7 +233,7 @@ public final class HlsMediaSource implements MediaSource, * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A {@link MediaSourceEventListener}. May be null if delivery of events is * not required. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated public HlsMediaSource( @@ -261,7 +254,7 @@ public final class HlsMediaSource implements MediaSource, * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A {@link MediaSourceEventListener}. May be null if delivery of events is * not required. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated public HlsMediaSource( @@ -286,7 +279,7 @@ public final class HlsMediaSource implements MediaSource, * @param eventListener A {@link MediaSourceEventListener}. May be null if delivery of events is * not required. * @param playlistParser A {@link ParsingLoadable.Parser} for HLS playlists. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated public HlsMediaSource( From f8834dacc64027efaeef373c26d68d2abe93e920 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 11 Dec 2017 05:08:01 -0800 Subject: [PATCH 0853/2472] Use surfaceless context in DummySurface, if available Issue: #3558 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178604607 --- RELEASENOTES.md | 2 + .../exoplayer2/video/DummySurface.java | 108 ++++++++++-------- 2 files changed, 65 insertions(+), 45 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index a464b7e826..f6fb6f3611 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -41,6 +41,8 @@ implementations. * CEA-608: Fix handling of row count changes in roll-up mode ([#3513](https://github.com/google/ExoPlayer/issues/3513)). +* Use surfaceless context for secure DummySurface, if available + ([#3558](https://github.com/google/ExoPlayer/issues/3558)). * IMA extension: * Skip ads before the ad preceding the player's initial seek position ([#3527](https://github.com/google/ExoPlayer/issues/3527)). diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java index cc50443296..2c172c086b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java @@ -24,6 +24,7 @@ import static android.opengl.EGL14.EGL_DEPTH_SIZE; import static android.opengl.EGL14.EGL_GREEN_SIZE; import static android.opengl.EGL14.EGL_HEIGHT; import static android.opengl.EGL14.EGL_NONE; +import static android.opengl.EGL14.EGL_NO_SURFACE; import static android.opengl.EGL14.EGL_OPENGL_ES2_BIT; import static android.opengl.EGL14.EGL_RED_SIZE; import static android.opengl.EGL14.EGL_RENDERABLE_TYPE; @@ -56,10 +57,13 @@ import android.os.Handler; import android.os.Handler.Callback; import android.os.HandlerThread; import android.os.Message; +import android.support.annotation.IntDef; import android.util.Log; import android.view.Surface; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import javax.microedition.khronos.egl.EGL10; /** @@ -70,16 +74,27 @@ public final class DummySurface extends Surface { private static final String TAG = "DummySurface"; + private static final String EXTENSION_PROTECTED_CONTENT = "EGL_EXT_protected_content"; + private static final String EXTENSION_SURFACELESS_CONTEXT = "EGL_KHR_surfaceless_context"; + private static final int EGL_PROTECTED_CONTENT_EXT = 0x32C0; - private static boolean secureSupported; - private static boolean secureSupportedInitialized; + @Retention(RetentionPolicy.SOURCE) + @IntDef({SECURE_MODE_NONE, SECURE_MODE_SURFACELESS_CONTEXT, SECURE_MODE_PROTECTED_PBUFFER}) + private @interface SecureMode {} + + private static final int SECURE_MODE_NONE = 0; + private static final int SECURE_MODE_SURFACELESS_CONTEXT = 1; + private static final int SECURE_MODE_PROTECTED_PBUFFER = 2; /** * Whether the surface is secure. */ public final boolean secure; + private static @SecureMode int secureMode; + private static boolean secureModeInitialized; + private final DummySurfaceThread thread; private boolean threadReleased; @@ -90,11 +105,11 @@ public final class DummySurface extends Surface { * @return Whether the device supports secure dummy surfaces. */ public static synchronized boolean isSecureSupported(Context context) { - if (!secureSupportedInitialized) { - secureSupported = Util.SDK_INT >= 24 && enableSecureDummySurfaceV24(context); - secureSupportedInitialized = true; + if (!secureModeInitialized) { + secureMode = Util.SDK_INT < 24 ? SECURE_MODE_NONE : getSecureModeV24(context); + secureModeInitialized = true; } - return secureSupported; + return secureMode != SECURE_MODE_NONE; } /** @@ -113,7 +128,7 @@ public final class DummySurface extends Surface { assertApiLevel17OrHigher(); Assertions.checkState(!secure || isSecureSupported(context)); DummySurfaceThread thread = new DummySurfaceThread(); - return thread.init(secure); + return thread.init(secure ? secureMode : SECURE_MODE_NONE); } private DummySurface(DummySurfaceThread thread, SurfaceTexture surfaceTexture, boolean secure) { @@ -143,33 +158,34 @@ public final class DummySurface extends Surface { } } - /** - * Returns whether use of secure dummy surfaces should be enabled. - * - * @param context Any {@link Context}. - */ @TargetApi(24) - private static boolean enableSecureDummySurfaceV24(Context context) { + private static @SecureMode int getSecureModeV24(Context context) { if (Util.SDK_INT < 26 && ("samsung".equals(Util.MANUFACTURER) || "XT1650".equals(Util.MODEL))) { // Samsung devices running Nougat are known to be broken. See // https://github.com/google/ExoPlayer/issues/3373 and [Internal: b/37197802]. // Moto Z XT1650 is also affected. See // https://github.com/google/ExoPlayer/issues/3215. - return false; + return SECURE_MODE_NONE; } if (Util.SDK_INT < 26 && !context.getPackageManager().hasSystemFeature( PackageManager.FEATURE_VR_MODE_HIGH_PERFORMANCE)) { - // Pre API level 26 devices were not well tested unless they supported VR mode. See - // https://github.com/google/ExoPlayer/issues/3215. - return false; + // Pre API level 26 devices were not well tested unless they supported VR mode. + return SECURE_MODE_NONE; } EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY); String eglExtensions = EGL14.eglQueryString(display, EGL10.EGL_EXTENSIONS); - if (eglExtensions == null || !eglExtensions.contains("EGL_EXT_protected_content")) { - // EGL_EXT_protected_content is required to enable secure dummy surfaces. - return false; + if (eglExtensions == null) { + return SECURE_MODE_NONE; } - return true; + if (!eglExtensions.contains(EXTENSION_PROTECTED_CONTENT)) { + return SECURE_MODE_NONE; + } + // If we can't use surfaceless contexts, we use a protected 1 * 1 pixel buffer surface. This may + // require support for EXT_protected_surface, but in practice it works on some devices that + // don't have that extension. See also https://github.com/google/ExoPlayer/issues/3558. + return eglExtensions.contains(EXTENSION_SURFACELESS_CONTEXT) + ? SECURE_MODE_SURFACELESS_CONTEXT + : SECURE_MODE_PROTECTED_PBUFFER; } private static class DummySurfaceThread extends HandlerThread implements OnFrameAvailableListener, @@ -195,12 +211,12 @@ public final class DummySurface extends Surface { textureIdHolder = new int[1]; } - public DummySurface init(boolean secure) { + public DummySurface init(@SecureMode int secureMode) { start(); handler = new Handler(getLooper(), this); boolean wasInterrupted = false; synchronized (this) { - handler.obtainMessage(MSG_INIT, secure ? 1 : 0, 0).sendToTarget(); + handler.obtainMessage(MSG_INIT, secureMode, 0).sendToTarget(); while (surface == null && initException == null && initError == null) { try { wait(); @@ -236,7 +252,7 @@ public final class DummySurface extends Surface { switch (msg.what) { case MSG_INIT: try { - initInternal(msg.arg1 != 0); + initInternal(/* secureMode= */ msg.arg1); } catch (RuntimeException e) { Log.e(TAG, "Failed to initialize dummy surface", e); initException = e; @@ -266,7 +282,7 @@ public final class DummySurface extends Surface { } } - private void initInternal(boolean secure) { + private void initInternal(@SecureMode int secureMode) { display = eglGetDisplay(EGL_DEFAULT_DISPLAY); Assertions.checkState(display != null, "eglGetDisplay failed"); @@ -294,43 +310,45 @@ public final class DummySurface extends Surface { EGLConfig config = configs[0]; int[] glAttributes; - if (secure) { + if (secureMode == SECURE_MODE_NONE) { glAttributes = new int[] { EGL_CONTEXT_CLIENT_VERSION, 2, - EGL_PROTECTED_CONTENT_EXT, EGL_TRUE, EGL_NONE}; } else { - glAttributes = new int[] { - EGL_CONTEXT_CLIENT_VERSION, 2, - EGL_NONE}; + glAttributes = + new int[] { + EGL_CONTEXT_CLIENT_VERSION, 2, EGL_PROTECTED_CONTENT_EXT, EGL_TRUE, EGL_NONE + }; } context = eglCreateContext(display, config, android.opengl.EGL14.EGL_NO_CONTEXT, glAttributes, 0); Assertions.checkState(context != null, "eglCreateContext failed"); - int[] pbufferAttributes; - if (secure) { - pbufferAttributes = new int[] { - EGL_WIDTH, 1, - EGL_HEIGHT, 1, - EGL_PROTECTED_CONTENT_EXT, EGL_TRUE, - EGL_NONE}; + EGLSurface surface; + if (secureMode == SECURE_MODE_SURFACELESS_CONTEXT) { + surface = EGL_NO_SURFACE; } else { - pbufferAttributes = new int[] { - EGL_WIDTH, 1, - EGL_HEIGHT, 1, - EGL_NONE}; + int[] pbufferAttributes; + if (secureMode == SECURE_MODE_PROTECTED_PBUFFER) { + pbufferAttributes = + new int[] { + EGL_WIDTH, 1, EGL_HEIGHT, 1, EGL_PROTECTED_CONTENT_EXT, EGL_TRUE, EGL_NONE + }; + } else { + pbufferAttributes = new int[] {EGL_WIDTH, 1, EGL_HEIGHT, 1, EGL_NONE}; + } + pbuffer = eglCreatePbufferSurface(display, config, pbufferAttributes, 0); + Assertions.checkState(pbuffer != null, "eglCreatePbufferSurface failed"); + surface = pbuffer; } - pbuffer = eglCreatePbufferSurface(display, config, pbufferAttributes, 0); - Assertions.checkState(pbuffer != null, "eglCreatePbufferSurface failed"); - boolean eglMadeCurrent = eglMakeCurrent(display, pbuffer, pbuffer, context); + boolean eglMadeCurrent = eglMakeCurrent(display, surface, surface, context); Assertions.checkState(eglMadeCurrent, "eglMakeCurrent failed"); glGenTextures(1, textureIdHolder, 0); surfaceTexture = new SurfaceTexture(textureIdHolder[0]); surfaceTexture.setOnFrameAvailableListener(this); - surface = new DummySurface(this, surfaceTexture, secure); + this.surface = new DummySurface(this, surfaceTexture, secureMode != SECURE_MODE_NONE); } private void releaseInternal() { From 8c7fe8a258e88fa513c4a1a88a4fe70c65eec2d4 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 11 Dec 2017 05:23:00 -0800 Subject: [PATCH 0854/2472] Make ExtractorMediaSource.Builder a factory for ExtractorMediaSources ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178605481 --- .../exoplayer2/castdemo/PlayerManager.java | 2 +- .../exoplayer2/imademo/PlayerManager.java | 3 +- .../exoplayer2/demo/PlayerActivity.java | 5 +- .../exoplayer2/ext/flac/FlacPlaybackTest.java | 10 +- .../exoplayer2/ext/opus/OpusPlaybackTest.java | 10 +- .../exoplayer2/ext/vp9/VpxPlaybackTest.java | 10 +- .../source/ExtractorMediaSource.java | 185 +++++++++--------- .../exoplayer2/source/ads/AdsMediaSource.java | 26 +-- 8 files changed, 118 insertions(+), 133 deletions(-) diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java index aec53c8e8a..548482f61f 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java @@ -398,7 +398,7 @@ import java.util.ArrayList; case DemoUtil.MIME_TYPE_HLS: return new HlsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); case DemoUtil.MIME_TYPE_VIDEO_MP4: - return new ExtractorMediaSource.Builder(uri, DATA_SOURCE_FACTORY).build(); + return new ExtractorMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); default: { throw new IllegalStateException("Unsupported type: " + sample.mimeType); } diff --git a/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java b/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java index 6b840830c5..ec21f6d265 100644 --- a/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java +++ b/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java @@ -70,7 +70,8 @@ import com.google.android.exoplayer2.util.Util; // This is the MediaSource representing the content media (i.e. not the ad). String contentUrl = context.getString(R.string.content_url); MediaSource contentMediaSource = - new ExtractorMediaSource.Builder(Uri.parse(contentUrl), dataSourceFactory).build(); + new ExtractorMediaSource.Factory(dataSourceFactory) + .createMediaSource(Uri.parse(contentUrl)); // Compose the content media source into a new AdsMediaSource with both ads and content. MediaSource mediaSourceWithAds = new AdsMediaSource(contentMediaSource, dataSourceFactory, diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index 215c4708e8..a60ae0c876 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -378,9 +378,8 @@ public class PlayerActivity extends Activity implements OnClickListener, return new HlsMediaSource.Factory(mediaDataSourceFactory) .createMediaSource(uri, mainHandler, eventLogger); case C.TYPE_OTHER: - return new ExtractorMediaSource.Builder(uri, mediaDataSourceFactory) - .setEventListener(mainHandler, eventLogger) - .build(); + return new ExtractorMediaSource.Factory(mediaDataSourceFactory) + .createMediaSource(uri, mainHandler, eventLogger); default: { throw new IllegalStateException("Unsupported type: " + type); } 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 bd6e698dc6..fd18a3b1ae 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 @@ -26,6 +26,7 @@ import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; import com.google.android.exoplayer2.source.ExtractorMediaSource; +import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; @@ -76,10 +77,11 @@ public class FlacPlaybackTest extends InstrumentationTestCase { DefaultTrackSelector trackSelector = new DefaultTrackSelector(); player = ExoPlayerFactory.newInstance(new Renderer[] {audioRenderer}, trackSelector); player.addListener(this); - ExtractorMediaSource mediaSource = new ExtractorMediaSource.Builder( - uri, new DefaultDataSourceFactory(context, "ExoPlayerExtFlacTest")) - .setExtractorsFactory(MatroskaExtractor.FACTORY) - .build(); + MediaSource mediaSource = + new ExtractorMediaSource.Factory( + new DefaultDataSourceFactory(context, "ExoPlayerExtFlacTest")) + .setExtractorsFactory(MatroskaExtractor.FACTORY) + .createMediaSource(uri); player.prepare(mediaSource); player.setPlayWhenReady(true); Looper.loop(); 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 aa61df74d9..d3ab421655 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 @@ -26,6 +26,7 @@ import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; import com.google.android.exoplayer2.source.ExtractorMediaSource; +import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; @@ -76,10 +77,11 @@ public class OpusPlaybackTest extends InstrumentationTestCase { DefaultTrackSelector trackSelector = new DefaultTrackSelector(); player = ExoPlayerFactory.newInstance(new Renderer[] {audioRenderer}, trackSelector); player.addListener(this); - ExtractorMediaSource mediaSource = new ExtractorMediaSource.Builder( - uri, new DefaultDataSourceFactory(context, "ExoPlayerExtOpusTest")) - .setExtractorsFactory(MatroskaExtractor.FACTORY) - .build(); + MediaSource mediaSource = + new ExtractorMediaSource.Factory( + new DefaultDataSourceFactory(context, "ExoPlayerExtOpusTest")) + .setExtractorsFactory(MatroskaExtractor.FACTORY) + .createMediaSource(uri); player.prepare(mediaSource); player.setPlayWhenReady(true); Looper.loop(); 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 746f3d273f..3cc1a1d340 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 @@ -27,6 +27,7 @@ import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; import com.google.android.exoplayer2.source.ExtractorMediaSource; +import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; @@ -105,10 +106,11 @@ public class VpxPlaybackTest extends InstrumentationTestCase { DefaultTrackSelector trackSelector = new DefaultTrackSelector(); player = ExoPlayerFactory.newInstance(new Renderer[] {videoRenderer}, trackSelector); player.addListener(this); - ExtractorMediaSource mediaSource = new ExtractorMediaSource.Builder( - uri, new DefaultDataSourceFactory(context, "ExoPlayerExtVp9Test")) - .setExtractorsFactory(MatroskaExtractor.FACTORY) - .build(); + MediaSource mediaSource = + new ExtractorMediaSource.Factory( + new DefaultDataSourceFactory(context, "ExoPlayerExtVp9Test")) + .setExtractorsFactory(MatroskaExtractor.FACTORY) + .createMediaSource(uri); player.sendMessages(new ExoPlayer.ExoPlayerMessage(videoRenderer, LibvpxVideoRenderer.MSG_SET_OUTPUT_BUFFER_RENDERER, new VpxVideoSurfaceView(context))); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java index b97d957ec4..a2d7941c3f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java @@ -26,6 +26,7 @@ import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorsFactory; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; +import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; @@ -103,129 +104,113 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe private long timelineDurationUs; private boolean timelineIsSeekable; - /** - * Builder for {@link ExtractorMediaSource}. Each builder instance can only be used once. - */ - public static final class Builder { + /** Factory for {@link ExtractorMediaSource}s. */ + public static final class Factory implements AdsMediaSource.MediaSourceFactory { - private final Uri uri; private final DataSource.Factory dataSourceFactory; - private ExtractorsFactory extractorsFactory; + private @Nullable ExtractorsFactory extractorsFactory; + private @Nullable String customCacheKey; private int minLoadableRetryCount; - @Nullable private Handler eventHandler; - @Nullable private MediaSourceEventListener eventListener; - @Nullable private String customCacheKey; private int continueLoadingCheckIntervalBytes; - private boolean isBuildCalled; + private boolean isCreateCalled; /** - * @param uri The {@link Uri} of the media stream. + * Creates a new factory for {@link ExtractorMediaSource}s. + * * @param dataSourceFactory A factory for {@link DataSource}s to read the media. */ - public Builder(Uri uri, DataSource.Factory dataSourceFactory) { - this.uri = uri; + public Factory(DataSource.Factory dataSourceFactory) { this.dataSourceFactory = dataSourceFactory; - minLoadableRetryCount = MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA; continueLoadingCheckIntervalBytes = DEFAULT_LOADING_CHECK_INTERVAL_BYTES; } + /** + * Sets the factory for {@link Extractor}s to process the media stream. The default value is an + * instance of {@link DefaultExtractorsFactory}. + * + * @param extractorsFactory A factory for {@link Extractor}s to process the media stream. If the + * possible formats are known, pass a factory that instantiates extractors for those + * formats. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. + */ + public Factory setExtractorsFactory(ExtractorsFactory extractorsFactory) { + Assertions.checkState(!isCreateCalled); + this.extractorsFactory = extractorsFactory; + return this; + } + + /** + * Sets the custom key that uniquely identifies the original stream. Used for cache indexing. + * The default value is {@code null}. + * + * @param customCacheKey A custom key that uniquely identifies the original stream. Used for + * cache indexing. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. + */ + public Factory setCustomCacheKey(String customCacheKey) { + Assertions.checkState(!isCreateCalled); + this.customCacheKey = customCacheKey; + return this; + } + /** * Sets the minimum number of times to retry if a loading error occurs. The default value is * {@link #MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA}. * * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. - * @return This builder. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Builder setMinLoadableRetryCount(int minLoadableRetryCount) { + public Factory setMinLoadableRetryCount(int minLoadableRetryCount) { + Assertions.checkState(!isCreateCalled); this.minLoadableRetryCount = minLoadableRetryCount; return this; } /** - * Sets the factory for {@link Extractor}s to process the media stream. Default value is an - * instance of {@link DefaultExtractorsFactory}. - * - * @param extractorsFactory A factory for {@link Extractor}s to process the media stream. If the - * possible formats are known, pass a factory that instantiates extractors for those - * formats. - * @return This builder. - */ - public Builder setExtractorsFactory(ExtractorsFactory extractorsFactory) { - this.extractorsFactory = extractorsFactory; - return this; - } - - /** - * Sets the custom key that uniquely identifies the original stream. Used for cache indexing. - * Default value is null. - * - * @param customCacheKey A custom key that uniquely identifies the original stream. Used for - * cache indexing. - * @return This builder. - */ - public Builder setCustomCacheKey(String customCacheKey) { - this.customCacheKey = customCacheKey; - return this; - } - - /** - * Sets the number of bytes that should be loaded between each invocation of - * {@link MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}. Default value - * is {@link #DEFAULT_LOADING_CHECK_INTERVAL_BYTES}. + * Sets the number of bytes that should be loaded between each invocation of {@link + * MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}. The default value is + * {@link #DEFAULT_LOADING_CHECK_INTERVAL_BYTES}. * * @param continueLoadingCheckIntervalBytes The number of bytes that should be loaded between - * each invocation of - * {@link MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}. - * @return This builder. + * each invocation of {@link + * MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Builder setContinueLoadingCheckIntervalBytes(int continueLoadingCheckIntervalBytes) { + public Factory setContinueLoadingCheckIntervalBytes(int continueLoadingCheckIntervalBytes) { + Assertions.checkState(!isCreateCalled); this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes; return this; } /** - * Sets the listener to respond to {@link ExtractorMediaSource} events and the handler to - * deliver these events. + * Returns a new {@link ExtractorMediaSource} using the current parameters. Media source events + * will not be delivered. * - * @param eventHandler A handler for events. - * @param eventListener A listener of events. - * @return This builder. - * @deprecated Use {@link #setEventListener(Handler, MediaSourceEventListener)}. + * @param uri The {@link Uri}. + * @return The new {@link ExtractorMediaSource}. */ - @Deprecated - public Builder setEventListener(Handler eventHandler, EventListener eventListener) { - this.eventHandler = eventHandler; - this.eventListener = eventListener == null ? null : new EventListenerWrapper(eventListener); - return this; + public MediaSource createMediaSource(Uri uri) { + return createMediaSource(uri, null, null); } /** - * Sets the listener to respond to {@link ExtractorMediaSource} events and the handler to - * deliver these events. + * Returns a new {@link ExtractorMediaSource} using the current parameters. * + * @param uri The {@link Uri}. * @param eventHandler A handler for events. * @param eventListener A listener of events. - * @return This builder. + * @return The new {@link ExtractorMediaSource}. */ - public Builder setEventListener(Handler eventHandler, MediaSourceEventListener eventListener) { - this.eventHandler = eventHandler; - this.eventListener = eventListener; - return this; - } - - /** - * Builds a new {@link ExtractorMediaSource} using the current parameters. - *

          - * After this call, the builder should not be re-used. - * - * @return The newly built {@link ExtractorMediaSource}. - */ - public ExtractorMediaSource build() { - Assertions.checkArgument((eventListener == null) == (eventHandler == null)); - Assertions.checkState(!isBuildCalled); - isBuildCalled = true; + @Override + public MediaSource createMediaSource( + Uri uri, @Nullable Handler eventHandler, @Nullable MediaSourceEventListener eventListener) { + isCreateCalled = true; if (extractorsFactory == null) { extractorsFactory = new DefaultExtractorsFactory(); } @@ -234,6 +219,10 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe continueLoadingCheckIntervalBytes); } + @Override + public int[] getSupportedTypes() { + return new int[] {C.TYPE_OTHER}; + } } /** @@ -244,11 +233,15 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe * Otherwise, pass a {@link DefaultExtractorsFactory} to use default extractors. * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated - public ExtractorMediaSource(Uri uri, DataSource.Factory dataSourceFactory, - ExtractorsFactory extractorsFactory, Handler eventHandler, EventListener eventListener) { + public ExtractorMediaSource( + Uri uri, + DataSource.Factory dataSourceFactory, + ExtractorsFactory extractorsFactory, + Handler eventHandler, + EventListener eventListener) { this(uri, dataSourceFactory, extractorsFactory, eventHandler, eventListener, null); } @@ -262,11 +255,15 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe * @param eventListener A listener of events. May be null if delivery of events is not required. * @param customCacheKey A custom key that uniquely identifies the original stream. Used for cache * indexing. May be null. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated - public ExtractorMediaSource(Uri uri, DataSource.Factory dataSourceFactory, - ExtractorsFactory extractorsFactory, Handler eventHandler, EventListener eventListener, + public ExtractorMediaSource( + Uri uri, + DataSource.Factory dataSourceFactory, + ExtractorsFactory extractorsFactory, + Handler eventHandler, + EventListener eventListener, String customCacheKey) { this(uri, dataSourceFactory, extractorsFactory, MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA, eventHandler, eventListener, customCacheKey, DEFAULT_LOADING_CHECK_INTERVAL_BYTES); @@ -285,12 +282,18 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe * indexing. May be null. * @param continueLoadingCheckIntervalBytes The number of bytes that should be loaded between each * invocation of {@link MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated - public ExtractorMediaSource(Uri uri, DataSource.Factory dataSourceFactory, - ExtractorsFactory extractorsFactory, int minLoadableRetryCount, Handler eventHandler, - EventListener eventListener, String customCacheKey, int continueLoadingCheckIntervalBytes) { + public ExtractorMediaSource( + Uri uri, + DataSource.Factory dataSourceFactory, + ExtractorsFactory extractorsFactory, + int minLoadableRetryCount, + Handler eventHandler, + EventListener eventListener, + String customCacheKey, + int continueLoadingCheckIntervalBytes) { this( uri, dataSourceFactory, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java index c701d6ca64..5611bedcca 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java @@ -163,7 +163,7 @@ public final class AdsMediaSource implements MediaSource { this.eventListener = eventListener; mainHandler = new Handler(Looper.getMainLooper()); componentListener = new ComponentListener(); - adMediaSourceFactory = new ExtractorMediaSourceFactory(dataSourceFactory); + adMediaSourceFactory = new ExtractorMediaSource.Factory(dataSourceFactory); deferredMediaPeriodByAdMediaSource = new HashMap<>(); period = new Timeline.Period(); adGroupMediaSources = new MediaSource[0][]; @@ -396,28 +396,4 @@ public final class AdsMediaSource implements MediaSource { } - private static final class ExtractorMediaSourceFactory implements MediaSourceFactory { - - private final DataSource.Factory dataSourceFactory; - - public ExtractorMediaSourceFactory(DataSource.Factory dataSourceFactory) { - this.dataSourceFactory = dataSourceFactory; - } - - @Override - public MediaSource createMediaSource( - Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener) { - return new ExtractorMediaSource.Builder(uri, dataSourceFactory) - .setEventListener(handler, listener) - .build(); - } - - @Override - public int[] getSupportedTypes() { - // Only ExtractorMediaSource is supported. - return new int[] {C.TYPE_OTHER}; - } - - } - } From 2e3667eeff663a154395b48d9cb1e46cf0d8b99a Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 11 Dec 2017 05:31:54 -0800 Subject: [PATCH 0855/2472] Expose ability to get adjusted seek position from MediaPeriod Issue: #2882 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178606133 --- .../exoplayer2/ExoPlayerImplInternal.java | 16 ++++++++++---- .../source/ClippingMediaPeriod.java | 21 +++++++++++++++++++ .../source/DeferredMediaPeriod.java | 6 ++++++ .../source/ExtractorMediaPeriod.java | 7 +++++++ .../exoplayer2/source/MediaPeriod.java | 14 +++++++++++++ .../exoplayer2/source/MergingMediaPeriod.java | 6 ++++++ .../source/SingleSampleMediaPeriod.java | 10 +++++++-- .../source/dash/DashMediaPeriod.java | 6 ++++++ .../exoplayer2/source/hls/HlsMediaPeriod.java | 6 ++++++ .../source/smoothstreaming/SsMediaPeriod.java | 6 ++++++ .../exoplayer2/testutil/FakeMediaPeriod.java | 6 ++++++ .../testutil/FakeSimpleExoPlayer.java | 8 +++++-- 12 files changed, 104 insertions(+), 8 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 8f59451c48..20d75ec1bd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -660,10 +660,18 @@ import java.io.IOException; periodPositionUs = 0; } try { - if (periodId.equals(playbackInfo.periodId) - && ((periodPositionUs / 1000) == (playbackInfo.positionUs / 1000))) { - // Seek position equals the current position. Do nothing. - return; + if (periodId.equals(playbackInfo.periodId)) { + long adjustedPeriodPositionUs = periodPositionUs; + if (playingPeriodHolder != null) { + adjustedPeriodPositionUs = + playingPeriodHolder.mediaPeriod.getAdjustedSeekPositionUs( + adjustedPeriodPositionUs, SeekParameters.DEFAULT); + } + if ((adjustedPeriodPositionUs / 1000) == (playbackInfo.positionUs / 1000)) { + // Seek will be performed to the current position. Do nothing. + periodPositionUs = playbackInfo.positionUs; + return; + } } long newPeriodPositionUs = seekToPeriodPosition(periodId, periodPositionUs); seekPositionAdjusted |= periodPositionUs != newPeriodPositionUs; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java index 539c4841e9..b1c12d6192 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.source; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; +import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.util.Assertions; @@ -170,6 +171,12 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb return seekUs - startUs; } + @Override + public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { + return mediaPeriod.getAdjustedSeekPositionUs( + positionUs + startUs, adjustSeekParameters(positionUs + startUs, seekParameters)); + } + @Override public long getNextLoadPositionUs() { long nextLoadPositionUs = mediaPeriod.getNextLoadPositionUs(); @@ -202,6 +209,20 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb return pendingInitialDiscontinuityPositionUs != C.TIME_UNSET; } + private SeekParameters adjustSeekParameters(long positionUs, SeekParameters seekParameters) { + long toleranceBeforeMs = Math.min(positionUs - startUs, seekParameters.toleranceBeforeUs); + long toleranceAfterMs = + endUs == C.TIME_END_OF_SOURCE + ? seekParameters.toleranceAfterUs + : Math.min(endUs - positionUs, seekParameters.toleranceAfterUs); + if (toleranceBeforeMs == seekParameters.toleranceBeforeUs + && toleranceAfterMs == seekParameters.toleranceAfterUs) { + return seekParameters; + } else { + return new SeekParameters(toleranceBeforeMs, toleranceAfterMs); + } + } + private static boolean shouldKeepInitialDiscontinuity(long startUs, TrackSelection[] selections) { // If the clipping start position is non-zero, the clipping sample streams will adjust // timestamps on buffers they read from the unclipped sample streams. These adjusted buffer diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java index 32a180b956..1895f10d53 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source; +import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.Allocator; @@ -114,6 +115,11 @@ public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callb return mediaPeriod.seekToUs(positionUs); } + @Override + public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { + return mediaPeriod.getAdjustedSeekPositionUs(positionUs, seekParameters); + } + @Override public long getNextLoadPositionUs() { return mediaPeriod.getNextLoadPositionUs(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java index 6b9aeb39da..f8021c24df 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -21,6 +21,7 @@ import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; +import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.extractor.DefaultExtractorInput; import com.google.android.exoplayer2.extractor.Extractor; @@ -369,6 +370,12 @@ import java.util.Arrays; return positionUs; } + @Override + public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { + // Treat all seeks into non-seekable media as being to t=0. + return seekMap.isSeekable() ? positionUs : 0; + } + // SampleStream methods. /* package */ boolean isReady(int track) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java index 54b34bc531..a5b2314d78 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.trackselection.TrackSelection; import java.io.IOException; @@ -149,6 +150,19 @@ public interface MediaPeriod extends SequenceableLoader { */ long seekToUs(long positionUs); + /** + * Returns the position to which a seek will be performed, given the specified seek position and + * {@link SeekParameters}. + * + *

          This method should only be called after the period has been prepared. + * + * @param positionUs The seek position in microseconds. + * @param seekParameters Parameters that control how the seek is performed. Implementations may + * apply seek parameters on a best effort basis. + * @return The actual position to which a seek will be performed, in microseconds. + */ + long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters); + // SequenceableLoader interface. Overridden to provide more specific documentation. /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java index 5ac9fc8d97..cc0c63ef41 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaPeriod.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.source; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; @@ -192,6 +193,11 @@ import java.util.IdentityHashMap; return positionUs; } + @Override + public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { + return enabledPeriods[0].getAdjustedSeekPositionUs(positionUs, seekParameters); + } + // MediaPeriod.Callback implementation @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java index 7b8b54eedc..9fff3b4d85 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java @@ -20,6 +20,7 @@ import android.os.Handler; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; +import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.source.SingleSampleMediaSource.EventListener; import com.google.android.exoplayer2.trackselection.TrackSelection; @@ -153,11 +154,16 @@ import java.util.Arrays; @Override public long seekToUs(long positionUs) { for (int i = 0; i < sampleStreams.size(); i++) { - sampleStreams.get(i).seekToUs(positionUs); + sampleStreams.get(i).reset(); } return positionUs; } + @Override + public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { + return positionUs; + } + // Loader.Callback implementation. @Override @@ -208,7 +214,7 @@ import java.util.Arrays; private int streamState; - public void seekToUs(long positionUs) { + public void reset() { if (streamState == STREAM_STATE_END_OF_STREAM) { streamState = STREAM_STATE_SEND_SAMPLE; } diff --git a/library/dash/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 index f320ad2844..2b7b16228e 100644 --- a/library/dash/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 @@ -20,6 +20,7 @@ import android.util.Pair; import android.util.SparseIntArray; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.EmptySampleStream; import com.google.android.exoplayer2.source.MediaPeriod; @@ -306,6 +307,11 @@ import java.util.Map; return positionUs; } + @Override + public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { + return positionUs; + } + // SequenceableLoader.Callback implementation. @Override diff --git a/library/hls/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 index 24acf0f84d..11602c722f 100644 --- a/library/hls/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 @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.source.hls; import android.os.Handler; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; @@ -244,6 +245,11 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper return positionUs; } + @Override + public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { + return positionUs; + } + // HlsSampleStreamWrapper.Callback implementation. @Override diff --git a/library/smoothstreaming/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 index 564993befe..5ee60bdeed 100644 --- a/library/smoothstreaming/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 @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source.smoothstreaming; import android.util.Base64; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.extractor.mp4.TrackEncryptionBox; import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.MediaPeriod; @@ -182,6 +183,11 @@ import java.util.ArrayList; return positionUs; } + @Override + public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { + return positionUs; + } + // SequenceableLoader.Callback implementation @Override diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java index d34c1d1c0c..0a5dcd5741 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaPeriod.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.testutil; import android.os.Handler; import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.source.TrackGroup; @@ -176,6 +177,11 @@ public class FakeMediaPeriod implements MediaPeriod { return positionUs + seekOffsetUs; } + @Override + public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { + return positionUs; + } + @Override public long getNextLoadPositionUs() { Assert.assertTrue(prepared); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java index 1e7e0cd933..d568770219 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java @@ -422,8 +422,12 @@ public class FakeSimpleExoPlayer extends SimpleExoPlayer { SampleStream[] sampleStreams = new SampleStream[renderers.length]; boolean[] mayRetainStreamFlags = new boolean[renderers.length]; Arrays.fill(mayRetainStreamFlags, true); - mediaPeriod.selectTracks(selectorResult.selections.getAll(), mayRetainStreamFlags, - sampleStreams, new boolean[renderers.length], 0); + mediaPeriod.selectTracks( + selectorResult.selections.getAll(), + mayRetainStreamFlags, + sampleStreams, + new boolean[renderers.length], + /* positionUs = */ 0); eventListenerHandler.post(new Runnable() { @Override public void run() { From a4ae206ebefd22dcc58fd20e6766f42695b15f9f Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 11 Dec 2017 07:19:53 -0800 Subject: [PATCH 0856/2472] Support non-extractor ads in AdsMediaSource and demo apps Issue: #3302 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178615074 --- RELEASENOTES.md | 2 + demos/ima/build.gradle | 2 + .../exoplayer2/imademo/PlayerManager.java | 62 +++++++++++++++++-- .../exoplayer2/demo/PlayerActivity.java | 44 +++++++++---- .../exoplayer2/source/ads/AdsMediaSource.java | 58 ++++++++++++----- 5 files changed, 136 insertions(+), 32 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index f6fb6f3611..700bd025a9 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -44,6 +44,8 @@ * Use surfaceless context for secure DummySurface, if available ([#3558](https://github.com/google/ExoPlayer/issues/3558)). * IMA extension: + * Support non-ExtractorMediaSource ads + ([#3302](https://github.com/google/ExoPlayer/issues/3302)). * Skip ads before the ad preceding the player's initial seek position ([#3527](https://github.com/google/ExoPlayer/issues/3527)). * Fix ad loading when there is no preroll. diff --git a/demos/ima/build.gradle b/demos/ima/build.gradle index c32228de28..536d8d4662 100644 --- a/demos/ima/build.gradle +++ b/demos/ima/build.gradle @@ -43,5 +43,7 @@ android { dependencies { compile project(modulePrefix + 'library-core') compile project(modulePrefix + 'library-ui') + compile project(modulePrefix + 'library-dash') + compile project(modulePrefix + 'library-hls') compile project(modulePrefix + 'extension-ima') } diff --git a/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java b/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java index ec21f6d265..51959451d1 100644 --- a/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java +++ b/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java @@ -17,13 +17,21 @@ package com.google.android.exoplayer2.imademo; import android.content.Context; import android.net.Uri; +import android.os.Handler; +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.C.ContentType; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.ext.ima.ImaAdsLoader; import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.source.ads.AdsMediaSource; +import com.google.android.exoplayer2.source.dash.DashMediaSource; +import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource; +import com.google.android.exoplayer2.source.hls.HlsMediaSource; import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelection; @@ -35,12 +43,12 @@ import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; import com.google.android.exoplayer2.util.Util; -/** - * Manages the {@link ExoPlayer}, the IMA plugin and all video playback. - */ -/* package */ final class PlayerManager { +/** Manages the {@link ExoPlayer}, the IMA plugin and all video playback. */ +/* package */ final class PlayerManager implements AdsMediaSource.MediaSourceFactory { private final ImaAdsLoader adsLoader; + private final DataSource.Factory manifestDataSourceFactory; + private final DataSource.Factory mediaDataSourceFactory; private SimpleExoPlayer player; private long contentPosition; @@ -48,6 +56,14 @@ import com.google.android.exoplayer2.util.Util; public PlayerManager(Context context) { String adTag = context.getString(R.string.ad_tag_url); adsLoader = new ImaAdsLoader(context, Uri.parse(adTag)); + manifestDataSourceFactory = + new DefaultDataSourceFactory( + context, Util.getUserAgent(context, context.getString(R.string.application_name))); + mediaDataSourceFactory = + new DefaultDataSourceFactory( + context, + Util.getUserAgent(context, context.getString(R.string.application_name)), + new DefaultBandwidthMeter()); } public void init(Context context, SimpleExoPlayerView simpleExoPlayerView) { @@ -74,8 +90,14 @@ import com.google.android.exoplayer2.util.Util; .createMediaSource(Uri.parse(contentUrl)); // Compose the content media source into a new AdsMediaSource with both ads and content. - MediaSource mediaSourceWithAds = new AdsMediaSource(contentMediaSource, dataSourceFactory, - adsLoader, simpleExoPlayerView.getOverlayFrameLayout()); + MediaSource mediaSourceWithAds = + new AdsMediaSource( + contentMediaSource, + /* adMediaSourceFactory= */ this, + adsLoader, + simpleExoPlayerView.getOverlayFrameLayout(), + /* eventHandler= */ null, + /* eventListener= */ null); // Prepare the player with the source. player.seekTo(contentPosition); @@ -99,4 +121,32 @@ import com.google.android.exoplayer2.util.Util; adsLoader.release(); } + // AdsMediaSource.MediaSourceFactory implementation. + + @Override + public MediaSource createMediaSource( + Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener) { + @ContentType int type = Util.inferContentType(uri); + switch (type) { + case C.TYPE_DASH: + return new DashMediaSource.Factory( + new DefaultDashChunkSource.Factory(mediaDataSourceFactory), + manifestDataSourceFactory) + .createMediaSource(uri, handler, listener); + case C.TYPE_HLS: + return new HlsMediaSource.Factory(mediaDataSourceFactory) + .createMediaSource(uri, handler, listener); + case C.TYPE_OTHER: + return new ExtractorMediaSource.Factory(mediaDataSourceFactory) + .createMediaSource(uri, handler, listener); + case C.TYPE_SS: + default: + throw new IllegalStateException("Unsupported type: " + type); + } + } + + @Override + public int[] getSupportedTypes() { + return new int[] {C.TYPE_DASH, C.TYPE_HLS, C.TYPE_OTHER}; + } } diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index a60ae0c876..fa3c7d401a 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -23,6 +23,7 @@ import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.text.TextUtils; import android.view.KeyEvent; import android.view.View; @@ -52,6 +53,7 @@ import com.google.android.exoplayer2.source.BehindLiveWindowException; import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.ads.AdsLoader; import com.google.android.exoplayer2.source.ads.AdsMediaSource; @@ -332,7 +334,7 @@ public class PlayerActivity extends Activity implements OnClickListener, } MediaSource[] mediaSources = new MediaSource[uris.length]; for (int i = 0; i < uris.length; i++) { - mediaSources[i] = buildMediaSource(uris[i], extensions[i]); + mediaSources[i] = buildMediaSource(uris[i], extensions[i], mainHandler, eventLogger); } MediaSource mediaSource = mediaSources.length == 1 ? mediaSources[0] : new ConcatenatingMediaSource(mediaSources); @@ -360,26 +362,30 @@ public class PlayerActivity extends Activity implements OnClickListener, updateButtonVisibilities(); } - private MediaSource buildMediaSource(Uri uri, String overrideExtension) { + private MediaSource buildMediaSource( + Uri uri, + String overrideExtension, + @Nullable Handler handler, + @Nullable MediaSourceEventListener listener) { @ContentType int type = TextUtils.isEmpty(overrideExtension) ? Util.inferContentType(uri) : Util.inferContentType("." + overrideExtension); switch (type) { - case C.TYPE_SS: - return new SsMediaSource.Factory( - new DefaultSsChunkSource.Factory(mediaDataSourceFactory), - buildDataSourceFactory(false)) - .createMediaSource(uri, mainHandler, eventLogger); case C.TYPE_DASH: return new DashMediaSource.Factory( new DefaultDashChunkSource.Factory(mediaDataSourceFactory), buildDataSourceFactory(false)) - .createMediaSource(uri, mainHandler, eventLogger); + .createMediaSource(uri, handler, listener); + case C.TYPE_SS: + return new SsMediaSource.Factory( + new DefaultSsChunkSource.Factory(mediaDataSourceFactory), + buildDataSourceFactory(false)) + .createMediaSource(uri, handler, listener); case C.TYPE_HLS: return new HlsMediaSource.Factory(mediaDataSourceFactory) - .createMediaSource(uri, mainHandler, eventLogger); + .createMediaSource(uri, handler, listener); case C.TYPE_OTHER: return new ExtractorMediaSource.Factory(mediaDataSourceFactory) - .createMediaSource(uri, mainHandler, eventLogger); + .createMediaSource(uri, handler, listener); default: { throw new IllegalStateException("Unsupported type: " + type); } @@ -466,8 +472,22 @@ public class PlayerActivity extends Activity implements OnClickListener, // The demo app has a non-null overlay frame layout. simpleExoPlayerView.getOverlayFrameLayout().addView(adUiViewGroup); } - return new AdsMediaSource(mediaSource, mediaDataSourceFactory, adsLoader, adUiViewGroup, - mainHandler, eventLogger); + AdsMediaSource.MediaSourceFactory adMediaSourceFactory = + new AdsMediaSource.MediaSourceFactory() { + @Override + public MediaSource createMediaSource( + Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener) { + return PlayerActivity.this.buildMediaSource( + uri, /* overrideExtension= */ null, handler, listener); + } + + @Override + public int[] getSupportedTypes() { + return new int[] {C.TYPE_DASH, C.TYPE_SS, C.TYPE_HLS, C.TYPE_OTHER}; + } + }; + return new AdsMediaSource( + mediaSource, adMediaSourceFactory, adsLoader, adUiViewGroup, mainHandler, eventLogger); } private void releaseAdsLoader() { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java index 5611bedcca..0980e9d011 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java @@ -96,13 +96,13 @@ public final class AdsMediaSource implements MediaSource { private static final String TAG = "AdsMediaSource"; private final MediaSource contentMediaSource; + private final MediaSourceFactory adMediaSourceFactory; private final AdsLoader adsLoader; private final ViewGroup adUiViewGroup; @Nullable private final Handler eventHandler; @Nullable private final EventListener eventListener; private final Handler mainHandler; private final ComponentListener componentListener; - private final MediaSourceFactory adMediaSourceFactory; private final Map> deferredMediaPeriodByAdMediaSource; private final Timeline.Period period; @@ -119,28 +119,31 @@ public final class AdsMediaSource implements MediaSource { private MediaSource.Listener listener; /** - * Constructs a new source that inserts ads linearly with the content specified by - * {@code contentMediaSource}. - *

          - * Ad media is loaded using {@link ExtractorMediaSource}. If {@code eventListener} is - * non-{@code null} it will be notified of both ad tag and ad media load errors. + * Constructs a new source that inserts ads linearly with the content specified by {@code + * contentMediaSource}. Ad media is loaded using {@link ExtractorMediaSource}. * * @param contentMediaSource The {@link MediaSource} providing the content to play. * @param dataSourceFactory Factory for data sources used to load ad media. * @param adsLoader The loader for ads. * @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI. */ - public AdsMediaSource(MediaSource contentMediaSource, DataSource.Factory dataSourceFactory, - AdsLoader adsLoader, ViewGroup adUiViewGroup) { - this(contentMediaSource, dataSourceFactory, adsLoader, adUiViewGroup, null, null); + public AdsMediaSource( + MediaSource contentMediaSource, + DataSource.Factory dataSourceFactory, + AdsLoader adsLoader, + ViewGroup adUiViewGroup) { + this( + contentMediaSource, + dataSourceFactory, + adsLoader, + adUiViewGroup, + /* eventHandler= */ null, + /* eventListener= */ null); } /** * Constructs a new source that inserts ads linearly with the content specified by {@code - * contentMediaSource}. - * - *

          Ad media is loaded using {@link ExtractorMediaSource}. If {@code eventListener} is - * non-{@code null} it will be notified of both ad tag and ad media load errors. + * contentMediaSource}. Ad media is loaded using {@link ExtractorMediaSource}. * * @param contentMediaSource The {@link MediaSource} providing the content to play. * @param dataSourceFactory Factory for data sources used to load ad media. @@ -156,14 +159,41 @@ public final class AdsMediaSource implements MediaSource { ViewGroup adUiViewGroup, @Nullable Handler eventHandler, @Nullable EventListener eventListener) { + this( + contentMediaSource, + new ExtractorMediaSource.Factory(dataSourceFactory), + adsLoader, + adUiViewGroup, + eventHandler, + eventListener); + } + + /** + * Constructs a new source that inserts ads linearly with the content specified by {@code + * contentMediaSource}. + * + * @param contentMediaSource The {@link MediaSource} providing the content to play. + * @param adMediaSourceFactory Factory for media sources used to load ad media. + * @param adsLoader The loader for ads. + * @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI. + * @param eventHandler A handler for events. May be null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + */ + public AdsMediaSource( + MediaSource contentMediaSource, + MediaSourceFactory adMediaSourceFactory, + AdsLoader adsLoader, + ViewGroup adUiViewGroup, + @Nullable Handler eventHandler, + @Nullable EventListener eventListener) { this.contentMediaSource = contentMediaSource; + this.adMediaSourceFactory = adMediaSourceFactory; this.adsLoader = adsLoader; this.adUiViewGroup = adUiViewGroup; this.eventHandler = eventHandler; this.eventListener = eventListener; mainHandler = new Handler(Looper.getMainLooper()); componentListener = new ComponentListener(); - adMediaSourceFactory = new ExtractorMediaSource.Factory(dataSourceFactory); deferredMediaPeriodByAdMediaSource = new HashMap<>(); period = new Timeline.Period(); adGroupMediaSources = new MediaSource[0][]; From e2bba1567e55eea81552a8810949d78f6d0b71db Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 12 Dec 2017 09:03:15 -0800 Subject: [PATCH 0857/2472] Add Builder for ImaAdsLoader and allow early requestAds Also fix propagation of ad errors that occur when no player is attached. Issue: #3548 Issue: #3556 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178767997 --- RELEASENOTES.md | 4 + .../exoplayer2/ext/ima/ImaAdsLoader.java | 259 +++++++++++++----- 2 files changed, 195 insertions(+), 68 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 700bd025a9..b71faff349 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -51,6 +51,10 @@ * Fix ad loading when there is no preroll. * Add an option to turn off hiding controls during ad playback ([#3532](https://github.com/google/ExoPlayer/issues/3532)). + * Support specifying an ads response instead of an ad tag + ([#3548](https://github.com/google/ExoPlayer/issues/3548)). + * Support overriding the ad load timeout + ([#3556](https://github.com/google/ExoPlayer/issues/3556)). ### 2.6.0 ### diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index 19dfa1e83f..acfe143952 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -19,6 +19,7 @@ import android.content.Context; import android.net.Uri; import android.os.SystemClock; import android.support.annotation.IntDef; +import android.support.annotation.Nullable; import android.util.Log; import android.view.ViewGroup; import android.webkit.WebView; @@ -65,10 +66,80 @@ import java.util.Map; */ public final class ImaAdsLoader extends Player.DefaultEventListener implements AdsLoader, VideoAdPlayer, ContentProgressProvider, AdErrorListener, AdsLoadedListener, AdEventListener { + static { ExoPlayerLibraryInfo.registerModule("goog.exo.ima"); } + /** Builder for {@link ImaAdsLoader}. */ + public static final class Builder { + + private final Context context; + + private @Nullable ImaSdkSettings imaSdkSettings; + private long vastLoadTimeoutMs; + + /** + * Creates a new builder for {@link ImaAdsLoader}. + * + * @param context The context; + */ + public Builder(Context context) { + this.context = Assertions.checkNotNull(context); + vastLoadTimeoutMs = C.TIME_UNSET; + } + + /** + * Sets the IMA SDK settings. The provided settings instance's player type and version fields + * may be overwritten. + * + *

          If this method is not called the default settings will be used. + * + * @param imaSdkSettings The {@link ImaSdkSettings}. + * @return This builder, for convenience. + */ + public Builder setImaSdkSettings(ImaSdkSettings imaSdkSettings) { + this.imaSdkSettings = Assertions.checkNotNull(imaSdkSettings); + return this; + } + + /** + * Sets the VAST load timeout, in milliseconds. + * + * @param vastLoadTimeoutMs The VAST load timeout, in milliseconds. + * @return This builder, for convenience. + * @see AdsRequest#setVastLoadTimeout(float) + */ + public Builder setVastLoadTimeoutMs(long vastLoadTimeoutMs) { + Assertions.checkArgument(vastLoadTimeoutMs >= 0); + this.vastLoadTimeoutMs = vastLoadTimeoutMs; + return this; + } + + /** + * Returns a new {@link ImaAdsLoader} for the specified ad tag. + * + * @param adTagUri The URI of a compatible ad tag to load. See + * https://developers.google.com/interactive-media-ads/docs/sdks/android/compatibility for + * information on compatible ad tags. + * @return The new {@link ImaAdsLoader}. + */ + public ImaAdsLoader buildForAdTag(Uri adTagUri) { + return new ImaAdsLoader(context, adTagUri, imaSdkSettings, null, vastLoadTimeoutMs); + } + + /** + * Returns a new {@link ImaAdsLoader} with the specified sideloaded ads response. + * + * @param adsResponse The sideloaded VAST, VMAP, or ad rules response to be used instead of + * making a request via an ad tag URL. + * @return The new {@link ImaAdsLoader}. + */ + public ImaAdsLoader buildForAdsResponse(String adsResponse) { + return new ImaAdsLoader(context, null, imaSdkSettings, adsResponse, vastLoadTimeoutMs); + } + } + private static final boolean DEBUG = false; private static final String TAG = "ImaAdsLoader"; @@ -94,9 +165,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A private static final String FOCUS_SKIP_BUTTON_WORKAROUND_JS = "javascript:" + "try{ document.getElementsByClassName(\"videoAdUiSkipButton\")[0].focus(); } catch (e) {}"; - /** - * The state of ad playback based on IMA's calls to {@link #playAd()} and {@link #pauseAd()}. - */ + /** The state of ad playback. */ @Retention(RetentionPolicy.SOURCE) @IntDef({IMA_AD_STATE_NONE, IMA_AD_STATE_PLAYING, IMA_AD_STATE_PAUSED}) private @interface ImaAdState {} @@ -113,7 +182,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A */ private static final int IMA_AD_STATE_PAUSED = 2; - private final Uri adTagUri; + private final @Nullable Uri adTagUri; + private final @Nullable String adsResponse; + private final long vastLoadTimeoutMs; private final Timeline.Period period; private final List adCallbacks; private final ImaSdkFactory imaSdkFactory; @@ -129,6 +200,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A private VideoProgressUpdate lastAdProgress; private AdsManager adsManager; + private AdErrorEvent pendingAdErrorEvent; private Timeline timeline; private long contentDurationMs; private int podIndexOffset; @@ -144,9 +216,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A * Whether IMA has sent an ad event to pause content since the last resume content event. */ private boolean imaPausedContent; - /** - * The current ad playback state based on IMA's calls to {@link #playAd()} and {@link #stopAd()}. - */ + /** The current ad playback state. */ private @ImaAdState int imaAdState; /** * Whether {@link com.google.ads.interactivemedia.v3.api.AdsLoader#contentComplete()} has been @@ -189,13 +259,15 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A /** * Creates a new IMA ads loader. * + *

          If you need to customize the ad request, use {@link ImaAdsLoader.Builder} instead. + * * @param context The context. * @param adTagUri The {@link Uri} of an ad tag compatible with the Android IMA SDK. See * https://developers.google.com/interactive-media-ads/docs/sdks/android/compatibility for * more information. */ public ImaAdsLoader(Context context, Uri adTagUri) { - this(context, adTagUri, null); + this(context, adTagUri, null, null, C.TIME_UNSET); } /** @@ -207,9 +279,23 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A * more information. * @param imaSdkSettings {@link ImaSdkSettings} used to configure the IMA SDK, or {@code null} to * use the default settings. If set, the player type and version fields may be overwritten. + * @deprecated Use {@link ImaAdsLoader.Builder}. */ + @Deprecated public ImaAdsLoader(Context context, Uri adTagUri, ImaSdkSettings imaSdkSettings) { + this(context, adTagUri, imaSdkSettings, null, C.TIME_UNSET); + } + + private ImaAdsLoader( + Context context, + @Nullable Uri adTagUri, + @Nullable ImaSdkSettings imaSdkSettings, + @Nullable String adsResponse, + long vastLoadTimeoutMs) { + Assertions.checkArgument(adTagUri != null || adsResponse != null); this.adTagUri = adTagUri; + this.adsResponse = adsResponse; + this.vastLoadTimeoutMs = vastLoadTimeoutMs; period = new Timeline.Period(); adCallbacks = new ArrayList<>(1); imaSdkFactory = ImaSdkFactory.getInstance(); @@ -238,6 +324,37 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A return adsLoader; } + /** + * Requests ads, if they have not already been requested. Must be called on the main thread. + * + *

          Ads will be requested automatically when the player is prepared if this method has not been + * called, so it is only necessary to call this method if you want to request ads before preparing + * the player + * + * @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI. + */ + public void requestAds(ViewGroup adUiViewGroup) { + if (adPlaybackState != null || adsManager != null || pendingAdRequestContext != null) { + // Ads have already been requested. + return; + } + adDisplayContainer.setAdContainer(adUiViewGroup); + pendingAdRequestContext = new Object(); + AdsRequest request = imaSdkFactory.createAdsRequest(); + if (adTagUri != null) { + request.setAdTagUrl(adTagUri.toString()); + } else /* adsResponse != null */ { + request.setAdsResponse(adsResponse); + } + if (vastLoadTimeoutMs != C.TIME_UNSET) { + request.setVastLoadTimeout(vastLoadTimeoutMs); + } + request.setAdDisplayContainer(adDisplayContainer); + request.setContentProgressProvider(this); + request.setUserRequestContext(pendingAdRequestContext); + adsLoader.requestAds(request); + } + // AdsLoader implementation. @Override @@ -268,14 +385,19 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A lastContentProgress = null; adDisplayContainer.setAdContainer(adUiViewGroup); player.addListener(this); + maybeNotifyAdError(); if (adPlaybackState != null) { + // Pass the ad playback state to the player, and resume ads if necessary. eventListener.onAdPlaybackState(adPlaybackState.copy()); if (imaPausedContent && player.getPlayWhenReady()) { adsManager.resume(); } + } else if (adsManager != null) { + // Ads have loaded but the ads manager is not initialized. + startAdPlayback(); } else { - pendingContentPositionMs = player.getCurrentPosition(); - requestAds(); + // Ads haven't loaded yet, so request them. + requestAds(adUiViewGroup); } } @@ -312,49 +434,13 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A return; } pendingAdRequestContext = null; - - long[] adGroupTimesUs = getAdGroupTimesUs(adsManager.getAdCuePoints()); - adPlaybackState = new AdPlaybackState(adGroupTimesUs); - this.adsManager = adsManager; adsManager.addAdErrorListener(this); adsManager.addAdEventListener(this); - - ImaSdkFactory imaSdkFactory = ImaSdkFactory.getInstance(); - AdsRenderingSettings adsRenderingSettings = imaSdkFactory.createAdsRenderingSettings(); - adsRenderingSettings.setEnablePreloading(ENABLE_PRELOADING); - adsRenderingSettings.setMimeTypes(supportedMimeTypes); - int adGroupIndexForPosition = - getAdGroupIndexForPosition(adGroupTimesUs, C.msToUs(pendingContentPositionMs)); - if (adGroupIndexForPosition == 0) { - podIndexOffset = 0; - } else if (adGroupIndexForPosition == C.INDEX_UNSET) { - pendingContentPositionMs = C.TIME_UNSET; - // There is no preroll and midroll pod indices start at 1. - podIndexOffset = -1; - } else /* adGroupIndexForPosition > 0 */ { - // Skip ad groups before the one at or immediately before the playback position. - for (int i = 0; i < adGroupIndexForPosition; i++) { - adPlaybackState.playedAdGroup(i); - } - // Play ads after the midpoint between the ad to play and the one before it, to avoid issues - // with rounding one of the two ad times. - long adGroupForPositionTimeUs = adGroupTimesUs[adGroupIndexForPosition]; - long adGroupBeforeTimeUs = adGroupTimesUs[adGroupIndexForPosition - 1]; - double midpointTimeUs = (adGroupForPositionTimeUs + adGroupBeforeTimeUs) / 2d; - adsRenderingSettings.setPlayAdsAfterTime(midpointTimeUs / C.MICROS_PER_SECOND); - - // We're removing one or more ads, which means that the earliest ad (if any) will be a - // midroll/postroll. Midroll pod indices start at 1. - podIndexOffset = adGroupIndexForPosition - 1; + if (player != null) { + // If a player is attached already, start playback immediately. + startAdPlayback(); } - - adsManager.init(adsRenderingSettings); - if (DEBUG) { - Log.d(TAG, "Initialized with ads rendering settings: " + adsRenderingSettings); - } - - updateAdPlaybackState(); } // AdEvent.AdEventListener implementation. @@ -384,14 +470,12 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A adGroupIndex = podIndex == -1 ? (adPlaybackState.adGroupCount - 1) : (podIndex + podIndexOffset); int adPosition = adPodInfo.getAdPosition(); - int adCountInAdGroup = adPodInfo.getTotalAds(); + int adCount = adPodInfo.getTotalAds(); adsManager.start(); if (DEBUG) { - Log.d( - TAG, - "Loaded ad " + adPosition + " of " + adCountInAdGroup + " in group " + adGroupIndex); + Log.d(TAG, "Loaded ad " + adPosition + " of " + adCount + " in group " + adGroupIndex); } - adPlaybackState.setAdCount(adGroupIndex, adCountInAdGroup); + adPlaybackState.setAdCount(adGroupIndex, adCount); updateAdPlaybackState(); break; case CONTENT_PAUSE_REQUESTED: @@ -434,14 +518,15 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A Log.d(TAG, "onAdError " + adErrorEvent); } if (adsManager == null) { + // No ads were loaded, so allow playback to start without any ads. pendingAdRequestContext = null; adPlaybackState = new AdPlaybackState(new long[0]); updateAdPlaybackState(); } - if (eventListener != null) { - IOException exception = new IOException("Ad error: " + adErrorEvent, adErrorEvent.getError()); - eventListener.onLoadError(exception); + if (pendingAdErrorEvent == null) { + pendingAdErrorEvent = adErrorEvent; } + maybeNotifyAdError(); } // ContentProgressProvider implementation. @@ -654,18 +739,56 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A // Internal methods. - private void requestAds() { - if (pendingAdRequestContext != null) { - // Ad request already in flight. - return; + private void startAdPlayback() { + ImaSdkFactory imaSdkFactory = ImaSdkFactory.getInstance(); + AdsRenderingSettings adsRenderingSettings = imaSdkFactory.createAdsRenderingSettings(); + adsRenderingSettings.setEnablePreloading(ENABLE_PRELOADING); + adsRenderingSettings.setMimeTypes(supportedMimeTypes); + + // Set up the ad playback state, skipping ads based on the start position as required. + pendingContentPositionMs = player.getCurrentPosition(); + long[] adGroupTimesUs = getAdGroupTimesUs(adsManager.getAdCuePoints()); + adPlaybackState = new AdPlaybackState(adGroupTimesUs); + int adGroupIndexForPosition = + getAdGroupIndexForPosition(adGroupTimesUs, C.msToUs(pendingContentPositionMs)); + if (adGroupIndexForPosition == 0) { + podIndexOffset = 0; + } else if (adGroupIndexForPosition == C.INDEX_UNSET) { + pendingContentPositionMs = C.TIME_UNSET; + // There is no preroll and midroll pod indices start at 1. + podIndexOffset = -1; + } else /* adGroupIndexForPosition > 0 */ { + // Skip ad groups before the one at or immediately before the playback position. + for (int i = 0; i < adGroupIndexForPosition; i++) { + adPlaybackState.playedAdGroup(i); + } + // Play ads after the midpoint between the ad to play and the one before it, to avoid issues + // with rounding one of the two ad times. + long adGroupForPositionTimeUs = adGroupTimesUs[adGroupIndexForPosition]; + long adGroupBeforeTimeUs = adGroupTimesUs[adGroupIndexForPosition - 1]; + double midpointTimeUs = (adGroupForPositionTimeUs + adGroupBeforeTimeUs) / 2d; + adsRenderingSettings.setPlayAdsAfterTime(midpointTimeUs / C.MICROS_PER_SECOND); + + // We're removing one or more ads, which means that the earliest ad (if any) will be a + // midroll/postroll. Midroll pod indices start at 1. + podIndexOffset = adGroupIndexForPosition - 1; + } + + // Start ad playback. + adsManager.init(adsRenderingSettings); + updateAdPlaybackState(); + if (DEBUG) { + Log.d(TAG, "Initialized with ads rendering settings: " + adsRenderingSettings); + } + } + + private void maybeNotifyAdError() { + if (eventListener != null && pendingAdErrorEvent != null) { + IOException exception = + new IOException("Ad error: " + pendingAdErrorEvent, pendingAdErrorEvent.getError()); + eventListener.onLoadError(exception); + pendingAdErrorEvent = null; } - pendingAdRequestContext = new Object(); - AdsRequest request = imaSdkFactory.createAdsRequest(); - request.setAdTagUrl(adTagUri.toString()); - request.setAdDisplayContainer(adDisplayContainer); - request.setContentProgressProvider(this); - request.setUserRequestContext(pendingAdRequestContext); - adsLoader.requestAds(request); } private void updateImaStateForPlayerState() { From 566170f308d2e29d7ee568f98be3344a1a3743ed Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 10 Nov 2017 08:55:50 -0800 Subject: [PATCH 0858/2472] Add javadoc to ExoPlayerTestRunner. Someone must have forgotten to do this when rewriting this class. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175296249 --- .../testutil/ExoPlayerTestRunner.java | 167 +++++++++++++++++- 1 file changed, 165 insertions(+), 2 deletions(-) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java index a87066415d..a1f8fc7861 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java @@ -62,14 +62,30 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { */ public interface PlayerFactory { + /** + * Creates a new {@link SimpleExoPlayer} using the provided renderers factory, track selector, + * and load control. + * + * @param renderersFactory A {@link RenderersFactory} to be used for the new player. + * @param trackSelector A {@link MappingTrackSelector} to be used for the new player. + * @param loadControl A {@link LoadControl} to be used for the new player. + * @return A new {@link SimpleExoPlayer}. + */ SimpleExoPlayer createExoPlayer(RenderersFactory renderersFactory, MappingTrackSelector trackSelector, LoadControl loadControl); } + /** + * A generic video {@link Format} which can be used to set up media sources and renderers. + */ public static final Format VIDEO_FORMAT = Format.createVideoSampleFormat(null, MimeTypes.VIDEO_H264, null, Format.NO_VALUE, Format.NO_VALUE, 1280, 720, Format.NO_VALUE, null, null); + + /** + * A generic audio {@link Format} which can be used to set up media sources and renderers. + */ public static final Format AUDIO_FORMAT = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, Format.NO_VALUE, 2, 44100, null, null, 0, null); @@ -85,19 +101,45 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { private ActionSchedule actionSchedule; private Player.EventListener eventListener; + /** + * Sets a {@link Timeline} to be used by a {@link FakeMediaSource} in the test runner. The + * default value is a non-seekable, non-dynamic {@link FakeTimeline} with zero duration. Setting + * the timeline is not allowed after a call to {@link #setMediaSource(MediaSource)}. + * + * @param timeline A {@link Timeline} to be used by a {@link FakeMediaSource} in the test + * runner. + * @return This builder. + */ public Builder setTimeline(Timeline timeline) { Assert.assertNull(mediaSource); this.timeline = timeline; return this; } + /** + * Sets a manifest to be used by a {@link FakeMediaSource} in the test runner. The default value + * is null. Setting the manifest is not allowed after a call to + * {@link #setMediaSource(MediaSource)}. + * + * @param manifest A manifest to be used by a {@link FakeMediaSource} in the test runner. + * @return This builder. + */ public Builder setManifest(Object manifest) { Assert.assertNull(mediaSource); this.manifest = manifest; return this; } - /** Replaces {@link #setTimeline(Timeline)} and {@link #setManifest(Object)}. */ + /** + * Sets a {@link MediaSource} to be used by the test runner. The default value is a + * {@link FakeMediaSource} with the timeline and manifest provided by + * {@link #setTimeline(Timeline)} and {@link #setManifest(Object)}. Setting the media source is + * not allowed after calls to {@link #setTimeline(Timeline)} and/or + * {@link #setManifest(Object)}. + * + * @param mediaSource A {@link MediaSource} to be used by the test runner. + * @return This builder. + */ public Builder setMediaSource(MediaSource mediaSource) { Assert.assertNull(timeline); Assert.assertNull(manifest); @@ -105,49 +147,118 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { return this; } + /** + * Sets a {@link MappingTrackSelector} to be used by the test runner. The default value is a + * {@link DefaultTrackSelector}. + * + * @param trackSelector A {@link MappingTrackSelector} to be used by the test runner. + * @return This builder. + */ public Builder setTrackSelector(MappingTrackSelector trackSelector) { this.trackSelector = trackSelector; return this; } + /** + * Sets a {@link LoadControl} to be used by the test runner. The default value is a + * {@link DefaultLoadControl}. + * + * @param loadControl A {@link LoadControl} to be used by the test runner. + * @return This builder. + */ public Builder setLoadControl(LoadControl loadControl) { this.loadControl = loadControl; return this; } + /** + * Sets a list of {@link Format}s to be used by a {@link FakeMediaSource} to create media + * periods and for setting up a {@link FakeRenderer}. The default value is a single + * {@link #VIDEO_FORMAT}. Note that this parameter doesn't have any influence if both a media + * source with {@link #setMediaSource(MediaSource)} and renderers with + * {@link #setRenderers(Renderer...)} or {@link #setRenderersFactory(RenderersFactory)} are set. + * + * @param supportedFormats A list of supported {@link Format}s. + * @return This builder. + */ public Builder setSupportedFormats(Format... supportedFormats) { this.supportedFormats = supportedFormats; return this; } + /** + * Sets the {@link Renderer}s to be used by the test runner. The default value is a single + * {@link FakeRenderer} supporting the formats set by {@link #setSupportedFormats(Format...)}. + * Setting the renderers is not allowed after a call to + * {@link #setRenderersFactory(RenderersFactory)}. + * + * @param renderers A list of {@link Renderer}s to be used by the test runner. + * @return This builder. + */ public Builder setRenderers(Renderer... renderers) { Assert.assertNull(renderersFactory); this.renderers = renderers; return this; } - /** Replaces {@link #setRenderers(Renderer...)}. */ + /** + * Sets the {@link RenderersFactory} to be used by the test runner. The default factory creates + * all renderers set by {@link #setRenderers(Renderer...)}. Setting the renderer factory is not + * allowed after a call to {@link #setRenderers(Renderer...)}. + * + * @param renderersFactory A {@link RenderersFactory} to be used by the test runner. + * @return This builder. + */ public Builder setRenderersFactory(RenderersFactory renderersFactory) { Assert.assertNull(renderers); this.renderersFactory = renderersFactory; return this; } + /** + * Sets the {@link PlayerFactory} which creates the {@link SimpleExoPlayer} to be used by the + * test runner. The default value is a {@link SimpleExoPlayer} with the renderers provided by + * {@link #setRenderers(Renderer...)} or {@link #setRenderersFactory(RenderersFactory)}, the + * track selector provided by {@link #setTrackSelector(MappingTrackSelector)} and the load + * control provided by {@link #setLoadControl(LoadControl)}. + * + * @param playerFactory A {@link PlayerFactory} to create the player. + * @return This builder. + */ public Builder setExoPlayer(PlayerFactory playerFactory) { this.playerFactory = playerFactory; return this; } + /** + * Sets an {@link ActionSchedule} to be run by the test runner. The first action will be + * executed immediately before {@link SimpleExoPlayer#prepare(MediaSource)}. + * + * @param actionSchedule An {@link ActionSchedule} to be used by the test runner. + * @return This builder. + */ public Builder setActionSchedule(ActionSchedule actionSchedule) { this.actionSchedule = actionSchedule; return this; } + /** + * Sets an {@link Player.EventListener} to be registered to listen to player events. + * + * @param eventListener A {@link Player.EventListener} to be registered by the test runner to + * listen to player events. + * @return This builder. + */ public Builder setEventListener(Player.EventListener eventListener) { this.eventListener = eventListener; return this; } + /** + * Builds an {@link ExoPlayerTestRunner} using the provided values or their defaults. + * + * @return The built {@link ExoPlayerTestRunner}. + */ public ExoPlayerTestRunner build() { if (supportedFormats == null) { supportedFormats = new Format[] { VIDEO_FORMAT }; @@ -234,6 +345,13 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { // Called on the test thread to run the test. + /** + * Starts the test runner on its own thread. This will trigger the creation of the player, the + * listener registration, the start of the action schedule, and the preparation of the player + * with the provided media source. + * + * @return This test runner. + */ public ExoPlayerTestRunner start() { handler.post(new Runnable() { @Override @@ -257,6 +375,16 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { return this; } + /** + * Blocks the current thread until the test runner finishes. A test is deemed to be finished when + * the playback state transitions to {@link Player#STATE_ENDED} or {@link Player#STATE_IDLE}, or + * when am {@link ExoPlaybackException} is thrown. + * + * @param timeoutMs The maximum time to wait for the test runner to finish. If this time elapsed + * the method will throw a {@link TimeoutException}. + * @return This test runner. + * @throws Exception If any exception occurred during playback, release, or due to a timeout. + */ public ExoPlayerTestRunner blockUntilEnded(long timeoutMs) throws Exception { if (!endedCountDownLatch.await(timeoutMs, TimeUnit.MILLISECONDS)) { exception = new TimeoutException("Test playback timed out waiting for playback to end."); @@ -271,6 +399,13 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { // Assertions called on the test thread after test finished. + /** + * Asserts that the timelines reported by + * {@link Player.EventListener#onTimelineChanged(Timeline, Object, int)} are equal to the provided + * timelines. + * + * @param timelines A list of expected {@link Timeline}s. + */ public void assertTimelinesEqual(Timeline... timelines) { Assert.assertEquals(timelines.length, this.timelines.size()); for (Timeline timeline : timelines) { @@ -278,6 +413,13 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { } } + /** + * Asserts that the manifests reported by + * {@link Player.EventListener#onTimelineChanged(Timeline, Object, int)} are equal to the provided + * manifest. + * + * @param manifests A list of expected manifests. + */ public void assertManifestsEqual(Object... manifests) { Assert.assertEquals(manifests.length, this.manifests.size()); for (Object manifest : manifests) { @@ -285,14 +427,35 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { } } + /** + * Asserts that the last track group array reported by + * {@link Player.EventListener#onTracksChanged(TrackGroupArray, TrackSelectionArray)} is equal to + * the provided track group array. + * + * @param trackGroupArray The expected {@link TrackGroupArray}. + */ public void assertTrackGroupsEqual(TrackGroupArray trackGroupArray) { Assert.assertEquals(trackGroupArray, this.trackGroups); } + /** + * Asserts that the number of reported discontinuities by + * {@link Player.EventListener#onPositionDiscontinuity(int)} is equal to the provided number. + * + * @param expectedCount The expected number of position discontinuities. + */ public void assertPositionDiscontinuityCount(int expectedCount) { Assert.assertEquals(expectedCount, positionDiscontinuityCount); } + /** + * Asserts that the indices of played periods is equal to the provided list of periods. A period + * is considered to be played if it was the current period after a position discontinuity or a + * media source preparation. When the same period is repeated automatically due to enabled repeat + * modes, it is reported twice. Seeks within the current period are not reported. + * + * @param periodIndices A list of expected period indices. + */ public void assertPlayedPeriodIndices(int... periodIndices) { Assert.assertEquals(periodIndices.length, this.periodIndices.size()); for (int periodIndex : periodIndices) { From bff221b85e365733cbc9bcf630fc5b27efcfe3c6 Mon Sep 17 00:00:00 2001 From: hoangtc Date: Mon, 13 Nov 2017 07:28:57 -0800 Subject: [PATCH 0859/2472] Introduce Builder pattern to create MediaSource. Start with DASH MediaSource. The number of injected arguments is getting out-of-control. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175529031 --- .../exoplayer2/demo/PlayerActivity.java | 7 +- .../source/dash/DashMediaSource.java | 147 ++++++++++++++++++ .../playbacktests/gts/DashTestRunner.java | 7 +- 3 files changed, 157 insertions(+), 4 deletions(-) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index 08c5bddb09..614626077a 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -365,8 +365,11 @@ public class PlayerActivity extends Activity implements OnClickListener, return new SsMediaSource(uri, buildDataSourceFactory(false), new DefaultSsChunkSource.Factory(mediaDataSourceFactory), mainHandler, eventLogger); case C.TYPE_DASH: - return new DashMediaSource(uri, buildDataSourceFactory(false), - new DefaultDashChunkSource.Factory(mediaDataSourceFactory), mainHandler, eventLogger); + return DashMediaSource.Builder + .forManifestUri(uri, buildDataSourceFactory(false), + new DefaultDashChunkSource.Factory(mediaDataSourceFactory)) + .setEventListener(mainHandler, eventLogger) + .build(); case C.TYPE_HLS: return new HlsMediaSource(uri, mediaDataSourceFactory, mainHandler, eventLogger); case C.TYPE_OTHER: diff --git a/library/dash/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 index c529fcab4b..3d5a9c393d 100644 --- a/library/dash/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 @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.source.dash; import android.net.Uri; import android.os.Handler; import android.os.SystemClock; +import android.support.annotation.Nullable; import android.util.Log; import android.util.SparseArray; import com.google.android.exoplayer2.C; @@ -114,6 +115,142 @@ public final class DashMediaSource implements MediaSource { private int firstPeriodId; + /** + * Builder for {@link DashMediaSource}. Each builder instance can only be used once. + */ + public static final class Builder { + + private final DashManifest manifest; + private final Uri manifestUri; + private final DataSource.Factory manifestDataSourceFactory; + private final DashChunkSource.Factory chunkSourceFactory; + + private ParsingLoadable.Parser manifestParser; + private AdaptiveMediaSourceEventListener eventListener; + private Handler eventHandler; + + private int minLoadableRetryCount; + private long livePresentationDelayMs; + private boolean isBuildCalled; + + /** + * Creates a {@link Builder} for a {@link DashMediaSource} with a side-loaded manifest. + * + * @param manifest The manifest. {@link DashManifest#dynamic} must be false. + * @param chunkSourceFactory A factory for {@link DashChunkSource} instances. + * @return A new builder. + */ + public static Builder forSideLoadedManifest(DashManifest manifest, + DashChunkSource.Factory chunkSourceFactory) { + return new Builder(manifest, null, null, chunkSourceFactory); + } + + /** + * Creates a {@link Builder} for a {@link DashMediaSource} with a loadable manifest Uri. + * + * @param manifestUri The manifest {@link Uri}. + * @param manifestDataSourceFactory A factory for {@link DataSource} instances that will be used + * to load (and refresh) the manifest. + * @param chunkSourceFactory A factory for {@link DashChunkSource} instances. + * @return A new builder. + */ + public static Builder forManifestUri(Uri manifestUri, + DataSource.Factory manifestDataSourceFactory, DashChunkSource.Factory chunkSourceFactory) { + return new Builder(null, manifestUri, manifestDataSourceFactory, chunkSourceFactory); + } + + private Builder(@Nullable DashManifest manifest, @Nullable Uri manifestUri, + @Nullable DataSource.Factory manifestDataSourceFactory, + DashChunkSource.Factory chunkSourceFactory) { + this.manifest = manifest; + this.manifestUri = manifestUri; + this.manifestDataSourceFactory = manifestDataSourceFactory; + this.chunkSourceFactory = chunkSourceFactory; + + minLoadableRetryCount = DEFAULT_MIN_LOADABLE_RETRY_COUNT; + livePresentationDelayMs = DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS; + } + + /** + * Sets the minimum number of times to retry if a loading error occurs. The default value is + * {@link #DEFAULT_MIN_LOADABLE_RETRY_COUNT}. + * + * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. + * @return This builder. + */ + public Builder setMinLoadableRetryCount(int minLoadableRetryCount) { + this.minLoadableRetryCount = minLoadableRetryCount; + return this; + } + + /** + * Sets the duration in milliseconds by which the default start position should precede the end + * of the live window for live playbacks. The default value is + * {@link #DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS}. + * + * @param livePresentationDelayMs For live playbacks, the duration in milliseconds by which the + * default start position should precede the end of the live window. Use + * {@link #DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS} to use the value specified by + * the manifest, if present. + * @return This builder. + */ + public Builder setLivePresentationDelayMs(long livePresentationDelayMs) { + this.livePresentationDelayMs = livePresentationDelayMs; + return this; + } + + /** + * Sets the listener to respond to adaptive {@link MediaSource} events and the handler to + * deliver these events. + * + * @param eventHandler A handler for events. + * @param eventListener A listener of events. + * @return This builder. + */ + public Builder setEventListener(Handler eventHandler, + AdaptiveMediaSourceEventListener eventListener) { + this.eventHandler = eventHandler; + this.eventListener = eventListener; + return this; + } + + /** + * Sets the manifest parser to parse loaded manifest data. The default is + * {@link DashManifestParser}, or {@code null} if the manifest is sideloaded. + * + * @param manifestParser A parser for loaded manifest data. + * @return This builder. + */ + public Builder setManifestParser( + ParsingLoadable.Parser manifestParser) { + this.manifestParser = manifestParser; + return this; + } + + + /** + * Builds a new {@link DashMediaSource} using the current parameters. + *

          + * After this call, the builder should not be re-used. + * + * @return The newly built {@link DashMediaSource}. + */ + public DashMediaSource build() { + Assertions.checkArgument(manifest == null || !manifest.dynamic); + Assertions.checkArgument((eventListener == null) == (eventHandler == null)); + Assertions.checkState(!isBuildCalled); + isBuildCalled = true; + boolean loadableManifestUri = manifestUri != null; + if (loadableManifestUri && manifestParser == null) { + manifestParser = new DashManifestParser(); + } + return new DashMediaSource(manifest, manifestUri, manifestDataSourceFactory, manifestParser, + chunkSourceFactory, minLoadableRetryCount, livePresentationDelayMs, eventHandler, + eventListener); + } + + } + /** * Constructs an instance to play a given {@link DashManifest}, which must be static. * @@ -121,7 +258,9 @@ public final class DashMediaSource implements MediaSource { * @param chunkSourceFactory A factory for {@link DashChunkSource} instances. * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. + * @deprecated Use {@link Builder} instead. */ + @Deprecated public DashMediaSource(DashManifest manifest, DashChunkSource.Factory chunkSourceFactory, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { this(manifest, chunkSourceFactory, DEFAULT_MIN_LOADABLE_RETRY_COUNT, eventHandler, @@ -136,7 +275,9 @@ public final class DashMediaSource implements MediaSource { * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. + * @deprecated Use {@link Builder} instead. */ + @Deprecated public DashMediaSource(DashManifest manifest, DashChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { @@ -154,7 +295,9 @@ public final class DashMediaSource implements MediaSource { * @param chunkSourceFactory A factory for {@link DashChunkSource} instances. * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. + * @deprecated Use {@link Builder} instead. */ + @Deprecated public DashMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, DashChunkSource.Factory chunkSourceFactory, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { @@ -178,7 +321,9 @@ public final class DashMediaSource implements MediaSource { * the manifest, if present. * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. + * @deprecated Use {@link Builder} instead. */ + @Deprecated public DashMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, DashChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, long livePresentationDelayMs, Handler eventHandler, @@ -203,7 +348,9 @@ public final class DashMediaSource implements MediaSource { * the manifest, if present. * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. + * @deprecated Use {@link Builder} instead. */ + @Deprecated public DashMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, ParsingLoadable.Parser manifestParser, DashChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java index 85cefbc2f6..215d8a0518 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java @@ -316,8 +316,11 @@ public final class DashTestRunner { Uri manifestUri = Uri.parse(manifestUrl); DefaultDashChunkSource.Factory chunkSourceFactory = new DefaultDashChunkSource.Factory( mediaDataSourceFactory); - return new DashMediaSource(manifestUri, manifestDataSourceFactory, chunkSourceFactory, - MIN_LOADABLE_RETRY_COUNT, 0 /* livePresentationDelayMs */, null, null); + return DashMediaSource.Builder + .forManifestUri(manifestUri, manifestDataSourceFactory, chunkSourceFactory) + .setMinLoadableRetryCount(MIN_LOADABLE_RETRY_COUNT) + .setLivePresentationDelayMs(0) + .build(); } @Override From 877c89a0e1f40b08c624ab5b0c2cb1bba3836846 Mon Sep 17 00:00:00 2001 From: arnaudberry Date: Mon, 13 Nov 2017 10:49:09 -0800 Subject: [PATCH 0860/2472] Make it possible to extend DashManifestParser to parse revision-id. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175554723 --- .../source/dash/manifest/DashManifestParser.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/library/dash/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 index 7ffb429784..137e29c5ab 100644 --- a/library/dash/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 @@ -488,7 +488,7 @@ public class DashManifestParser extends DefaultHandler segmentBase = segmentBase != null ? segmentBase : new SingleSegmentBase(); return new RepresentationInfo(format, baseUrl, segmentBase, drmSchemeType, drmSchemeDatas, - inbandEventStreams); + inbandEventStreams, Representation.REVISION_ID_DEFAULT); } protected Format buildFormat(String id, String containerMimeType, int width, int height, @@ -535,7 +535,7 @@ public class DashManifestParser extends DefaultHandler } ArrayList inbandEventStreams = representationInfo.inbandEventStreams; inbandEventStreams.addAll(extraInbandEventStreams); - return Representation.newInstance(contentId, Representation.REVISION_ID_DEFAULT, format, + return Representation.newInstance(contentId, representationInfo.revisionId, format, representationInfo.baseUrl, representationInfo.segmentBase, inbandEventStreams); } @@ -986,7 +986,8 @@ public class DashManifestParser extends DefaultHandler } } - private static final class RepresentationInfo { + /** A parsed Representation element. */ + protected static final class RepresentationInfo { public final Format format; public final String baseUrl; @@ -994,16 +995,18 @@ public class DashManifestParser extends DefaultHandler public final String drmSchemeType; public final ArrayList drmSchemeDatas; public final ArrayList inbandEventStreams; + public final long revisionId; public RepresentationInfo(Format format, String baseUrl, SegmentBase segmentBase, String drmSchemeType, ArrayList drmSchemeDatas, - ArrayList inbandEventStreams) { + ArrayList inbandEventStreams, long revisionId) { this.format = format; this.baseUrl = baseUrl; this.segmentBase = segmentBase; this.drmSchemeType = drmSchemeType; this.drmSchemeDatas = drmSchemeDatas; this.inbandEventStreams = inbandEventStreams; + this.revisionId = revisionId; } } From 5bf4c249a28b13acb06f8cd27db5fbe497237cc8 Mon Sep 17 00:00:00 2001 From: hoangtc Date: Mon, 13 Nov 2017 11:10:54 -0800 Subject: [PATCH 0861/2472] Notify TrackSelection when it's enabled and disabled. Add onEnable() and onDisable() call-backs to TrackSelection. This allows TrackSelection to perform interesting operations (like subscribe to NetworkStatus) and clean up after itself. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175558485 --- .../android/exoplayer2/ExoPlayerTest.java | 141 ++++++++++++++++++ .../exoplayer2/ExoPlayerImplInternal.java | 33 +++- .../trackselection/BaseTrackSelection.java | 10 ++ .../trackselection/TrackSelection.java | 20 ++- .../testutil/FakeTrackSelection.java | 132 ++++++++++++++++ .../testutil/FakeTrackSelector.java | 86 +++++++++++ 6 files changed, 420 insertions(+), 2 deletions(-) create mode 100644 testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelection.java create mode 100644 testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelector.java diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index 56d5f05d00..0edd19bc09 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -28,6 +28,8 @@ import com.google.android.exoplayer2.testutil.FakeRenderer; import com.google.android.exoplayer2.testutil.FakeShuffleOrder; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; +import com.google.android.exoplayer2.testutil.FakeTrackSelection; +import com.google.android.exoplayer2.testutil.FakeTrackSelector; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; @@ -311,4 +313,143 @@ public final class ExoPlayerTest extends TestCase { assertEquals(Player.STATE_BUFFERING, (int) playbackStatesWhenSeekProcessed.get(2)); } + public void testAllActivatedTrackSelectionAreReleasedForSinglePeriod() throws Exception { + Timeline timeline = + new FakeTimeline(new TimelineWindowDefinition(false, false, /* durationUs= */ 500_000)); + MediaSource mediaSource = + new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT); + FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT); + FakeRenderer audioRenderer = new FakeRenderer(Builder.AUDIO_FORMAT); + FakeTrackSelector trackSelector = new FakeTrackSelector(); + + new ExoPlayerTestRunner.Builder() + .setMediaSource(mediaSource) + .setRenderers(videoRenderer, audioRenderer) + .setTrackSelector(trackSelector) + .build().start().blockUntilEnded(TIMEOUT_MS); + + List createdTrackSelections = trackSelector.getSelectedTrackSelections(); + int numSelectionsEnabled = 0; + // Assert that all tracks selection are disabled at the end of the playback. + for (FakeTrackSelection trackSelection : createdTrackSelections) { + assertFalse(trackSelection.isEnabled); + numSelectionsEnabled += trackSelection.enableCount; + } + // There are 2 renderers, and track selections are made once (1 period). + // Track selections are not reused, so there are 2 track selections made. + assertEquals(2, createdTrackSelections.size()); + // There should be 2 track selections enabled in total. + assertEquals(2, numSelectionsEnabled); + } + + public void testAllActivatedTrackSelectionAreReleasedForMultiPeriods() throws Exception { + Timeline timeline = + new FakeTimeline(new TimelineWindowDefinition(false, false, /* durationUs= */ 500_000), + new TimelineWindowDefinition(false, false, /* durationUs= */ 500_000)); + MediaSource mediaSource = + new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT); + FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT); + FakeRenderer audioRenderer = new FakeRenderer(Builder.AUDIO_FORMAT); + FakeTrackSelector trackSelector = new FakeTrackSelector(); + + new ExoPlayerTestRunner.Builder() + .setMediaSource(mediaSource) + .setRenderers(videoRenderer, audioRenderer) + .setTrackSelector(trackSelector) + .build().start().blockUntilEnded(TIMEOUT_MS); + + List createdTrackSelections = trackSelector.getSelectedTrackSelections(); + int numSelectionsEnabled = 0; + // Assert that all tracks selection are disabled at the end of the playback. + for (FakeTrackSelection trackSelection : createdTrackSelections) { + assertFalse(trackSelection.isEnabled); + numSelectionsEnabled += trackSelection.enableCount; + } + // There are 2 renderers, and track selections are made twice (2 periods). + // Track selections are not reused, so there are 4 track selections made. + assertEquals(4, createdTrackSelections.size()); + // There should be 4 track selections enabled in total. + assertEquals(4, numSelectionsEnabled); + } + + public void testAllActivatedTrackSelectionAreReleasedWhenTrackSelectionsAreRemade() + throws Exception { + Timeline timeline = + new FakeTimeline(new TimelineWindowDefinition(false, false, /* durationUs= */ 500_000)); + MediaSource mediaSource = + new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT); + FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT); + FakeRenderer audioRenderer = new FakeRenderer(Builder.AUDIO_FORMAT); + final FakeTrackSelector trackSelector = new FakeTrackSelector(); + ActionSchedule disableTrackAction = new ActionSchedule.Builder("testChangeTrackSelection") + .waitForPlaybackState(Player.STATE_READY) + .executeRunnable(new Runnable() { + @Override + public void run() { + trackSelector.setRendererDisabled(0, true); + } + }).build(); + + new ExoPlayerTestRunner.Builder() + .setMediaSource(mediaSource) + .setRenderers(videoRenderer, audioRenderer) + .setTrackSelector(trackSelector) + .setActionSchedule(disableTrackAction) + .build().start().blockUntilEnded(TIMEOUT_MS); + + List createdTrackSelections = trackSelector.getSelectedTrackSelections(); + int numSelectionsEnabled = 0; + // Assert that all tracks selection are disabled at the end of the playback. + for (FakeTrackSelection trackSelection : createdTrackSelections) { + assertFalse(trackSelection.isEnabled); + numSelectionsEnabled += trackSelection.enableCount; + } + // There are 2 renderers, and track selections are made twice. + // Track selections are not reused, so there are 4 track selections made. + assertEquals(4, createdTrackSelections.size()); + // Initially there are 2 track selections enabled. + // The second time one renderer is disabled, so only 1 track selection should be enabled. + assertEquals(3, numSelectionsEnabled); + } + + public void testAllActivatedTrackSelectionAreReleasedWhenTrackSelectionsAreUsed() + throws Exception { + Timeline timeline = + new FakeTimeline(new TimelineWindowDefinition(false, false, /* durationUs= */ 500_000)); + MediaSource mediaSource = + new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT); + FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT); + FakeRenderer audioRenderer = new FakeRenderer(Builder.AUDIO_FORMAT); + final FakeTrackSelector trackSelector = new FakeTrackSelector(/* reuse track selection */ true); + ActionSchedule disableTrackAction = new ActionSchedule.Builder("testReuseTrackSelection") + .waitForPlaybackState(Player.STATE_READY) + .executeRunnable(new Runnable() { + @Override + public void run() { + trackSelector.setRendererDisabled(0, true); + } + }).build(); + + new ExoPlayerTestRunner.Builder() + .setMediaSource(mediaSource) + .setRenderers(videoRenderer, audioRenderer) + .setTrackSelector(trackSelector) + .setActionSchedule(disableTrackAction) + .build().start().blockUntilEnded(TIMEOUT_MS); + + List createdTrackSelections = trackSelector.getSelectedTrackSelections(); + int numSelectionsEnabled = 0; + // Assert that all tracks selection are disabled at the end of the playback. + for (FakeTrackSelection trackSelection : createdTrackSelections) { + assertFalse(trackSelection.isEnabled); + numSelectionsEnabled += trackSelection.enableCount; + } + // There are 2 renderers, and track selections are made twice. + // TrackSelections are reused, so there are only 2 track selections made for 2 renderers. + assertEquals(2, createdTrackSelections.size()); + // Initially there are 2 track selections enabled. + // The second time one renderer is disabled, so only 1 track selection should be enabled. + assertEquals(3, numSelectionsEnabled); + } + } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 4d1767b64c..33889a2b57 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -1666,11 +1666,11 @@ import java.io.IOException; // Undo the effect of previous call to associate no-sample renderers with empty tracks // so the mediaPeriod receives back whatever it sent us before. disassociateNoSampleRenderersWithEmptySampleStream(sampleStreams); + updatePeriodTrackSelectorResult(trackSelectorResult); // Disable streams on the period and get new streams for updated/newly-enabled tracks. positionUs = mediaPeriod.selectTracks(trackSelections.getAll(), mayRetainStreamFlags, sampleStreams, streamResetFlags, positionUs); associateNoSampleRenderersWithEmptySampleStream(sampleStreams); - periodTrackSelectorResult = trackSelectorResult; // Update whether we have enabled tracks and sanity check the expected streams are non-null. hasEnabledTracks = false; @@ -1692,6 +1692,7 @@ import java.io.IOException; } public void release() { + updatePeriodTrackSelectorResult(null); try { if (info.endPositionUs != C.TIME_END_OF_SOURCE) { mediaSource.releasePeriod(((ClippingMediaPeriod) mediaPeriod).mediaPeriod); @@ -1704,6 +1705,36 @@ import java.io.IOException; } } + private void updatePeriodTrackSelectorResult(TrackSelectorResult trackSelectorResult) { + if (periodTrackSelectorResult != null) { + disableTrackSelectionsInResult(periodTrackSelectorResult); + } + periodTrackSelectorResult = trackSelectorResult; + if (periodTrackSelectorResult != null) { + enableTrackSelectionsInResult(periodTrackSelectorResult); + } + } + + private void enableTrackSelectionsInResult(TrackSelectorResult trackSelectorResult) { + for (int i = 0; i < trackSelectorResult.renderersEnabled.length; i++) { + boolean rendererEnabled = trackSelectorResult.renderersEnabled[i]; + TrackSelection trackSelection = trackSelectorResult.selections.get(i); + if (rendererEnabled && trackSelection != null) { + trackSelection.enable(); + } + } + } + + private void disableTrackSelectionsInResult(TrackSelectorResult trackSelectorResult) { + for (int i = 0; i < trackSelectorResult.renderersEnabled.length; i++) { + boolean rendererEnabled = trackSelectorResult.renderersEnabled[i]; + TrackSelection trackSelection = trackSelectorResult.selections.get(i); + if (rendererEnabled && trackSelection != null) { + trackSelection.disable(); + } + } + } + /** * For each renderer of type {@link C#TRACK_TYPE_NONE}, we will remove the dummy * {@link EmptySampleStream} that was associated with it. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java index 054ee7973f..6bc6afb88b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BaseTrackSelection.java @@ -78,6 +78,16 @@ public abstract class BaseTrackSelection implements TrackSelection { blacklistUntilTimes = new long[length]; } + @Override + public void enable() { + // Do nothing. + } + + @Override + public void disable() { + // Do nothing. + } + @Override public final TrackGroup getTrackGroup() { return group; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java index ad02b6c775..027b2abde9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelection.java @@ -47,6 +47,20 @@ public interface TrackSelection { } + /** + * Enables the track selection. + *

          + * This method may not be called when the track selection is already enabled. + */ + void enable(); + + /** + * Disables this track selection. + *

          + * This method may only be called when the track selection is already enabled. + */ + void disable(); + /** * Returns the {@link TrackGroup} to which the selected tracks belong. */ @@ -124,6 +138,8 @@ public interface TrackSelection { /** * Updates the selected track. + *

          + * This method may only be called when the selection is enabled. * * @param playbackPositionUs The current playback position in microseconds. If playback of the * period to which this track selection belongs has not yet started, the value will be the @@ -150,7 +166,7 @@ public interface TrackSelection { * An example of a case where a smaller value may be returned is if network conditions have * improved dramatically, allowing chunks to be discarded and re-buffered in a track of * significantly higher quality. Discarding chunks may allow faster switching to a higher quality - * track in this case. + * track in this case. This method may only be called when the selection is enabled. * * @param playbackPositionUs The current playback position in microseconds. If playback of the * period to which this track selection belongs has not yet started, the value will be the @@ -167,6 +183,8 @@ public interface TrackSelection { * period of time. Blacklisting will fail if all other tracks are currently blacklisted. If * blacklisting the currently selected track, note that it will remain selected until the next * call to {@link #updateSelectedTrack(long, long, long)}. + *

          + * This method may only be called when the selection is enabled. * * @param index The index of the track in the selection. * @param blacklistDurationMs The duration of time for which the track should be blacklisted, in diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelection.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelection.java new file mode 100644 index 0000000000..20346a0355 --- /dev/null +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelection.java @@ -0,0 +1,132 @@ +/* + * 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.testutil; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.source.TrackGroup; +import com.google.android.exoplayer2.source.chunk.MediaChunk; +import com.google.android.exoplayer2.trackselection.TrackSelection; +import java.util.List; +import junit.framework.Assert; + +/** + * A fake {@link TrackSelection} that only returns 1 fixed track, and allows querying the number + * of calls to its methods. + */ +public final class FakeTrackSelection implements TrackSelection { + + private final TrackGroup rendererTrackGroup; + + public int enableCount; + public int releaseCount; + public boolean isEnabled; + + public FakeTrackSelection(TrackGroup rendererTrackGroup) { + this.rendererTrackGroup = rendererTrackGroup; + } + + @Override + public void enable() { + // assert that track selection is in disabled state before this call. + Assert.assertFalse(isEnabled); + enableCount++; + isEnabled = true; + } + + @Override + public void disable() { + // assert that track selection is in enabled state before this call. + Assert.assertTrue(isEnabled); + releaseCount++; + isEnabled = false; + } + + @Override + public TrackGroup getTrackGroup() { + return rendererTrackGroup; + } + + @Override + public int length() { + return rendererTrackGroup.length; + } + + @Override + public Format getFormat(int index) { + return rendererTrackGroup.getFormat(0); + } + + @Override + public int getIndexInTrackGroup(int index) { + return 0; + } + + @Override + public int indexOf(Format format) { + Assert.assertTrue(isEnabled); + return 0; + } + + @Override + public int indexOf(int indexInTrackGroup) { + return 0; + } + + @Override + public Format getSelectedFormat() { + return rendererTrackGroup.getFormat(0); + } + + @Override + public int getSelectedIndexInTrackGroup() { + return 0; + } + + @Override + public int getSelectedIndex() { + return 0; + } + + @Override + public int getSelectionReason() { + return C.SELECTION_REASON_UNKNOWN; + } + + @Override + public Object getSelectionData() { + return null; + } + + @Override + public void updateSelectedTrack(long playbackPositionUs, long bufferedDurationUs, + long availableDurationUs) { + Assert.assertTrue(isEnabled); + } + + @Override + public int evaluateQueueSize(long playbackPositionUs, List queue) { + Assert.assertTrue(isEnabled); + return 0; + } + + @Override + public boolean blacklist(int index, long blacklistDurationMs) { + Assert.assertTrue(isEnabled); + return false; + } + +} diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelector.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelector.java new file mode 100644 index 0000000000..da9a1a18ad --- /dev/null +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackSelector.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.testutil; + +import android.support.annotation.NonNull; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.RendererCapabilities; +import com.google.android.exoplayer2.source.TrackGroup; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.trackselection.MappingTrackSelector; +import com.google.android.exoplayer2.trackselection.TrackSelection; +import java.util.ArrayList; +import java.util.List; + +/** + * A fake {@link MappingTrackSelector} that returns {@link FakeTrackSelection}s. + */ +public class FakeTrackSelector extends MappingTrackSelector { + + private final List selectedTrackSelections = new ArrayList<>(); + private final boolean mayReuseTrackSelection; + + public FakeTrackSelector() { + this(false); + } + + /** + * @param mayReuseTrackSelection Whether this {@link FakeTrackSelector} will reuse + * {@link TrackSelection}s during track selection, when it finds previously-selected track + * selection using the same {@link TrackGroup}. + */ + public FakeTrackSelector(boolean mayReuseTrackSelection) { + this.mayReuseTrackSelection = mayReuseTrackSelection; + } + + @Override + protected TrackSelection[] selectTracks(RendererCapabilities[] rendererCapabilities, + TrackGroupArray[] rendererTrackGroupArrays, int[][][] rendererFormatSupports) + throws ExoPlaybackException { + List resultList = new ArrayList<>(); + for (TrackGroupArray trackGroupArray : rendererTrackGroupArrays) { + TrackGroup trackGroup = trackGroupArray.get(0); + FakeTrackSelection trackSelectionForRenderer = reuseOrCreateTrackSelection(trackGroup); + resultList.add(trackSelectionForRenderer); + } + return resultList.toArray(new TrackSelection[resultList.size()]); + } + + @NonNull + private FakeTrackSelection reuseOrCreateTrackSelection(TrackGroup trackGroup) { + FakeTrackSelection trackSelectionForRenderer = null; + if (mayReuseTrackSelection) { + for (FakeTrackSelection selectedTrackSelection : selectedTrackSelections) { + if (selectedTrackSelection.getTrackGroup().equals(trackGroup)) { + trackSelectionForRenderer = selectedTrackSelection; + } + } + } + if (trackSelectionForRenderer == null) { + trackSelectionForRenderer = new FakeTrackSelection(trackGroup); + selectedTrackSelections.add(trackSelectionForRenderer); + } + return trackSelectionForRenderer; + } + + /** + * Returns list of all {@link FakeTrackSelection}s that this track selector has made so far. + */ + public List getSelectedTrackSelections() { + return selectedTrackSelections; + } + +} From 79a9155438d92e459cd320b2f540e34cd029f325 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Fri, 3 Nov 2017 07:24:10 -0700 Subject: [PATCH 0862/2472] Add support for 608/708 captions in HLS+fMP4 This also allows exposing multiple CC channels to any fMP4 extractor client. Issue:#1661 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=174458725 --- .../mp4/FragmentedMp4ExtractorTest.java | 23 ++++---- .../extractor/mp4/FragmentedMp4Extractor.java | 54 ++++++++++++------- .../source/dash/DefaultDashChunkSource.java | 11 ++-- .../hls/DefaultHlsExtractorFactory.java | 3 +- 4 files changed, 56 insertions(+), 35 deletions(-) diff --git a/library/core/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 index c9364aa605..d24788f74a 100644 --- a/library/core/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 @@ -16,9 +16,13 @@ package com.google.android.exoplayer2.extractor.mp4; import android.test.InstrumentationTestCase; +import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.testutil.ExtractorAsserts; import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; +import com.google.android.exoplayer2.util.MimeTypes; +import java.util.Collections; +import java.util.List; /** * Unit test for {@link FragmentedMp4Extractor}. @@ -26,26 +30,23 @@ import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; public final class FragmentedMp4ExtractorTest extends InstrumentationTestCase { public void testSample() throws Exception { - ExtractorAsserts.assertBehavior(getExtractorFactory(), "mp4/sample_fragmented.mp4", - getInstrumentation()); + ExtractorAsserts.assertBehavior(getExtractorFactory(Collections.emptyList()), + "mp4/sample_fragmented.mp4", getInstrumentation()); } public void testSampleWithSeiPayloadParsing() throws Exception { // Enabling the CEA-608 track enables SEI payload parsing. - ExtractorAsserts.assertBehavior( - getExtractorFactory(FragmentedMp4Extractor.FLAG_ENABLE_CEA608_TRACK), - "mp4/sample_fragmented_sei.mp4", getInstrumentation()); + ExtractorFactory extractorFactory = getExtractorFactory(Collections.singletonList( + Format.createTextSampleFormat(null, MimeTypes.APPLICATION_CEA608, 0, null))); + ExtractorAsserts.assertBehavior(extractorFactory, "mp4/sample_fragmented_sei.mp4", + getInstrumentation()); } - private static ExtractorFactory getExtractorFactory() { - return getExtractorFactory(0); - } - - private static ExtractorFactory getExtractorFactory(final int flags) { + private static ExtractorFactory getExtractorFactory(final List closedCaptionFormats) { return new ExtractorFactory() { @Override public Extractor create() { - return new FragmentedMp4Extractor(flags, null); + return new FragmentedMp4Extractor(0, null, null, null, closedCaptionFormats); } }; } diff --git a/library/core/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 index 867e4501fa..e86157dd92 100644 --- a/library/core/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 @@ -46,6 +46,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.Stack; @@ -73,8 +74,8 @@ public final class FragmentedMp4Extractor implements Extractor { */ @Retention(RetentionPolicy.SOURCE) @IntDef(flag = true, value = {FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME, - FLAG_WORKAROUND_IGNORE_TFDT_BOX, FLAG_ENABLE_EMSG_TRACK, FLAG_ENABLE_CEA608_TRACK, - FLAG_SIDELOADED, FLAG_WORKAROUND_IGNORE_EDIT_LISTS}) + FLAG_WORKAROUND_IGNORE_TFDT_BOX, FLAG_ENABLE_EMSG_TRACK, FLAG_SIDELOADED, + FLAG_WORKAROUND_IGNORE_EDIT_LISTS}) public @interface Flags {} /** * Flag to work around an issue in some video streams where every frame is marked as a sync frame. @@ -93,20 +94,15 @@ public final class FragmentedMp4Extractor implements Extractor { * messages in the stream will be delivered as samples to this track. */ public static final int FLAG_ENABLE_EMSG_TRACK = 4; - /** - * Flag to indicate that the extractor should output a CEA-608 text track. Any CEA-608 messages - * contained within SEI NAL units in the stream will be delivered as samples to this track. - */ - public static final int FLAG_ENABLE_CEA608_TRACK = 8; /** * Flag to indicate that the {@link Track} was sideloaded, instead of being declared by the MP4 * container. */ - private static final int FLAG_SIDELOADED = 16; + private static final int FLAG_SIDELOADED = 8; /** * Flag to ignore any edit lists in the stream. */ - public static final int FLAG_WORKAROUND_IGNORE_EDIT_LISTS = 32; + public static final int FLAG_WORKAROUND_IGNORE_EDIT_LISTS = 16; private static final String TAG = "FragmentedMp4Extractor"; private static final int SAMPLE_GROUP_TYPE_seig = Util.getIntegerCodeForString("seig"); @@ -124,7 +120,8 @@ public final class FragmentedMp4Extractor implements Extractor { @Flags private final int flags; private final Track sideloadedTrack; - // Manifest DRM data. + // Sideloaded data. + private final List closedCaptionFormats; private final DrmInitData sideloadedDrmInitData; // Track-linked data bundle, accessible as a whole through trackID. @@ -193,15 +190,33 @@ public final class FragmentedMp4Extractor implements Extractor { * @param flags Flags that control the extractor's behavior. * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed. * @param sideloadedTrack Sideloaded track information, in the case that the extractor - * will not receive a moov box in the input data. - * @param sideloadedDrmInitData The {@link DrmInitData} to use for encrypted tracks. + * will not receive a moov box in the input data. Null if a moov box is expected. + * @param sideloadedDrmInitData The {@link DrmInitData} to use for encrypted tracks. If null, the + * pssh boxes (if present) will be used. */ public FragmentedMp4Extractor(@Flags int flags, TimestampAdjuster timestampAdjuster, Track sideloadedTrack, DrmInitData sideloadedDrmInitData) { + this(flags, timestampAdjuster, sideloadedTrack, sideloadedDrmInitData, + Collections.emptyList()); + } + + /** + * @param flags Flags that control the extractor's behavior. + * @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed. + * @param sideloadedTrack Sideloaded track information, in the case that the extractor + * will not receive a moov box in the input data. Null if a moov box is expected. + * @param sideloadedDrmInitData The {@link DrmInitData} to use for encrypted tracks. If null, the + * pssh boxes (if present) will be used. + * @param closedCaptionFormats For tracks that contain SEI messages, the formats of the closed + * caption channels to expose. + */ + public FragmentedMp4Extractor(@Flags int flags, TimestampAdjuster timestampAdjuster, + Track sideloadedTrack, DrmInitData sideloadedDrmInitData, List closedCaptionFormats) { this.flags = flags | (sideloadedTrack != null ? FLAG_SIDELOADED : 0); this.timestampAdjuster = timestampAdjuster; this.sideloadedTrack = sideloadedTrack; this.sideloadedDrmInitData = sideloadedDrmInitData; + this.closedCaptionFormats = Collections.unmodifiableList(closedCaptionFormats); atomHeader = new ParsableByteArray(Atom.LONG_HEADER_SIZE); nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE); nalPrefix = new ParsableByteArray(5); @@ -483,12 +498,13 @@ public final class FragmentedMp4Extractor implements Extractor { eventMessageTrackOutput.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_EMSG, Format.OFFSET_SAMPLE_RELATIVE)); } - if ((flags & FLAG_ENABLE_CEA608_TRACK) != 0 && cea608TrackOutputs == null) { - TrackOutput cea608TrackOutput = extractorOutput.track(trackBundles.size() + 1, - C.TRACK_TYPE_TEXT); - cea608TrackOutput.format(Format.createTextSampleFormat(null, MimeTypes.APPLICATION_CEA608, 0, - null)); - cea608TrackOutputs = new TrackOutput[] {cea608TrackOutput}; + if (cea608TrackOutputs == null) { + cea608TrackOutputs = new TrackOutput[closedCaptionFormats.size()]; + for (int i = 0; i < cea608TrackOutputs.length; i++) { + TrackOutput output = extractorOutput.track(trackBundles.size() + 1 + i, C.TRACK_TYPE_TEXT); + output.format(closedCaptionFormats.get(i)); + cea608TrackOutputs[i] = output; + } } } @@ -1123,7 +1139,7 @@ public final class FragmentedMp4Extractor implements Extractor { output.sampleData(nalStartCode, 4); // Write the NAL unit type byte. output.sampleData(nalPrefix, 1); - processSeiNalUnitPayload = cea608TrackOutputs != null + processSeiNalUnitPayload = cea608TrackOutputs.length > 0 && NalUnitUtil.isNalUnitSei(track.format.sampleMimeType, nalPrefixData[4]); sampleBytesWritten += 5; sampleSize += nalUnitLengthFieldLengthDiff; diff --git a/library/dash/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 index 1eac1b5616..66455b2f04 100644 --- a/library/dash/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 @@ -47,6 +47,7 @@ import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; import java.util.List; /** @@ -424,10 +425,12 @@ public class DefaultDashChunkSource implements DashChunkSource { if (enableEventMessageTrack) { flags |= FragmentedMp4Extractor.FLAG_ENABLE_EMSG_TRACK; } - if (enableCea608Track) { - flags |= FragmentedMp4Extractor.FLAG_ENABLE_CEA608_TRACK; - } - extractor = new FragmentedMp4Extractor(flags); + // TODO: Use caption format information from the manifest if available. + List closedCaptionFormats = enableCea608Track + ? Collections.singletonList( + Format.createTextSampleFormat(null, MimeTypes.APPLICATION_CEA608, 0, null)) + : Collections.emptyList(); + extractor = new FragmentedMp4Extractor(flags, null, null, null, closedCaptionFormats); } // Prefer drmInitData obtained from the manifest over drmInitData obtained from the stream, // as per DASH IF Interoperability Recommendations V3.0, 7.5.3. diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java index 957aefcdbc..c801520927 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java @@ -72,7 +72,8 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { extractor = previousExtractor; } else if (lastPathSegment.endsWith(MP4_FILE_EXTENSION) || lastPathSegment.startsWith(M4_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 4)) { - extractor = new FragmentedMp4Extractor(0, timestampAdjuster, null, drmInitData); + extractor = new FragmentedMp4Extractor(0, timestampAdjuster, null, drmInitData, + muxedCaptionFormats != null ? muxedCaptionFormats : Collections.emptyList()); } else { // For any other file extension, we assume TS format. @DefaultTsPayloadReaderFactory.Flags From 25dd8aa1f8c173563d114e5a55f9c08cbd60b840 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Mon, 13 Nov 2017 13:18:42 -0800 Subject: [PATCH 0863/2472] Fix cenc mode support and add support for the .mp4a extension. Also add encrypted HLS internal sample streams. Issue:#1661 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175577648 --- .../exoplayer2/source/hls/DefaultHlsExtractorFactory.java | 4 +++- .../exoplayer2/source/hls/playlist/HlsPlaylistParser.java | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java index c801520927..dc838c9506 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/DefaultHlsExtractorFactory.java @@ -43,6 +43,7 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { public static final String MP3_FILE_EXTENSION = ".mp3"; public static final String MP4_FILE_EXTENSION = ".mp4"; public static final String M4_FILE_EXTENSION_PREFIX = ".m4"; + public static final String MP4_FILE_EXTENSION_PREFIX = ".mp4"; public static final String VTT_FILE_EXTENSION = ".vtt"; public static final String WEBVTT_FILE_EXTENSION = ".webvtt"; @@ -71,7 +72,8 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { // Only reuse TS and fMP4 extractors. extractor = previousExtractor; } else if (lastPathSegment.endsWith(MP4_FILE_EXTENSION) - || lastPathSegment.startsWith(M4_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 4)) { + || lastPathSegment.startsWith(M4_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 4) + || lastPathSegment.startsWith(MP4_FILE_EXTENSION_PREFIX, lastPathSegment.length() - 5)) { extractor = new FragmentedMp4Extractor(0, timestampAdjuster, null, drmInitData, muxedCaptionFormats != null ? muxedCaptionFormats : Collections.emptyList()); } else { diff --git a/library/hls/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 index c63ded6275..90644125b1 100644 --- a/library/hls/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 @@ -107,7 +107,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser Date: Tue, 14 Nov 2017 03:48:47 -0800 Subject: [PATCH 0864/2472] Continue adding Builder to MediaSource. Add Builder pattern to SsMediaSource and mark existing constructors as deprecated. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175659618 --- .../exoplayer2/demo/PlayerActivity.java | 7 +- .../source/dash/DashMediaSource.java | 117 +++++++------- .../source/smoothstreaming/SsMediaSource.java | 143 ++++++++++++++++++ 3 files changed, 206 insertions(+), 61 deletions(-) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index 614626077a..65e1c0e083 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -362,8 +362,11 @@ public class PlayerActivity extends Activity implements OnClickListener, : Util.inferContentType("." + overrideExtension); switch (type) { case C.TYPE_SS: - return new SsMediaSource(uri, buildDataSourceFactory(false), - new DefaultSsChunkSource.Factory(mediaDataSourceFactory), mainHandler, eventLogger); + return SsMediaSource.Builder + .forManifestUri(uri, buildDataSourceFactory(false), + new DefaultSsChunkSource.Factory(mediaDataSourceFactory)) + .setEventListener(mainHandler, eventLogger) + .build(); case C.TYPE_DASH: return DashMediaSource.Builder .forManifestUri(uri, buildDataSourceFactory(false), diff --git a/library/dash/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 index 3d5a9c393d..54a5086d3b 100644 --- a/library/dash/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 @@ -58,63 +58,6 @@ public final class DashMediaSource implements MediaSource { ExoPlayerLibraryInfo.registerModule("goog.exo.dash"); } - /** - * The default minimum number of times to retry loading data prior to failing. - */ - public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 3; - /** - * A constant indicating that the presentation delay for live streams should be set to - * {@link DashManifest#suggestedPresentationDelay} if specified by the manifest, or - * {@link #DEFAULT_LIVE_PRESENTATION_DELAY_FIXED_MS} otherwise. The presentation delay is the - * duration by which the default start position precedes the end of the live window. - */ - public static final long DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS = -1; - /** - * A fixed default presentation delay for live streams. The presentation delay is the duration - * by which the default start position precedes the end of the live window. - */ - public static final long DEFAULT_LIVE_PRESENTATION_DELAY_FIXED_MS = 30000; - - /** - * The interval in milliseconds between invocations of - * {@link MediaSource.Listener#onSourceInfoRefreshed(MediaSource, Timeline, Object)} when the - * source's {@link Timeline} is changing dynamically (for example, for incomplete live streams). - */ - private static final int NOTIFY_MANIFEST_INTERVAL_MS = 5000; - /** - * The minimum default start position for live streams, relative to the start of the live window. - */ - private static final long MIN_LIVE_DEFAULT_START_POSITION_US = 5000000; - - private static final String TAG = "DashMediaSource"; - - private final boolean sideloadedManifest; - private final DataSource.Factory manifestDataSourceFactory; - private final DashChunkSource.Factory chunkSourceFactory; - private final int minLoadableRetryCount; - private final long livePresentationDelayMs; - private final EventDispatcher eventDispatcher; - private final ParsingLoadable.Parser manifestParser; - private final ManifestCallback manifestCallback; - private final Object manifestUriLock; - private final SparseArray periodsById; - private final Runnable refreshManifestRunnable; - private final Runnable simulateManifestRefreshRunnable; - - private Listener sourceListener; - private DataSource dataSource; - private Loader loader; - private LoaderErrorThrower loaderErrorThrower; - - private Uri manifestUri; - private long manifestLoadStartTimestamp; - private long manifestLoadEndTimestamp; - private DashManifest manifest; - private Handler handler; - private long elapsedRealtimeOffsetMs; - - private int firstPeriodId; - /** * Builder for {@link DashMediaSource}. Each builder instance can only be used once. */ @@ -142,6 +85,7 @@ public final class DashMediaSource implements MediaSource { */ public static Builder forSideLoadedManifest(DashManifest manifest, DashChunkSource.Factory chunkSourceFactory) { + Assertions.checkArgument(!manifest.dynamic); return new Builder(manifest, null, null, chunkSourceFactory); } @@ -227,7 +171,6 @@ public final class DashMediaSource implements MediaSource { return this; } - /** * Builds a new {@link DashMediaSource} using the current parameters. *

          @@ -236,7 +179,6 @@ public final class DashMediaSource implements MediaSource { * @return The newly built {@link DashMediaSource}. */ public DashMediaSource build() { - Assertions.checkArgument(manifest == null || !manifest.dynamic); Assertions.checkArgument((eventListener == null) == (eventHandler == null)); Assertions.checkState(!isBuildCalled); isBuildCalled = true; @@ -251,6 +193,63 @@ public final class DashMediaSource implements MediaSource { } + /** + * The default minimum number of times to retry loading data prior to failing. + */ + public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 3; + /** + * A constant indicating that the presentation delay for live streams should be set to + * {@link DashManifest#suggestedPresentationDelay} if specified by the manifest, or + * {@link #DEFAULT_LIVE_PRESENTATION_DELAY_FIXED_MS} otherwise. The presentation delay is the + * duration by which the default start position precedes the end of the live window. + */ + public static final long DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS = -1; + /** + * A fixed default presentation delay for live streams. The presentation delay is the duration + * by which the default start position precedes the end of the live window. + */ + public static final long DEFAULT_LIVE_PRESENTATION_DELAY_FIXED_MS = 30000; + + /** + * The interval in milliseconds between invocations of + * {@link MediaSource.Listener#onSourceInfoRefreshed(MediaSource, Timeline, Object)} when the + * source's {@link Timeline} is changing dynamically (for example, for incomplete live streams). + */ + private static final int NOTIFY_MANIFEST_INTERVAL_MS = 5000; + /** + * The minimum default start position for live streams, relative to the start of the live window. + */ + private static final long MIN_LIVE_DEFAULT_START_POSITION_US = 5000000; + + private static final String TAG = "DashMediaSource"; + + private final boolean sideloadedManifest; + private final DataSource.Factory manifestDataSourceFactory; + private final DashChunkSource.Factory chunkSourceFactory; + private final int minLoadableRetryCount; + private final long livePresentationDelayMs; + private final EventDispatcher eventDispatcher; + private final ParsingLoadable.Parser manifestParser; + private final ManifestCallback manifestCallback; + private final Object manifestUriLock; + private final SparseArray periodsById; + private final Runnable refreshManifestRunnable; + private final Runnable simulateManifestRefreshRunnable; + + private Listener sourceListener; + private DataSource dataSource; + private Loader loader; + private LoaderErrorThrower loaderErrorThrower; + + private Uri manifestUri; + private long manifestLoadStartTimestamp; + private long manifestLoadEndTimestamp; + private DashManifest manifest; + private Handler handler; + private long elapsedRealtimeOffsetMs; + + private int firstPeriodId; + /** * Constructs an instance to play a given {@link DashManifest}, which must be static. * diff --git a/library/smoothstreaming/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 index 548f787741..5a93847428 100644 --- a/library/smoothstreaming/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 @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.source.smoothstreaming; import android.net.Uri; import android.os.Handler; import android.os.SystemClock; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; @@ -51,6 +52,138 @@ public final class SsMediaSource implements MediaSource, ExoPlayerLibraryInfo.registerModule("goog.exo.smoothstreaming"); } + /** + * Builder for {@link SsMediaSource}. Each builder instance can only be used once. + */ + public static final class Builder { + + private final SsManifest manifest; + private final Uri manifestUri; + private final DataSource.Factory manifestDataSourceFactory; + private final SsChunkSource.Factory chunkSourceFactory; + + private ParsingLoadable.Parser manifestParser; + private AdaptiveMediaSourceEventListener eventListener; + private Handler eventHandler; + + private int minLoadableRetryCount; + private long livePresentationDelayMs; + private boolean isBuildCalled; + + /** + * Creates a {@link Builder} for a {@link SsMediaSource} with a side-loaded manifest. + * + * @param manifest The manifest. {@link SsManifest#isLive} must be false. + * @param chunkSourceFactory A factory for {@link SsChunkSource} instances. + * @return A new builder. + */ + public static Builder forSideLoadedManifest(SsManifest manifest, + SsChunkSource.Factory chunkSourceFactory) { + Assertions.checkArgument(!manifest.isLive); + return new Builder(manifest, null, null, chunkSourceFactory); + } + + /** + * Creates a {@link Builder} for a {@link SsMediaSource} with a loadable manifest Uri. + * + * @param manifestUri The manifest {@link Uri}. + * @param manifestDataSourceFactory A factory for {@link DataSource} instances that will be used + * to load (and refresh) the manifest. + * @param chunkSourceFactory A factory for {@link SsChunkSource} instances. + * @return A new builder. + */ + public static Builder forManifestUri(Uri manifestUri, + DataSource.Factory manifestDataSourceFactory, SsChunkSource.Factory chunkSourceFactory) { + return new Builder(null, manifestUri, manifestDataSourceFactory, chunkSourceFactory); + } + + private Builder(@Nullable SsManifest manifest, @Nullable Uri manifestUri, + @Nullable DataSource.Factory manifestDataSourceFactory, + SsChunkSource.Factory chunkSourceFactory) { + this.manifest = manifest; + this.manifestUri = manifestUri; + this.manifestDataSourceFactory = manifestDataSourceFactory; + this.chunkSourceFactory = chunkSourceFactory; + + minLoadableRetryCount = DEFAULT_MIN_LOADABLE_RETRY_COUNT; + livePresentationDelayMs = DEFAULT_LIVE_PRESENTATION_DELAY_MS; + } + + /** + * Sets the minimum number of times to retry if a loading error occurs. The default value is + * {@link #DEFAULT_MIN_LOADABLE_RETRY_COUNT}. + * + * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. + * @return This builder. + */ + public Builder setMinLoadableRetryCount(int minLoadableRetryCount) { + this.minLoadableRetryCount = minLoadableRetryCount; + return this; + } + + /** + * Sets the duration in milliseconds by which the default start position should precede the end + * of the live window for live playbacks. The default value is + * {@link #DEFAULT_LIVE_PRESENTATION_DELAY_MS}. + * + * @param livePresentationDelayMs For live playbacks, the duration in milliseconds by which the + * default start position should precede the end of the live window. + * @return This builder. + */ + public Builder setLivePresentationDelayMs(long livePresentationDelayMs) { + this.livePresentationDelayMs = livePresentationDelayMs; + return this; + } + + /** + * Sets the listener to respond to adaptive {@link MediaSource} events and the handler to + * deliver these events. + * + * @param eventHandler A handler for events. + * @param eventListener A listener of events. + * @return This builder. + */ + public Builder setEventListener(Handler eventHandler, + AdaptiveMediaSourceEventListener eventListener) { + this.eventHandler = eventHandler; + this.eventListener = eventListener; + return this; + } + + /** + * Sets the manifest parser to parse loaded manifest data. The default is an instance of + * {@link SsManifestParser}, or {@code null} if the manifest is sideloaded. + * + * @param manifestParser A parser for loaded manifest data. + * @return This builder. + */ + public Builder setManifestParser(ParsingLoadable.Parser manifestParser) { + this.manifestParser = manifestParser; + return this; + } + + /** + * Builds a new {@link SsMediaSource} using the current parameters. + *

          + * After this call, the builder should not be re-used. + * + * @return The newly built {@link SsMediaSource}. + */ + public SsMediaSource build() { + Assertions.checkArgument((eventListener == null) == (eventHandler == null)); + Assertions.checkState(!isBuildCalled); + isBuildCalled = true; + boolean loadableManifestUri = manifestUri != null; + if (loadableManifestUri && manifestParser == null) { + manifestParser = new SsManifestParser(); + } + return new SsMediaSource(manifest, manifestUri, manifestDataSourceFactory, manifestParser, + chunkSourceFactory, minLoadableRetryCount, livePresentationDelayMs, eventHandler, + eventListener); + } + + } + /** * The default minimum number of times to retry loading data prior to failing. */ @@ -96,7 +229,9 @@ public final class SsMediaSource implements MediaSource, * @param chunkSourceFactory A factory for {@link SsChunkSource} instances. * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. + * @deprecated Use {@link Builder} instead. */ + @Deprecated public SsMediaSource(SsManifest manifest, SsChunkSource.Factory chunkSourceFactory, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { this(manifest, chunkSourceFactory, DEFAULT_MIN_LOADABLE_RETRY_COUNT, @@ -111,7 +246,9 @@ public final class SsMediaSource implements MediaSource, * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. + * @deprecated Use {@link Builder} instead. */ + @Deprecated public SsMediaSource(SsManifest manifest, SsChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { @@ -129,7 +266,9 @@ public final class SsMediaSource implements MediaSource, * @param chunkSourceFactory A factory for {@link SsChunkSource} instances. * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. + * @deprecated Use {@link Builder} instead. */ + @Deprecated public SsMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, SsChunkSource.Factory chunkSourceFactory, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { @@ -151,7 +290,9 @@ public final class SsMediaSource implements MediaSource, * default start position should precede the end of the live window. * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. + * @deprecated Use {@link Builder} instead. */ + @Deprecated public SsMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, SsChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, long livePresentationDelayMs, Handler eventHandler, @@ -174,7 +315,9 @@ public final class SsMediaSource implements MediaSource, * default start position should precede the end of the live window. * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. + * @deprecated Use {@link Builder} instead. */ + @Deprecated public SsMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, ParsingLoadable.Parser manifestParser, SsChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, From 77b48691aa0f5fc5218bfc9cb3d6cff4c8b06179 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 14 Nov 2017 04:08:38 -0800 Subject: [PATCH 0865/2472] Suppress reference equality warning in EventLogger. We deliberately compare the track group returned by the track selection with the track group in the parameter to check if the track selection is referring to this particular track group. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175660909 --- .../java/com/google/android/exoplayer2/demo/EventLogger.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java index 27a5c68e28..9233b016f5 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java @@ -467,6 +467,9 @@ import java.util.Locale; } } + // Suppressing reference equality warning because the track group stored in the track selection + // must point to the exact track group object to be considered part of it. + @SuppressWarnings("ReferenceEquality") private static String getTrackStatusString(TrackSelection selection, TrackGroup group, int trackIndex) { return getTrackStatusString(selection != null && selection.getTrackGroup() == group From eea8cd169c76b52bcf303c137ceab60e6773c47f Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 14 Nov 2017 06:17:02 -0800 Subject: [PATCH 0866/2472] Replaced the duplicated EMPTY track group array with the one already defined in TrackGroupArray. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175670266 --- .../android/exoplayer2/source/TrackGroupArray.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroupArray.java b/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroupArray.java index 394cec891b..fb28da581c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroupArray.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/TrackGroupArray.java @@ -62,8 +62,11 @@ public final class TrackGroupArray { * @param group The group. * @return The index of the group, or {@link C#INDEX_UNSET} if no such group exists. */ + @SuppressWarnings("ReferenceEquality") public int indexOf(TrackGroup group) { for (int i = 0; i < length; i++) { + // Suppressed reference equality warning because this is looking for the index of a specific + // TrackGroup object, not the index of a potential equal TrackGroup. if (trackGroups[i] == group) { return i; } @@ -71,6 +74,13 @@ public final class TrackGroupArray { return C.INDEX_UNSET; } + /** + * Returns whether this track group array is empty. + */ + public boolean isEmpty() { + return length == 0; + } + @Override public int hashCode() { if (hashCode == 0) { From 76ba1890aa48202d46cbbeccc7b69286e272d263 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 14 Nov 2017 08:06:50 -0800 Subject: [PATCH 0867/2472] Add method to FakeMediaSource to trigger source info refresh. This allows to remove the LazyMediaSource used within DynamicConcatenatingMediaSourceTest and also allows to write test which simulates dynamic timeline or manifest updates. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175680371 --- .../DynamicConcatenatingMediaSourceTest.java | 77 ++++++++----------- .../exoplayer2/testutil/FakeMediaSource.java | 38 +++++++-- 2 files changed, 61 insertions(+), 54 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java index e506d0a4b3..e7b2a8d963 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java @@ -33,15 +33,12 @@ import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaPeriod.Callback; import com.google.android.exoplayer2.source.MediaSource.Listener; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; -import com.google.android.exoplayer2.testutil.FakeMediaPeriod; import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeShuffleOrder; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.testutil.TimelineAsserts; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; -import com.google.android.exoplayer2.upstream.Allocator; -import java.io.IOException; import java.util.Arrays; import junit.framework.TestCase; import org.mockito.Mockito; @@ -216,19 +213,26 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { public void testPlaylistWithLazyMediaSource() throws InterruptedException { timeline = null; - FakeMediaSource[] childSources = createMediaSources(2); - LazyMediaSource[] lazySources = new LazyMediaSource[4]; + + // Create some normal (immediately preparing) sources and some lazy sources whose timeline + // updates need to be triggered. + FakeMediaSource[] fastSources = createMediaSources(2); + FakeMediaSource[] lazySources = new FakeMediaSource[4]; for (int i = 0; i < 4; i++) { - lazySources[i] = new LazyMediaSource(); + lazySources[i] = new FakeMediaSource(null, null); } - //Add lazy sources before preparation + // Add lazy sources and normal sources before preparation. Also remove one lazy source again + // before preparation to check it doesn't throw or change the result. DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); mediaSource.addMediaSource(lazySources[0]); - mediaSource.addMediaSource(0, childSources[0]); + mediaSource.addMediaSource(0, fastSources[0]); mediaSource.removeMediaSource(1); mediaSource.addMediaSource(1, lazySources[1]); assertNull(timeline); + + // Prepare and assert that the timeline contains all information for normal sources while having + // placeholder information for lazy sources. prepareAndListenToTimelineUpdates(mediaSource); waitForTimelineUpdate(); assertNotNull(timeline); @@ -236,7 +240,9 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { TimelineAsserts.assertWindowIds(timeline, 111, null); TimelineAsserts.assertWindowIsDynamic(timeline, false, true); - lazySources[1].triggerTimelineUpdate(createFakeTimeline(8)); + // Trigger source info refresh for lazy source and check that the timeline now contains all + // information for all windows. + lazySources[1].setNewSourceInfo(createFakeTimeline(8), null); waitForTimelineUpdate(); TimelineAsserts.assertPeriodCounts(timeline, 1, 9); TimelineAsserts.assertWindowIds(timeline, 111, 999); @@ -244,10 +250,11 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { TimelineAsserts.assertAllPeriodsCanBeCreatedPreparedAndReleased(mediaSource, timeline, TIMEOUT_MS); - //Add lazy sources after preparation (and also try to prepare media period from lazy source). + // Add further lazy and normal sources after preparation. Also remove one lazy source again to + // check it doesn't throw or change the result. mediaSource.addMediaSource(1, lazySources[2]); waitForTimelineUpdate(); - mediaSource.addMediaSource(2, childSources[1]); + mediaSource.addMediaSource(2, fastSources[1]); waitForTimelineUpdate(); mediaSource.addMediaSource(0, lazySources[3]); waitForTimelineUpdate(); @@ -257,6 +264,8 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { TimelineAsserts.assertWindowIds(timeline, null, 111, 222, 999); TimelineAsserts.assertWindowIsDynamic(timeline, true, false, false, false); + // Create a period from an unprepared lazy media source and assert Callback.onPrepared is not + // called yet. MediaPeriod lazyPeriod = mediaSource.createPeriod(new MediaPeriodId(0), null); assertNotNull(lazyPeriod); final ConditionVariable lazyPeriodPrepared = new ConditionVariable(); @@ -269,11 +278,14 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { public void onContinueLoadingRequested(MediaPeriod source) {} }, 0); assertFalse(lazyPeriodPrepared.block(1)); + // Assert that a second period can also be created and released without problems. MediaPeriod secondLazyPeriod = mediaSource.createPeriod(new MediaPeriodId(0), null); assertNotNull(secondLazyPeriod); mediaSource.releasePeriod(secondLazyPeriod); - lazySources[3].triggerTimelineUpdate(createFakeTimeline(7)); + // Trigger source info refresh for lazy media source. Assert that now all information is + // available again and the previously created period now also finished preparing. + lazySources[3].setNewSourceInfo(createFakeTimeline(7), null); waitForTimelineUpdate(); TimelineAsserts.assertPeriodCounts(timeline, 8, 1, 2, 9); TimelineAsserts.assertWindowIds(timeline, 888, 111, 222, 999); @@ -281,9 +293,14 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { assertTrue(lazyPeriodPrepared.block(TIMEOUT_MS)); mediaSource.releasePeriod(lazyPeriod); + // Release media source and assert all normal and lazy media sources are fully released as well. mediaSource.releaseSource(); - childSources[0].assertReleased(); - childSources[1].assertReleased(); + for (FakeMediaSource fastSource : fastSources) { + fastSource.assertReleased(); + } + for (FakeMediaSource lazySource : lazySources) { + lazySource.assertReleased(); + } } public void testEmptyTimelineMediaSource() throws InterruptedException { @@ -662,38 +679,6 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { } - private static class LazyMediaSource implements MediaSource { - - private Listener listener; - - public void triggerTimelineUpdate(Timeline timeline) { - listener.onSourceInfoRefreshed(this, timeline, null); - } - - @Override - public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { - this.listener = listener; - } - - @Override - public void maybeThrowSourceInfoRefreshError() throws IOException { - } - - @Override - public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { - return new FakeMediaPeriod(TrackGroupArray.EMPTY); - } - - @Override - public void releasePeriod(MediaPeriod mediaPeriod) { - } - - @Override - public void releaseSource() { - } - - } - /** * Stub ExoPlayer which only accepts custom messages and runs them on a separate handler thread. */ diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java index 1f2524110a..f4c8435801 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.testutil; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Timeline; @@ -34,28 +35,34 @@ import junit.framework.Assert; */ public class FakeMediaSource implements MediaSource { - protected final Timeline timeline; private final Object manifest; private final TrackGroupArray trackGroupArray; private final ArrayList activeMediaPeriods; private final ArrayList createdMediaPeriods; + protected Timeline timeline; private boolean preparedSource; private boolean releasedSource; + private Listener listener; /** * Creates a {@link FakeMediaSource}. This media source creates {@link FakeMediaPeriod}s with a - * {@link TrackGroupArray} using the given {@link Format}s. + * {@link TrackGroupArray} using the given {@link Format}s. The provided {@link Timeline} may be + * null to prevent an immediate source info refresh message when preparing the media source. It + * can be manually set later using {@link #setNewSourceInfo(Timeline, Object)}. */ - public FakeMediaSource(Timeline timeline, Object manifest, Format... formats) { + public FakeMediaSource(@Nullable Timeline timeline, Object manifest, Format... formats) { this(timeline, manifest, buildTrackGroupArray(formats)); } /** * Creates a {@link FakeMediaSource}. This media source creates {@link FakeMediaPeriod}s with the - * given {@link TrackGroupArray}. + * given {@link TrackGroupArray}. The provided {@link Timeline} may be null to prevent an + * immediate source info refresh message when preparing the media source. It can be manually set + * later using {@link #setNewSourceInfo(Timeline, Object)}. */ - public FakeMediaSource(Timeline timeline, Object manifest, TrackGroupArray trackGroupArray) { + public FakeMediaSource(@Nullable Timeline timeline, Object manifest, + TrackGroupArray trackGroupArray) { this.timeline = timeline; this.manifest = manifest; this.activeMediaPeriods = new ArrayList<>(); @@ -67,7 +74,10 @@ public class FakeMediaSource implements MediaSource { public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { Assert.assertFalse(preparedSource); preparedSource = true; - listener.onSourceInfoRefreshed(this, timeline, manifest); + this.listener = listener; + if (timeline != null) { + listener.onSourceInfoRefreshed(this, timeline, manifest); + } } @Override @@ -77,9 +87,9 @@ public class FakeMediaSource implements MediaSource { @Override public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { - Assertions.checkIndex(id.periodIndex, 0, timeline.getPeriodCount()); Assert.assertTrue(preparedSource); Assert.assertFalse(releasedSource); + Assertions.checkIndex(id.periodIndex, 0, timeline.getPeriodCount()); FakeMediaPeriod mediaPeriod = createFakeMediaPeriod(id, trackGroupArray, allocator); activeMediaPeriods.add(mediaPeriod); createdMediaPeriods.add(id); @@ -103,11 +113,23 @@ public class FakeMediaSource implements MediaSource { releasedSource = true; } + /** + * Sets a new timeline and manifest. If the source is already prepared, this triggers a source + * info refresh message being sent to the listener. + */ + public void setNewSourceInfo(Timeline newTimeline, Object manifest) { + Assert.assertFalse(releasedSource); + this.timeline = newTimeline; + if (preparedSource) { + listener.onSourceInfoRefreshed(this, timeline, manifest); + } + } + /** * Assert that the source and all periods have been released. */ public void assertReleased() { - Assert.assertTrue(releasedSource); + Assert.assertTrue(releasedSource || !preparedSource); } /** From 4bb5cda5f1c48075675c45ba49ff29057ab19f8f Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 15 Nov 2017 02:19:48 -0800 Subject: [PATCH 0868/2472] Some test cleanup The purpose of this change isn't to fix anything. It's just to simplify things a little bit. There will be following CLs that make some changes to get things onto correct threads. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175800354 --- .../DynamicConcatenatingMediaSourceTest.java | 263 ++---------------- .../testutil/FakeSimpleExoPlayer.java | 47 +--- .../exoplayer2/testutil/OggTestData.java | 1 - .../exoplayer2/testutil/StubExoPlayer.java | 248 +++++++++++++++++ 4 files changed, 270 insertions(+), 289 deletions(-) create mode 100644 testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java index e7b2a8d963..96d11678c9 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java @@ -15,19 +15,14 @@ */ package com.google.android.exoplayer2.source; -import static org.mockito.Matchers.any; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; import android.os.ConditionVariable; import android.os.Handler; import android.os.HandlerThread; -import android.os.Looper; import android.os.Message; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; -import com.google.android.exoplayer2.ExoPlayer; -import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaPeriod.Callback; @@ -37,8 +32,8 @@ import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeShuffleOrder; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; +import com.google.android.exoplayer2.testutil.StubExoPlayer; import com.google.android.exoplayer2.testutil.TimelineAsserts; -import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import java.util.Arrays; import junit.framework.TestCase; import org.mockito.Mockito; @@ -456,7 +451,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { setUpDynamicMediaSourceOnHandlerThread(); final Runnable runnable = createCustomRunnable(); - sourceHandlerPair.handler.post(new Runnable() { + sourceHandlerPair.mainHandler.post(new Runnable() { @Override public void run() { sourceHandlerPair.mediaSource.addMediaSource(createFakeMediaSource(), runnable); @@ -470,7 +465,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { setUpDynamicMediaSourceOnHandlerThread(); final Runnable runnable = createCustomRunnable(); - sourceHandlerPair.handler.post(new Runnable() { + sourceHandlerPair.mainHandler.post(new Runnable() { @Override public void run() { sourceHandlerPair.mediaSource.addMediaSources(Arrays.asList( @@ -485,7 +480,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { setUpDynamicMediaSourceOnHandlerThread(); final Runnable runnable = createCustomRunnable(); - sourceHandlerPair.handler.post(new Runnable() { + sourceHandlerPair.mainHandler.post(new Runnable() { @Override public void run() { sourceHandlerPair.mediaSource.addMediaSource(/* index */ 0, createFakeMediaSource(), @@ -500,7 +495,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { setUpDynamicMediaSourceOnHandlerThread(); final Runnable runnable = createCustomRunnable(); - sourceHandlerPair.handler.post(new Runnable() { + sourceHandlerPair.mainHandler.post(new Runnable() { @Override public void run() { sourceHandlerPair.mediaSource.addMediaSources(/* index */ 0, Arrays.asList( @@ -514,7 +509,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { final DynamicConcatenatingMediaSourceAndHandler sourceHandlerPair = setUpDynamicMediaSourceOnHandlerThread(); final Runnable runnable = createCustomRunnable(); - sourceHandlerPair.handler.post(new Runnable() { + sourceHandlerPair.mainHandler.post(new Runnable() { @Override public void run() { sourceHandlerPair.mediaSource.addMediaSource(createFakeMediaSource()); @@ -522,7 +517,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { }); waitForTimelineUpdate(); - sourceHandlerPair.handler.post(new Runnable() { + sourceHandlerPair.mainHandler.post(new Runnable() { @Override public void run() { sourceHandlerPair.mediaSource.removeMediaSource(/* index */ 0, runnable); @@ -535,7 +530,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { final DynamicConcatenatingMediaSourceAndHandler sourceHandlerPair = setUpDynamicMediaSourceOnHandlerThread(); final Runnable runnable = createCustomRunnable(); - sourceHandlerPair.handler.post(new Runnable() { + sourceHandlerPair.mainHandler.post(new Runnable() { @Override public void run() { sourceHandlerPair.mediaSource.addMediaSources(Arrays.asList( @@ -544,7 +539,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { }); waitForTimelineUpdate(); - sourceHandlerPair.handler.post(new Runnable() { + sourceHandlerPair.mainHandler.post(new Runnable() { @Override public void run() { sourceHandlerPair.mediaSource.moveMediaSource(/* fromIndex */ 1, /* toIndex */ 0, @@ -585,24 +580,17 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { private DynamicConcatenatingMediaSourceAndHandler setUpDynamicMediaSourceOnHandlerThread() throws InterruptedException { + final DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); + prepareAndListenToTimelineUpdates(mediaSource); + waitForTimelineUpdate(); HandlerThread handlerThread = new HandlerThread("TestCustomCallbackExecutionThread"); handlerThread.start(); - Handler.Callback handlerCallback = Mockito.mock(Handler.Callback.class); - when(handlerCallback.handleMessage(any(Message.class))).thenReturn(false); - Handler handler = new Handler(handlerThread.getLooper(), handlerCallback); - final DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); - handler.post(new Runnable() { - @Override - public void run() { - prepareAndListenToTimelineUpdates(mediaSource); - } - }); - waitForTimelineUpdate(); + Handler handler = new Handler(handlerThread.getLooper()); return new DynamicConcatenatingMediaSourceAndHandler(mediaSource, handler); } private void prepareAndListenToTimelineUpdates(MediaSource mediaSource) { - mediaSource.prepareSource(new StubExoPlayer(), true, new Listener() { + mediaSource.prepareSource(new MessageHandlingExoPlayer(), true, new Listener() { @Override public void onSourceInfoRefreshed(MediaSource source, Timeline newTimeline, Object manifest) { timeline = newTimeline; @@ -669,244 +657,34 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { private static class DynamicConcatenatingMediaSourceAndHandler { public final DynamicConcatenatingMediaSource mediaSource; - public final Handler handler; + public final Handler mainHandler; public DynamicConcatenatingMediaSourceAndHandler(DynamicConcatenatingMediaSource mediaSource, - Handler handler) { + Handler mainHandler) { this.mediaSource = mediaSource; - this.handler = handler; + this.mainHandler = mainHandler; } } /** - * Stub ExoPlayer which only accepts custom messages and runs them on a separate handler thread. + * ExoPlayer that only accepts custom messages and runs them on a separate handler thread. */ - private static class StubExoPlayer implements ExoPlayer, Handler.Callback { + private static class MessageHandlingExoPlayer extends StubExoPlayer implements Handler.Callback { private final Handler handler; - public StubExoPlayer() { + public MessageHandlingExoPlayer() { HandlerThread handlerThread = new HandlerThread("StubExoPlayerThread"); handlerThread.start(); handler = new Handler(handlerThread.getLooper(), this); } - @Override - public Looper getPlaybackLooper() { - throw new UnsupportedOperationException(); - } - - @Override - public void addListener(Player.EventListener listener) { - throw new UnsupportedOperationException(); - } - - @Override - public void removeListener(Player.EventListener listener) { - throw new UnsupportedOperationException(); - } - - @Override - public int getPlaybackState() { - throw new UnsupportedOperationException(); - } - - @Override - public void prepare(MediaSource mediaSource) { - throw new UnsupportedOperationException(); - } - - @Override - public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) { - throw new UnsupportedOperationException(); - } - - @Override - public void setPlayWhenReady(boolean playWhenReady) { - throw new UnsupportedOperationException(); - } - - @Override - public boolean getPlayWhenReady() { - throw new UnsupportedOperationException(); - } - - @Override - public void setRepeatMode(@RepeatMode int repeatMode) { - throw new UnsupportedOperationException(); - } - - @Override - public int getRepeatMode() { - throw new UnsupportedOperationException(); - } - - @Override - public void setShuffleModeEnabled(boolean shuffleModeEnabled) { - throw new UnsupportedOperationException(); - } - - @Override - public boolean getShuffleModeEnabled() { - throw new UnsupportedOperationException(); - } - - @Override - public boolean isLoading() { - throw new UnsupportedOperationException(); - } - - @Override - public void seekToDefaultPosition() { - throw new UnsupportedOperationException(); - } - - @Override - public void seekToDefaultPosition(int windowIndex) { - throw new UnsupportedOperationException(); - } - - @Override - public void seekTo(long positionMs) { - throw new UnsupportedOperationException(); - } - - @Override - public void seekTo(int windowIndex, long positionMs) { - throw new UnsupportedOperationException(); - } - - @Override - public void setPlaybackParameters(PlaybackParameters playbackParameters) { - throw new UnsupportedOperationException(); - } - - @Override - public PlaybackParameters getPlaybackParameters() { - throw new UnsupportedOperationException(); - } - - @Override - public void stop() { - throw new UnsupportedOperationException(); - } - - @Override - public void release() { - throw new UnsupportedOperationException(); - } - @Override public void sendMessages(ExoPlayerMessage... messages) { handler.obtainMessage(0, messages).sendToTarget(); } - @Override - public void blockingSendMessages(ExoPlayerMessage... messages) { - throw new UnsupportedOperationException(); - } - - @Override - public int getRendererCount() { - throw new UnsupportedOperationException(); - } - - @Override - public int getRendererType(int index) { - throw new UnsupportedOperationException(); - } - - @Override - public TrackGroupArray getCurrentTrackGroups() { - throw new UnsupportedOperationException(); - } - - @Override - public TrackSelectionArray getCurrentTrackSelections() { - throw new UnsupportedOperationException(); - } - - @Override - public Object getCurrentManifest() { - throw new UnsupportedOperationException(); - } - - @Override - public Timeline getCurrentTimeline() { - throw new UnsupportedOperationException(); - } - - @Override - public int getCurrentPeriodIndex() { - throw new UnsupportedOperationException(); - } - - @Override - public int getCurrentWindowIndex() { - throw new UnsupportedOperationException(); - } - - @Override - public int getNextWindowIndex() { - throw new UnsupportedOperationException(); - } - - @Override - public int getPreviousWindowIndex() { - throw new UnsupportedOperationException(); - } - - @Override - public long getDuration() { - throw new UnsupportedOperationException(); - } - - @Override - public long getCurrentPosition() { - throw new UnsupportedOperationException(); - } - - @Override - public long getBufferedPosition() { - throw new UnsupportedOperationException(); - } - - @Override - public int getBufferedPercentage() { - throw new UnsupportedOperationException(); - } - - @Override - public boolean isCurrentWindowDynamic() { - throw new UnsupportedOperationException(); - } - - @Override - public boolean isCurrentWindowSeekable() { - throw new UnsupportedOperationException(); - } - - @Override - public boolean isPlayingAd() { - throw new UnsupportedOperationException(); - } - - @Override - public int getCurrentAdGroupIndex() { - throw new UnsupportedOperationException(); - } - - @Override - public int getCurrentAdIndexInAdGroup() { - throw new UnsupportedOperationException(); - } - - @Override - public long getContentPosition() { - throw new UnsupportedOperationException(); - } - @Override public boolean handleMessage(Message msg) { ExoPlayerMessage[] messages = (ExoPlayerMessage[]) msg.obj; @@ -919,6 +697,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { } return true; } + } } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java index 4d53a6c89d..4a5beb0501 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java @@ -69,7 +69,7 @@ public class FakeSimpleExoPlayer extends SimpleExoPlayer { return player; } - private static class FakeExoPlayer implements ExoPlayer, MediaSource.Listener, + private static class FakeExoPlayer extends StubExoPlayer implements MediaSource.Listener, MediaPeriod.Callback, Runnable { private final Renderer[] renderers; @@ -144,21 +144,11 @@ public class FakeSimpleExoPlayer extends SimpleExoPlayer { return true; } - @Override - public void setRepeatMode(@RepeatMode int repeatMode) { - throw new UnsupportedOperationException(); - } - @Override public int getRepeatMode() { return Player.REPEAT_MODE_OFF; } - @Override - public void setShuffleModeEnabled(boolean shuffleModeEnabled) { - throw new UnsupportedOperationException(); - } - @Override public boolean getShuffleModeEnabled() { return false; @@ -169,31 +159,6 @@ public class FakeSimpleExoPlayer extends SimpleExoPlayer { return isLoading; } - @Override - public void seekToDefaultPosition() { - throw new UnsupportedOperationException(); - } - - @Override - public void seekToDefaultPosition(int windowIndex) { - throw new UnsupportedOperationException(); - } - - @Override - public void seekTo(long positionMs) { - throw new UnsupportedOperationException(); - } - - @Override - public void seekTo(int windowIndex, long positionMs) { - throw new UnsupportedOperationException(); - } - - @Override - public void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters) { - throw new UnsupportedOperationException(); - } - @Override public PlaybackParameters getPlaybackParameters() { return PlaybackParameters.DEFAULT; @@ -357,16 +322,6 @@ public class FakeSimpleExoPlayer extends SimpleExoPlayer { }); } - @Override - public void sendMessages(ExoPlayerMessage... messages) { - throw new UnsupportedOperationException(); - } - - @Override - public void blockingSendMessages(ExoPlayerMessage... messages) { - throw new UnsupportedOperationException(); - } - // MediaSource.Listener @Override diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/OggTestData.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/OggTestData.java index 88b5de7f65..7cae709438 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/OggTestData.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/OggTestData.java @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.testutil; - /** * Provides ogg/vorbis test data in bytes for unit tests. */ diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java new file mode 100644 index 0000000000..e03f6fbad9 --- /dev/null +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java @@ -0,0 +1,248 @@ +/* + * 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.testutil; + +import android.os.Looper; +import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; + +/** + * An abstract {@link ExoPlayer} implementation that throws {@link UnsupportedOperationException} + * from every method. + */ +public abstract class StubExoPlayer implements ExoPlayer { + + @Override + public Looper getPlaybackLooper() { + throw new UnsupportedOperationException(); + } + + @Override + public void addListener(Player.EventListener listener) { + throw new UnsupportedOperationException(); + } + + @Override + public void removeListener(Player.EventListener listener) { + throw new UnsupportedOperationException(); + } + + @Override + public int getPlaybackState() { + throw new UnsupportedOperationException(); + } + + @Override + public void prepare(MediaSource mediaSource) { + throw new UnsupportedOperationException(); + } + + @Override + public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) { + throw new UnsupportedOperationException(); + } + + @Override + public void setPlayWhenReady(boolean playWhenReady) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean getPlayWhenReady() { + throw new UnsupportedOperationException(); + } + + @Override + public void setRepeatMode(@RepeatMode int repeatMode) { + throw new UnsupportedOperationException(); + } + + @Override + public int getRepeatMode() { + throw new UnsupportedOperationException(); + } + + @Override + public void setShuffleModeEnabled(boolean shuffleModeEnabled) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean getShuffleModeEnabled() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isLoading() { + throw new UnsupportedOperationException(); + } + + @Override + public void seekToDefaultPosition() { + throw new UnsupportedOperationException(); + } + + @Override + public void seekToDefaultPosition(int windowIndex) { + throw new UnsupportedOperationException(); + } + + @Override + public void seekTo(long positionMs) { + throw new UnsupportedOperationException(); + } + + @Override + public void seekTo(int windowIndex, long positionMs) { + throw new UnsupportedOperationException(); + } + + @Override + public void setPlaybackParameters(PlaybackParameters playbackParameters) { + throw new UnsupportedOperationException(); + } + + @Override + public PlaybackParameters getPlaybackParameters() { + throw new UnsupportedOperationException(); + } + + @Override + public void stop() { + throw new UnsupportedOperationException(); + } + + @Override + public void release() { + throw new UnsupportedOperationException(); + } + + @Override + public void sendMessages(ExoPlayerMessage... messages) { + throw new UnsupportedOperationException(); + } + + @Override + public void blockingSendMessages(ExoPlayerMessage... messages) { + throw new UnsupportedOperationException(); + } + + @Override + public int getRendererCount() { + throw new UnsupportedOperationException(); + } + + @Override + public int getRendererType(int index) { + throw new UnsupportedOperationException(); + } + + @Override + public TrackGroupArray getCurrentTrackGroups() { + throw new UnsupportedOperationException(); + } + + @Override + public TrackSelectionArray getCurrentTrackSelections() { + throw new UnsupportedOperationException(); + } + + @Override + public Object getCurrentManifest() { + throw new UnsupportedOperationException(); + } + + @Override + public Timeline getCurrentTimeline() { + throw new UnsupportedOperationException(); + } + + @Override + public int getCurrentPeriodIndex() { + throw new UnsupportedOperationException(); + } + + @Override + public int getCurrentWindowIndex() { + throw new UnsupportedOperationException(); + } + + @Override + public int getNextWindowIndex() { + throw new UnsupportedOperationException(); + } + + @Override + public int getPreviousWindowIndex() { + throw new UnsupportedOperationException(); + } + + @Override + public long getDuration() { + throw new UnsupportedOperationException(); + } + + @Override + public long getCurrentPosition() { + throw new UnsupportedOperationException(); + } + + @Override + public long getBufferedPosition() { + throw new UnsupportedOperationException(); + } + + @Override + public int getBufferedPercentage() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isCurrentWindowDynamic() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isCurrentWindowSeekable() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isPlayingAd() { + throw new UnsupportedOperationException(); + } + + @Override + public int getCurrentAdGroupIndex() { + throw new UnsupportedOperationException(); + } + + @Override + public int getCurrentAdIndexInAdGroup() { + throw new UnsupportedOperationException(); + } + + @Override + public long getContentPosition() { + throw new UnsupportedOperationException(); + } + +} From 57868092eaec2cb02ba6bd74407d0621ec08e734 Mon Sep 17 00:00:00 2001 From: hoangtc Date: Wed, 15 Nov 2017 03:08:11 -0800 Subject: [PATCH 0869/2472] Add Builder pattern to HlsMediaSource. Add Builder pattern to HlsMediaSource and mark existing constructors as deprecated. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175803853 --- .../exoplayer2/demo/PlayerActivity.java | 5 +- .../exoplayer2/source/hls/HlsMediaSource.java | 136 +++++++++++++++++- 2 files changed, 138 insertions(+), 3 deletions(-) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index 65e1c0e083..3d669c9477 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -374,7 +374,10 @@ public class PlayerActivity extends Activity implements OnClickListener, .setEventListener(mainHandler, eventLogger) .build(); case C.TYPE_HLS: - return new HlsMediaSource(uri, mediaDataSourceFactory, mainHandler, eventLogger); + return HlsMediaSource.Builder + .forDataSource(uri, mediaDataSourceFactory) + .setEventListener(mainHandler, eventLogger) + .build(); case C.TYPE_OTHER: return new ExtractorMediaSource(uri, mediaDataSourceFactory, new DefaultExtractorsFactory(), mainHandler, eventLogger); diff --git a/library/hls/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 index 21b27e655d..3f28981f0e 100644 --- a/library/hls/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 @@ -47,6 +47,132 @@ public final class HlsMediaSource implements MediaSource, ExoPlayerLibraryInfo.registerModule("goog.exo.hls"); } + /** + * Builder for {@link HlsMediaSource}. Each builder instance can only be used once. + */ + public static final class Builder { + + private final Uri manifestUri; + private final HlsDataSourceFactory hlsDataSourceFactory; + + private HlsExtractorFactory extractorFactory; + private ParsingLoadable.Parser playlistParser; + private AdaptiveMediaSourceEventListener eventListener; + private Handler eventHandler; + private int minLoadableRetryCount; + private boolean isBuildCalled; + + /** + * Creates a {@link Builder} for a {@link HlsMediaSource} with a loadable manifest Uri and + * a {@link DataSource.Factory}. + * + * @param manifestUri The {@link Uri} of the HLS manifest. + * @param dataSourceFactory A data source factory that will be wrapped by a + * {@link DefaultHlsDataSourceFactory} to build {@link DataSource}s for manifests, + * segments and keys. + * @return A new builder. + */ + public static Builder forDataSource(Uri manifestUri, DataSource.Factory dataSourceFactory) { + return new Builder(manifestUri, new DefaultHlsDataSourceFactory(dataSourceFactory)); + } + + /** + * Creates a {@link Builder} for a {@link HlsMediaSource} with a loadable manifest Uri and + * a {@link HlsDataSourceFactory}. + * + * @param manifestUri The {@link Uri} of the HLS manifest. + * @param dataSourceFactory An {@link HlsDataSourceFactory} for {@link DataSource}s for + * manifests, segments and keys. + * @return A new builder. + */ + public static Builder forHlsDataSource(Uri manifestUri, + HlsDataSourceFactory dataSourceFactory) { + return new Builder(manifestUri, dataSourceFactory); + } + + private Builder(Uri manifestUri, HlsDataSourceFactory hlsDataSourceFactory) { + this.manifestUri = manifestUri; + this.hlsDataSourceFactory = hlsDataSourceFactory; + + minLoadableRetryCount = DEFAULT_MIN_LOADABLE_RETRY_COUNT; + } + + /** + * Sets the factory for {@link Extractor}s for the segments. Default value is + * {@link HlsExtractorFactory#DEFAULT}. + * + * @param extractorFactory An {@link HlsExtractorFactory} for {@link Extractor}s for the + * segments. + * @return This builder. + */ + public Builder setExtractorFactory(HlsExtractorFactory extractorFactory) { + this.extractorFactory = extractorFactory; + return this; + } + + /** + * Sets the minimum number of times to retry if a loading error occurs. The default value is + * {@link #DEFAULT_MIN_LOADABLE_RETRY_COUNT}. + * + * @param minLoadableRetryCount The minimum number of times loads must be retried before + * errors are propagated. + * @return This builder. + */ + public Builder setMinLoadableRetryCount(int minLoadableRetryCount) { + this.minLoadableRetryCount = minLoadableRetryCount; + return this; + } + + /** + * Sets the listener to respond to adaptive {@link MediaSource} events and the handler to + * deliver these events. + * + * @param eventHandler A handler for events. + * @param eventListener A listener of events. + * @return This builder. + */ + public Builder setEventListener(Handler eventHandler, + AdaptiveMediaSourceEventListener eventListener) { + this.eventHandler = eventHandler; + this.eventListener = eventListener; + return this; + } + + /** + * Sets the parser to parse HLS playlists. The default is an instance of + * {@link HlsPlaylistParser}. + * + * @param playlistParser A {@link ParsingLoadable.Parser} for HLS playlists. + * @return This builder. + */ + public Builder setPlaylistParser(ParsingLoadable.Parser playlistParser) { + this.playlistParser = playlistParser; + return this; + } + + /** + * Builds a new {@link HlsMediaSource} using the current parameters. + *

          + * After this call, the builder should not be re-used. + * + * @return The newly built {@link HlsMediaSource}. + */ + public HlsMediaSource build() { + Assertions.checkArgument((eventListener == null) == (eventHandler == null)); + Assertions.checkState(!isBuildCalled); + isBuildCalled = true; + if (extractorFactory == null) { + extractorFactory = HlsExtractorFactory.DEFAULT; + } + if (playlistParser == null) { + playlistParser = new HlsPlaylistParser(); + } + return new HlsMediaSource(manifestUri, hlsDataSourceFactory, extractorFactory, + minLoadableRetryCount, eventHandler, eventListener, playlistParser); + } + + } + /** * The default minimum number of times to retry loading data prior to failing. */ @@ -69,7 +195,9 @@ public final class HlsMediaSource implements MediaSource, * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener An {@link AdaptiveMediaSourceEventListener}. May be null if delivery of * events is not required. + * @deprecated Use {@link Builder} instead. */ + @Deprecated public HlsMediaSource(Uri manifestUri, DataSource.Factory dataSourceFactory, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { this(manifestUri, dataSourceFactory, DEFAULT_MIN_LOADABLE_RETRY_COUNT, eventHandler, @@ -85,7 +213,9 @@ public final class HlsMediaSource implements MediaSource, * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener An {@link AdaptiveMediaSourceEventListener}. May be null if delivery of * events is not required. + * @deprecated Use {@link Builder} instead. */ + @Deprecated public HlsMediaSource(Uri manifestUri, DataSource.Factory dataSourceFactory, int minLoadableRetryCount, Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { @@ -105,10 +235,12 @@ public final class HlsMediaSource implements MediaSource, * @param eventListener An {@link AdaptiveMediaSourceEventListener}. May be null if delivery of * events is not required. * @param playlistParser A {@link ParsingLoadable.Parser} for HLS playlists. + * @deprecated Use {@link Builder} instead. */ + @Deprecated public HlsMediaSource(Uri manifestUri, HlsDataSourceFactory dataSourceFactory, - HlsExtractorFactory extractorFactory, int minLoadableRetryCount, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener, + HlsExtractorFactory extractorFactory, int minLoadableRetryCount, Handler eventHandler, + AdaptiveMediaSourceEventListener eventListener, ParsingLoadable.Parser playlistParser) { this.manifestUri = manifestUri; this.dataSourceFactory = dataSourceFactory; From d49fc54968b33c027ca205597a81c1f36cf110ec Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 15 Nov 2017 09:46:28 -0800 Subject: [PATCH 0870/2472] Use ArrayDeque for playback parameters checkpoints ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175837754 --- .../google/android/exoplayer2/audio/DefaultAudioSink.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java index 73c0bc20be..2180601481 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java @@ -36,8 +36,8 @@ import java.lang.annotation.RetentionPolicy; import java.lang.reflect.Method; import java.nio.ByteBuffer; import java.nio.ByteOrder; +import java.util.ArrayDeque; import java.util.ArrayList; -import java.util.LinkedList; /** * Plays audio data. The implementation delegates to an {@link AudioTrack} and handles playback @@ -174,7 +174,7 @@ public final class DefaultAudioSink implements AudioSink { private final ConditionVariable releasingConditionVariable; private final long[] playheadOffsets; private final AudioTrackUtil audioTrackUtil; - private final LinkedList playbackParametersCheckpoints; + private final ArrayDeque playbackParametersCheckpoints; @Nullable private Listener listener; /** @@ -277,7 +277,7 @@ public final class DefaultAudioSink implements AudioSink { drainingAudioProcessorIndex = C.INDEX_UNSET; this.audioProcessors = new AudioProcessor[0]; outputBuffers = new ByteBuffer[0]; - playbackParametersCheckpoints = new LinkedList<>(); + playbackParametersCheckpoints = new ArrayDeque<>(); } @Override From 108900cf5292211e2dc54c1b22ba1fd46cf63ea0 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 16 Nov 2017 01:07:51 -0800 Subject: [PATCH 0871/2472] Add support for float output in DefaultAudioSink Also switch from using MIME types to C.ENCODING_* encodings in DefaultAudioSink. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175936872 --- RELEASENOTES.md | 4 + .../java/com/google/android/exoplayer2/C.java | 10 +- .../android/exoplayer2/audio/AudioSink.java | 30 ++-- .../exoplayer2/audio/DefaultAudioSink.java | 134 ++++++++---------- .../audio/MediaCodecAudioRenderer.java | 20 ++- .../audio/SimpleDecoderAudioRenderer.java | 4 +- .../android/exoplayer2/util/MimeTypes.java | 30 +++- .../google/android/exoplayer2/util/Util.java | 1 + 8 files changed, 124 insertions(+), 109 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 579c2a92ac..cca26f8063 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,9 @@ # Release notes # +### dev-v2 (not yet released) ### + +* Support 32-bit PCM float output from `DefaultAudioSink`. + ### 2.6.0 ### * Removed "r" prefix from versions. This release is "2.6.0", not "r2.6.0". diff --git a/library/core/src/main/java/com/google/android/exoplayer2/C.java b/library/core/src/main/java/com/google/android/exoplayer2/C.java index 9d4049ada9..592589e221 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/C.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/C.java @@ -127,8 +127,8 @@ public final class C { */ @Retention(RetentionPolicy.SOURCE) @IntDef({Format.NO_VALUE, ENCODING_INVALID, ENCODING_PCM_8BIT, ENCODING_PCM_16BIT, - ENCODING_PCM_24BIT, ENCODING_PCM_32BIT, ENCODING_AC3, ENCODING_E_AC3, ENCODING_DTS, - ENCODING_DTS_HD}) + ENCODING_PCM_24BIT, ENCODING_PCM_32BIT, ENCODING_PCM_FLOAT, ENCODING_AC3, ENCODING_E_AC3, + ENCODING_DTS, ENCODING_DTS_HD}) public @interface Encoding {} /** @@ -136,7 +136,7 @@ public final class C { */ @Retention(RetentionPolicy.SOURCE) @IntDef({Format.NO_VALUE, ENCODING_INVALID, ENCODING_PCM_8BIT, ENCODING_PCM_16BIT, - ENCODING_PCM_24BIT, ENCODING_PCM_32BIT}) + ENCODING_PCM_24BIT, ENCODING_PCM_32BIT, ENCODING_PCM_FLOAT}) public @interface PcmEncoding {} /** * @see AudioFormat#ENCODING_INVALID @@ -158,6 +158,10 @@ public final class C { * PCM encoding with 32 bits per sample. */ public static final int ENCODING_PCM_32BIT = 0x40000000; + /** + * @see AudioFormat#ENCODING_PCM_FLOAT + */ + public static final int ENCODING_PCM_FLOAT = AudioFormat.ENCODING_PCM_FLOAT; /** * @see AudioFormat#ENCODING_AC3 */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java index 5408032907..faf3160018 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java @@ -25,14 +25,13 @@ import java.nio.ByteBuffer; * A sink that consumes audio data. *

          * Before starting playback, specify the input audio format by calling - * {@link #configure(String, int, int, int, int, int[], int, int)}. + * {@link #configure(int, int, int, int, int[], int, int)}. *

          * Call {@link #handleBuffer(ByteBuffer, long)} to write data, and {@link #handleDiscontinuity()} * when the data being fed is discontinuous. Call {@link #play()} to start playing the written data. *

          - * Call {@link #configure(String, int, int, int, int, int[], int, int)} whenever the input format - * changes. The sink will be reinitialized on the next call to - * {@link #handleBuffer(ByteBuffer, long)}. + * Call {@link #configure(int, int, int, int, int[], int, int)} whenever the input format changes. + * The sink will be reinitialized on the next call to {@link #handleBuffer(ByteBuffer, long)}. *

          * Call {@link #reset()} to prepare the sink to receive audio data from a new playback position. *

          @@ -166,13 +165,12 @@ public interface AudioSink { void setListener(Listener listener); /** - * Returns whether it's possible to play audio in the specified format using encoded audio - * passthrough. + * Returns whether it's possible to play audio in the specified encoding using passthrough. * - * @param mimeType The format mime type. - * @return Whether it's possible to play audio in the format using encoded audio passthrough. + * @param encoding The audio encoding. + * @return Whether it's possible to play audio in the specified encoding using passthrough. */ - boolean isPassthroughSupported(String mimeType); + boolean isPassthroughSupported(@C.Encoding int encoding); /** * Returns the playback position in the stream starting at zero, in microseconds, or @@ -186,12 +184,9 @@ public interface AudioSink { /** * Configures (or reconfigures) the sink. * - * @param inputMimeType The MIME type of audio data provided in the input buffers. + * @param inputEncoding The encoding of audio data provided in the input buffers. * @param inputChannelCount The number of channels. * @param inputSampleRate The sample rate in Hz. - * @param inputPcmEncoding For PCM formats, the encoding used. One of - * {@link C#ENCODING_PCM_16BIT}, {@link C#ENCODING_PCM_16BIT}, {@link C#ENCODING_PCM_24BIT} - * and {@link C#ENCODING_PCM_32BIT}. * @param specifiedBufferSize A specific size for the playback buffer in bytes, or 0 to infer a * suitable buffer size. * @param outputChannels A mapping from input to output channels that is applied to this sink's @@ -205,9 +200,9 @@ public interface AudioSink { * immediately preceding the next call to {@link #reset()} or this method. * @throws ConfigurationException If an error occurs configuring the sink. */ - void configure(String inputMimeType, int inputChannelCount, int inputSampleRate, - @C.PcmEncoding int inputPcmEncoding, int specifiedBufferSize, @Nullable int[] outputChannels, - int trimStartSamples, int trimEndSamples) throws ConfigurationException; + void configure(@C.Encoding int inputEncoding, int inputChannelCount, int inputSampleRate, + int specifiedBufferSize, @Nullable int[] outputChannels, int trimStartSamples, + int trimEndSamples) throws ConfigurationException; /** * Starts or resumes consuming audio if initialized. @@ -228,8 +223,7 @@ public interface AudioSink { * Returns whether the data was handled in full. If the data was not handled in full then the same * {@link ByteBuffer} must be provided to subsequent calls until it has been fully consumed, * except in the case of an intervening call to {@link #reset()} (or to - * {@link #configure(String, int, int, int, int, int[], int, int)} that causes the sink to be - * reset). + * {@link #configure(int, int, int, int, int[], int, int)} that causes the sink to be reset). * * @param buffer The buffer containing audio data. * @param presentationTimeUs The presentation timestamp of the buffer in microseconds. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java index 2180601481..0d3365b5d8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java @@ -29,7 +29,6 @@ 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; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -182,13 +181,13 @@ public final class DefaultAudioSink implements AudioSink { */ private AudioTrack keepSessionIdAudioTrack; private AudioTrack audioTrack; + private boolean isInputPcm; private int inputSampleRate; private int sampleRate; private int channelConfig; - private @C.Encoding int encoding; private @C.Encoding int outputEncoding; private AudioAttributes audioAttributes; - private boolean passthrough; + private boolean processingEnabled; private int bufferSize; private long bufferSizeUs; @@ -286,9 +285,8 @@ public final class DefaultAudioSink implements AudioSink { } @Override - public boolean isPassthroughSupported(String mimeType) { - return audioCapabilities != null - && audioCapabilities.supportsEncoding(getEncodingForMimeType(mimeType)); + public boolean isPassthroughSupported(@C.Encoding int encoding) { + return audioCapabilities != null && audioCapabilities.supportsEncoding(encoding); } @Override @@ -331,18 +329,20 @@ public final class DefaultAudioSink implements AudioSink { } @Override - public void configure(String inputMimeType, int inputChannelCount, int inputSampleRate, - @C.PcmEncoding int inputPcmEncoding, int specifiedBufferSize, @Nullable int[] outputChannels, - int trimStartSamples, int trimEndSamples) throws ConfigurationException { + public void configure(@C.Encoding int inputEncoding, int inputChannelCount, int inputSampleRate, + int specifiedBufferSize, @Nullable int[] outputChannels, int trimStartSamples, + int trimEndSamples) throws ConfigurationException { + boolean flush = false; this.inputSampleRate = inputSampleRate; int channelCount = inputChannelCount; int sampleRate = inputSampleRate; - @C.Encoding int encoding; - boolean passthrough = !MimeTypes.AUDIO_RAW.equals(inputMimeType); - boolean flush = false; - if (!passthrough) { - encoding = inputPcmEncoding; - pcmFrameSize = Util.getPcmFrameSize(inputPcmEncoding, channelCount); + isInputPcm = isEncodingPcm(inputEncoding); + if (isInputPcm) { + pcmFrameSize = Util.getPcmFrameSize(inputEncoding, channelCount); + } + @C.Encoding int encoding = inputEncoding; + boolean processingEnabled = isInputPcm && inputEncoding != C.ENCODING_PCM_FLOAT; + if (processingEnabled) { trimmingAudioProcessor.setTrimSampleCount(trimStartSamples, trimEndSamples); channelMappingAudioProcessor.setChannelMap(outputChannels); for (AudioProcessor audioProcessor : availableAudioProcessors) { @@ -360,8 +360,6 @@ public final class DefaultAudioSink implements AudioSink { if (flush) { resetAudioProcessors(); } - } else { - encoding = getEncodingForMimeType(inputMimeType); } int channelConfig; @@ -411,11 +409,11 @@ public final class DefaultAudioSink implements AudioSink { // Workaround for Nexus Player not reporting support for mono passthrough. // (See [Internal: b/34268671].) - if (Util.SDK_INT <= 25 && "fugu".equals(Util.DEVICE) && passthrough && channelCount == 1) { + if (Util.SDK_INT <= 25 && "fugu".equals(Util.DEVICE) && !isInputPcm && channelCount == 1) { channelConfig = AudioFormat.CHANNEL_OUT_STEREO; } - if (!flush && isInitialized() && this.encoding == encoding && this.sampleRate == sampleRate + if (!flush && isInitialized() && outputEncoding == encoding && this.sampleRate == sampleRate && this.channelConfig == channelConfig) { // We already have an audio track with the correct sample rate, channel config and encoding. return; @@ -423,16 +421,24 @@ public final class DefaultAudioSink implements AudioSink { reset(); - this.encoding = encoding; - this.passthrough = passthrough; + this.processingEnabled = processingEnabled; this.sampleRate = sampleRate; this.channelConfig = channelConfig; - outputEncoding = passthrough ? encoding : C.ENCODING_PCM_16BIT; - outputPcmFrameSize = Util.getPcmFrameSize(C.ENCODING_PCM_16BIT, channelCount); - + outputEncoding = encoding; + if (isInputPcm) { + outputPcmFrameSize = Util.getPcmFrameSize(outputEncoding, channelCount); + } if (specifiedBufferSize != 0) { bufferSize = specifiedBufferSize; - } else if (passthrough) { + } else if (isInputPcm) { + int minBufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, outputEncoding); + Assertions.checkState(minBufferSize != ERROR_BAD_VALUE); + int multipliedBufferSize = minBufferSize * BUFFER_MULTIPLICATION_FACTOR; + int minAppBufferSize = (int) durationUsToFrames(MIN_BUFFER_DURATION_US) * outputPcmFrameSize; + int maxAppBufferSize = (int) Math.max(minBufferSize, + durationUsToFrames(MAX_BUFFER_DURATION_US) * outputPcmFrameSize); + bufferSize = Util.constrainValue(multipliedBufferSize, minAppBufferSize, maxAppBufferSize); + } else { // TODO: Set the minimum buffer size using getMinBufferSize when it takes the encoding into // account. [Internal: b/25181305] if (outputEncoding == C.ENCODING_AC3 || outputEncoding == C.ENCODING_E_AC3) { @@ -442,21 +448,9 @@ public final class DefaultAudioSink implements AudioSink { // DTS allows an 'open' bitrate, but we assume the maximum listed value: 1536 kbit/s. bufferSize = (int) (PASSTHROUGH_BUFFER_DURATION_US * 192 * 1024 / C.MICROS_PER_SECOND); } - } else { - int minBufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, outputEncoding); - Assertions.checkState(minBufferSize != ERROR_BAD_VALUE); - int multipliedBufferSize = minBufferSize * BUFFER_MULTIPLICATION_FACTOR; - int minAppBufferSize = (int) durationUsToFrames(MIN_BUFFER_DURATION_US) * outputPcmFrameSize; - int maxAppBufferSize = (int) Math.max(minBufferSize, - durationUsToFrames(MAX_BUFFER_DURATION_US) * outputPcmFrameSize); - bufferSize = multipliedBufferSize < minAppBufferSize ? minAppBufferSize - : multipliedBufferSize > maxAppBufferSize ? maxAppBufferSize - : 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); + bufferSizeUs = + isInputPcm ? framesToDurationUs(bufferSize / outputPcmFrameSize) : C.TIME_UNSET; } private void resetAudioProcessors() { @@ -487,6 +481,10 @@ public final class DefaultAudioSink implements AudioSink { releasingConditionVariable.block(); audioTrack = initializeAudioTrack(); + + // The old playback parameters may no longer be applicable so try to reset them now. + setPlaybackParameters(playbackParameters); + int audioSessionId = audioTrack.getAudioSessionId(); if (enablePreV21AudioSessionWorkaround) { if (Util.SDK_INT < 21) { @@ -574,7 +572,7 @@ public final class DefaultAudioSink implements AudioSink { return true; } - if (passthrough && framesPerEncodedSample == 0) { + if (!isInputPcm && framesPerEncodedSample == 0) { // If this is the first encoded sample, calculate the sample size in frames. framesPerEncodedSample = getFramesPerEncodedSample(outputEncoding, buffer); } @@ -618,20 +616,19 @@ public final class DefaultAudioSink implements AudioSink { } } - if (passthrough) { - submittedEncodedFrames += framesPerEncodedSample; - } else { + if (isInputPcm) { submittedPcmBytes += buffer.remaining(); + } else { + submittedEncodedFrames += framesPerEncodedSample; } inputBuffer = buffer; } - if (passthrough) { - // Passthrough buffers are not processed. - writeBuffer(inputBuffer, presentationTimeUs); - } else { + if (processingEnabled) { processBuffers(presentationTimeUs); + } else { + writeBuffer(inputBuffer, presentationTimeUs); } if (!inputBuffer.hasRemaining()) { @@ -679,10 +676,9 @@ public final class DefaultAudioSink implements AudioSink { } @SuppressWarnings("ReferenceEquality") - private boolean writeBuffer(ByteBuffer buffer, long avSyncPresentationTimeUs) - throws WriteException { + private void writeBuffer(ByteBuffer buffer, long avSyncPresentationTimeUs) throws WriteException { if (!buffer.hasRemaining()) { - return true; + return; } if (outputBuffer != null) { Assertions.checkArgument(outputBuffer == buffer); @@ -701,7 +697,7 @@ public final class DefaultAudioSink implements AudioSink { } int bytesRemaining = buffer.remaining(); int bytesWritten = 0; - if (Util.SDK_INT < 21) { // passthrough == false + if (Util.SDK_INT < 21) { // isInputPcm == true // Work out how many bytes we can write without the risk of blocking. int bytesPending = (int) (writtenPcmBytes - (audioTrackUtil.getPlaybackHeadPosition() * outputPcmFrameSize)); @@ -728,17 +724,15 @@ public final class DefaultAudioSink implements AudioSink { throw new WriteException(bytesWritten); } - if (!passthrough) { + if (isInputPcm) { writtenPcmBytes += bytesWritten; } if (bytesWritten == bytesRemaining) { - if (passthrough) { + if (!isInputPcm) { writtenEncodedFrames += framesPerEncodedSample; } outputBuffer = null; - return true; } - return false; } @Override @@ -758,7 +752,7 @@ public final class DefaultAudioSink implements AudioSink { private boolean drainAudioProcessorsToEndOfStream() throws WriteException { boolean audioProcessorNeedsEndOfStream = false; if (drainingAudioProcessorIndex == C.INDEX_UNSET) { - drainingAudioProcessorIndex = passthrough ? audioProcessors.length : 0; + drainingAudioProcessorIndex = processingEnabled ? 0 : audioProcessors.length; audioProcessorNeedsEndOfStream = true; } while (drainingAudioProcessorIndex < audioProcessors.length) { @@ -799,8 +793,8 @@ public final class DefaultAudioSink implements AudioSink { @Override public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) { - if (passthrough) { - // The playback parameters are always the default in passthrough mode. + if (isInitialized() && !processingEnabled) { + // The playback parameters are always the default if processing is disabled. this.playbackParameters = PlaybackParameters.DEFAULT; return this.playbackParameters; } @@ -1076,7 +1070,7 @@ public final class DefaultAudioSink implements AudioSink { audioTimestampSet = false; } } - if (getLatencyMethod != null && !passthrough) { + if (getLatencyMethod != null && isInputPcm) { try { // Compute the audio track latency, excluding the latency due to the buffer (leaving // latency due to the mixer and audio hardware driver). @@ -1115,11 +1109,11 @@ public final class DefaultAudioSink implements AudioSink { } private long getSubmittedFrames() { - return passthrough ? submittedEncodedFrames : (submittedPcmBytes / pcmFrameSize); + return isInputPcm ? (submittedPcmBytes / pcmFrameSize) : submittedEncodedFrames; } private long getWrittenFrames() { - return passthrough ? writtenEncodedFrames : (writtenPcmBytes / outputPcmFrameSize); + return isInputPcm ? (writtenPcmBytes / outputPcmFrameSize) : writtenEncodedFrames; } private void resetSyncParams() { @@ -1212,20 +1206,10 @@ public final class DefaultAudioSink implements AudioSink { MODE_STATIC, audioSessionId); } - @C.Encoding - private static int getEncodingForMimeType(String mimeType) { - switch (mimeType) { - case MimeTypes.AUDIO_AC3: - return C.ENCODING_AC3; - case MimeTypes.AUDIO_E_AC3: - return C.ENCODING_E_AC3; - case MimeTypes.AUDIO_DTS: - return C.ENCODING_DTS; - case MimeTypes.AUDIO_DTS_HD: - return C.ENCODING_DTS_HD; - default: - return C.ENCODING_INVALID; - } + private static boolean isEncodingPcm(@C.Encoding int encoding) { + return encoding == C.ENCODING_PCM_8BIT || encoding == C.ENCODING_PCM_16BIT + || encoding == C.ENCODING_PCM_24BIT || encoding == C.ENCODING_PCM_32BIT + || encoding == C.ENCODING_PCM_FLOAT; } private static int getFramesPerEncodedSample(@C.Encoding int encoding, ByteBuffer buffer) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index f8206e94cf..18cbcea115 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java @@ -51,6 +51,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media private boolean passthroughEnabled; private boolean codecNeedsDiscardChannelsWorkaround; private android.media.MediaFormat passthroughMediaFormat; + @C.Encoding private int pcmEncoding; private int channelCount; private int encoderDelay; @@ -226,7 +227,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media * @return Whether passthrough playback is supported. */ protected boolean allowPassthrough(String mimeType) { - return audioSink.isPassthroughSupported(mimeType); + return audioSink.isPassthroughSupported(MimeTypes.getEncoding(mimeType)); } @Override @@ -272,10 +273,15 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media @Override protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputFormat) throws ExoPlaybackException { - boolean passthrough = passthroughMediaFormat != null; - String mimeType = passthrough ? passthroughMediaFormat.getString(MediaFormat.KEY_MIME) - : MimeTypes.AUDIO_RAW; - MediaFormat format = passthrough ? passthroughMediaFormat : outputFormat; + @C.Encoding int encoding; + MediaFormat format; + if (passthroughMediaFormat != null) { + encoding = MimeTypes.getEncoding(passthroughMediaFormat.getString(MediaFormat.KEY_MIME)); + format = passthroughMediaFormat; + } else { + encoding = pcmEncoding; + format = outputFormat; + } int channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT); int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE); int[] channelMap; @@ -289,8 +295,8 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } try { - audioSink.configure(mimeType, channelCount, sampleRate, pcmEncoding, 0, channelMap, - encoderDelay, encoderPadding); + audioSink.configure(encoding, channelCount, sampleRate, 0, channelMap, encoderDelay, + encoderPadding); } catch (AudioSink.ConfigurationException e) { throw ExoPlaybackException.createForRenderer(e, getIndex()); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java index 98a84fdff8..6be4b1d35d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java @@ -329,8 +329,8 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements if (audioTrackNeedsConfigure) { Format outputFormat = getOutputFormat(); - audioSink.configure(outputFormat.sampleMimeType, outputFormat.channelCount, - outputFormat.sampleRate, outputFormat.pcmEncoding, 0, null, encoderDelay, encoderPadding); + audioSink.configure(outputFormat.pcmEncoding, outputFormat.channelCount, + outputFormat.sampleRate, 0, null, encoderDelay, encoderPadding); audioTrackNeedsConfigure = false; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java b/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java index 2daf16d3d2..d48d28caa5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java @@ -208,12 +208,12 @@ public final class MimeTypes { } /** - * Returns the {@link C}{@code .TRACK_TYPE_*} constant that corresponds to a specified mime type. - * {@link C#TRACK_TYPE_UNKNOWN} if the mime type is not known or the mapping cannot be + * Returns the {@link C}{@code .TRACK_TYPE_*} constant that corresponds to a specified MIME type. + * {@link C#TRACK_TYPE_UNKNOWN} if the MIME type is not known or the mapping cannot be * established. * - * @param mimeType The mimeType. - * @return The {@link C}{@code .TRACK_TYPE_*} constant that corresponds to a specified mime type. + * @param mimeType The MIME type. + * @return The {@link C}{@code .TRACK_TYPE_*} constant that corresponds to a specified MIME type. */ public static int getTrackType(String mimeType) { if (TextUtils.isEmpty(mimeType)) { @@ -239,6 +239,28 @@ public final class MimeTypes { } } + /** + * Returns the {@link C}{@code .ENCODING_*} constant that corresponds to a specified MIME type, or + * {@link C#ENCODING_INVALID} if the mapping cannot be established. + * + * @param mimeType The MIME type. + * @return The {@link C}{@code .ENCODING_*} constant that corresponds to a specified MIME type. + */ + public static @C.Encoding int getEncoding(String mimeType) { + switch (mimeType) { + case MimeTypes.AUDIO_AC3: + return C.ENCODING_AC3; + case MimeTypes.AUDIO_E_AC3: + return C.ENCODING_E_AC3; + case MimeTypes.AUDIO_DTS: + return C.ENCODING_DTS; + case MimeTypes.AUDIO_DTS_HD: + return C.ENCODING_DTS_HD; + default: + return C.ENCODING_INVALID; + } + } + /** * Equivalent to {@code getTrackType(getMediaMimeType(codec))}. * diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java index a79ed38755..5b2de1042e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -801,6 +801,7 @@ public final class Util { case C.ENCODING_PCM_24BIT: return channelCount * 3; case C.ENCODING_PCM_32BIT: + case C.ENCODING_PCM_FLOAT: return channelCount * 4; case C.ENCODING_INVALID: case Format.NO_VALUE: From 820a4459448176a9b06a64983127d92a52b4e0b3 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 16 Nov 2017 02:03:02 -0800 Subject: [PATCH 0872/2472] Add support for float output for FfmpegAudioRenderer ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175940553 --- RELEASENOTES.md | 3 +- .../ext/ffmpeg/FfmpegAudioRenderer.java | 52 +++++++++++++++++-- .../exoplayer2/ext/ffmpeg/FfmpegDecoder.java | 29 ++++++++--- extensions/ffmpeg/src/main/jni/ffmpeg_jni.cc | 27 ++++++---- .../ext/flac/LibflacAudioRenderer.java | 3 ++ .../ext/opus/LibopusAudioRenderer.java | 2 + .../android/exoplayer2/audio/AudioSink.java | 8 +-- .../exoplayer2/audio/DefaultAudioSink.java | 11 +++- .../audio/MediaCodecAudioRenderer.java | 12 +++-- .../audio/SimpleDecoderAudioRenderer.java | 10 ++++ .../android/exoplayer2/util/MimeTypes.java | 7 +-- 11 files changed, 130 insertions(+), 34 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index cca26f8063..d2d0105b55 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,7 +2,8 @@ ### dev-v2 (not yet released) ### -* Support 32-bit PCM float output from `DefaultAudioSink`. +* Support 32-bit PCM float output from `DefaultAudioSink`, and add an option to + use this with `FfmpegAudioRenderer`. ### 2.6.0 ### diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java index ed8a5b0eac..3e23659bf8 100644 --- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java +++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegAudioRenderer.java @@ -21,6 +21,8 @@ import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.audio.AudioProcessor; import com.google.android.exoplayer2.audio.AudioRendererEventListener; +import com.google.android.exoplayer2.audio.AudioSink; +import com.google.android.exoplayer2.audio.DefaultAudioSink; import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.ExoMediaCrypto; @@ -41,6 +43,8 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer { */ private static final int INITIAL_INPUT_BUFFER_SIZE = 960 * 6; + private final boolean enableFloatOutput; + private FfmpegDecoder decoder; public FfmpegAudioRenderer() { @@ -55,7 +59,23 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer { */ public FfmpegAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, AudioProcessor... audioProcessors) { - super(eventHandler, eventListener, audioProcessors); + this(eventHandler, eventListener, new DefaultAudioSink(null, audioProcessors), false); + } + + /** + * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be + * null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + * @param audioSink The sink to which audio will be output. + * @param enableFloatOutput Whether to enable 32-bit float audio format, if supported on the + * device/build and if the input format may have bit depth higher than 16-bit. When using + * 32-bit float output, any audio processing will be disabled, including playback speed/pitch + * adjustment. + */ + public FfmpegAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, + AudioSink audioSink, boolean enableFloatOutput) { + super(eventHandler, eventListener, null, false, audioSink); + this.enableFloatOutput = enableFloatOutput; } @Override @@ -64,7 +84,7 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer { String sampleMimeType = format.sampleMimeType; if (!FfmpegLibrary.isAvailable() || !MimeTypes.isAudio(sampleMimeType)) { return FORMAT_UNSUPPORTED_TYPE; - } else if (!FfmpegLibrary.supportsFormat(sampleMimeType)) { + } else if (!FfmpegLibrary.supportsFormat(sampleMimeType) || !isOutputSupported(format)) { return FORMAT_UNSUPPORTED_SUBTYPE; } else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) { return FORMAT_UNSUPPORTED_DRM; @@ -82,7 +102,7 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer { protected FfmpegDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto) throws FfmpegDecoderException { decoder = new FfmpegDecoder(NUM_BUFFERS, NUM_BUFFERS, INITIAL_INPUT_BUFFER_SIZE, - format.sampleMimeType, format.initializationData); + format.sampleMimeType, format.initializationData, shouldUseFloatOutput(format)); return decoder; } @@ -90,8 +110,32 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer { public Format getOutputFormat() { int channelCount = decoder.getChannelCount(); int sampleRate = decoder.getSampleRate(); + @C.PcmEncoding int encoding = decoder.getEncoding(); return Format.createAudioSampleFormat(null, MimeTypes.AUDIO_RAW, null, Format.NO_VALUE, - Format.NO_VALUE, channelCount, sampleRate, C.ENCODING_PCM_16BIT, null, null, 0, null); + Format.NO_VALUE, channelCount, sampleRate, encoding, null, null, 0, null); + } + + private boolean isOutputSupported(Format inputFormat) { + return shouldUseFloatOutput(inputFormat) || supportsOutputEncoding(C.ENCODING_PCM_16BIT); + } + + private boolean shouldUseFloatOutput(Format inputFormat) { + if (!enableFloatOutput || !supportsOutputEncoding(C.ENCODING_PCM_FLOAT)) { + return false; + } + switch (inputFormat.sampleMimeType) { + case MimeTypes.AUDIO_RAW: + // For raw audio, output in 32-bit float encoding if the bit depth is > 16-bit. + return inputFormat.pcmEncoding == C.ENCODING_PCM_24BIT + || inputFormat.pcmEncoding == C.ENCODING_PCM_32BIT + || inputFormat.pcmEncoding == C.ENCODING_PCM_FLOAT; + case MimeTypes.AUDIO_AC3: + // AC-3 is always 16-bit, so there is no point outputting in 32-bit float encoding. + return false; + default: + // For all other formats, assume that it's worth using 32-bit float encoding. + return true; + } } } diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java index 2af2101ee7..8807738cfa 100644 --- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java +++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.ext.ffmpeg; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.decoder.SimpleDecoder; import com.google.android.exoplayer2.decoder.SimpleOutputBuffer; @@ -29,11 +30,15 @@ import java.util.List; /* package */ final class FfmpegDecoder extends SimpleDecoder { - // Space for 64 ms of 6 channel 48 kHz 16-bit PCM audio. - private static final int OUTPUT_BUFFER_SIZE = 1536 * 6 * 2 * 2; + // Space for 64 ms of 48 kHz 8 channel 16-bit PCM audio. + private static final int OUTPUT_BUFFER_SIZE_16BIT = 64 * 48 * 8 * 2; + // Space for 64 ms of 48 KhZ 8 channel 32-bit PCM audio. + private static final int OUTPUT_BUFFER_SIZE_32BIT = OUTPUT_BUFFER_SIZE_16BIT * 2; private final String codecName; private final byte[] extraData; + private final @C.Encoding int encoding; + private final int outputBufferSize; private long nativeContext; // May be reassigned on resetting the codec. private boolean hasOutputFormat; @@ -41,14 +46,17 @@ import java.util.List; private volatile int sampleRate; public FfmpegDecoder(int numInputBuffers, int numOutputBuffers, int initialInputBufferSize, - String mimeType, List initializationData) throws FfmpegDecoderException { + String mimeType, List initializationData, boolean outputFloat) + throws FfmpegDecoderException { super(new DecoderInputBuffer[numInputBuffers], new SimpleOutputBuffer[numOutputBuffers]); if (!FfmpegLibrary.isAvailable()) { throw new FfmpegDecoderException("Failed to load decoder native libraries."); } codecName = FfmpegLibrary.getCodecName(mimeType); extraData = getExtraData(mimeType, initializationData); - nativeContext = ffmpegInitialize(codecName, extraData); + encoding = outputFloat ? C.ENCODING_PCM_FLOAT : C.ENCODING_PCM_16BIT; + outputBufferSize = outputFloat ? OUTPUT_BUFFER_SIZE_32BIT : OUTPUT_BUFFER_SIZE_16BIT; + nativeContext = ffmpegInitialize(codecName, extraData, outputFloat); if (nativeContext == 0) { throw new FfmpegDecoderException("Initialization failed."); } @@ -81,8 +89,8 @@ import java.util.List; } ByteBuffer inputData = inputBuffer.data; int inputSize = inputData.limit(); - ByteBuffer outputData = outputBuffer.init(inputBuffer.timeUs, OUTPUT_BUFFER_SIZE); - int result = ffmpegDecode(nativeContext, inputData, inputSize, outputData, OUTPUT_BUFFER_SIZE); + ByteBuffer outputData = outputBuffer.init(inputBuffer.timeUs, outputBufferSize); + int result = ffmpegDecode(nativeContext, inputData, inputSize, outputData, outputBufferSize); if (result < 0) { return new FfmpegDecoderException("Error decoding (see logcat). Code: " + result); } @@ -124,6 +132,13 @@ import java.util.List; return sampleRate; } + /** + * Returns the encoding of output audio. + */ + public @C.Encoding int getEncoding() { + return encoding; + } + /** * Returns FFmpeg-compatible codec-specific initialization data ("extra data"), or {@code null} if * not required. @@ -153,7 +168,7 @@ import java.util.List; } } - private native long ffmpegInitialize(String codecName, byte[] extraData); + private native long ffmpegInitialize(String codecName, byte[] extraData, boolean outputFloat); private native int ffmpegDecode(long context, ByteBuffer inputData, int inputSize, ByteBuffer outputData, int outputSize); private native int ffmpegGetChannelCount(long context); diff --git a/extensions/ffmpeg/src/main/jni/ffmpeg_jni.cc b/extensions/ffmpeg/src/main/jni/ffmpeg_jni.cc index fa615f2ec1..d077c819ab 100644 --- a/extensions/ffmpeg/src/main/jni/ffmpeg_jni.cc +++ b/extensions/ffmpeg/src/main/jni/ffmpeg_jni.cc @@ -57,8 +57,10 @@ extern "C" { #define ERROR_STRING_BUFFER_LENGTH 256 -// Request a format corresponding to AudioFormat.ENCODING_PCM_16BIT. -static const AVSampleFormat OUTPUT_FORMAT = AV_SAMPLE_FMT_S16; +// Output format corresponding to AudioFormat.ENCODING_PCM_16BIT. +static const AVSampleFormat OUTPUT_FORMAT_PCM_16BIT = AV_SAMPLE_FMT_S16; +// Output format corresponding to AudioFormat.ENCODING_PCM_FLOAT. +static const AVSampleFormat OUTPUT_FORMAT_PCM_FLOAT = AV_SAMPLE_FMT_FLT; /** * Returns the AVCodec with the specified name, or NULL if it is not available. @@ -71,7 +73,7 @@ AVCodec *getCodecByName(JNIEnv* env, jstring codecName); * Returns the created context. */ AVCodecContext *createContext(JNIEnv *env, AVCodec *codec, - jbyteArray extraData); + jbyteArray extraData, jboolean outputFloat); /** * Decodes the packet into the output buffer, returning the number of bytes @@ -107,13 +109,14 @@ LIBRARY_FUNC(jboolean, ffmpegHasDecoder, jstring codecName) { return getCodecByName(env, codecName) != NULL; } -DECODER_FUNC(jlong, ffmpegInitialize, jstring codecName, jbyteArray extraData) { +DECODER_FUNC(jlong, ffmpegInitialize, jstring codecName, jbyteArray extraData, + jboolean outputFloat) { AVCodec *codec = getCodecByName(env, codecName); if (!codec) { LOGE("Codec not found."); return 0L; } - return (jlong) createContext(env, codec, extraData); + return (jlong) createContext(env, codec, extraData, outputFloat); } DECODER_FUNC(jint, ffmpegDecode, jlong context, jobject inputData, @@ -177,7 +180,8 @@ DECODER_FUNC(jlong, ffmpegReset, jlong jContext, jbyteArray extraData) { LOGE("Unexpected error finding codec %d.", codecId); return 0L; } - return (jlong) createContext(env, codec, extraData); + return (jlong) createContext(env, codec, extraData, + context->request_sample_fmt == OUTPUT_FORMAT_PCM_FLOAT); } avcodec_flush_buffers(context); @@ -201,13 +205,14 @@ AVCodec *getCodecByName(JNIEnv* env, jstring codecName) { } AVCodecContext *createContext(JNIEnv *env, AVCodec *codec, - jbyteArray extraData) { + jbyteArray extraData, jboolean outputFloat) { AVCodecContext *context = avcodec_alloc_context3(codec); if (!context) { LOGE("Failed to allocate context."); return NULL; } - context->request_sample_fmt = OUTPUT_FORMAT; + context->request_sample_fmt = + outputFloat ? OUTPUT_FORMAT_PCM_FLOAT : OUTPUT_FORMAT_PCM_16BIT; if (extraData) { jsize size = env->GetArrayLength(extraData); context->extradata_size = size; @@ -275,7 +280,9 @@ int decodePacket(AVCodecContext *context, AVPacket *packet, av_opt_set_int(resampleContext, "in_sample_rate", sampleRate, 0); av_opt_set_int(resampleContext, "out_sample_rate", sampleRate, 0); av_opt_set_int(resampleContext, "in_sample_fmt", sampleFormat, 0); - av_opt_set_int(resampleContext, "out_sample_fmt", OUTPUT_FORMAT, 0); + // The output format is always the requested format. + av_opt_set_int(resampleContext, "out_sample_fmt", + context->request_sample_fmt, 0); result = avresample_open(resampleContext); if (result < 0) { logError("avresample_open", result); @@ -285,7 +292,7 @@ int decodePacket(AVCodecContext *context, AVPacket *packet, context->opaque = resampleContext; } int inSampleSize = av_get_bytes_per_sample(sampleFormat); - int outSampleSize = av_get_bytes_per_sample(OUTPUT_FORMAT); + int outSampleSize = av_get_bytes_per_sample(context->request_sample_fmt); int outSamples = avresample_get_out_samples(resampleContext, sampleCount); int bufferOutSize = outSampleSize * channelCount * outSamples; if (outSize + bufferOutSize > outputSize) { diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java index dc376d2ea4..a72b03cd44 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/LibflacAudioRenderer.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.ext.flac; import android.os.Handler; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.audio.AudioProcessor; import com.google.android.exoplayer2.audio.AudioRendererEventListener; @@ -52,6 +53,8 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer { if (!FlacLibrary.isAvailable() || !MimeTypes.AUDIO_FLAC.equalsIgnoreCase(format.sampleMimeType)) { return FORMAT_UNSUPPORTED_TYPE; + } else if (!supportsOutputEncoding(C.ENCODING_PCM_16BIT)) { + return FORMAT_UNSUPPORTED_SUBTYPE; } else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) { return FORMAT_UNSUPPORTED_DRM; } else { diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java index e4745d0c29..b94f3e9332 100644 --- a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java +++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/LibopusAudioRenderer.java @@ -76,6 +76,8 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer { if (!OpusLibrary.isAvailable() || !MimeTypes.AUDIO_OPUS.equalsIgnoreCase(format.sampleMimeType)) { return FORMAT_UNSUPPORTED_TYPE; + } else if (!supportsOutputEncoding(C.ENCODING_PCM_16BIT)) { + return FORMAT_UNSUPPORTED_SUBTYPE; } else if (!supportsFormatDrm(drmSessionManager, format.drmInitData)) { return FORMAT_UNSUPPORTED_DRM; } else { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java index faf3160018..6bb5bf7d8e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java @@ -75,7 +75,7 @@ public interface AudioSink { * * @param bufferSize The size of the sink's buffer, in bytes. * @param bufferSizeMs The size of the sink's buffer, in milliseconds, if it is configured for - * PCM output. {@link C#TIME_UNSET} if it is configured for passthrough output, as the + * PCM output. {@link C#TIME_UNSET} if it is configured for encoded audio output, as the * buffered media can have a variable bitrate so the duration may be unknown. * @param elapsedSinceLastFeedMs The time since the sink was last fed data, in milliseconds. */ @@ -165,12 +165,12 @@ public interface AudioSink { void setListener(Listener listener); /** - * Returns whether it's possible to play audio in the specified encoding using passthrough. + * Returns whether it's possible to play audio in the specified encoding. * * @param encoding The audio encoding. - * @return Whether it's possible to play audio in the specified encoding using passthrough. + * @return Whether it's possible to play audio in the specified encoding. */ - boolean isPassthroughSupported(@C.Encoding int encoding); + boolean isEncodingSupported(@C.Encoding int encoding); /** * Returns the playback position in the stream starting at zero, in microseconds, or diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java index 0d3365b5d8..ba62ac126e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java @@ -285,8 +285,15 @@ public final class DefaultAudioSink implements AudioSink { } @Override - public boolean isPassthroughSupported(@C.Encoding int encoding) { - return audioCapabilities != null && audioCapabilities.supportsEncoding(encoding); + public boolean isEncodingSupported(@C.Encoding int encoding) { + if (isEncodingPcm(encoding)) { + // AudioTrack supports 16-bit integer PCM output in all platform API versions, and float + // output from platform API version 21 only. Other integer PCM encodings are resampled by this + // sink to 16-bit PCM. + return encoding != C.ENCODING_PCM_FLOAT || Util.SDK_INT >= 21; + } else { + return audioCapabilities != null && audioCapabilities.supportsEncoding(encoding); + } } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index 18cbcea115..25ad847f7e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java @@ -178,6 +178,11 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media && mediaCodecSelector.getPassthroughDecoderInfo() != null) { return ADAPTIVE_NOT_SEAMLESS | tunnelingSupport | FORMAT_HANDLED; } + if ((MimeTypes.AUDIO_RAW.equals(mimeType) && !audioSink.isEncodingSupported(format.pcmEncoding)) + || !audioSink.isEncodingSupported(C.ENCODING_PCM_16BIT)) { + // Assume the decoder outputs 16-bit PCM, unless the input is raw. + return FORMAT_UNSUPPORTED_SUBTYPE; + } boolean requiresSecureDecryption = false; DrmInitData drmInitData = format.drmInitData; if (drmInitData != null) { @@ -220,14 +225,15 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media /** * Returns whether encoded audio passthrough should be used for playing back the input format. - * This implementation returns true if the {@link AudioSink} indicates that passthrough is - * supported. + * This implementation returns true if the {@link AudioSink} indicates that encoded audio output + * is supported. * * @param mimeType The type of input media. * @return Whether passthrough playback is supported. */ protected boolean allowPassthrough(String mimeType) { - return audioSink.isPassthroughSupported(MimeTypes.getEncoding(mimeType)); + @C.Encoding int encoding = MimeTypes.getEncoding(mimeType); + return encoding != C.ENCODING_INVALID && audioSink.isEncodingSupported(encoding); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java index 6be4b1d35d..d9ad549104 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java @@ -200,6 +200,16 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements protected abstract int supportsFormatInternal(DrmSessionManager drmSessionManager, Format format); + /** + * Returns whether the audio sink can accept audio in the specified encoding. + * + * @param encoding The audio encoding. + * @return Whether the audio sink can accept audio in the specified encoding. + */ + protected final boolean supportsOutputEncoding(@C.Encoding int encoding) { + return audioSink.isEncodingSupported(encoding); + } + @Override public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { if (outputStreamEnded) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java b/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java index d48d28caa5..c29a4c3717 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java @@ -240,11 +240,12 @@ public final class MimeTypes { } /** - * Returns the {@link C}{@code .ENCODING_*} constant that corresponds to a specified MIME type, or - * {@link C#ENCODING_INVALID} if the mapping cannot be established. + * Returns the {@link C}{@code .ENCODING_*} constant that corresponds to specified MIME type, if + * it is an encoded (non-PCM) audio format, or {@link C#ENCODING_INVALID} otherwise. * * @param mimeType The MIME type. - * @return The {@link C}{@code .ENCODING_*} constant that corresponds to a specified MIME type. + * @return The {@link C}{@code .ENCODING_*} constant that corresponds to a specified MIME type, or + * {@link C#ENCODING_INVALID}. */ public static @C.Encoding int getEncoding(String mimeType) { switch (mimeType) { From f5a3a277263aaa05057ac06100ad8d997ce7fbf2 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 16 Nov 2017 05:08:44 -0800 Subject: [PATCH 0873/2472] Deduplicate ExtractorMediaPeriod discontinuity reporting This change makes sure progress is being made before reporting a discontinuity. Else in cases like having no network and playing a live stream, we allow the discontinuity to be read each time an internal retry occurs, meaning it gets read repeatedly. This does no harm, but is noisy and unnecessary. We should also not allow skipping whilst there is a pending reset or discontinuity notification, just like we don't allow reads. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=175953064 --- .../google/android/exoplayer2/source/ExtractorMediaPeriod.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java index 1228061cde..d43b2d87b2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -303,7 +303,8 @@ import java.util.Arrays; @Override public long readDiscontinuity() { - if (notifyDiscontinuity) { + if (notifyDiscontinuity + && (loadingFinished || getExtractedSamplesCount() > extractedSamplesCountAtStartOfLoad)) { notifyDiscontinuity = false; return lastSeekPositionUs; } From 1f70d3cdd7b8487c8754f303932cd16a9e7c2e42 Mon Sep 17 00:00:00 2001 From: hoangtc Date: Fri, 17 Nov 2017 03:08:55 -0800 Subject: [PATCH 0874/2472] Add Builder to ExtractorMediaSource. Add Builder pattern to ExtractorMediaSource and mark existing constructors as deprecated. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176088810 --- RELEASENOTES.md | 2 + .../exoplayer2/imademo/PlayerManager.java | 9 +- .../exoplayer2/demo/PlayerActivity.java | 6 +- .../exoplayer2/ext/flac/FlacPlaybackTest.java | 10 +- .../exoplayer2/ext/opus/OpusPlaybackTest.java | 10 +- .../exoplayer2/ext/vp9/VpxPlaybackTest.java | 10 +- .../source/ExtractorMediaSource.java | 123 ++++++++++++++++++ .../exoplayer2/source/ads/AdsMediaSource.java | 8 +- 8 files changed, 146 insertions(+), 32 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index d2d0105b55..2a5ccb583c 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,6 +2,8 @@ ### dev-v2 (not yet released) ### +* Add Builder to ExtractorMediaSource, HlsMediaSource, SsMediaSource, + DashMediaSource. * Support 32-bit PCM float output from `DefaultAudioSink`, and add an option to use this with `FfmpegAudioRenderer`. diff --git a/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java b/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java index e11c840d12..6b840830c5 100644 --- a/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java +++ b/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java @@ -21,8 +21,6 @@ import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.ext.ima.ImaAdsLoader; -import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; -import com.google.android.exoplayer2.extractor.ExtractorsFactory; import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.ads.AdsMediaSource; @@ -69,13 +67,10 @@ import com.google.android.exoplayer2.util.Util; DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(context, Util.getUserAgent(context, context.getString(R.string.application_name))); - // Produces Extractor instances for parsing the content media (i.e. not the ad). - ExtractorsFactory extractorsFactory = new DefaultExtractorsFactory(); - // This is the MediaSource representing the content media (i.e. not the ad). String contentUrl = context.getString(R.string.content_url); - MediaSource contentMediaSource = new ExtractorMediaSource( - Uri.parse(contentUrl), dataSourceFactory, extractorsFactory, null, null); + MediaSource contentMediaSource = + new ExtractorMediaSource.Builder(Uri.parse(contentUrl), dataSourceFactory).build(); // Compose the content media source into a new AdsMediaSource with both ads and content. MediaSource mediaSourceWithAds = new AdsMediaSource(contentMediaSource, dataSourceFactory, diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index 3d669c9477..ca253db809 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -46,7 +46,6 @@ import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.drm.FrameworkMediaDrm; import com.google.android.exoplayer2.drm.HttpMediaDrmCallback; import com.google.android.exoplayer2.drm.UnsupportedDrmException; -import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; import com.google.android.exoplayer2.source.BehindLiveWindowException; @@ -379,8 +378,9 @@ public class PlayerActivity extends Activity implements OnClickListener, .setEventListener(mainHandler, eventLogger) .build(); case C.TYPE_OTHER: - return new ExtractorMediaSource(uri, mediaDataSourceFactory, new DefaultExtractorsFactory(), - mainHandler, eventLogger); + return new ExtractorMediaSource.Builder(uri, mediaDataSourceFactory) + .setEventListener(mainHandler, eventLogger) + .build(); default: { throw new IllegalStateException("Unsupported type: " + type); } 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 65fb4c8195..bd6e698dc6 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 @@ -76,12 +76,10 @@ public class FlacPlaybackTest extends InstrumentationTestCase { DefaultTrackSelector trackSelector = new DefaultTrackSelector(); player = ExoPlayerFactory.newInstance(new Renderer[] {audioRenderer}, trackSelector); player.addListener(this); - ExtractorMediaSource mediaSource = new ExtractorMediaSource( - uri, - new DefaultDataSourceFactory(context, "ExoPlayerExtFlacTest"), - MatroskaExtractor.FACTORY, - null, - null); + ExtractorMediaSource mediaSource = new ExtractorMediaSource.Builder( + uri, new DefaultDataSourceFactory(context, "ExoPlayerExtFlacTest")) + .setExtractorsFactory(MatroskaExtractor.FACTORY) + .build(); player.prepare(mediaSource); player.setPlayWhenReady(true); Looper.loop(); 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 591f43f38a..aa61df74d9 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 @@ -76,12 +76,10 @@ public class OpusPlaybackTest extends InstrumentationTestCase { DefaultTrackSelector trackSelector = new DefaultTrackSelector(); player = ExoPlayerFactory.newInstance(new Renderer[] {audioRenderer}, trackSelector); player.addListener(this); - ExtractorMediaSource mediaSource = new ExtractorMediaSource( - uri, - new DefaultDataSourceFactory(context, "ExoPlayerExtOpusTest"), - MatroskaExtractor.FACTORY, - null, - null); + ExtractorMediaSource mediaSource = new ExtractorMediaSource.Builder( + uri, new DefaultDataSourceFactory(context, "ExoPlayerExtOpusTest")) + .setExtractorsFactory(MatroskaExtractor.FACTORY) + .build(); player.prepare(mediaSource); player.setPlayWhenReady(true); Looper.loop(); 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 c2c1867a90..746f3d273f 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 @@ -105,12 +105,10 @@ public class VpxPlaybackTest extends InstrumentationTestCase { DefaultTrackSelector trackSelector = new DefaultTrackSelector(); player = ExoPlayerFactory.newInstance(new Renderer[] {videoRenderer}, trackSelector); player.addListener(this); - ExtractorMediaSource mediaSource = new ExtractorMediaSource( - uri, - new DefaultDataSourceFactory(context, "ExoPlayerExtVp9Test"), - MatroskaExtractor.FACTORY, - null, - null); + ExtractorMediaSource mediaSource = new ExtractorMediaSource.Builder( + uri, new DefaultDataSourceFactory(context, "ExoPlayerExtVp9Test")) + .setExtractorsFactory(MatroskaExtractor.FACTORY) + .build(); player.sendMessages(new ExoPlayer.ExoPlayerMessage(videoRenderer, LibvpxVideoRenderer.MSG_SET_OUTPUT_BUFFER_RENDERER, new VpxVideoSurfaceView(context))); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java index 1b3f6cb95c..066953b998 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java @@ -98,6 +98,123 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe private long timelineDurationUs; private boolean timelineIsSeekable; + /** + * Builder for {@link ExtractorMediaSource}. Each builder instance can only be used once. + */ + public static final class Builder { + + private final Uri uri; + private final DataSource.Factory dataSourceFactory; + + private ExtractorsFactory extractorsFactory; + private int minLoadableRetryCount; + private Handler eventHandler; + private EventListener eventListener; + private String customCacheKey; + private int continueLoadingCheckIntervalBytes; + private boolean isBuildCalled; + + /** + * @param uri The {@link Uri} of the media stream. + * @param dataSourceFactory A factory for {@link DataSource}s to read the media. + */ + public Builder(Uri uri, DataSource.Factory dataSourceFactory) { + this.uri = uri; + this.dataSourceFactory = dataSourceFactory; + + minLoadableRetryCount = MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA; + continueLoadingCheckIntervalBytes = DEFAULT_LOADING_CHECK_INTERVAL_BYTES; + } + + /** + * Sets the minimum number of times to retry if a loading error occurs. The default value is + * {@link #MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA}. + * + * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. + * @return This builder. + */ + public Builder setMinLoadableRetryCount(int minLoadableRetryCount) { + this.minLoadableRetryCount = minLoadableRetryCount; + return this; + } + + /** + * Sets the factory for {@link Extractor}s to process the media stream. Default value is an + * instance of {@link DefaultExtractorsFactory}. + * + * @param extractorsFactory A factory for {@link Extractor}s to process the media stream. If the + * possible formats are known, pass a factory that instantiates extractors for those + * formats. + * @return This builder. + */ + public Builder setExtractorsFactory(ExtractorsFactory extractorsFactory) { + this.extractorsFactory = extractorsFactory; + return this; + } + + /** + * Sets the custom key that uniquely identifies the original stream. Used for cache indexing. + * Default value is null. + * + * @param customCacheKey A custom key that uniquely identifies the original stream. Used for + * cache indexing. + * @return This builder. + */ + public Builder setCustomCacheKey(String customCacheKey) { + this.customCacheKey = customCacheKey; + return this; + } + + /** + * Sets the number of bytes that should be loaded between each invocation of + * {@link MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}. Default value + * is {@link #DEFAULT_LOADING_CHECK_INTERVAL_BYTES}. + * + * @param continueLoadingCheckIntervalBytes The number of bytes that should be loaded between + * each invocation of + * {@link MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}. + * @return This builder. + */ + public Builder setContinueLoadingCheckIntervalBytes(int continueLoadingCheckIntervalBytes) { + this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes; + return this; + } + + /** + * Sets the listener to respond to {@link ExtractorMediaSource} events and the handler to + * deliver these events. + * + * @param eventHandler A handler for events. + * @param eventListener A listener of events. + * @return This builder. + */ + public Builder setEventListener(Handler eventHandler, EventListener eventListener) { + this.eventHandler = eventHandler; + this.eventListener = eventListener; + return this; + } + + /** + * Builds a new {@link ExtractorMediaSource} using the current parameters. + *

          + * After this call, the builder should not be re-used. + * + * @return The newly built {@link ExtractorMediaSource}. + */ + public ExtractorMediaSource build() { + Assertions.checkArgument((eventListener == null) == (eventHandler == null)); + Assertions.checkState(!isBuildCalled); + isBuildCalled = true; + if (extractorsFactory == null) { + extractorsFactory = new DefaultExtractorsFactory(); + } + return new ExtractorMediaSource(uri, dataSourceFactory, extractorsFactory, + minLoadableRetryCount, eventHandler, eventListener, customCacheKey, + continueLoadingCheckIntervalBytes); + } + + } + /** * @param uri The {@link Uri} of the media stream. * @param dataSourceFactory A factory for {@link DataSource}s to read the media. @@ -106,7 +223,9 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe * Otherwise, pass a {@link DefaultExtractorsFactory} to use default extractors. * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. + * @deprecated Use {@link Builder} instead. */ + @Deprecated public ExtractorMediaSource(Uri uri, DataSource.Factory dataSourceFactory, ExtractorsFactory extractorsFactory, Handler eventHandler, EventListener eventListener) { this(uri, dataSourceFactory, extractorsFactory, eventHandler, eventListener, null); @@ -122,7 +241,9 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe * @param eventListener A listener of events. May be null if delivery of events is not required. * @param customCacheKey A custom key that uniquely identifies the original stream. Used for cache * indexing. May be null. + * @deprecated Use {@link Builder} instead. */ + @Deprecated public ExtractorMediaSource(Uri uri, DataSource.Factory dataSourceFactory, ExtractorsFactory extractorsFactory, Handler eventHandler, EventListener eventListener, String customCacheKey) { @@ -143,7 +264,9 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe * indexing. May be null. * @param continueLoadingCheckIntervalBytes The number of bytes that should be loaded between each * invocation of {@link MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}. + * @deprecated Use {@link Builder} instead. */ + @Deprecated public ExtractorMediaSource(Uri uri, DataSource.Factory dataSourceFactory, ExtractorsFactory extractorsFactory, int minLoadableRetryCount, Handler eventHandler, EventListener eventListener, String customCacheKey, int continueLoadingCheckIntervalBytes) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java index 18aa8a63e7..397b8effd3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java @@ -23,7 +23,6 @@ import android.view.ViewGroup; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; @@ -173,9 +172,10 @@ public final class AdsMediaSource implements MediaSource { final int adGroupIndex = id.adGroupIndex; final int adIndexInAdGroup = id.adIndexInAdGroup; if (adGroupMediaSources[adGroupIndex].length <= adIndexInAdGroup) { - MediaSource adMediaSource = new ExtractorMediaSource( - adPlaybackState.adUris[id.adGroupIndex][id.adIndexInAdGroup], dataSourceFactory, - new DefaultExtractorsFactory(), mainHandler, componentListener); + MediaSource adMediaSource = new ExtractorMediaSource.Builder( + adPlaybackState.adUris[id.adGroupIndex][id.adIndexInAdGroup], dataSourceFactory) + .setEventListener(mainHandler, componentListener) + .build(); int oldAdCount = adGroupMediaSources[id.adGroupIndex].length; if (adIndexInAdGroup >= oldAdCount) { int adCount = adIndexInAdGroup + 1; From bb4be4314a942998e895114e471bd38c3e2ce62e Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 17 Nov 2017 05:26:58 -0800 Subject: [PATCH 0875/2472] Add simplified FakeTimeline constructor. This is helpful for tests which don't care about detailled timeline set-ups besides the number of windows. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176097369 --- .../android/exoplayer2/ExoPlayerTest.java | 42 +++------- .../testutil/ExoPlayerTestRunner.java | 8 +- .../exoplayer2/testutil/FakeTimeline.java | 76 ++++++++++++++++++- 3 files changed, 89 insertions(+), 37 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index 0edd19bc09..95d5d96163 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -27,7 +27,6 @@ import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeRenderer; import com.google.android.exoplayer2.testutil.FakeShuffleOrder; import com.google.android.exoplayer2.testutil.FakeTimeline; -import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.testutil.FakeTrackSelection; import com.google.android.exoplayer2.testutil.FakeTrackSelector; import java.util.ArrayList; @@ -68,7 +67,7 @@ public final class ExoPlayerTest extends TestCase { * Tests playback of a source that exposes a single period. */ public void testPlaySinglePeriodTimeline() throws Exception { - Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(false, false, 0)); + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); Object manifest = new Object(); FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() @@ -87,10 +86,7 @@ public final class ExoPlayerTest extends TestCase { * Tests playback of a source that exposes three periods. */ public void testPlayMultiPeriodTimeline() throws Exception { - Timeline timeline = new FakeTimeline( - new TimelineWindowDefinition(false, false, 0), - new TimelineWindowDefinition(false, false, 0), - new TimelineWindowDefinition(false, false, 0)); + Timeline timeline = new FakeTimeline(/* windowCount= */ 3); FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() .setTimeline(timeline).setRenderers(renderer) @@ -107,10 +103,7 @@ public final class ExoPlayerTest extends TestCase { * source. */ public void testReadAheadToEndDoesNotResetRenderer() throws Exception { - Timeline timeline = new FakeTimeline( - new TimelineWindowDefinition(false, false, 10), - new TimelineWindowDefinition(false, false, 10), - new TimelineWindowDefinition(false, false, 10)); + Timeline timeline = new FakeTimeline(/* windowCount= */ 3); final FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT); FakeMediaClockRenderer audioRenderer = new FakeMediaClockRenderer(Builder.AUDIO_FORMAT) { @@ -151,7 +144,7 @@ public final class ExoPlayerTest extends TestCase { } public void testRepreparationGivesFreshSourceInfo() throws Exception { - Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(false, false, 0)); + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); Object firstSourceManifest = new Object(); MediaSource firstSource = new FakeMediaSource(timeline, firstSourceManifest, @@ -218,10 +211,7 @@ public final class ExoPlayerTest extends TestCase { } public void testRepeatModeChanges() throws Exception { - Timeline timeline = new FakeTimeline( - new TimelineWindowDefinition(true, false, 100000), - new TimelineWindowDefinition(true, false, 100000), - new TimelineWindowDefinition(true, false, 100000)); + Timeline timeline = new FakeTimeline(/* windowCount= */ 3); FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); ActionSchedule actionSchedule = new ActionSchedule.Builder("testRepeatMode") // 0 -> 1 .waitForPositionDiscontinuity().setRepeatMode(Player.REPEAT_MODE_ONE) // 1 -> 1 @@ -241,7 +231,7 @@ public final class ExoPlayerTest extends TestCase { } public void testShuffleModeEnabledChanges() throws Exception { - Timeline fakeTimeline = new FakeTimeline(new TimelineWindowDefinition(true, false, 100000)); + Timeline fakeTimeline = new FakeTimeline(/* windowCount= */ 1); MediaSource[] fakeMediaSources = { new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT), new FakeMediaSource(fakeTimeline, null, Builder.VIDEO_FORMAT), @@ -264,7 +254,6 @@ public final class ExoPlayerTest extends TestCase { } public void testPeriodHoldersReleasedAfterSeekWithRepeatModeAll() throws Exception { - Timeline fakeTimeline = new FakeTimeline(new TimelineWindowDefinition(true, false, 100000)); FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); ActionSchedule actionSchedule = new ActionSchedule.Builder("testPeriodHoldersReleased") .setRepeatMode(Player.REPEAT_MODE_ALL) @@ -274,15 +263,13 @@ public final class ExoPlayerTest extends TestCase { .setRepeatMode(Player.REPEAT_MODE_OFF) // Turn off repeat so that playback can finish. .build(); new ExoPlayerTestRunner.Builder() - .setTimeline(fakeTimeline).setRenderers(renderer).setActionSchedule(actionSchedule) + .setRenderers(renderer).setActionSchedule(actionSchedule) .build().start().blockUntilEnded(TIMEOUT_MS); assertTrue(renderer.isEnded); } public void testSeekProcessedCallback() throws Exception { - Timeline timeline = new FakeTimeline( - new TimelineWindowDefinition(true, false, 100000), - new TimelineWindowDefinition(true, false, 100000)); + Timeline timeline = new FakeTimeline(/* windowCount= */ 2); ActionSchedule actionSchedule = new ActionSchedule.Builder("testSeekProcessedCallback") // Initial seek before timeline preparation finished. .pause().seek(10).waitForPlaybackState(Player.STATE_READY) @@ -314,8 +301,7 @@ public final class ExoPlayerTest extends TestCase { } public void testAllActivatedTrackSelectionAreReleasedForSinglePeriod() throws Exception { - Timeline timeline = - new FakeTimeline(new TimelineWindowDefinition(false, false, /* durationUs= */ 500_000)); + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); MediaSource mediaSource = new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT); FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT); @@ -343,9 +329,7 @@ public final class ExoPlayerTest extends TestCase { } public void testAllActivatedTrackSelectionAreReleasedForMultiPeriods() throws Exception { - Timeline timeline = - new FakeTimeline(new TimelineWindowDefinition(false, false, /* durationUs= */ 500_000), - new TimelineWindowDefinition(false, false, /* durationUs= */ 500_000)); + Timeline timeline = new FakeTimeline(/* windowCount= */ 2); MediaSource mediaSource = new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT); FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT); @@ -374,8 +358,7 @@ public final class ExoPlayerTest extends TestCase { public void testAllActivatedTrackSelectionAreReleasedWhenTrackSelectionsAreRemade() throws Exception { - Timeline timeline = - new FakeTimeline(new TimelineWindowDefinition(false, false, /* durationUs= */ 500_000)); + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); MediaSource mediaSource = new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT); FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT); @@ -414,8 +397,7 @@ public final class ExoPlayerTest extends TestCase { public void testAllActivatedTrackSelectionAreReleasedWhenTrackSelectionsAreUsed() throws Exception { - Timeline timeline = - new FakeTimeline(new TimelineWindowDefinition(false, false, /* durationUs= */ 500_000)); + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); MediaSource mediaSource = new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT, Builder.AUDIO_FORMAT); FakeRenderer videoRenderer = new FakeRenderer(Builder.VIDEO_FORMAT); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java index a1f8fc7861..591e63dc5b 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java @@ -32,7 +32,6 @@ import com.google.android.exoplayer2.metadata.MetadataOutput; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner.Builder.PlayerFactory; -import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.text.TextOutput; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.MappingTrackSelector; @@ -103,8 +102,9 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { /** * Sets a {@link Timeline} to be used by a {@link FakeMediaSource} in the test runner. The - * default value is a non-seekable, non-dynamic {@link FakeTimeline} with zero duration. Setting - * the timeline is not allowed after a call to {@link #setMediaSource(MediaSource)}. + * default value is a seekable, non-dynamic {@link FakeTimeline} with a duration of + * {@link FakeTimeline.TimelineWindowDefinition#DEFAULT_WINDOW_DURATION_US}. Setting the + * timeline is not allowed after a call to {@link #setMediaSource(MediaSource)}. * * @param timeline A {@link Timeline} to be used by a {@link FakeMediaSource} in the test * runner. @@ -294,7 +294,7 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { } if (mediaSource == null) { if (timeline == null) { - timeline = new FakeTimeline(new TimelineWindowDefinition(false, false, 0)); + timeline = new FakeTimeline(1); } mediaSource = new FakeMediaSource(timeline, manifest, supportedFormats); } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java index 2937ee2770..4a9d79f906 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java @@ -30,7 +30,10 @@ public final class FakeTimeline extends Timeline { */ public static final class TimelineWindowDefinition { - private static final int WINDOW_DURATION_US = 100000; + /** + * Default test window duration in microseconds. + */ + public static final int DEFAULT_WINDOW_DURATION_US = 100_000; public final int periodCount; public final Object id; @@ -40,19 +43,65 @@ public final class FakeTimeline extends Timeline { public final int adGroupsPerPeriodCount; public final int adsPerAdGroupCount; - public TimelineWindowDefinition(int periodCount, Object id) { - this(periodCount, id, true, false, WINDOW_DURATION_US); + /** + * Creates a seekable, non-dynamic window definition with one period with a duration of + * {@link #DEFAULT_WINDOW_DURATION_US}. + */ + public TimelineWindowDefinition() { + this(1, 0, true, false, DEFAULT_WINDOW_DURATION_US); } + /** + * Creates a seekable, non-dynamic window definition with a duration of + * {@link #DEFAULT_WINDOW_DURATION_US}. + * + * @param periodCount The number of periods in the window. Each period get an equal slice of the + * total window duration. + * @param id The UID of the window. + */ + public TimelineWindowDefinition(int periodCount, Object id) { + this(periodCount, id, true, false, DEFAULT_WINDOW_DURATION_US); + } + + /** + * Creates a window definition with one period. + * + * @param isSeekable Whether the window is seekable. + * @param isDynamic Whether the window is dynamic. + * @param durationUs The duration of the window in microseconds. + */ public TimelineWindowDefinition(boolean isSeekable, boolean isDynamic, long durationUs) { this(1, 0, isSeekable, isDynamic, durationUs); } + /** + * Creates a window definition. + * + * @param periodCount The number of periods in the window. Each period get an equal slice of the + * total window duration. + * @param id The UID of the window. + * @param isSeekable Whether the window is seekable. + * @param isDynamic Whether the window is dynamic. + * @param durationUs The duration of the window in microseconds. + */ public TimelineWindowDefinition(int periodCount, Object id, boolean isSeekable, boolean isDynamic, long durationUs) { this(periodCount, id, isSeekable, isDynamic, durationUs, 0, 0); } + /** + * Creates a window definition with ad groups. + * + * @param periodCount The number of periods in the window. Each period get an equal slice of the + * total window duration. + * @param id The UID of the window. + * @param isSeekable Whether the window is seekable. + * @param isDynamic Whether the window is dynamic. + * @param durationUs The duration of the window in microseconds. + * @param adGroupsCountPerPeriod The number of ad groups in each period. The position of the ad + * groups is equally distributed in each period starting. + * @param adsPerAdGroupCount The number of ads in each ad group. + */ public TimelineWindowDefinition(int periodCount, Object id, boolean isSeekable, boolean isDynamic, long durationUs, int adGroupsCountPerPeriod, int adsPerAdGroupCount) { this.periodCount = periodCount; @@ -71,6 +120,21 @@ public final class FakeTimeline extends Timeline { private final TimelineWindowDefinition[] windowDefinitions; private final int[] periodOffsets; + /** + * Creates a fake timeline with the given number of seekable, non-dynamic windows with one period + * with a duration of {@link TimelineWindowDefinition#DEFAULT_WINDOW_DURATION_US} each. + * + * @param windowCount The number of windows. + */ + public FakeTimeline(int windowCount) { + this(createDefaultWindowDefinitions(windowCount)); + } + + /** + * Creates a fake timeline with the given window definitions. + * + * @param windowDefinitions A list of {@link TimelineWindowDefinition}s. + */ public FakeTimeline(TimelineWindowDefinition... windowDefinitions) { this.windowDefinitions = windowDefinitions; periodOffsets = new int[windowDefinitions.length + 1]; @@ -141,4 +205,10 @@ public final class FakeTimeline extends Timeline { return index >= 0 && index < getPeriodCount() ? index : C.INDEX_UNSET; } + private static TimelineWindowDefinition[] createDefaultWindowDefinitions(int windowCount) { + TimelineWindowDefinition[] windowDefinitions = new TimelineWindowDefinition[windowCount]; + Arrays.fill(windowDefinitions, new TimelineWindowDefinition()); + return windowDefinitions; + } + } From 14299e0643cca287e3c4920c258ef9340e7ba38f Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 17 Nov 2017 09:18:06 -0800 Subject: [PATCH 0876/2472] Simplify LoopingMediaSourceTest setup This test seems to obtain a timeline from a prepared FakeMediaSource, but that's identical to the timeline passed into that source to start with :). ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176117133 --- .../exoplayer2/source/LoopingMediaSourceTest.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java index 2c8deb74b4..79f646b5c4 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java @@ -30,12 +30,13 @@ import junit.framework.TestCase; */ public class LoopingMediaSourceTest extends TestCase { - private final Timeline multiWindowTimeline; + private FakeTimeline multiWindowTimeline; - public LoopingMediaSourceTest() { - multiWindowTimeline = TestUtil.extractTimelineFromMediaSource(new FakeMediaSource( - new FakeTimeline(new TimelineWindowDefinition(1, 111), - new TimelineWindowDefinition(1, 222), new TimelineWindowDefinition(1, 333)), null)); + @Override + public void setUp() throws Exception { + super.setUp(); + multiWindowTimeline = new FakeTimeline(new TimelineWindowDefinition(1, 111), + new TimelineWindowDefinition(1, 222), new TimelineWindowDefinition(1, 333)); } public void testSingleLoop() { From 1b7c950d1e6639e2a2f10140598d6646c8257122 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 17 Nov 2017 09:22:17 -0800 Subject: [PATCH 0877/2472] Add MediaSourceTestRunner for MediaSource tests. - MediaSourceTestRunner aims to encapsulate some of the logic currently used in DynamicConcatenatingMediaSourceTest, so it can be re-used for testing other MediaSource implementations. - The change also fixes DynamicConcatenatingMediaSourceTest to execute calls on the correct threads, and to release handler threads at the end of each test. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176117535 --- .../source/ConcatenatingMediaSourceTest.java | 29 +- .../DynamicConcatenatingMediaSourceTest.java | 561 ++++++++---------- .../testutil/MediaSourceTestRunner.java | 290 +++++++++ .../android/exoplayer2/testutil/TestUtil.java | 1 + .../exoplayer2/testutil/TimelineAsserts.java | 50 -- 5 files changed, 572 insertions(+), 359 deletions(-) create mode 100644 testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java index 6f6556225e..429325defc 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java @@ -23,6 +23,7 @@ import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeShuffleOrder; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; +import com.google.android.exoplayer2.testutil.MediaSourceTestRunner; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.testutil.TimelineAsserts; import junit.framework.TestCase; @@ -32,6 +33,8 @@ import junit.framework.TestCase; */ public final class ConcatenatingMediaSourceTest extends TestCase { + private static final int TIMEOUT_MS = 10000; + public void testEmptyConcatenation() { for (boolean atomic : new boolean[] {false, true}) { Timeline timeline = getConcatenatedTimeline(atomic); @@ -208,18 +211,22 @@ public final class ConcatenatingMediaSourceTest extends TestCase { ConcatenatingMediaSource mediaSource = new ConcatenatingMediaSource(mediaSourceContentOnly, mediaSourceWithAds); - // Prepare and assert timeline contains ad groups. - Timeline timeline = TestUtil.extractTimelineFromMediaSource(mediaSource); - TimelineAsserts.assertAdGroupCounts(timeline, 0, 0, 1, 1); + MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null, TIMEOUT_MS); + try { + Timeline timeline = testRunner.prepareSource(); + TimelineAsserts.assertAdGroupCounts(timeline, 0, 0, 1, 1); - // Create all periods and assert period creation of child media sources has been called. - TimelineAsserts.assertAllPeriodsCanBeCreatedPreparedAndReleased(mediaSource, timeline, 10_000); - mediaSourceContentOnly.assertMediaPeriodCreated(new MediaPeriodId(0)); - mediaSourceContentOnly.assertMediaPeriodCreated(new MediaPeriodId(1)); - mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(0)); - mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(1)); - mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(0, 0, 0)); - mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(1, 0, 0)); + // Create all periods and assert period creation of child media sources has been called. + testRunner.assertPrepareAndReleaseAllPeriods(); + mediaSourceContentOnly.assertMediaPeriodCreated(new MediaPeriodId(0)); + mediaSourceContentOnly.assertMediaPeriodCreated(new MediaPeriodId(1)); + mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(0)); + mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(1)); + mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(0, 0, 0)); + mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(1, 0, 0)); + } finally { + testRunner.release(); + } } /** diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java index 96d11678c9..536180fafc 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java @@ -20,19 +20,15 @@ import static org.mockito.Mockito.verify; import android.os.ConditionVariable; import android.os.Handler; import android.os.HandlerThread; -import android.os.Message; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.source.MediaPeriod.Callback; -import com.google.android.exoplayer2.source.MediaSource.Listener; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeShuffleOrder; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; -import com.google.android.exoplayer2.testutil.StubExoPlayer; +import com.google.android.exoplayer2.testutil.MediaSourceTestRunner; import com.google.android.exoplayer2.testutil.TimelineAsserts; import java.util.Arrays; import junit.framework.TestCase; @@ -45,78 +41,84 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { private static final int TIMEOUT_MS = 10000; - private Timeline timeline; - private boolean timelineUpdated; - private boolean customRunnableCalled; + private DynamicConcatenatingMediaSource mediaSource; + private MediaSourceTestRunner testRunner; - public void testPlaylistChangesAfterPreparation() throws InterruptedException { - timeline = null; - FakeMediaSource[] childSources = createMediaSources(7); - DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource( - new FakeShuffleOrder(0)); - prepareAndListenToTimelineUpdates(mediaSource); - assertNotNull(timeline); - waitForTimelineUpdate(); + @Override + public void setUp() { + mediaSource = new DynamicConcatenatingMediaSource(new FakeShuffleOrder(0)); + testRunner = new MediaSourceTestRunner(mediaSource, null, TIMEOUT_MS); + } + + @Override + public void tearDown() { + testRunner.release(); + } + + public void testPlaylistChangesAfterPreparation() { + Timeline timeline = testRunner.prepareSource(); TimelineAsserts.assertEmpty(timeline); + FakeMediaSource[] childSources = createMediaSources(7); + // Add first source. mediaSource.addMediaSource(childSources[0]); - waitForTimelineUpdate(); + timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertPeriodCounts(timeline, 1); TimelineAsserts.assertWindowIds(timeline, 111); // Add at front of queue. mediaSource.addMediaSource(0, childSources[1]); - waitForTimelineUpdate(); + timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertPeriodCounts(timeline, 2, 1); TimelineAsserts.assertWindowIds(timeline, 222, 111); // Add at back of queue. mediaSource.addMediaSource(childSources[2]); - waitForTimelineUpdate(); + timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertPeriodCounts(timeline, 2, 1, 3); TimelineAsserts.assertWindowIds(timeline, 222, 111, 333); // Add in the middle. mediaSource.addMediaSource(1, childSources[3]); - waitForTimelineUpdate(); + timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 1, 3); TimelineAsserts.assertWindowIds(timeline, 222, 444, 111, 333); // Add bulk. - mediaSource.addMediaSources(3, Arrays.asList((MediaSource) childSources[4], - (MediaSource) childSources[5], (MediaSource) childSources[6])); - waitForTimelineUpdate(); + mediaSource.addMediaSources(3, Arrays.asList(childSources[4], childSources[5], + childSources[6])); + timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 1, 5, 6, 7, 3); TimelineAsserts.assertWindowIds(timeline, 222, 444, 111, 555, 666, 777, 333); // Move sources. mediaSource.moveMediaSource(2, 3); - waitForTimelineUpdate(); + timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 5, 1, 6, 7, 3); TimelineAsserts.assertWindowIds(timeline, 222, 444, 555, 111, 666, 777, 333); mediaSource.moveMediaSource(3, 2); - waitForTimelineUpdate(); + timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 1, 5, 6, 7, 3); TimelineAsserts.assertWindowIds(timeline, 222, 444, 111, 555, 666, 777, 333); mediaSource.moveMediaSource(0, 6); - waitForTimelineUpdate(); + timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertPeriodCounts(timeline, 4, 1, 5, 6, 7, 3, 2); TimelineAsserts.assertWindowIds(timeline, 444, 111, 555, 666, 777, 333, 222); mediaSource.moveMediaSource(6, 0); - waitForTimelineUpdate(); + timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertPeriodCounts(timeline, 2, 4, 1, 5, 6, 7, 3); TimelineAsserts.assertWindowIds(timeline, 222, 444, 111, 555, 666, 777, 333); // Remove in the middle. mediaSource.removeMediaSource(3); - waitForTimelineUpdate(); + testRunner.assertTimelineChangeBlocking(); mediaSource.removeMediaSource(3); - waitForTimelineUpdate(); + testRunner.assertTimelineChangeBlocking(); mediaSource.removeMediaSource(3); - waitForTimelineUpdate(); + testRunner.assertTimelineChangeBlocking(); mediaSource.removeMediaSource(1); - waitForTimelineUpdate(); + timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertPeriodCounts(timeline, 2, 1, 3); TimelineAsserts.assertWindowIds(timeline, 222, 111, 333); for (int i = 3; i <= 6; i++) { @@ -146,35 +148,31 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { assertEquals(0, timeline.getLastWindowIndex(true)); // Assert all periods can be prepared. - TimelineAsserts.assertAllPeriodsCanBeCreatedPreparedAndReleased(mediaSource, timeline, - TIMEOUT_MS); + testRunner.assertPrepareAndReleaseAllPeriods(); // Remove at front of queue. mediaSource.removeMediaSource(0); - waitForTimelineUpdate(); + timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertPeriodCounts(timeline, 1, 3); TimelineAsserts.assertWindowIds(timeline, 111, 333); childSources[1].assertReleased(); // Remove at back of queue. mediaSource.removeMediaSource(1); - waitForTimelineUpdate(); + timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertPeriodCounts(timeline, 1); TimelineAsserts.assertWindowIds(timeline, 111); childSources[2].assertReleased(); // Remove last source. mediaSource.removeMediaSource(0); - waitForTimelineUpdate(); + timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertEmpty(timeline); childSources[3].assertReleased(); } - public void testPlaylistChangesBeforePreparation() throws InterruptedException { - timeline = null; + public void testPlaylistChangesBeforePreparation() { FakeMediaSource[] childSources = createMediaSources(4); - DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource( - new FakeShuffleOrder(0)); mediaSource.addMediaSource(childSources[0]); mediaSource.addMediaSource(childSources[1]); mediaSource.addMediaSource(0, childSources[2]); @@ -182,11 +180,9 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { mediaSource.removeMediaSource(0); mediaSource.moveMediaSource(1, 0); mediaSource.addMediaSource(1, childSources[3]); - assertNull(timeline); + testRunner.assertNoTimelineChange(); - prepareAndListenToTimelineUpdates(mediaSource); - waitForTimelineUpdate(); - assertNotNull(timeline); + Timeline timeline = testRunner.prepareSource(); TimelineAsserts.assertPeriodCounts(timeline, 3, 4, 2); TimelineAsserts.assertWindowIds(timeline, 333, 444, 222); TimelineAsserts.assertNextWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, @@ -198,98 +194,94 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, true, 1, 2, C.INDEX_UNSET); - TimelineAsserts.assertAllPeriodsCanBeCreatedPreparedAndReleased(mediaSource, timeline, - TIMEOUT_MS); + testRunner.assertPrepareAndReleaseAllPeriods(); mediaSource.releaseSource(); for (int i = 1; i < 4; i++) { childSources[i].assertReleased(); } } - public void testPlaylistWithLazyMediaSource() throws InterruptedException { - timeline = null; - + public void testPlaylistWithLazyMediaSource() { // Create some normal (immediately preparing) sources and some lazy sources whose timeline // updates need to be triggered. FakeMediaSource[] fastSources = createMediaSources(2); - FakeMediaSource[] lazySources = new FakeMediaSource[4]; + final FakeMediaSource[] lazySources = new FakeMediaSource[4]; for (int i = 0; i < 4; i++) { lazySources[i] = new FakeMediaSource(null, null); } // Add lazy sources and normal sources before preparation. Also remove one lazy source again // before preparation to check it doesn't throw or change the result. - DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); mediaSource.addMediaSource(lazySources[0]); mediaSource.addMediaSource(0, fastSources[0]); mediaSource.removeMediaSource(1); mediaSource.addMediaSource(1, lazySources[1]); - assertNull(timeline); + testRunner.assertNoTimelineChange(); // Prepare and assert that the timeline contains all information for normal sources while having // placeholder information for lazy sources. - prepareAndListenToTimelineUpdates(mediaSource); - waitForTimelineUpdate(); - assertNotNull(timeline); + Timeline timeline = testRunner.prepareSource(); TimelineAsserts.assertPeriodCounts(timeline, 1, 1); TimelineAsserts.assertWindowIds(timeline, 111, null); TimelineAsserts.assertWindowIsDynamic(timeline, false, true); // Trigger source info refresh for lazy source and check that the timeline now contains all // information for all windows. - lazySources[1].setNewSourceInfo(createFakeTimeline(8), null); - waitForTimelineUpdate(); + testRunner.runOnPlaybackThread(new Runnable() { + @Override + public void run() { + lazySources[1].setNewSourceInfo(createFakeTimeline(8), null); + } + }); + timeline = testRunner.assertTimelineChange(); TimelineAsserts.assertPeriodCounts(timeline, 1, 9); TimelineAsserts.assertWindowIds(timeline, 111, 999); TimelineAsserts.assertWindowIsDynamic(timeline, false, false); - TimelineAsserts.assertAllPeriodsCanBeCreatedPreparedAndReleased(mediaSource, timeline, - TIMEOUT_MS); + testRunner.assertPrepareAndReleaseAllPeriods(); // Add further lazy and normal sources after preparation. Also remove one lazy source again to // check it doesn't throw or change the result. mediaSource.addMediaSource(1, lazySources[2]); - waitForTimelineUpdate(); + testRunner.assertTimelineChangeBlocking(); mediaSource.addMediaSource(2, fastSources[1]); - waitForTimelineUpdate(); + testRunner.assertTimelineChangeBlocking(); mediaSource.addMediaSource(0, lazySources[3]); - waitForTimelineUpdate(); + testRunner.assertTimelineChangeBlocking(); mediaSource.removeMediaSource(2); - waitForTimelineUpdate(); + timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertPeriodCounts(timeline, 1, 1, 2, 9); TimelineAsserts.assertWindowIds(timeline, null, 111, 222, 999); TimelineAsserts.assertWindowIsDynamic(timeline, true, false, false, false); // Create a period from an unprepared lazy media source and assert Callback.onPrepared is not // called yet. - MediaPeriod lazyPeriod = mediaSource.createPeriod(new MediaPeriodId(0), null); - assertNotNull(lazyPeriod); - final ConditionVariable lazyPeriodPrepared = new ConditionVariable(); - lazyPeriod.prepare(new Callback() { - @Override - public void onPrepared(MediaPeriod mediaPeriod) { - lazyPeriodPrepared.open(); - } - @Override - public void onContinueLoadingRequested(MediaPeriod source) {} - }, 0); - assertFalse(lazyPeriodPrepared.block(1)); + MediaPeriod lazyPeriod = testRunner.createPeriod(new MediaPeriodId(0)); + ConditionVariable preparedCondition = testRunner.preparePeriod(lazyPeriod, 0); + assertFalse(preparedCondition.block(1)); + // Assert that a second period can also be created and released without problems. - MediaPeriod secondLazyPeriod = mediaSource.createPeriod(new MediaPeriodId(0), null); - assertNotNull(secondLazyPeriod); - mediaSource.releasePeriod(secondLazyPeriod); + MediaPeriod secondLazyPeriod = testRunner.createPeriod(new MediaPeriodId(0)); + testRunner.releasePeriod(secondLazyPeriod); // Trigger source info refresh for lazy media source. Assert that now all information is // available again and the previously created period now also finished preparing. - lazySources[3].setNewSourceInfo(createFakeTimeline(7), null); - waitForTimelineUpdate(); + testRunner.runOnPlaybackThread(new Runnable() { + @Override + public void run() { + lazySources[3].setNewSourceInfo(createFakeTimeline(7), null); + } + }); + timeline = testRunner.assertTimelineChange(); TimelineAsserts.assertPeriodCounts(timeline, 8, 1, 2, 9); TimelineAsserts.assertWindowIds(timeline, 888, 111, 222, 999); TimelineAsserts.assertWindowIsDynamic(timeline, false, false, false, false); - assertTrue(lazyPeriodPrepared.block(TIMEOUT_MS)); - mediaSource.releasePeriod(lazyPeriod); + assertTrue(preparedCondition.block(1)); - // Release media source and assert all normal and lazy media sources are fully released as well. - mediaSource.releaseSource(); + // Release the period and source. + testRunner.releasePeriod(lazyPeriod); + testRunner.releaseSource(); + + // Assert all sources were fully released. for (FakeMediaSource fastSource : fastSources) { fastSource.assertReleased(); } @@ -298,17 +290,12 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { } } - public void testEmptyTimelineMediaSource() throws InterruptedException { - timeline = null; - DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource( - new FakeShuffleOrder(0)); - prepareAndListenToTimelineUpdates(mediaSource); - assertNotNull(timeline); - waitForTimelineUpdate(); + public void testEmptyTimelineMediaSource() { + Timeline timeline = testRunner.prepareSource(); TimelineAsserts.assertEmpty(timeline); mediaSource.addMediaSource(new FakeMediaSource(Timeline.EMPTY, null)); - waitForTimelineUpdate(); + timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertEmpty(timeline); mediaSource.addMediaSources(Arrays.asList(new MediaSource[] { @@ -316,18 +303,18 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { new FakeMediaSource(Timeline.EMPTY, null), new FakeMediaSource(Timeline.EMPTY, null), new FakeMediaSource(Timeline.EMPTY, null), new FakeMediaSource(Timeline.EMPTY, null) })); - waitForTimelineUpdate(); + timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertEmpty(timeline); // Insert non-empty media source to leave empty sources at the start, the end, and the middle // (with single and multiple empty sources in a row). MediaSource[] mediaSources = createMediaSources(3); mediaSource.addMediaSource(1, mediaSources[0]); - waitForTimelineUpdate(); + testRunner.assertTimelineChangeBlocking(); mediaSource.addMediaSource(4, mediaSources[1]); - waitForTimelineUpdate(); + testRunner.assertTimelineChangeBlocking(); mediaSource.addMediaSource(6, mediaSources[2]); - waitForTimelineUpdate(); + timeline = testRunner.assertTimelineChangeBlocking(); TimelineAsserts.assertWindowIds(timeline, 111, 222, 333); TimelineAsserts.assertPeriodCounts(timeline, 1, 2, 3); TimelineAsserts.assertPreviousWindowIndices(timeline, Player.REPEAT_MODE_OFF, false, @@ -350,12 +337,10 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { assertEquals(2, timeline.getLastWindowIndex(false)); assertEquals(2, timeline.getFirstWindowIndex(true)); assertEquals(0, timeline.getLastWindowIndex(true)); - TimelineAsserts.assertAllPeriodsCanBeCreatedPreparedAndReleased(mediaSource, timeline, - TIMEOUT_MS); + testRunner.assertPrepareAndReleaseAllPeriods(); } public void testIllegalArguments() { - DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); MediaSource validSource = new FakeMediaSource(createFakeTimeline(1), null); // Null sources. @@ -394,7 +379,6 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { } public void testCustomCallbackBeforePreparationAddSingle() { - DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); Runnable runnable = Mockito.mock(Runnable.class); mediaSource.addMediaSource(createFakeMediaSource(), runnable); @@ -402,7 +386,6 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { } public void testCustomCallbackBeforePreparationAddMultiple() { - DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); Runnable runnable = Mockito.mock(Runnable.class); mediaSource.addMediaSources(Arrays.asList( @@ -411,7 +394,6 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { } public void testCustomCallbackBeforePreparationAddSingleWithIndex() { - DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); Runnable runnable = Mockito.mock(Runnable.class); mediaSource.addMediaSource(/* index */ 0, createFakeMediaSource(), runnable); @@ -419,134 +401,159 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { } public void testCustomCallbackBeforePreparationAddMultipleWithIndex() { - DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); Runnable runnable = Mockito.mock(Runnable.class); - mediaSource.addMediaSources(/* index */ 0, Arrays.asList( - new MediaSource[]{createFakeMediaSource(), createFakeMediaSource()}), runnable); + mediaSource.addMediaSources(/* index */ 0, + Arrays.asList(new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()}), + runnable); verify(runnable).run(); } - public void testCustomCallbackBeforePreparationRemove() throws InterruptedException { - DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); + public void testCustomCallbackBeforePreparationRemove() { Runnable runnable = Mockito.mock(Runnable.class); - mediaSource.addMediaSource(createFakeMediaSource()); + mediaSource.addMediaSource(createFakeMediaSource()); mediaSource.removeMediaSource(/* index */ 0, runnable); verify(runnable).run(); } - public void testCustomCallbackBeforePreparationMove() throws InterruptedException { - DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); + public void testCustomCallbackBeforePreparationMove() { Runnable runnable = Mockito.mock(Runnable.class); - mediaSource.addMediaSources(Arrays.asList( - new MediaSource[]{createFakeMediaSource(), createFakeMediaSource()})); + mediaSource.addMediaSources( + Arrays.asList(new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()})); mediaSource.moveMediaSource(/* fromIndex */ 1, /* toIndex */ 0, runnable); verify(runnable).run(); } - public void testCustomCallbackAfterPreparationAddSingle() throws InterruptedException { - final DynamicConcatenatingMediaSourceAndHandler sourceHandlerPair = - setUpDynamicMediaSourceOnHandlerThread(); - final Runnable runnable = createCustomRunnable(); - - sourceHandlerPair.mainHandler.post(new Runnable() { - @Override - public void run() { - sourceHandlerPair.mediaSource.addMediaSource(createFakeMediaSource(), runnable); - } - }); - waitForCustomRunnable(); + public void testCustomCallbackAfterPreparationAddSingle() { + DummyMainThread dummyMainThread = new DummyMainThread(); + try { + testRunner.prepareSource(); + final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner); + dummyMainThread.runOnMainThread(new Runnable() { + @Override + public void run() { + mediaSource.addMediaSource(createFakeMediaSource(), timelineGrabber); + } + }); + Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); + assertEquals(1, timeline.getWindowCount()); + } finally { + dummyMainThread.release(); + } } - public void testCustomCallbackAfterPreparationAddMultiple() throws InterruptedException { - final DynamicConcatenatingMediaSourceAndHandler sourceHandlerPair = - setUpDynamicMediaSourceOnHandlerThread(); - final Runnable runnable = createCustomRunnable(); - - sourceHandlerPair.mainHandler.post(new Runnable() { - @Override - public void run() { - sourceHandlerPair.mediaSource.addMediaSources(Arrays.asList( - new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()}), runnable); - } - }); - waitForCustomRunnable(); + public void testCustomCallbackAfterPreparationAddMultiple() { + DummyMainThread dummyMainThread = new DummyMainThread(); + try { + testRunner.prepareSource(); + final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner); + dummyMainThread.runOnMainThread(new Runnable() { + @Override + public void run() { + mediaSource.addMediaSources( + Arrays.asList(new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()}), + timelineGrabber); + } + }); + Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); + assertEquals(2, timeline.getWindowCount()); + } finally { + dummyMainThread.release(); + } } - public void testCustomCallbackAfterPreparationAddSingleWithIndex() throws InterruptedException { - final DynamicConcatenatingMediaSourceAndHandler sourceHandlerPair = - setUpDynamicMediaSourceOnHandlerThread(); - final Runnable runnable = createCustomRunnable(); - - sourceHandlerPair.mainHandler.post(new Runnable() { - @Override - public void run() { - sourceHandlerPair.mediaSource.addMediaSource(/* index */ 0, createFakeMediaSource(), - runnable); - } - }); - waitForCustomRunnable(); + public void testCustomCallbackAfterPreparationAddSingleWithIndex() { + DummyMainThread dummyMainThread = new DummyMainThread(); + try { + testRunner.prepareSource(); + final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner); + dummyMainThread.runOnMainThread(new Runnable() { + @Override + public void run() { + mediaSource.addMediaSource(/* index */ 0, createFakeMediaSource(), timelineGrabber); + } + }); + Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); + assertEquals(1, timeline.getWindowCount()); + } finally { + dummyMainThread.release(); + } } - public void testCustomCallbackAfterPreparationAddMultipleWithIndex() throws InterruptedException { - final DynamicConcatenatingMediaSourceAndHandler sourceHandlerPair = - setUpDynamicMediaSourceOnHandlerThread(); - final Runnable runnable = createCustomRunnable(); - - sourceHandlerPair.mainHandler.post(new Runnable() { - @Override - public void run() { - sourceHandlerPair.mediaSource.addMediaSources(/* index */ 0, Arrays.asList( - new MediaSource[]{createFakeMediaSource(), createFakeMediaSource()}), runnable); - } - }); - waitForCustomRunnable(); + public void testCustomCallbackAfterPreparationAddMultipleWithIndex() { + DummyMainThread dummyMainThread = new DummyMainThread(); + try { + testRunner.prepareSource(); + final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner); + dummyMainThread.runOnMainThread(new Runnable() { + @Override + public void run() { + mediaSource.addMediaSources(/* index */ 0, + Arrays.asList(new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()}), + timelineGrabber); + } + }); + Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); + assertEquals(2, timeline.getWindowCount()); + } finally { + dummyMainThread.release(); + } } - public void testCustomCallbackAfterPreparationRemove() throws InterruptedException { - final DynamicConcatenatingMediaSourceAndHandler sourceHandlerPair = - setUpDynamicMediaSourceOnHandlerThread(); - final Runnable runnable = createCustomRunnable(); - sourceHandlerPair.mainHandler.post(new Runnable() { - @Override - public void run() { - sourceHandlerPair.mediaSource.addMediaSource(createFakeMediaSource()); - } - }); - waitForTimelineUpdate(); + public void testCustomCallbackAfterPreparationRemove() { + DummyMainThread dummyMainThread = new DummyMainThread(); + try { + testRunner.prepareSource(); + dummyMainThread.runOnMainThread(new Runnable() { + @Override + public void run() { + mediaSource.addMediaSource(createFakeMediaSource()); + } + }); + testRunner.assertTimelineChangeBlocking(); - sourceHandlerPair.mainHandler.post(new Runnable() { - @Override - public void run() { - sourceHandlerPair.mediaSource.removeMediaSource(/* index */ 0, runnable); - } - }); - waitForCustomRunnable(); + final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner); + dummyMainThread.runOnMainThread(new Runnable() { + @Override + public void run() { + mediaSource.removeMediaSource(/* index */ 0, timelineGrabber); + } + }); + Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); + assertEquals(0, timeline.getWindowCount()); + } finally { + dummyMainThread.release(); + } } - public void testCustomCallbackAfterPreparationMove() throws InterruptedException { - final DynamicConcatenatingMediaSourceAndHandler sourceHandlerPair = - setUpDynamicMediaSourceOnHandlerThread(); - final Runnable runnable = createCustomRunnable(); - sourceHandlerPair.mainHandler.post(new Runnable() { - @Override - public void run() { - sourceHandlerPair.mediaSource.addMediaSources(Arrays.asList( - new MediaSource[]{createFakeMediaSource(), createFakeMediaSource()})); - } - }); - waitForTimelineUpdate(); + public void testCustomCallbackAfterPreparationMove() { + DummyMainThread dummyMainThread = new DummyMainThread(); + try { + testRunner.prepareSource(); + dummyMainThread.runOnMainThread(new Runnable() { + @Override + public void run() { + mediaSource.addMediaSources(Arrays.asList( + new MediaSource[] {createFakeMediaSource(), createFakeMediaSource()})); + } + }); + testRunner.assertTimelineChangeBlocking(); - sourceHandlerPair.mainHandler.post(new Runnable() { - @Override - public void run() { - sourceHandlerPair.mediaSource.moveMediaSource(/* fromIndex */ 1, /* toIndex */ 0, - runnable); - } - }); - waitForCustomRunnable(); + final TimelineGrabber timelineGrabber = new TimelineGrabber(testRunner); + dummyMainThread.runOnMainThread(new Runnable() { + @Override + public void run() { + mediaSource.moveMediaSource(/* fromIndex */ 1, /* toIndex */ 0, + timelineGrabber); + } + }); + Timeline timeline = timelineGrabber.assertTimelineChangeBlocking(); + assertEquals(2, timeline.getWindowCount()); + } finally { + dummyMainThread.release(); + } } public void testPeriodCreationWithAds() throws InterruptedException { @@ -557,19 +564,16 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { new TimelineWindowDefinition(2, 222, true, false, 10 * C.MICROS_PER_SECOND, 1, 1)); FakeMediaSource mediaSourceContentOnly = new FakeMediaSource(timelineContentOnly, null); FakeMediaSource mediaSourceWithAds = new FakeMediaSource(timelineWithAds, null); - DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); mediaSource.addMediaSource(mediaSourceContentOnly); mediaSource.addMediaSource(mediaSourceWithAds); - assertNull(timeline); - // Prepare and assert timeline contains ad groups. - prepareAndListenToTimelineUpdates(mediaSource); - waitForTimelineUpdate(); + Timeline timeline = testRunner.prepareSource(); + + // Assert the timeline contains ad groups. TimelineAsserts.assertAdGroupCounts(timeline, 0, 0, 1, 1); // Create all periods and assert period creation of child media sources has been called. - TimelineAsserts.assertAllPeriodsCanBeCreatedPreparedAndReleased(mediaSource, timeline, - TIMEOUT_MS); + testRunner.assertPrepareAndReleaseAllPeriods(); mediaSourceContentOnly.assertMediaPeriodCreated(new MediaPeriodId(0)); mediaSourceContentOnly.assertMediaPeriodCreated(new MediaPeriodId(1)); mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(0)); @@ -578,66 +582,6 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { mediaSourceWithAds.assertMediaPeriodCreated(new MediaPeriodId(1, 0, 0)); } - private DynamicConcatenatingMediaSourceAndHandler setUpDynamicMediaSourceOnHandlerThread() - throws InterruptedException { - final DynamicConcatenatingMediaSource mediaSource = new DynamicConcatenatingMediaSource(); - prepareAndListenToTimelineUpdates(mediaSource); - waitForTimelineUpdate(); - HandlerThread handlerThread = new HandlerThread("TestCustomCallbackExecutionThread"); - handlerThread.start(); - Handler handler = new Handler(handlerThread.getLooper()); - return new DynamicConcatenatingMediaSourceAndHandler(mediaSource, handler); - } - - private void prepareAndListenToTimelineUpdates(MediaSource mediaSource) { - mediaSource.prepareSource(new MessageHandlingExoPlayer(), true, new Listener() { - @Override - public void onSourceInfoRefreshed(MediaSource source, Timeline newTimeline, Object manifest) { - timeline = newTimeline; - synchronized (DynamicConcatenatingMediaSourceTest.this) { - timelineUpdated = true; - DynamicConcatenatingMediaSourceTest.this.notify(); - } - } - }); - } - - private synchronized void waitForTimelineUpdate() throws InterruptedException { - long deadlineMs = System.currentTimeMillis() + TIMEOUT_MS; - while (!timelineUpdated) { - wait(TIMEOUT_MS); - if (System.currentTimeMillis() >= deadlineMs) { - fail("No timeline update occurred within timeout."); - } - } - timelineUpdated = false; - } - - private Runnable createCustomRunnable() { - return new Runnable() { - @Override - public void run() { - synchronized (DynamicConcatenatingMediaSourceTest.this) { - assertTrue(timelineUpdated); - timelineUpdated = false; - customRunnableCalled = true; - DynamicConcatenatingMediaSourceTest.this.notify(); - } - } - }; - } - - private synchronized void waitForCustomRunnable() throws InterruptedException { - long deadlineMs = System.currentTimeMillis() + TIMEOUT_MS; - while (!customRunnableCalled) { - wait(TIMEOUT_MS); - if (System.currentTimeMillis() >= deadlineMs) { - fail("No custom runnable call occurred within timeout."); - } - } - customRunnableCalled = false; - } - private static FakeMediaSource[] createMediaSources(int count) { FakeMediaSource[] sources = new FakeMediaSource[count]; for (int i = 0; i < count; i++) { @@ -654,48 +598,69 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { return new FakeTimeline(new TimelineWindowDefinition(index + 1, (index + 1) * 111)); } - private static class DynamicConcatenatingMediaSourceAndHandler { + private static final class DummyMainThread { - public final DynamicConcatenatingMediaSource mediaSource; - public final Handler mainHandler; + private final HandlerThread thread; + private final Handler handler; - public DynamicConcatenatingMediaSourceAndHandler(DynamicConcatenatingMediaSource mediaSource, - Handler mainHandler) { - this.mediaSource = mediaSource; - this.mainHandler = mainHandler; + private DummyMainThread() { + thread = new HandlerThread("DummyMainThread"); + thread.start(); + handler = new Handler(thread.getLooper()); + } + + /** + * Runs the provided {@link Runnable} on the main thread, blocking until execution completes. + * + * @param runnable The {@link Runnable} to run. + */ + public void runOnMainThread(final Runnable runnable) { + final ConditionVariable finishedCondition = new ConditionVariable(); + handler.post(new Runnable() { + @Override + public void run() { + runnable.run(); + finishedCondition.open(); + } + }); + assertTrue(finishedCondition.block(TIMEOUT_MS)); + } + + public void release() { + thread.quit(); } } - /** - * ExoPlayer that only accepts custom messages and runs them on a separate handler thread. - */ - private static class MessageHandlingExoPlayer extends StubExoPlayer implements Handler.Callback { + private static final class TimelineGrabber implements Runnable { - private final Handler handler; + private final MediaSourceTestRunner testRunner; + private final ConditionVariable finishedCondition; - public MessageHandlingExoPlayer() { - HandlerThread handlerThread = new HandlerThread("StubExoPlayerThread"); - handlerThread.start(); - handler = new Handler(handlerThread.getLooper(), this); + private Timeline timeline; + private AssertionError error; + + public TimelineGrabber(MediaSourceTestRunner testRunner) { + this.testRunner = testRunner; + finishedCondition = new ConditionVariable(); } @Override - public void sendMessages(ExoPlayerMessage... messages) { - handler.obtainMessage(0, messages).sendToTarget(); - } - - @Override - public boolean handleMessage(Message msg) { - ExoPlayerMessage[] messages = (ExoPlayerMessage[]) msg.obj; - for (ExoPlayerMessage message : messages) { - try { - message.target.handleMessage(message.messageType, message.message); - } catch (ExoPlaybackException e) { - fail("Unexpected ExoPlaybackException."); - } + public void run() { + try { + timeline = testRunner.assertTimelineChange(); + } catch (AssertionError e) { + error = e; } - return true; + finishedCondition.open(); + } + + public Timeline assertTimelineChangeBlocking() { + assertTrue(finishedCondition.block(TIMEOUT_MS)); + if (error != null) { + throw error; + } + return timeline; } } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java new file mode 100644 index 0000000000..df1282c7e1 --- /dev/null +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java @@ -0,0 +1,290 @@ +/* + * 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.testutil; + +import static junit.framework.Assert.assertNotNull; +import static junit.framework.Assert.assertTrue; +import static junit.framework.Assert.fail; + +import android.os.ConditionVariable; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; +import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.MediaPeriod; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import com.google.android.exoplayer2.upstream.Allocator; +import com.google.android.exoplayer2.util.Assertions; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.TimeUnit; + +/** + * A runner for {@link MediaSource} tests. + */ +public class MediaSourceTestRunner { + + private final long timeoutMs; + private final StubExoPlayer player; + private final MediaSource mediaSource; + private final MediaSourceListener mediaSourceListener; + private final HandlerThread playbackThread; + private final Handler playbackHandler; + private final Allocator allocator; + + private final LinkedBlockingDeque timelines; + private Timeline timeline; + + /** + * @param mediaSource The source under test. + * @param allocator The allocator to use during the test run. + * @param timeoutMs The timeout for operations in milliseconds. + */ + public MediaSourceTestRunner(MediaSource mediaSource, Allocator allocator, long timeoutMs) { + this.mediaSource = mediaSource; + this.allocator = allocator; + this.timeoutMs = timeoutMs; + playbackThread = new HandlerThread("PlaybackThread"); + playbackThread.start(); + Looper playbackLooper = playbackThread.getLooper(); + playbackHandler = new Handler(playbackLooper); + player = new EventHandlingExoPlayer(playbackLooper); + mediaSourceListener = new MediaSourceListener(); + timelines = new LinkedBlockingDeque<>(); + } + + /** + * Runs the provided {@link Runnable} on the playback thread, blocking until execution completes. + * + * @param runnable The {@link Runnable} to run. + */ + public void runOnPlaybackThread(final Runnable runnable) { + final ConditionVariable finishedCondition = new ConditionVariable(); + playbackHandler.post(new Runnable() { + @Override + public void run() { + runnable.run(); + finishedCondition.open(); + } + }); + assertTrue(finishedCondition.block(timeoutMs)); + } + + /** + * Prepares the source on the playback thread, asserting that it provides an initial timeline. + * + * @return The initial {@link Timeline}. + */ + public Timeline prepareSource() { + runOnPlaybackThread(new Runnable() { + @Override + public void run() { + mediaSource.prepareSource(player, true, mediaSourceListener); + } + }); + return assertTimelineChangeBlocking(); + } + + /** + * Calls {@link MediaSource#createPeriod(MediaSource.MediaPeriodId, Allocator)} on the playback + * thread, asserting that a non-null {@link MediaPeriod} is returned. + * + * @param periodId The id of the period to create. + * @return The created {@link MediaPeriod}. + */ + public MediaPeriod createPeriod(final MediaPeriodId periodId) { + final MediaPeriod[] holder = new MediaPeriod[1]; + runOnPlaybackThread(new Runnable() { + @Override + public void run() { + holder[0] = mediaSource.createPeriod(periodId, allocator); + } + }); + assertNotNull(holder[0]); + return holder[0]; + } + + /** + * Calls {@link MediaPeriod#prepare(MediaPeriod.Callback, long)} on the playback thread. + * + * @param mediaPeriod The {@link MediaPeriod} to prepare. + * @param positionUs The position at which to prepare. + * @return A {@link ConditionVariable} that will be opened when preparation completes. + */ + public ConditionVariable preparePeriod(final MediaPeriod mediaPeriod, final long positionUs) { + final ConditionVariable preparedCondition = new ConditionVariable(); + runOnPlaybackThread(new Runnable() { + @Override + public void run() { + mediaPeriod.prepare(new MediaPeriod.Callback() { + @Override + public void onPrepared(MediaPeriod mediaPeriod) { + preparedCondition.open(); + } + @Override + public void onContinueLoadingRequested(MediaPeriod source) { + // Do nothing. + } + }, positionUs); + } + }); + return preparedCondition; + } + + /** + * Calls {@link MediaSource#releasePeriod(MediaPeriod)} on the playback thread. + * + * @param mediaPeriod The {@link MediaPeriod} to release. + */ + public void releasePeriod(final MediaPeriod mediaPeriod) { + runOnPlaybackThread(new Runnable() { + @Override + public void run() { + mediaSource.releasePeriod(mediaPeriod); + } + }); + } + + /** + * Calls {@link MediaSource#releaseSource()} on the playback thread. + */ + public void releaseSource() { + runOnPlaybackThread(new Runnable() { + @Override + public void run() { + mediaSource.releaseSource(); + } + }); + } + + /** + * Asserts that the source has not notified its listener of a timeline change since the last call + * to {@link #assertTimelineChangeBlocking()} or {@link #assertTimelineChange()} (or since the + * runner was created if neither method has been called). + */ + public void assertNoTimelineChange() { + assertTrue(timelines.isEmpty()); + } + + /** + * Asserts that the source has notified its listener of a single timeline change. + * + * @return The new {@link Timeline}. + */ + public Timeline assertTimelineChange() { + timeline = timelines.removeFirst(); + assertNoTimelineChange(); + return timeline; + } + + /** + * Asserts that the source notifies its listener of a single timeline change. If the source has + * not yet notified its listener, it has up to the timeout passed to the constructor to do so. + * + * @return The new {@link Timeline}. + */ + public Timeline assertTimelineChangeBlocking() { + try { + timeline = timelines.poll(timeoutMs, TimeUnit.MILLISECONDS); + assertNotNull(timeline); // Null indicates the poll timed out. + assertNoTimelineChange(); + return timeline; + } catch (InterruptedException e) { + // Should never happen. + throw new RuntimeException(e); + } + } + + /** + * Creates and releases all periods (including ad periods) defined in the last timeline to be + * returned from {@link #prepareSource()}, {@link #assertTimelineChange()} or + * {@link #assertTimelineChangeBlocking()}. + */ + public void assertPrepareAndReleaseAllPeriods() { + Timeline.Period period = new Timeline.Period(); + for (int i = 0; i < timeline.getPeriodCount(); i++) { + assertPrepareAndReleasePeriod(new MediaPeriodId(i)); + timeline.getPeriod(i, period); + for (int adGroupIndex = 0; adGroupIndex < period.getAdGroupCount(); adGroupIndex++) { + for (int adIndex = 0; adIndex < period.getAdCountInAdGroup(adGroupIndex); adIndex++) { + assertPrepareAndReleasePeriod(new MediaPeriodId(i, adGroupIndex, adIndex)); + } + } + } + } + + private void assertPrepareAndReleasePeriod(MediaPeriodId mediaPeriodId) { + MediaPeriod mediaPeriod = createPeriod(mediaPeriodId); + ConditionVariable preparedCondition = preparePeriod(mediaPeriod, 0); + assertTrue(preparedCondition.block(timeoutMs)); + // MediaSource is supposed to support multiple calls to createPeriod with the same id without an + // intervening call to releasePeriod. + MediaPeriod secondMediaPeriod = createPeriod(mediaPeriodId); + ConditionVariable secondPreparedCondition = preparePeriod(secondMediaPeriod, 0); + assertTrue(secondPreparedCondition.block(timeoutMs)); + // Release the periods. + releasePeriod(mediaPeriod); + releasePeriod(secondMediaPeriod); + } + + /** + * Releases the runner. Should be called when the runner is no longer required. + */ + public void release() { + playbackThread.quit(); + } + + private class MediaSourceListener implements MediaSource.Listener { + + @Override + public void onSourceInfoRefreshed(MediaSource source, Timeline timeline, Object manifest) { + Assertions.checkState(Looper.myLooper() == playbackThread.getLooper()); + timelines.addLast(timeline); + } + + } + + private static class EventHandlingExoPlayer extends StubExoPlayer implements Handler.Callback { + + private final Handler handler; + + public EventHandlingExoPlayer(Looper looper) { + this.handler = new Handler(looper, this); + } + + @Override + public void sendMessages(ExoPlayerMessage... messages) { + handler.obtainMessage(0, messages).sendToTarget(); + } + + @Override + public boolean handleMessage(Message msg) { + ExoPlayerMessage[] messages = (ExoPlayerMessage[]) msg.obj; + for (ExoPlayerMessage message : messages) { + try { + message.target.handleMessage(message.messageType, message.message); + } catch (ExoPlaybackException e) { + fail("Unexpected ExoPlaybackException."); + } + } + return true; + } + + } + +} diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java index 61d1ecaeea..9ee181024c 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java @@ -146,6 +146,7 @@ public class TestUtil { /** * Extracts the timeline from a media source. */ + // TODO: Remove this method and transition callers over to MediaSourceTestRunner. public static Timeline extractTimelineFromMediaSource(MediaSource mediaSource) { class TimelineListener implements Listener { private Timeline timeline; diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java index b1df8f62e1..62af44f32f 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TimelineAsserts.java @@ -16,19 +16,11 @@ package com.google.android.exoplayer2.testutil; import static junit.framework.Assert.assertEquals; -import static junit.framework.Assert.assertNotNull; -import static junit.framework.Assert.assertTrue; - -import android.os.ConditionVariable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline.Period; import com.google.android.exoplayer2.Timeline.Window; -import com.google.android.exoplayer2.source.MediaPeriod; -import com.google.android.exoplayer2.source.MediaPeriod.Callback; -import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; /** * Unit test for {@link Timeline}. @@ -157,46 +149,4 @@ public final class TimelineAsserts { } } - /** - * Asserts that all period (including ad periods) can be created from the source, prepared, and - * released without exception and within timeout. - */ - public static void assertAllPeriodsCanBeCreatedPreparedAndReleased(MediaSource mediaSource, - Timeline timeline, long timeoutMs) { - Period period = new Period(); - for (int i = 0; i < timeline.getPeriodCount(); i++) { - assertPeriodCanBeCreatedPreparedAndReleased(mediaSource, new MediaPeriodId(i), timeoutMs); - timeline.getPeriod(i, period); - for (int adGroupIndex = 0; adGroupIndex < period.getAdGroupCount(); adGroupIndex++) { - for (int adIndex = 0; adIndex < period.getAdCountInAdGroup(adGroupIndex); adIndex++) { - assertPeriodCanBeCreatedPreparedAndReleased(mediaSource, - new MediaPeriodId(i, adGroupIndex, adIndex), timeoutMs); - } - } - } - } - - private static void assertPeriodCanBeCreatedPreparedAndReleased(MediaSource mediaSource, - MediaPeriodId mediaPeriodId, long timeoutMs) { - MediaPeriod mediaPeriod = mediaSource.createPeriod(mediaPeriodId, null); - assertNotNull(mediaPeriod); - final ConditionVariable mediaPeriodPrepared = new ConditionVariable(); - mediaPeriod.prepare(new Callback() { - @Override - public void onPrepared(MediaPeriod mediaPeriod) { - mediaPeriodPrepared.open(); - } - @Override - public void onContinueLoadingRequested(MediaPeriod source) {} - }, /* positionUs= */ 0); - assertTrue(mediaPeriodPrepared.block(timeoutMs)); - // MediaSource is supposed to support multiple calls to createPeriod with the same id without an - // intervening call to releasePeriod. - MediaPeriod secondMediaPeriod = mediaSource.createPeriod(mediaPeriodId, null); - assertNotNull(secondMediaPeriod); - mediaSource.releasePeriod(secondMediaPeriod); - mediaSource.releasePeriod(mediaPeriod); - } - } - From 09f3055badc4ac99cd3a7639cef9a099891483a9 Mon Sep 17 00:00:00 2001 From: hoangtc Date: Mon, 20 Nov 2017 02:41:38 -0800 Subject: [PATCH 0878/2472] Add Builder to SingleSampleMediaSource. Add Builder pattern to SingleSampleMediaSource and mark existing constructors as deprecated. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176332964 --- RELEASENOTES.md | 2 +- .../source/SingleSampleMediaSource.java | 107 ++++++++++++++++++ .../exoplayer2/upstream/DummyDataSource.java | 2 +- 3 files changed, 109 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 2a5ccb583c..6683ee3f55 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -3,7 +3,7 @@ ### dev-v2 (not yet released) ### * Add Builder to ExtractorMediaSource, HlsMediaSource, SsMediaSource, - DashMediaSource. + DashMediaSource, SingleSampleMediaSource. * Support 32-bit PCM float output from `DefaultAudioSink`, and add an option to use this with `FfmpegAudioRenderer`. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java index dd901958fd..2aa8ccc712 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java @@ -45,6 +45,107 @@ public final class SingleSampleMediaSource implements MediaSource { } + /** + * Builder for {@link SingleSampleMediaSource}. Each builder instance can only be used once. + */ + public static final class Builder { + + private final Uri uri; + private final DataSource.Factory dataSourceFactory; + private final Format format; + private final long durationUs; + + private int minLoadableRetryCount; + private Handler eventHandler; + private EventListener eventListener; + private int eventSourceId; + private boolean treatLoadErrorsAsEndOfStream; + private boolean isBuildCalled; + + /** + * @param uri The {@link Uri} of the media stream. + * @param dataSourceFactory The factory from which the {@link DataSource} to read the media will + * be obtained. + * @param format The {@link Format} associated with the output track. + * @param durationUs The duration of the media stream in microseconds. + */ + public Builder(Uri uri, DataSource.Factory dataSourceFactory, Format format, long durationUs) { + this.uri = uri; + this.dataSourceFactory = dataSourceFactory; + this.format = format; + this.durationUs = durationUs; + this.minLoadableRetryCount = DEFAULT_MIN_LOADABLE_RETRY_COUNT; + } + + /** + * Sets the minimum number of times to retry if a loading error occurs. The default value is + * {@link #DEFAULT_MIN_LOADABLE_RETRY_COUNT}. + * + * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. + * @return This builder. + */ + public Builder setMinLoadableRetryCount(int minLoadableRetryCount) { + this.minLoadableRetryCount = minLoadableRetryCount; + return this; + } + + /** + * Sets the listener to respond to events and the handler to deliver these events. + * + * @param eventHandler A handler for events. + * @param eventListener A listener of events. + * @return This builder. + */ + public Builder setEventListener(Handler eventHandler, EventListener eventListener) { + this.eventHandler = eventHandler; + this.eventListener = eventListener; + return this; + } + + /** + * Sets an identifier that gets passed to {@code eventListener} methods. The default value is 0. + * + * @param eventSourceId An identifier that gets passed to {@code eventListener} methods. + * @return This builder. + */ + public Builder setEventSourceId(int eventSourceId) { + this.eventSourceId = eventSourceId; + return this; + } + + /** + * Sets whether load errors will be treated as end-of-stream signal (load errors will not be + * propagated). The default value is false. + * + * @param treatLoadErrorsAsEndOfStream If true, load errors will not be propagated by sample + * streams, treating them as ended instead. If false, load errors will be propagated + * normally by {@link SampleStream#maybeThrowError()}. + * @return This builder. + */ + public Builder setTreatLoadErrorsAsEndOfStream(boolean treatLoadErrorsAsEndOfStream) { + this.treatLoadErrorsAsEndOfStream = treatLoadErrorsAsEndOfStream; + return this; + } + + /** + * Builds a new {@link SingleSampleMediaSource} using the current parameters. + *

          + * After this call, the builder should not be re-used. + * + * @return The newly built {@link SingleSampleMediaSource}. + */ + public SingleSampleMediaSource build() { + Assertions.checkArgument((eventListener == null) == (eventHandler == null)); + Assertions.checkState(!isBuildCalled); + isBuildCalled = true; + + return new SingleSampleMediaSource(uri, dataSourceFactory, format, durationUs, + minLoadableRetryCount, eventHandler, eventListener, eventSourceId, + treatLoadErrorsAsEndOfStream); + } + + } + /** * The default minimum number of times to retry loading data prior to failing. */ @@ -66,7 +167,9 @@ public final class SingleSampleMediaSource implements MediaSource { * be obtained. * @param format The {@link Format} associated with the output track. * @param durationUs The duration of the media stream in microseconds. + * @deprecated Use {@link Builder} instead. */ + @Deprecated public SingleSampleMediaSource(Uri uri, DataSource.Factory dataSourceFactory, Format format, long durationUs) { this(uri, dataSourceFactory, format, durationUs, DEFAULT_MIN_LOADABLE_RETRY_COUNT); @@ -79,7 +182,9 @@ public final class SingleSampleMediaSource implements MediaSource { * @param format The {@link Format} associated with the output track. * @param durationUs The duration of the media stream in microseconds. * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. + * @deprecated Use {@link Builder} instead. */ + @Deprecated public SingleSampleMediaSource(Uri uri, DataSource.Factory dataSourceFactory, Format format, long durationUs, int minLoadableRetryCount) { this(uri, dataSourceFactory, format, durationUs, minLoadableRetryCount, null, null, 0, false); @@ -98,7 +203,9 @@ public final class SingleSampleMediaSource implements MediaSource { * @param treatLoadErrorsAsEndOfStream If true, load errors will not be propagated by sample * streams, treating them as ended instead. If false, load errors will be propagated normally * by {@link SampleStream#maybeThrowError()}. + * @deprecated Use {@link Builder} instead. */ + @Deprecated public SingleSampleMediaSource(Uri uri, DataSource.Factory dataSourceFactory, Format format, long durationUs, int minLoadableRetryCount, Handler eventHandler, EventListener eventListener, int eventSourceId, boolean treatLoadErrorsAsEndOfStream) { 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 index c20868ef00..fa3e14f1c9 100644 --- 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 @@ -25,7 +25,7 @@ public final class DummyDataSource implements DataSource { public static final DummyDataSource INSTANCE = new DummyDataSource(); - /** A factory that that produces {@link DummyDataSource}. */ + /** A factory that produces {@link DummyDataSource}. */ public static final Factory FACTORY = new Factory() { @Override public DataSource createDataSource() { From dc425a942c3cc6181113a3aa0549665af64825ea Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Mon, 20 Nov 2017 02:49:39 -0800 Subject: [PATCH 0879/2472] Use consistent case for sideloaded ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176333544 --- .../google/android/exoplayer2/source/dash/DashMediaSource.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/dash/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 index 54a5086d3b..02f928544b 100644 --- a/library/dash/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 @@ -83,7 +83,7 @@ public final class DashMediaSource implements MediaSource { * @param chunkSourceFactory A factory for {@link DashChunkSource} instances. * @return A new builder. */ - public static Builder forSideLoadedManifest(DashManifest manifest, + public static Builder forSideloadedManifest(DashManifest manifest, DashChunkSource.Factory chunkSourceFactory) { Assertions.checkArgument(!manifest.dynamic); return new Builder(manifest, null, null, chunkSourceFactory); From 2ad87f4f5b08a8f201b2ed750ac028933aaf46ff Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Mon, 20 Nov 2017 03:17:29 -0800 Subject: [PATCH 0880/2472] Add time unit and javadocs to fields in DashManifest ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176335667 --- .../dash/manifest/DashManifestTest.java | 12 +-- .../source/dash/DashMediaSource.java | 28 +++---- .../source/dash/DefaultDashChunkSource.java | 6 +- .../source/dash/manifest/DashManifest.java | 75 +++++++++++++------ 4 files changed, 77 insertions(+), 44 deletions(-) diff --git a/library/dash/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 index 7d77ae82d9..dfcb9e72a5 100644 --- a/library/dash/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 @@ -129,13 +129,13 @@ public class DashManifestTest extends TestCase { } private static void assertManifestEquals(DashManifest expected, DashManifest actual) { - assertEquals(expected.availabilityStartTime, actual.availabilityStartTime); - assertEquals(expected.duration, actual.duration); - assertEquals(expected.minBufferTime, actual.minBufferTime); + assertEquals(expected.availabilityStartTimeMs, actual.availabilityStartTimeMs); + assertEquals(expected.durationMs, actual.durationMs); + assertEquals(expected.minBufferTimeMs, actual.minBufferTimeMs); assertEquals(expected.dynamic, actual.dynamic); - assertEquals(expected.minUpdatePeriod, actual.minUpdatePeriod); - assertEquals(expected.timeShiftBufferDepth, actual.timeShiftBufferDepth); - assertEquals(expected.suggestedPresentationDelay, actual.suggestedPresentationDelay); + assertEquals(expected.minUpdatePeriodMs, actual.minUpdatePeriodMs); + assertEquals(expected.timeShiftBufferDepthMs, actual.timeShiftBufferDepthMs); + assertEquals(expected.suggestedPresentationDelayMs, actual.suggestedPresentationDelayMs); assertEquals(expected.utcTiming, actual.utcTiming); assertEquals(expected.location, actual.location); assertEquals(expected.getPeriodCount(), actual.getPeriodCount()); diff --git a/library/dash/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 index 02f928544b..a82b5af583 100644 --- a/library/dash/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 @@ -199,7 +199,7 @@ public final class DashMediaSource implements MediaSource { public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 3; /** * A constant indicating that the presentation delay for live streams should be set to - * {@link DashManifest#suggestedPresentationDelay} if specified by the manifest, or + * {@link DashManifest#suggestedPresentationDelayMs} if specified by the manifest, or * {@link #DEFAULT_LIVE_PRESENTATION_DELAY_FIXED_MS} otherwise. The presentation delay is the * duration by which the default start position precedes the end of the live window. */ @@ -626,12 +626,12 @@ public final class DashMediaSource implements MediaSource { if (manifest.dynamic && !lastPeriodSeekInfo.isIndexExplicit) { // The manifest describes an incomplete live stream. Update the start/end times to reflect the // live stream duration and the manifest's time shift buffer depth. - long liveStreamDurationUs = getNowUnixTimeUs() - C.msToUs(manifest.availabilityStartTime); + long liveStreamDurationUs = getNowUnixTimeUs() - C.msToUs(manifest.availabilityStartTimeMs); long liveStreamEndPositionInLastPeriodUs = liveStreamDurationUs - C.msToUs(manifest.getPeriod(lastPeriodIndex).startMs); currentEndTimeUs = Math.min(liveStreamEndPositionInLastPeriodUs, currentEndTimeUs); - if (manifest.timeShiftBufferDepth != C.TIME_UNSET) { - long timeShiftBufferDepthUs = C.msToUs(manifest.timeShiftBufferDepth); + if (manifest.timeShiftBufferDepthMs != C.TIME_UNSET) { + long timeShiftBufferDepthUs = C.msToUs(manifest.timeShiftBufferDepthMs); long offsetInPeriodUs = currentEndTimeUs - timeShiftBufferDepthUs; int periodIndex = lastPeriodIndex; while (offsetInPeriodUs < 0 && periodIndex > 0) { @@ -655,8 +655,8 @@ public final class DashMediaSource implements MediaSource { if (manifest.dynamic) { long presentationDelayForManifestMs = livePresentationDelayMs; if (presentationDelayForManifestMs == DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS) { - presentationDelayForManifestMs = manifest.suggestedPresentationDelay != C.TIME_UNSET - ? manifest.suggestedPresentationDelay : DEFAULT_LIVE_PRESENTATION_DELAY_FIXED_MS; + presentationDelayForManifestMs = manifest.suggestedPresentationDelayMs != C.TIME_UNSET + ? manifest.suggestedPresentationDelayMs : DEFAULT_LIVE_PRESENTATION_DELAY_FIXED_MS; } // Snap the default position to the start of the segment containing it. windowDefaultStartPositionUs = windowDurationUs - C.msToUs(presentationDelayForManifestMs); @@ -668,9 +668,9 @@ public final class DashMediaSource implements MediaSource { windowDurationUs / 2); } } - long windowStartTimeMs = manifest.availabilityStartTime + long windowStartTimeMs = manifest.availabilityStartTimeMs + manifest.getPeriod(0).startMs + C.usToMs(currentStartTimeUs); - DashTimeline timeline = new DashTimeline(manifest.availabilityStartTime, windowStartTimeMs, + DashTimeline timeline = new DashTimeline(manifest.availabilityStartTimeMs, windowStartTimeMs, firstPeriodId, currentStartTimeUs, windowDurationUs, windowDefaultStartPositionUs, manifest); sourceListener.onSourceInfoRefreshed(this, timeline, manifest); @@ -693,15 +693,15 @@ public final class DashMediaSource implements MediaSource { if (!manifest.dynamic) { return; } - long minUpdatePeriod = manifest.minUpdatePeriod; - if (minUpdatePeriod == 0) { + long minUpdatePeriodMs = manifest.minUpdatePeriodMs; + if (minUpdatePeriodMs == 0) { // TODO: This is a temporary hack to avoid constantly refreshing the MPD in cases where - // minUpdatePeriod is set to 0. In such cases we shouldn't refresh unless there is explicit - // signaling in the stream, according to: + // minimumUpdatePeriod is set to 0. In such cases we shouldn't refresh unless there is + // explicit signaling in the stream, according to: // http://azure.microsoft.com/blog/2014/09/13/dash-live-streaming-with-azure-media-service/ - minUpdatePeriod = 5000; + minUpdatePeriodMs = 5000; } - long nextLoadTimestamp = manifestLoadStartTimestamp + minUpdatePeriod; + long nextLoadTimestamp = manifestLoadStartTimestamp + minUpdatePeriodMs; long delayUntilNextLoad = Math.max(0, nextLoadTimestamp - SystemClock.elapsedRealtime()); handler.postDelayed(refreshManifestRunnable, delayUntilNextLoad); } diff --git a/library/dash/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 index 66455b2f04..b254c4f09a 100644 --- a/library/dash/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 @@ -220,11 +220,11 @@ public class DefaultDashChunkSource implements DashChunkSource { if (availableSegmentCount == DashSegmentIndex.INDEX_UNBOUNDED) { // The index is itself unbounded. We need to use the current time to calculate the range of // available segments. - long liveEdgeTimeUs = getNowUnixTimeUs() - C.msToUs(manifest.availabilityStartTime); + long liveEdgeTimeUs = getNowUnixTimeUs() - C.msToUs(manifest.availabilityStartTimeMs); long periodStartUs = C.msToUs(manifest.getPeriod(periodIndex).startMs); long liveEdgeTimeInPeriodUs = liveEdgeTimeUs - periodStartUs; - if (manifest.timeShiftBufferDepth != C.TIME_UNSET) { - long bufferDepthUs = C.msToUs(manifest.timeShiftBufferDepth); + if (manifest.timeShiftBufferDepthMs != C.TIME_UNSET) { + long bufferDepthUs = C.msToUs(manifest.timeShiftBufferDepthMs); firstAvailableSegmentNum = Math.max(firstAvailableSegmentNum, representationHolder.getSegmentNum(liveEdgeTimeInPeriodUs - bufferDepthUs)); } diff --git a/library/dash/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 index cd02e27fce..6cc9397596 100644 --- a/library/dash/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 @@ -23,41 +23,74 @@ import java.util.LinkedList; import java.util.List; /** - * Represents a DASH media presentation description (mpd). + * Represents a DASH media presentation description (mpd), as defined by ISO/IEC 23009-1:2014 + * Section 5.3.1.2. */ public class DashManifest { - public final long availabilityStartTime; + /** + * The {@code availabilityStartTime} value in milliseconds since epoch, or {@link C#TIME_UNSET} if + * not present. + */ + public final long availabilityStartTimeMs; - public final long duration; + /** + * The duration of the presentation in milliseconds, or {@link C#TIME_UNSET} if not applicable. + */ + public final long durationMs; - public final long minBufferTime; + /** + * The {@code minBufferTime} value in milliseconds, or {@link C#TIME_UNSET} if not present. + */ + public final long minBufferTimeMs; + /** + * Whether the manifest has value "dynamic" for the {@code type} attribute. + */ public final boolean dynamic; - public final long minUpdatePeriod; + /** + * The {@code minimumUpdatePeriod} value in milliseconds, or {@link C#TIME_UNSET} if not + * applicable. + */ + public final long minUpdatePeriodMs; - public final long timeShiftBufferDepth; + /** + * The {@code timeShiftBufferDepth} value in milliseconds, or {@link C#TIME_UNSET} if not + * present. + */ + public final long timeShiftBufferDepthMs; - public final long suggestedPresentationDelay; + /** + * The {@code suggestedPresentationDelay} value in milliseconds, or {@link C#TIME_UNSET} if not + * present. + */ + public final long suggestedPresentationDelayMs; + /** + * The {@link UtcTimingElement}, or null if not present. Defined in DVB A168:7/2016, Section + * 4.7.2. + */ public final UtcTimingElement utcTiming; + /** + * The location of this manifest. + */ public final Uri location; private final List periods; - public DashManifest(long availabilityStartTime, long duration, long minBufferTime, - boolean dynamic, long minUpdatePeriod, long timeShiftBufferDepth, - long suggestedPresentationDelay, UtcTimingElement utcTiming, Uri location, + public DashManifest(long availabilityStartTimeMs, long durationMs, long minBufferTimeMs, + boolean dynamic, long minUpdatePeriodMs, long timeShiftBufferDepthMs, + long suggestedPresentationDelayMs, UtcTimingElement utcTiming, Uri location, List periods) { - this.availabilityStartTime = availabilityStartTime; - this.duration = duration; - this.minBufferTime = minBufferTime; + this.availabilityStartTimeMs = availabilityStartTimeMs; + this.durationMs = durationMs; + this.minBufferTimeMs = minBufferTimeMs; this.dynamic = dynamic; - this.minUpdatePeriod = minUpdatePeriod; - this.timeShiftBufferDepth = timeShiftBufferDepth; - this.suggestedPresentationDelay = suggestedPresentationDelay; + this.minUpdatePeriodMs = minUpdatePeriodMs; + this.timeShiftBufferDepthMs = timeShiftBufferDepthMs; + this.suggestedPresentationDelayMs = suggestedPresentationDelayMs; this.utcTiming = utcTiming; this.location = location; this.periods = periods == null ? Collections.emptyList() : periods; @@ -73,7 +106,7 @@ public class DashManifest { public final long getPeriodDurationMs(int index) { return index == periods.size() - 1 - ? (duration == C.TIME_UNSET ? C.TIME_UNSET : (duration - periods.get(index).startMs)) + ? (durationMs == C.TIME_UNSET ? C.TIME_UNSET : (durationMs - periods.get(index).startMs)) : (periods.get(index + 1).startMs - periods.get(index).startMs); } @@ -110,10 +143,10 @@ public class DashManifest { copyPeriods.add(new Period(period.id, period.startMs - shiftMs, copyAdaptationSets)); } } - long newDuration = duration != C.TIME_UNSET ? duration - shiftMs : C.TIME_UNSET; - return new DashManifest(availabilityStartTime, newDuration, minBufferTime, dynamic, - minUpdatePeriod, timeShiftBufferDepth, suggestedPresentationDelay, utcTiming, location, - copyPeriods); + long newDuration = durationMs != C.TIME_UNSET ? durationMs - shiftMs : C.TIME_UNSET; + return new DashManifest(availabilityStartTimeMs, newDuration, minBufferTimeMs, dynamic, + minUpdatePeriodMs, timeShiftBufferDepthMs, suggestedPresentationDelayMs, utcTiming, + location, copyPeriods); } private static ArrayList copyAdaptationSets( From 82ce68009cf84e681272cd628289b91e06af0097 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 20 Nov 2017 04:20:52 -0800 Subject: [PATCH 0881/2472] Move MockitoUtils to testutils and use it for all Mockito set-ups. In particular this allows to have the workaround for https://code.google.com/p/dexmaker/issues/detail?id=2 in one place only. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176340526 --- extensions/cronet/build.gradle | 1 + .../ByteArrayUploadDataProviderTest.java | 6 ++---- .../ext/cronet/CronetDataSourceTest.java | 6 ++---- .../drm/OfflineLicenseHelperTest.java | 14 ++------------ .../cache/CachedRegionTrackerTest.java | 14 ++------------ .../dash/offline/DashDownloaderTest.java | 2 +- .../exoplayer2/testutil}/MockitoUtil.java | 18 +++++++++++++++++- 7 files changed, 27 insertions(+), 34 deletions(-) rename {library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash => testutils/src/main/java/com/google/android/exoplayer2/testutil}/MockitoUtil.java (65%) diff --git a/extensions/cronet/build.gradle b/extensions/cronet/build.gradle index 197dec80a5..0b6f9a587c 100644 --- a/extensions/cronet/build.gradle +++ b/extensions/cronet/build.gradle @@ -40,6 +40,7 @@ dependencies { compile files('libs/cronet_impl_common_java.jar') compile files('libs/cronet_impl_native_java.jar') androidTestCompile project(modulePrefix + 'library') + androidTestCompile project(modulePrefix + 'testutils') androidTestCompile 'com.google.dexmaker:dexmaker:' + dexmakerVersion androidTestCompile 'com.google.dexmaker:dexmaker-mockito:' + dexmakerVersion androidTestCompile 'org.mockito:mockito-core:' + mockitoVersion diff --git a/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/ByteArrayUploadDataProviderTest.java b/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/ByteArrayUploadDataProviderTest.java index a65bb0951b..bd81750fcb 100644 --- a/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/ByteArrayUploadDataProviderTest.java +++ b/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/ByteArrayUploadDataProviderTest.java @@ -19,10 +19,10 @@ import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import static org.mockito.MockitoAnnotations.initMocks; import android.support.test.InstrumentationRegistry; import android.support.test.runner.AndroidJUnit4; +import com.google.android.exoplayer2.testutil.MockitoUtil; import java.io.IOException; import java.nio.ByteBuffer; import java.util.Arrays; @@ -46,9 +46,7 @@ public final class ByteArrayUploadDataProviderTest { @Before public void setUp() { - System.setProperty("dexmaker.dexcache", - InstrumentationRegistry.getTargetContext().getCacheDir().getPath()); - initMocks(this); + MockitoUtil.setUpMockito(InstrumentationRegistry.getTargetContext(), this); byteBuffer = ByteBuffer.allocate(TEST_DATA.length); byteArrayUploadDataProvider = new ByteArrayUploadDataProvider(TEST_DATA); } diff --git a/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java b/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java index 4c6a42849f..f92574b7ab 100644 --- a/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java +++ b/extensions/cronet/src/androidTest/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceTest.java @@ -31,13 +31,13 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import static org.mockito.MockitoAnnotations.initMocks; import android.net.Uri; import android.os.ConditionVariable; import android.support.test.InstrumentationRegistry; import android.support.test.runner.AndroidJUnit4; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.testutil.MockitoUtil; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.HttpDataSource.HttpDataSourceException; @@ -107,9 +107,7 @@ public final class CronetDataSourceTest { @Before public void setUp() throws Exception { - System.setProperty("dexmaker.dexcache", - InstrumentationRegistry.getTargetContext().getCacheDir().getPath()); - initMocks(this); + MockitoUtil.setUpMockito(InstrumentationRegistry.getTargetContext(), this); dataSourceUnderTest = spy( new CronetDataSource( mockCronetEngine, diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java index 22ae57932b..02b29a31b5 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java @@ -23,9 +23,9 @@ import android.test.MoreAsserts; import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; +import com.google.android.exoplayer2.testutil.MockitoUtil; import java.util.HashMap; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; /** * Tests {@link OfflineLicenseHelper}. @@ -38,7 +38,7 @@ public class OfflineLicenseHelperTest extends InstrumentationTestCase { @Override protected void setUp() throws Exception { - setUpMockito(this); + MockitoUtil.setUpMockito(this); when(mediaDrm.openSession()).thenReturn(new byte[] {1, 2, 3}); offlineLicenseHelper = new OfflineLicenseHelper<>(C.WIDEVINE_UUID, mediaDrm, mediaDrmCallback, null); @@ -156,14 +156,4 @@ public class OfflineLicenseHelperTest extends InstrumentationTestCase { new byte[] {1, 4, 7, 0, 3, 6})); } - /** - * Sets up Mockito for an instrumentation test. - */ - private static void setUpMockito(InstrumentationTestCase instrumentationTestCase) { - // Workaround for https://code.google.com/p/dexmaker/issues/detail?id=2. - System.setProperty("dexmaker.dexcache", - instrumentationTestCase.getInstrumentation().getTargetContext().getCacheDir().getPath()); - MockitoAnnotations.initMocks(instrumentationTestCase); - } - } diff --git a/library/core/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 index 472b5c724b..f40ae0bc7e 100644 --- a/library/core/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 @@ -17,11 +17,11 @@ package com.google.android.exoplayer2.upstream.cache; import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.extractor.ChunkIndex; +import com.google.android.exoplayer2.testutil.MockitoUtil; import com.google.android.exoplayer2.util.Util; import java.io.File; import java.io.IOException; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; /** * Tests for {@link CachedRegionTracker}. @@ -46,7 +46,7 @@ public final class CachedRegionTrackerTest extends InstrumentationTestCase { @Override protected void setUp() throws Exception { - setUpMockito(this); + MockitoUtil.setUpMockito(this); tracker = new CachedRegionTracker(cache, CACHE_KEY, CHUNK_INDEX); cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest"); @@ -123,14 +123,4 @@ public final class CachedRegionTrackerTest extends InstrumentationTestCase { return SimpleCacheSpanTest.createCacheSpan(index, cacheDir, CACHE_KEY, position, length, 0); } - /** - * Sets up Mockito for an instrumentation test. - */ - private static void setUpMockito(InstrumentationTestCase instrumentationTestCase) { - // Workaround for https://code.google.com/p/dexmaker/issues/detail?id=2. - System.setProperty("dexmaker.dexcache", - instrumentationTestCase.getInstrumentation().getTargetContext().getCacheDir().getPath()); - MockitoAnnotations.initMocks(instrumentationTestCase); - } - } diff --git a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java index 8532e65a68..ec0292514a 100644 --- a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java +++ b/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/offline/DashDownloaderTest.java @@ -27,12 +27,12 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.offline.DownloadException; import com.google.android.exoplayer2.offline.Downloader.ProgressListener; import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; -import com.google.android.exoplayer2.source.dash.MockitoUtil; import com.google.android.exoplayer2.source.dash.manifest.DashManifest; import com.google.android.exoplayer2.source.dash.manifest.RepresentationKey; import com.google.android.exoplayer2.testutil.FakeDataSet; import com.google.android.exoplayer2.testutil.FakeDataSource; import com.google.android.exoplayer2.testutil.FakeDataSource.Factory; +import com.google.android.exoplayer2.testutil.MockitoUtil; import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor; diff --git a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/MockitoUtil.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MockitoUtil.java similarity index 65% rename from library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/MockitoUtil.java rename to testutils/src/main/java/com/google/android/exoplayer2/testutil/MockitoUtil.java index e7cd9baf59..6bd1048bc0 100644 --- a/library/dash/src/androidTest/java/com/google/android/exoplayer2/source/dash/MockitoUtil.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MockitoUtil.java @@ -13,8 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.source.dash; +package com.google.android.exoplayer2.testutil; +import android.content.Context; import android.test.InstrumentationTestCase; import org.mockito.MockitoAnnotations; @@ -25,6 +26,8 @@ public final class MockitoUtil { /** * Sets up Mockito for an instrumentation test. + * + * @param instrumentationTestCase The instrumentation test case class. */ public static void setUpMockito(InstrumentationTestCase instrumentationTestCase) { // Workaround for https://code.google.com/p/dexmaker/issues/detail?id=2. @@ -33,6 +36,19 @@ public final class MockitoUtil { MockitoAnnotations.initMocks(instrumentationTestCase); } + /** + * Sets up Mockito for a JUnit4 test. + * + * @param targetContext The target context. Usually obtained from + * {@code InstrumentationRegistry.getTargetContext()} + * @param testClass The JUnit4 test class. + */ + public static void setUpMockito(Context targetContext, Object testClass) { + // Workaround for https://code.google.com/p/dexmaker/issues/detail?id=2. + System.setProperty("dexmaker.dexcache", targetContext.getCacheDir().getPath()); + MockitoAnnotations.initMocks(testClass); + } + private MockitoUtil() {} } From c3b92f84562aa55dc79d764f6fa4cd2e827f39e7 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 20 Nov 2017 04:32:56 -0800 Subject: [PATCH 0882/2472] Add support for Dolby Atmos Issue: #2465 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176341309 --- RELEASENOTES.md | 2 + .../android/exoplayer2/audio/Ac3Util.java | 170 +++++++++++++++++- .../exoplayer2/extractor/ts/Ac3Reader.java | 2 +- .../exoplayer2/mediacodec/MediaCodecUtil.java | 60 +++++-- .../android/exoplayer2/util/MimeTypes.java | 4 + .../dash/manifest/DashManifestParser.java | 22 ++- 6 files changed, 237 insertions(+), 23 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 6683ee3f55..f772bc9f19 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -6,6 +6,8 @@ DashMediaSource, SingleSampleMediaSource. * Support 32-bit PCM float output from `DefaultAudioSink`, and add an option to use this with `FfmpegAudioRenderer`. +* Support extraction and decoding of Dolby Atmos + ([#2465](https://github.com/google/ExoPlayer/issues/2465)). ### 2.6.0 ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java index e1a70e2579..e9ffab7ace 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java @@ -15,6 +15,10 @@ */ package com.google.android.exoplayer2.audio; +import static com.google.android.exoplayer2.audio.Ac3Util.Ac3SyncFrameInfo.STREAM_TYPE_TYPE0; +import static com.google.android.exoplayer2.audio.Ac3Util.Ac3SyncFrameInfo.STREAM_TYPE_TYPE1; +import static com.google.android.exoplayer2.audio.Ac3Util.Ac3SyncFrameInfo.STREAM_TYPE_UNDEFINED; + import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.drm.DrmInitData; @@ -181,7 +185,14 @@ public final class Ac3Util { channelCount += 2; } } - return Format.createAudioSampleFormat(trackId, MimeTypes.AUDIO_E_AC3, null, Format.NO_VALUE, + String mimeType = MimeTypes.AUDIO_E_AC3; + if (data.bytesLeft() > 0) { + nextByte = data.readUnsignedByte(); + if ((nextByte & 0x01) != 0) { // flag_ec3_extension_type_a + mimeType = MimeTypes.AUDIO_ATMOS; + } + } + return Format.createAudioSampleFormat(trackId, mimeType, null, Format.NO_VALUE, Format.NO_VALUE, channelCount, sampleRate, null, drmInitData, 0, language); } @@ -198,29 +209,176 @@ public final class Ac3Util { boolean isEac3 = data.readBits(5) == 16; data.setPosition(initialPosition); String mimeType; - int streamType = Ac3SyncFrameInfo.STREAM_TYPE_UNDEFINED; + int streamType = STREAM_TYPE_UNDEFINED; int sampleRate; int acmod; int frameSize; int sampleCount; + boolean lfeon; + int channelCount; if (isEac3) { - mimeType = MimeTypes.AUDIO_E_AC3; + // Syntax from ETSI TS 102 366 V1.2.1 subsections E.1.2.1 and E.1.2.2. data.skipBits(16); // syncword streamType = data.readBits(2); data.skipBits(3); // substreamid frameSize = (data.readBits(11) + 1) * 2; int fscod = data.readBits(2); int audioBlocks; + int numblkscod; if (fscod == 3) { + numblkscod = 3; sampleRate = SAMPLE_RATE_BY_FSCOD2[data.readBits(2)]; audioBlocks = 6; } else { - int numblkscod = data.readBits(2); + 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); + lfeon = data.readBit(); + channelCount = CHANNEL_COUNT_BY_ACMOD[acmod] + (lfeon ? 1 : 0); + data.skipBits(5 + 5); // bsid, dialnorm + if (data.readBit()) { // compre + data.skipBits(8); // compr + } + if (acmod == 0) { + data.skipBits(5); // dialnorm2 + if (data.readBit()) { // compr2e + data.skipBits(8); // compr2 + } + } + if (streamType == STREAM_TYPE_TYPE1 && data.readBit()) { // chanmape + data.skipBits(16); // chanmap + } + if (data.readBit()) { // mixmdate + if (acmod > 2) { + data.skipBits(2); // dmixmod + } + if ((acmod & 0x01) != 0 && acmod > 2) { + data.skipBits(3 + 3); // ltrtcmixlev, lorocmixlev + } + if ((acmod & 0x04) != 0) { + data.skipBits(6); // ltrtsurmixlev, lorosurmixlev + } + if (lfeon && data.readBit()) { // lfemixlevcode + data.skipBits(5); // lfemixlevcod + } + if (streamType == STREAM_TYPE_TYPE0) { + if (data.readBit()) { // pgmscle + data.skipBits(6); //pgmscl + } + if (acmod == 0 && data.readBit()) { // pgmscl2e + data.skipBits(6); // pgmscl2 + } + if (data.readBit()) { // extpgmscle + data.skipBits(6); // extpgmscl + } + int mixdef = data.readBits(2); + if (mixdef == 1) { + data.skipBits(1 + 1 + 3); // premixcmpsel, drcsrc, premixcmpscl + } else if (mixdef == 2) { + data.skipBits(12); // mixdata + } else if (mixdef == 3) { + int mixdeflen = data.readBits(5); + if (data.readBit()) { // mixdata2e + data.skipBits(1 + 1 + 3); // premixcmpsel, drcsrc, premixcmpscl + if (data.readBit()) { // extpgmlscle + data.skipBits(4); // extpgmlscl + } + if (data.readBit()) { // extpgmcscle + data.skipBits(4); // extpgmcscl + } + if (data.readBit()) { // extpgmrscle + data.skipBits(4); // extpgmrscl + } + if (data.readBit()) { // extpgmlsscle + data.skipBits(4); // extpgmlsscl + } + if (data.readBit()) { // extpgmrsscle + data.skipBits(4); // extpgmrsscl + } + if (data.readBit()) { // extpgmlfescle + data.skipBits(4); // extpgmlfescl + } + if (data.readBit()) { // dmixscle + data.skipBits(4); // dmixscl + } + if (data.readBit()) { // addche + if (data.readBit()) { // extpgmaux1scle + data.skipBits(4); // extpgmaux1scl + } + if (data.readBit()) { // extpgmaux2scle + data.skipBits(4); // extpgmaux2scl + } + } + } + if (data.readBit()) { // mixdata3e + data.skipBits(5); // spchdat + if (data.readBit()) { // addspchdate + data.skipBits(5 + 2); // spchdat1, spchan1att + if (data.readBit()) { // addspdat1e + data.skipBits(5 + 3); // spchdat2, spchan2att + } + } + } + data.skipBits(8 * (mixdeflen + 2)); // mixdata + data.byteAlign(); // mixdatafill + } + if (acmod < 2) { + if (data.readBit()) { // paninfoe + data.skipBits(8 + 6); // panmean, paninfo + } + if (acmod == 0) { + if (data.readBit()) { // paninfo2e + data.skipBits(8 + 6); // panmean2, paninfo2 + } + } + } + if (data.readBit()) { // frmmixcfginfoe + if (numblkscod == 0) { + data.skipBits(5); // blkmixcfginfo[0] + } else { + for (int blk = 0; blk < audioBlocks; blk++) { + if (data.readBit()) { // blkmixcfginfoe + data.skipBits(5); // blkmixcfginfo[blk] + } + } + } + } + } + } + if (data.readBit()) { // infomdate + data.skipBits(3 + 1 + 1); // bsmod, copyrightb, origbs + if (acmod == 2) { + data.skipBits(2 + 2); // dsurmod, dheadphonmod + } + if (acmod >= 6) { + data.skipBits(2); // dsurexmod + } + if (data.readBit()) { // audioprodie + data.skipBits(5 + 2 + 1); // mixlevel, roomtyp, adconvtyp + } + if (acmod == 0 && data.readBit()) { // audioprodi2e + data.skipBits(5 + 2 + 1); // mixlevel2, roomtyp2, adconvtyp2 + } + if (fscod < 3) { + data.skipBit(); // sourcefscod + } + } + if (streamType == 0 && numblkscod != 3) { + data.skipBit(); // convsync + } + if (streamType == 2 && (numblkscod == 3 || data.readBit())) { // blkid + data.skipBits(6); // frmsizecod + } + mimeType = MimeTypes.AUDIO_E_AC3; + if (data.readBit()) { // addbsie + int addbsil = data.readBits(6); + if (addbsil == 1 && data.readBits(8) == 1) { // addbsi + mimeType = MimeTypes.AUDIO_ATMOS; + } + } } else /* is AC-3 */ { mimeType = MimeTypes.AUDIO_AC3; data.skipBits(16 + 16); // syncword, crc1 @@ -240,9 +398,9 @@ public final class Ac3Util { } sampleRate = SAMPLE_RATE_BY_FSCOD[fscod]; sampleCount = AC3_SYNCFRAME_AUDIO_SAMPLE_COUNT; + lfeon = data.readBit(); + channelCount = CHANNEL_COUNT_BY_ACMOD[acmod] + (lfeon ? 1 : 0); } - boolean lfeon = data.readBit(); - int channelCount = CHANNEL_COUNT_BY_ACMOD[acmod] + (lfeon ? 1 : 0); return new Ac3SyncFrameInfo(mimeType, streamType, channelCount, sampleRate, frameSize, sampleCount); } diff --git a/library/core/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 index 6a1c566faf..8383bfb8d2 100644 --- a/library/core/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 @@ -39,7 +39,7 @@ public final class Ac3Reader implements ElementaryStreamReader { private static final int STATE_READING_HEADER = 1; private static final int STATE_READING_SAMPLE = 2; - private static final int HEADER_SIZE = 8; + private static final int HEADER_SIZE = 128; private final ParsableBitArray headerScratchBits; private final ParsableByteArray headerScratchBytes; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java index f75ce5a9e5..7ae8eb3cd4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java @@ -20,6 +20,7 @@ import android.annotation.TargetApi; import android.media.MediaCodecInfo.CodecCapabilities; import android.media.MediaCodecInfo.CodecProfileLevel; import android.media.MediaCodecList; +import android.support.annotation.Nullable; import android.text.TextUtils; import android.util.Log; import android.util.Pair; @@ -120,7 +121,7 @@ public final class MediaCodecUtil { * exists. * @throws DecoderQueryException If there was an error querying the available decoders. */ - public static MediaCodecInfo getDecoderInfo(String mimeType, boolean secure) + public static @Nullable MediaCodecInfo getDecoderInfo(String mimeType, boolean secure) throws DecoderQueryException { List decoderInfos = getDecoderInfos(mimeType, secure); return decoderInfos.isEmpty() ? null : decoderInfos.get(0); @@ -140,27 +141,34 @@ public final class MediaCodecUtil { public static synchronized List getDecoderInfos(String mimeType, boolean secure) throws DecoderQueryException { CodecKey key = new CodecKey(mimeType, secure); - List decoderInfos = decoderInfosCache.get(key); - if (decoderInfos != null) { - return decoderInfos; + List cachedDecoderInfos = decoderInfosCache.get(key); + if (cachedDecoderInfos != null) { + return cachedDecoderInfos; } MediaCodecListCompat mediaCodecList = Util.SDK_INT >= 21 ? new MediaCodecListCompatV21(secure) : new MediaCodecListCompatV16(); - decoderInfos = getDecoderInfosInternal(key, mediaCodecList); + ArrayList decoderInfos = getDecoderInfosInternal(key, mediaCodecList, mimeType); if (secure && decoderInfos.isEmpty() && 21 <= Util.SDK_INT && Util.SDK_INT <= 23) { // Some devices don't list secure decoders on API level 21 [Internal: b/18678462]. Try the // legacy path. We also try this path on API levels 22 and 23 as a defensive measure. mediaCodecList = new MediaCodecListCompatV16(); - decoderInfos = getDecoderInfosInternal(key, mediaCodecList); + decoderInfos = getDecoderInfosInternal(key, mediaCodecList, mimeType); if (!decoderInfos.isEmpty()) { Log.w(TAG, "MediaCodecList API didn't list secure decoder for: " + mimeType + ". Assuming: " + decoderInfos.get(0).name); } } + if (MimeTypes.AUDIO_ATMOS.equals(mimeType)) { + // E-AC3 decoders can decode Atmos streams, but in 2-D rather than 3-D. + CodecKey eac3Key = new CodecKey(MimeTypes.AUDIO_E_AC3, key.secure); + ArrayList eac3DecoderInfos = + getDecoderInfosInternal(eac3Key, mediaCodecList, mimeType); + decoderInfos.addAll(eac3DecoderInfos); + } applyWorkarounds(decoderInfos); - decoderInfos = Collections.unmodifiableList(decoderInfos); - decoderInfosCache.put(key, decoderInfos); - return decoderInfos; + List unmodifiableDecoderInfos = Collections.unmodifiableList(decoderInfos); + decoderInfosCache.put(key, unmodifiableDecoderInfos); + return unmodifiableDecoderInfos; } /** @@ -212,10 +220,21 @@ public final class MediaCodecUtil { // Internal methods. - private static List getDecoderInfosInternal( - CodecKey key, MediaCodecListCompat mediaCodecList) throws DecoderQueryException { + /** + * Returns {@link MediaCodecInfo}s for the given codec {@code key} in the order given by + * {@code mediaCodecList}. + * + * @param key The codec key. + * @param mediaCodecList The codec list. + * @param requestedMimeType The originally requested MIME type, which may differ from the codec + * key MIME type if the codec key is being considered as a fallback. + * @return The codec information for usable codecs matching the specified key. + * @throws DecoderQueryException If there was an error querying the available decoders. + */ + private static ArrayList getDecoderInfosInternal(CodecKey key, + MediaCodecListCompat mediaCodecList, String requestedMimeType) throws DecoderQueryException { try { - List decoderInfos = new ArrayList<>(); + ArrayList decoderInfos = new ArrayList<>(); String mimeType = key.mimeType; int numberOfCodecs = mediaCodecList.getCodecCount(); boolean secureDecodersExplicit = mediaCodecList.secureDecodersExplicit(); @@ -223,7 +242,7 @@ public final class MediaCodecUtil { for (int i = 0; i < numberOfCodecs; i++) { android.media.MediaCodecInfo codecInfo = mediaCodecList.getCodecInfoAt(i); String codecName = codecInfo.getName(); - if (isCodecUsableDecoder(codecInfo, codecName, secureDecodersExplicit)) { + if (isCodecUsableDecoder(codecInfo, codecName, secureDecodersExplicit, requestedMimeType)) { for (String supportedType : codecInfo.getSupportedTypes()) { if (supportedType.equalsIgnoreCase(mimeType)) { try { @@ -265,9 +284,16 @@ public final class MediaCodecUtil { /** * Returns whether the specified codec is usable for decoding on the current device. + * + * @param info The codec information. + * @param name The name of the codec + * @param secureDecodersExplicit Whether secure decoders were explicitly listed, if present. + * @param requestedMimeType The originally requested MIME type, which may differ from the codec + * key MIME type if the codec key is being considered as a fallback. + * @return Whether the specified codec is usable for decoding on the current device. */ private static boolean isCodecUsableDecoder(android.media.MediaCodecInfo info, String name, - boolean secureDecodersExplicit) { + boolean secureDecodersExplicit, String requestedMimeType) { if (info.isEncoder() || (!secureDecodersExplicit && name.endsWith(".secure"))) { return false; } @@ -356,6 +382,12 @@ public final class MediaCodecUtil { return false; } + // MTK E-AC3 decoder doesn't support decoding Atmos streams in 2-D. See [Internal: b/69400041]. + if (MimeTypes.AUDIO_ATMOS.equals(requestedMimeType) + && "OMX.MTK.AUDIO.DECODER.DSPAC3".equals(name)) { + return false; + } + return true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java b/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java index c29a4c3717..a68e0142d6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java @@ -51,6 +51,7 @@ public final class MimeTypes { public static final String AUDIO_MLAW = BASE_TYPE_AUDIO + "/g711-mlaw"; public static final String AUDIO_AC3 = BASE_TYPE_AUDIO + "/ac3"; public static final String AUDIO_E_AC3 = BASE_TYPE_AUDIO + "/eac3"; + public static final String AUDIO_ATMOS = BASE_TYPE_AUDIO + "/eac3-joc"; public static final String AUDIO_TRUEHD = BASE_TYPE_AUDIO + "/true-hd"; public static final String AUDIO_DTS = BASE_TYPE_AUDIO + "/vnd.dts"; public static final String AUDIO_DTS_HD = BASE_TYPE_AUDIO + "/vnd.dts.hd"; @@ -195,6 +196,8 @@ public final class MimeTypes { return MimeTypes.AUDIO_AC3; } else if (codec.startsWith("ec-3") || codec.startsWith("dec3")) { return MimeTypes.AUDIO_E_AC3; + } else if (codec.startsWith("ec+3")) { + return MimeTypes.AUDIO_ATMOS; } else if (codec.startsWith("dtsc") || codec.startsWith("dtse")) { return MimeTypes.AUDIO_DTS; } else if (codec.startsWith("dtsh") || codec.startsWith("dtsl")) { @@ -252,6 +255,7 @@ public final class MimeTypes { case MimeTypes.AUDIO_AC3: return C.ENCODING_AC3; case MimeTypes.AUDIO_E_AC3: + case MimeTypes.AUDIO_ATMOS: return C.ENCODING_E_AC3; case MimeTypes.AUDIO_DTS: return C.ENCODING_DTS; diff --git a/library/dash/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 index 137e29c5ab..aa4c6b1e30 100644 --- a/library/dash/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 @@ -452,6 +452,7 @@ public class DashManifestParser extends DefaultHandler String drmSchemeType = null; ArrayList drmSchemeDatas = new ArrayList<>(); ArrayList inbandEventStreams = new ArrayList<>(); + ArrayList supplementalProperties = new ArrayList<>(); boolean seenFirstBaseUrl = false; do { @@ -479,12 +480,14 @@ public class DashManifestParser extends DefaultHandler } } else if (XmlPullParserUtil.isStartTag(xpp, "InbandEventStream")) { inbandEventStreams.add(parseDescriptor(xpp, "InbandEventStream")); + } else if (XmlPullParserUtil.isStartTag(xpp, "SupplementalProperty")) { + supplementalProperties.add(parseDescriptor(xpp, "SupplementalProperty")); } } while (!XmlPullParserUtil.isEndTag(xpp, "Representation")); Format format = buildFormat(id, mimeType, width, height, frameRate, audioChannels, audioSamplingRate, bandwidth, adaptationSetLanguage, adaptationSetSelectionFlags, - adaptationSetAccessibilityDescriptors, codecs); + adaptationSetAccessibilityDescriptors, codecs, supplementalProperties); segmentBase = segmentBase != null ? segmentBase : new SingleSegmentBase(); return new RepresentationInfo(format, baseUrl, segmentBase, drmSchemeType, drmSchemeDatas, @@ -494,9 +497,12 @@ public class DashManifestParser extends DefaultHandler protected Format buildFormat(String id, String containerMimeType, int width, int height, float frameRate, int audioChannels, int audioSamplingRate, int bitrate, String language, @C.SelectionFlags int selectionFlags, List accessibilityDescriptors, - String codecs) { + String codecs, List supplementalProperties) { String sampleMimeType = getSampleMimeType(containerMimeType, codecs); if (sampleMimeType != null) { + if (MimeTypes.AUDIO_E_AC3.equals(sampleMimeType)) { + sampleMimeType = parseEac3SupplementalProperties(supplementalProperties); + } if (MimeTypes.isVideo(sampleMimeType)) { return Format.createVideoContainerFormat(id, containerMimeType, sampleMimeType, codecs, bitrate, width, height, frameRate, null, selectionFlags); @@ -900,6 +906,18 @@ public class DashManifestParser extends DefaultHandler return Format.NO_VALUE; } + protected static String parseEac3SupplementalProperties(List supplementalProperties) { + for (int i = 0; i < supplementalProperties.size(); i++) { + Descriptor descriptor = supplementalProperties.get(i); + String schemeIdUri = descriptor.schemeIdUri; + if ("tag:dolby.com,2014:dash:DolbyDigitalPlusExtensionType:2014".equals(schemeIdUri) + && "ec+3".equals(descriptor.value)) { + return MimeTypes.AUDIO_ATMOS; + } + } + return MimeTypes.AUDIO_E_AC3; + } + protected static float parseFrameRate(XmlPullParser xpp, float defaultValue) { float frameRate = defaultValue; String frameRateAttribute = xpp.getAttributeValue(null, "frameRate"); From 790316688657c5d9b7fa8a2a6bce58a1ee833f39 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Mon, 20 Nov 2017 06:22:54 -0800 Subject: [PATCH 0883/2472] Allow human readable strings as DRM intent extras. Issue:#3478 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176351086 --- .../android/exoplayer2/demo/DemoUtil.java | 31 ++++++++++++++++++- .../exoplayer2/demo/PlayerActivity.java | 12 ++++--- .../demo/SampleChooserActivity.java | 27 +++++----------- 3 files changed, 45 insertions(+), 25 deletions(-) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java index f9e9c34158..5ff7c5cb40 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java @@ -16,14 +16,43 @@ package com.google.android.exoplayer2.demo; import android.text.TextUtils; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.drm.UnsupportedDrmException; import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.Util; import java.util.Locale; +import java.util.UUID; /** * Utility methods for demo application. */ -/*package*/ final class DemoUtil { +/* package */ final class DemoUtil { + + /** + * Derives a DRM {@link UUID} from {@code drmScheme}. + * + * @param drmScheme A protection scheme UUID string; or {@code "widevine"}, {@code "playready"} or + * {@code "clearkey"}. + * @return The derived {@link UUID}. + * @throws UnsupportedDrmException If no {@link UUID} could be derived from {@code drmScheme}. + */ + public static UUID getDrmUuid(String drmScheme) throws UnsupportedDrmException { + switch (Util.toLowerInvariant(drmScheme)) { + case "widevine": + return C.WIDEVINE_UUID; + case "playready": + return C.PLAYREADY_UUID; + case "clearkey": + return C.CLEARKEY_UUID; + default: + try { + return UUID.fromString(drmScheme); + } catch (RuntimeException e) { + throw new UnsupportedDrmException(UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME); + } + } + } /** * Builds a track name for display. diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index ca253db809..efde775176 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -83,7 +83,7 @@ import java.util.UUID; public class PlayerActivity extends Activity implements OnClickListener, PlaybackControlView.VisibilityListener { - public static final String DRM_SCHEME_UUID_EXTRA = "drm_scheme_uuid"; + public static final String DRM_SCHEME_EXTRA = "drm_scheme"; public static final String DRM_LICENSE_URL = "drm_license_url"; public static final String DRM_KEY_REQUEST_PROPERTIES = "drm_key_request_properties"; public static final String DRM_MULTI_SESSION = "drm_multi_session"; @@ -98,6 +98,9 @@ public class PlayerActivity extends Activity implements OnClickListener, public static final String EXTENSION_LIST_EXTRA = "extension_list"; public static final String AD_TAG_URI_EXTRA = "ad_tag_uri"; + // For backwards compatibility. + private static final String DRM_SCHEME_UUID_EXTRA = "drm_scheme_uuid"; + private static final DefaultBandwidthMeter BANDWIDTH_METER = new DefaultBandwidthMeter(); private static final CookieManager DEFAULT_COOKIE_MANAGER; static { @@ -256,10 +259,8 @@ public class PlayerActivity extends Activity implements OnClickListener, lastSeenTrackGroupArray = null; eventLogger = new EventLogger(trackSelector); - UUID drmSchemeUuid = intent.hasExtra(DRM_SCHEME_UUID_EXTRA) - ? UUID.fromString(intent.getStringExtra(DRM_SCHEME_UUID_EXTRA)) : null; DrmSessionManager drmSessionManager = null; - if (drmSchemeUuid != null) { + if (intent.hasExtra(DRM_SCHEME_EXTRA) || intent.hasExtra(DRM_SCHEME_UUID_EXTRA)) { String drmLicenseUrl = intent.getStringExtra(DRM_LICENSE_URL); String[] keyRequestPropertiesArray = intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES); boolean multiSession = intent.getBooleanExtra(DRM_MULTI_SESSION, false); @@ -268,6 +269,9 @@ public class PlayerActivity extends Activity implements OnClickListener, errorStringId = R.string.error_drm_not_supported; } else { try { + String drmSchemeExtra = intent.hasExtra(DRM_SCHEME_EXTRA) ? DRM_SCHEME_EXTRA + : DRM_SCHEME_UUID_EXTRA; + UUID drmSchemeUuid = DemoUtil.getDrmUuid(intent.getStringExtra(drmSchemeExtra)); drmSessionManager = buildDrmSessionManagerV18(drmSchemeUuid, drmLicenseUrl, keyRequestPropertiesArray, multiSession); } catch (UnsupportedDrmException e) { diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java index 1f84b1f29c..308bab2a3b 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java @@ -32,8 +32,8 @@ import android.widget.ExpandableListView; import android.widget.ExpandableListView.OnChildClickListener; import android.widget.TextView; import android.widget.Toast; -import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.drm.UnsupportedDrmException; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSourceInputStream; import com.google.android.exoplayer2.upstream.DataSpec; @@ -202,7 +202,11 @@ public class SampleChooserActivity extends Activity { break; case "drm_scheme": Assertions.checkState(!insidePlaylist, "Invalid attribute on nested item: drm_scheme"); - drmUuid = getDrmUuid(reader.nextString()); + try { + drmUuid = DemoUtil.getDrmUuid(reader.nextString()); + } catch (UnsupportedDrmException e) { + throw new ParserException(e); + } break; case "drm_license_url": Assertions.checkState(!insidePlaylist, @@ -270,23 +274,6 @@ public class SampleChooserActivity extends Activity { return group; } - private UUID getDrmUuid(String typeString) throws ParserException { - switch (Util.toLowerInvariant(typeString)) { - case "widevine": - return C.WIDEVINE_UUID; - case "playready": - return C.PLAYREADY_UUID; - case "clearkey": - return C.CLEARKEY_UUID; - default: - try { - return UUID.fromString(typeString); - } catch (RuntimeException e) { - throw new ParserException("Unsupported drm type: " + typeString); - } - } - } - } private static final class SampleAdapter extends BaseExpandableListAdapter { @@ -393,7 +380,7 @@ public class SampleChooserActivity extends Activity { public void updateIntent(Intent intent) { Assertions.checkNotNull(intent); - intent.putExtra(PlayerActivity.DRM_SCHEME_UUID_EXTRA, drmSchemeUuid.toString()); + intent.putExtra(PlayerActivity.DRM_SCHEME_EXTRA, drmSchemeUuid.toString()); intent.putExtra(PlayerActivity.DRM_LICENSE_URL, drmLicenseUrl); intent.putExtra(PlayerActivity.DRM_KEY_REQUEST_PROPERTIES, drmKeyRequestProperties); intent.putExtra(PlayerActivity.DRM_MULTI_SESSION, drmMultiSession); From 5c0e1f3e8aa057d419d87db403bf7edaf5438563 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 20 Nov 2017 08:48:10 -0800 Subject: [PATCH 0884/2472] Use MediaSourceTestRunner in additional source tests ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176366471 --- .../source/ClippingMediaSourceTest.java | 13 +++++--- .../source/ConcatenatingMediaSourceTest.java | 12 ++++--- .../DynamicConcatenatingMediaSourceTest.java | 14 ++++----- .../source/LoopingMediaSourceTest.java | 14 ++++++--- .../testutil/MediaSourceTestRunner.java | 30 ++++++++++++------ .../android/exoplayer2/testutil/TestUtil.java | 31 ------------------- 6 files changed, 52 insertions(+), 62 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java index 5e615dbc7f..3c870f06f4 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ClippingMediaSourceTest.java @@ -24,7 +24,7 @@ import com.google.android.exoplayer2.Timeline.Window; import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; -import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.testutil.MediaSourceTestRunner; import com.google.android.exoplayer2.testutil.TimelineAsserts; /** @@ -123,9 +123,14 @@ public final class ClippingMediaSourceTest extends InstrumentationTestCase { * Wraps the specified timeline in a {@link ClippingMediaSource} and returns the clipped timeline. */ private static Timeline getClippedTimeline(Timeline timeline, long startMs, long endMs) { - MediaSource mediaSource = new FakeMediaSource(timeline, null); - return TestUtil.extractTimelineFromMediaSource( - new ClippingMediaSource(mediaSource, startMs, endMs)); + FakeMediaSource fakeMediaSource = new FakeMediaSource(timeline, null); + ClippingMediaSource mediaSource = new ClippingMediaSource(fakeMediaSource, startMs, endMs); + MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null); + try { + return testRunner.prepareSource(); + } finally { + testRunner.release(); + } } } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java index 429325defc..1ca32be46d 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/ConcatenatingMediaSourceTest.java @@ -24,7 +24,6 @@ import com.google.android.exoplayer2.testutil.FakeShuffleOrder; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; import com.google.android.exoplayer2.testutil.MediaSourceTestRunner; -import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.testutil.TimelineAsserts; import junit.framework.TestCase; @@ -33,8 +32,6 @@ import junit.framework.TestCase; */ public final class ConcatenatingMediaSourceTest extends TestCase { - private static final int TIMEOUT_MS = 10000; - public void testEmptyConcatenation() { for (boolean atomic : new boolean[] {false, true}) { Timeline timeline = getConcatenatedTimeline(atomic); @@ -211,7 +208,7 @@ public final class ConcatenatingMediaSourceTest extends TestCase { ConcatenatingMediaSource mediaSource = new ConcatenatingMediaSource(mediaSourceContentOnly, mediaSourceWithAds); - MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null, TIMEOUT_MS); + MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null); try { Timeline timeline = testRunner.prepareSource(); TimelineAsserts.assertAdGroupCounts(timeline, 0, 0, 1, 1); @@ -241,7 +238,12 @@ public final class ConcatenatingMediaSourceTest extends TestCase { } ConcatenatingMediaSource mediaSource = new ConcatenatingMediaSource(isRepeatOneAtomic, new FakeShuffleOrder(mediaSources.length), mediaSources); - return TestUtil.extractTimelineFromMediaSource(mediaSource); + MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null); + try { + return testRunner.prepareSource(); + } finally { + testRunner.release(); + } } private static FakeTimeline createFakeTimeline(int periodCount, int windowId) { diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java index 536180fafc..16c9e1a17c 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSourceTest.java @@ -39,19 +39,19 @@ import org.mockito.Mockito; */ public final class DynamicConcatenatingMediaSourceTest extends TestCase { - private static final int TIMEOUT_MS = 10000; - private DynamicConcatenatingMediaSource mediaSource; private MediaSourceTestRunner testRunner; @Override - public void setUp() { + public void setUp() throws Exception { + super.setUp(); mediaSource = new DynamicConcatenatingMediaSource(new FakeShuffleOrder(0)); - testRunner = new MediaSourceTestRunner(mediaSource, null, TIMEOUT_MS); + testRunner = new MediaSourceTestRunner(mediaSource, null); } @Override - public void tearDown() { + public void tearDown() throws Exception { + super.tearDown(); testRunner.release(); } @@ -623,7 +623,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { finishedCondition.open(); } }); - assertTrue(finishedCondition.block(TIMEOUT_MS)); + assertTrue(finishedCondition.block(MediaSourceTestRunner.TIMEOUT_MS)); } public void release() { @@ -656,7 +656,7 @@ public final class DynamicConcatenatingMediaSourceTest extends TestCase { } public Timeline assertTimelineChangeBlocking() { - assertTrue(finishedCondition.block(TIMEOUT_MS)); + assertTrue(finishedCondition.block(MediaSourceTestRunner.TIMEOUT_MS)); if (error != null) { throw error; } diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java index 79f646b5c4..6f69923ea2 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/source/LoopingMediaSourceTest.java @@ -21,7 +21,7 @@ import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeTimeline; import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; -import com.google.android.exoplayer2.testutil.TestUtil; +import com.google.android.exoplayer2.testutil.MediaSourceTestRunner; import com.google.android.exoplayer2.testutil.TimelineAsserts; import junit.framework.TestCase; @@ -110,10 +110,14 @@ public class LoopingMediaSourceTest extends TestCase { * the looping timeline. */ private static Timeline getLoopingTimeline(Timeline timeline, int loopCount) { - MediaSource mediaSource = new FakeMediaSource(timeline, null); - return TestUtil.extractTimelineFromMediaSource( - new LoopingMediaSource(mediaSource, loopCount)); + FakeMediaSource fakeMediaSource = new FakeMediaSource(timeline, null); + LoopingMediaSource mediaSource = new LoopingMediaSource(fakeMediaSource, loopCount); + MediaSourceTestRunner testRunner = new MediaSourceTestRunner(mediaSource, null); + try { + return testRunner.prepareSource(); + } finally { + testRunner.release(); + } } } - diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java index df1282c7e1..235c04bef5 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java @@ -31,6 +31,8 @@ import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; + import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.TimeUnit; @@ -39,7 +41,8 @@ import java.util.concurrent.TimeUnit; */ public class MediaSourceTestRunner { - private final long timeoutMs; + public static final int TIMEOUT_MS = 10000; + private final StubExoPlayer player; private final MediaSource mediaSource; private final MediaSourceListener mediaSourceListener; @@ -53,12 +56,10 @@ public class MediaSourceTestRunner { /** * @param mediaSource The source under test. * @param allocator The allocator to use during the test run. - * @param timeoutMs The timeout for operations in milliseconds. */ - public MediaSourceTestRunner(MediaSource mediaSource, Allocator allocator, long timeoutMs) { + public MediaSourceTestRunner(MediaSource mediaSource, Allocator allocator) { this.mediaSource = mediaSource; this.allocator = allocator; - this.timeoutMs = timeoutMs; playbackThread = new HandlerThread("PlaybackThread"); playbackThread.start(); Looper playbackLooper = playbackThread.getLooper(); @@ -74,15 +75,24 @@ public class MediaSourceTestRunner { * @param runnable The {@link Runnable} to run. */ public void runOnPlaybackThread(final Runnable runnable) { + final Throwable[] throwable = new Throwable[1]; final ConditionVariable finishedCondition = new ConditionVariable(); playbackHandler.post(new Runnable() { @Override public void run() { - runnable.run(); - finishedCondition.open(); + try { + runnable.run(); + } catch (Throwable e) { + throwable[0] = e; + } finally { + finishedCondition.open(); + } } }); - assertTrue(finishedCondition.block(timeoutMs)); + assertTrue(finishedCondition.block(TIMEOUT_MS)); + if (throwable[0] != null) { + Util.sneakyThrow(throwable[0]); + } } /** @@ -200,7 +210,7 @@ public class MediaSourceTestRunner { */ public Timeline assertTimelineChangeBlocking() { try { - timeline = timelines.poll(timeoutMs, TimeUnit.MILLISECONDS); + timeline = timelines.poll(TIMEOUT_MS, TimeUnit.MILLISECONDS); assertNotNull(timeline); // Null indicates the poll timed out. assertNoTimelineChange(); return timeline; @@ -231,12 +241,12 @@ public class MediaSourceTestRunner { private void assertPrepareAndReleasePeriod(MediaPeriodId mediaPeriodId) { MediaPeriod mediaPeriod = createPeriod(mediaPeriodId); ConditionVariable preparedCondition = preparePeriod(mediaPeriod, 0); - assertTrue(preparedCondition.block(timeoutMs)); + assertTrue(preparedCondition.block(TIMEOUT_MS)); // MediaSource is supposed to support multiple calls to createPeriod with the same id without an // intervening call to releasePeriod. MediaPeriod secondMediaPeriod = createPeriod(mediaPeriodId); ConditionVariable secondPreparedCondition = preparePeriod(secondMediaPeriod, 0); - assertTrue(secondPreparedCondition.block(timeoutMs)); + assertTrue(secondPreparedCondition.block(TIMEOUT_MS)); // Release the periods. releasePeriod(mediaPeriod); releasePeriod(secondMediaPeriod); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java index 9ee181024c..d10b8a8269 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestUtil.java @@ -19,10 +19,7 @@ import android.app.Instrumentation; import android.content.Context; import android.test.MoreAsserts; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.extractor.Extractor; -import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.MediaSource.Listener; import com.google.android.exoplayer2.testutil.FakeExtractorInput.SimulatedIOException; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; @@ -143,34 +140,6 @@ public class TestUtil { return new String(getByteArray(instrumentation, fileName)); } - /** - * Extracts the timeline from a media source. - */ - // TODO: Remove this method and transition callers over to MediaSourceTestRunner. - public static Timeline extractTimelineFromMediaSource(MediaSource mediaSource) { - class TimelineListener implements Listener { - private Timeline timeline; - @Override - public synchronized void onSourceInfoRefreshed(MediaSource source, Timeline timeline, - Object manifest) { - this.timeline = timeline; - this.notify(); - } - } - TimelineListener listener = new TimelineListener(); - mediaSource.prepareSource(null, true, listener); - synchronized (listener) { - while (listener.timeline == null) { - try { - listener.wait(); - } catch (InterruptedException e) { - Assert.fail(e.getMessage()); - } - } - } - return listener.timeline; - } - /** * Asserts that data read from a {@link DataSource} matches {@code expected}. * From fa5cc5be5104094a7368cdbc039351cc1003f418 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 20 Nov 2017 08:50:09 -0800 Subject: [PATCH 0885/2472] Bump target API level to 27 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176366693 --- constants.gradle | 4 ++-- demos/ima/src/main/AndroidManifest.xml | 2 +- demos/main/src/main/AndroidManifest.xml | 2 +- extensions/cronet/src/androidTest/AndroidManifest.xml | 2 +- extensions/flac/src/androidTest/AndroidManifest.xml | 2 +- extensions/opus/src/androidTest/AndroidManifest.xml | 2 +- extensions/vp9/src/androidTest/AndroidManifest.xml | 2 +- library/core/src/androidTest/AndroidManifest.xml | 2 +- library/dash/src/androidTest/AndroidManifest.xml | 2 +- library/hls/src/androidTest/AndroidManifest.xml | 2 +- library/smoothstreaming/src/androidTest/AndroidManifest.xml | 2 +- playbacktests/src/androidTest/AndroidManifest.xml | 2 +- 12 files changed, 13 insertions(+), 13 deletions(-) diff --git a/constants.gradle b/constants.gradle index 2a7754d65c..bad69389a5 100644 --- a/constants.gradle +++ b/constants.gradle @@ -17,8 +17,8 @@ project.ext { // However, please note that the core media playback functionality provided // by the library requires API level 16 or greater. minSdkVersion = 14 - compileSdkVersion = 26 - targetSdkVersion = 26 + compileSdkVersion = 27 + targetSdkVersion = 27 buildToolsVersion = '26.0.2' testSupportLibraryVersion = '0.5' supportLibraryVersion = '27.0.0' diff --git a/demos/ima/src/main/AndroidManifest.xml b/demos/ima/src/main/AndroidManifest.xml index 5252d2feeb..f14feeda74 100644 --- a/demos/ima/src/main/AndroidManifest.xml +++ b/demos/ima/src/main/AndroidManifest.xml @@ -19,7 +19,7 @@ android:versionName="2.6.0"> - + diff --git a/demos/main/src/main/AndroidManifest.xml b/demos/main/src/main/AndroidManifest.xml index d041e24d80..ec8016e8a3 100644 --- a/demos/main/src/main/AndroidManifest.xml +++ b/demos/main/src/main/AndroidManifest.xml @@ -23,7 +23,7 @@ - + - + - + - + - + - + - + - + - + - + Date: Tue, 21 Nov 2017 10:16:50 -0800 Subject: [PATCH 0886/2472] Parse DASH manifest's publish time. Parse DASH manifest's publishTime node as defined by ISO/IEC 23009-1:2014, section 5.3.1.2. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176525922 --- .../source/dash/manifest/DashManifestTest.java | 3 ++- .../source/dash/manifest/DashManifest.java | 15 +++++++++++---- .../source/dash/manifest/DashManifestParser.java | 13 +++++++------ 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/library/dash/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 index dfcb9e72a5..882b0eb374 100644 --- a/library/dash/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 @@ -136,6 +136,7 @@ public class DashManifestTest extends TestCase { assertEquals(expected.minUpdatePeriodMs, actual.minUpdatePeriodMs); assertEquals(expected.timeShiftBufferDepthMs, actual.timeShiftBufferDepthMs); assertEquals(expected.suggestedPresentationDelayMs, actual.suggestedPresentationDelayMs); + assertEquals(expected.publishTimeMs, actual.publishTimeMs); assertEquals(expected.utcTiming, actual.utcTiming); assertEquals(expected.location, actual.location); assertEquals(expected.getPeriodCount(), actual.getPeriodCount()); @@ -179,7 +180,7 @@ public class DashManifestTest extends TestCase { } private static DashManifest newDashManifest(int duration, Period... periods) { - return new DashManifest(0, duration, 1, false, 2, 3, 4, DUMMY_UTC_TIMING, Uri.EMPTY, + return new DashManifest(0, duration, 1, false, 2, 3, 4, 12345, DUMMY_UTC_TIMING, Uri.EMPTY, Arrays.asList(periods)); } diff --git a/library/dash/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 index 6cc9397596..cbfd0a5951 100644 --- a/library/dash/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 @@ -67,6 +67,12 @@ public class DashManifest { */ public final long suggestedPresentationDelayMs; + /** + * The {@code publishTime} value in milliseconds since epoch, or {@link C#TIME_UNSET} if + * not present. + */ + public final long publishTimeMs; + /** * The {@link UtcTimingElement}, or null if not present. Defined in DVB A168:7/2016, Section * 4.7.2. @@ -82,8 +88,8 @@ public class DashManifest { public DashManifest(long availabilityStartTimeMs, long durationMs, long minBufferTimeMs, boolean dynamic, long minUpdatePeriodMs, long timeShiftBufferDepthMs, - long suggestedPresentationDelayMs, UtcTimingElement utcTiming, Uri location, - List periods) { + long suggestedPresentationDelayMs, long publishTimeMs, UtcTimingElement utcTiming, + Uri location, List periods) { this.availabilityStartTimeMs = availabilityStartTimeMs; this.durationMs = durationMs; this.minBufferTimeMs = minBufferTimeMs; @@ -91,6 +97,7 @@ public class DashManifest { this.minUpdatePeriodMs = minUpdatePeriodMs; this.timeShiftBufferDepthMs = timeShiftBufferDepthMs; this.suggestedPresentationDelayMs = suggestedPresentationDelayMs; + this.publishTimeMs = publishTimeMs; this.utcTiming = utcTiming; this.location = location; this.periods = periods == null ? Collections.emptyList() : periods; @@ -145,8 +152,8 @@ public class DashManifest { } long newDuration = durationMs != C.TIME_UNSET ? durationMs - shiftMs : C.TIME_UNSET; return new DashManifest(availabilityStartTimeMs, newDuration, minBufferTimeMs, dynamic, - minUpdatePeriodMs, timeShiftBufferDepthMs, suggestedPresentationDelayMs, utcTiming, - location, copyPeriods); + minUpdatePeriodMs, timeShiftBufferDepthMs, suggestedPresentationDelayMs, publishTimeMs, + utcTiming, location, copyPeriods); } private static ArrayList copyAdaptationSets( diff --git a/library/dash/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 index aa4c6b1e30..9c50c6cf30 100644 --- a/library/dash/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 @@ -115,6 +115,7 @@ public class DashManifestParser extends DefaultHandler ? parseDuration(xpp, "timeShiftBufferDepth", C.TIME_UNSET) : C.TIME_UNSET; long suggestedPresentationDelayMs = dynamic ? parseDuration(xpp, "suggestedPresentationDelay", C.TIME_UNSET) : C.TIME_UNSET; + long publishTimeMs = parseDateTime(xpp, "publishTime", C.TIME_UNSET); UtcTimingElement utcTiming = null; Uri location = null; @@ -167,17 +168,17 @@ public class DashManifestParser extends DefaultHandler } return buildMediaPresentationDescription(availabilityStartTime, durationMs, minBufferTimeMs, - dynamic, minUpdateTimeMs, timeShiftBufferDepthMs, suggestedPresentationDelayMs, utcTiming, - location, periods); + dynamic, minUpdateTimeMs, timeShiftBufferDepthMs, suggestedPresentationDelayMs, + publishTimeMs, utcTiming, location, periods); } protected DashManifest buildMediaPresentationDescription(long availabilityStartTime, long durationMs, long minBufferTimeMs, boolean dynamic, long minUpdateTimeMs, - long timeShiftBufferDepthMs, long suggestedPresentationDelayMs, UtcTimingElement utcTiming, - Uri location, List periods) { + long timeShiftBufferDepthMs, long suggestedPresentationDelayMs, long publishTimeMs, + UtcTimingElement utcTiming, Uri location, List periods) { return new DashManifest(availabilityStartTime, durationMs, minBufferTimeMs, - dynamic, minUpdateTimeMs, timeShiftBufferDepthMs, suggestedPresentationDelayMs, utcTiming, - location, periods); + dynamic, minUpdateTimeMs, timeShiftBufferDepthMs, suggestedPresentationDelayMs, + publishTimeMs, utcTiming, location, periods); } protected UtcTimingElement parseUtcTiming(XmlPullParser xpp) { From d5b79f3a43ec2b996c71736b4b31fa8c34b30a84 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 22 Nov 2017 13:22:41 -0800 Subject: [PATCH 0887/2472] Update gradle wrapper ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176693785 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 2623db66fc..9f9081a945 100644 --- a/build.gradle +++ b/build.gradle @@ -17,7 +17,7 @@ buildscript { google() } dependencies { - classpath 'com.android.tools.build:gradle:3.0.0' + classpath 'com.android.tools.build:gradle:3.0.1' classpath 'com.novoda:bintray-release:0.5.0' } // Workaround for the following test coverage issue. Remove when fixed: From cf3ab9051df177335eacfe14076b5dfb08448801 Mon Sep 17 00:00:00 2001 From: simophin Date: Fri, 24 Nov 2017 17:27:35 +1300 Subject: [PATCH 0888/2472] Guard against out-of-range timestamp We've found that in our production environment, the AAC stream's timestamp exceeds the 33bit limit from time to time, when it happens, `peekId3PrivTimestamp` returns a value that is greater than `TimestampAdjuster.MAX_PTS_PLUS_ONE`, which causes a overflow in `TimestampAdjuster.adjustTsTimestamp` (overflow inside `ptsToUs`) after playing for a while . When the overflow happens, the start time of the stream becomes negative and the playback simply stucks at buffering forever. I fully understand that the 33bit is a spec requirement, thus I asked our stream provider to correct this mistake. But in the mean time, I'd also like ExoPlayer to handle this situation more error tolerance, as in other platforms (iOS, browsers) we see more tolerance behavior. --- .../com/google/android/exoplayer2/source/hls/HlsMediaChunk.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/hls/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 index 5ca8675dd9..83167c152f 100644 --- a/library/hls/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 @@ -306,7 +306,7 @@ import java.util.concurrent.atomic.AtomicInteger; if (PRIV_TIMESTAMP_FRAME_OWNER.equals(privFrame.owner)) { System.arraycopy(privFrame.privateData, 0, id3Data.data, 0, 8 /* timestamp size */); id3Data.reset(8); - return id3Data.readLong(); + return id3Data.readLong() & ((1L << 33) - 1L); } } } From b19512fb20e5f832066067480d9c9151e62ba964 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 24 Nov 2017 02:20:35 -0800 Subject: [PATCH 0889/2472] Propagate the player error to ExoPlayerTestRunner In a test run where no exceptions were thrown on the main thread and the test did not time out, exceptions from onPlayerError were not correctly propagated to the test thread (handleException would be called with null). Fix ExoPlayerTestRunner.onPlayerError to propagate the actual exception from the player. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176825907 --- .../google/android/exoplayer2/testutil/ExoPlayerTestRunner.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java index 591e63dc5b..30e0214b62 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java @@ -517,7 +517,7 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener { @Override public void onPlayerError(ExoPlaybackException error) { - handleException(exception); + handleException(error); } @Override From 6c1f562230f87bd9882a069d2f1b92fd00d31e9a Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 24 Nov 2017 10:00:44 -0800 Subject: [PATCH 0890/2472] Switch from currentTimeMillis to elapsedRealtime currentTimeMillis is not guaranteed to be monotonic and elapsedRealtime is recommend for interval timing. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176853118 --- .../google/android/exoplayer2/util/ConditionVariable.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/ConditionVariable.java b/library/core/src/main/java/com/google/android/exoplayer2/util/ConditionVariable.java index 262d120af8..058a5d6dd2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/ConditionVariable.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/ConditionVariable.java @@ -60,18 +60,18 @@ public final class ConditionVariable { } /** - * Blocks until the condition is opened or until timeout milliseconds have passed. + * Blocks until the condition is opened or until {@code timeout} milliseconds have passed. * * @param timeout The maximum time to wait in milliseconds. - * @return true If the condition was opened, false if the call returns because of the timeout. + * @return True if the condition was opened, false if the call returns because of the timeout. * @throws InterruptedException If the thread is interrupted. */ public synchronized boolean block(long timeout) throws InterruptedException { - long now = System.currentTimeMillis(); + long now = android.os.SystemClock.elapsedRealtime(); long end = now + timeout; while (!isOpen && now < end) { wait(end - now); - now = System.currentTimeMillis(); + now = android.os.SystemClock.elapsedRealtime(); } return isOpen; } From 86d91a59a0eb15d19952e01f09038d6f050874d0 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 27 Nov 2017 03:45:24 -0800 Subject: [PATCH 0891/2472] Remove race condition when stopping FakeExoPlayer. A message to stop the playback and to quit the playback thread was posted in release(). The stop message removed all other already queued messages which might include the second message to quit the thread. That led to infinite waiting in the release method because the playback thread never got the quit signal. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176997104 --- .../testutil/FakeSimpleExoPlayer.java | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java index 4a5beb0501..f6f56ead77 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java @@ -166,27 +166,13 @@ public class FakeSimpleExoPlayer extends SimpleExoPlayer { @Override public void stop() { - playbackHandler.post(new Runnable() { - @Override - public void run () { - playbackHandler.removeCallbacksAndMessages(null); - releaseMedia(); - changePlaybackState(Player.STATE_IDLE); - } - }); + stop(/* quitPlaybackThread= */ false); } @Override @SuppressWarnings("ThreadJoinLoop") public void release() { - stop(); - playbackHandler.post(new Runnable() { - @Override - public void run () { - playbackHandler.removeCallbacksAndMessages(null); - playbackThread.quit(); - } - }); + stop(/* quitPlaybackThread= */ true); while (playbackThread.isAlive()) { try { playbackThread.join(); @@ -525,6 +511,20 @@ public class FakeSimpleExoPlayer extends SimpleExoPlayer { } } + private void stop(boolean quitPlaybackThread) { + playbackHandler.post(new Runnable() { + @Override + public void run () { + playbackHandler.removeCallbacksAndMessages(null); + releaseMedia(); + changePlaybackState(Player.STATE_IDLE); + if (quitPlaybackThread) { + playbackThread.quit(); + } + } + }); + } + } } From ce557b11fcec3515fdd4315ab8deb5001e379f48 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 27 Nov 2017 03:57:45 -0800 Subject: [PATCH 0892/2472] Add final to boolean used within Runnable. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176997767 --- .../google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java index f6f56ead77..d8f71535da 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java @@ -511,7 +511,7 @@ public class FakeSimpleExoPlayer extends SimpleExoPlayer { } } - private void stop(boolean quitPlaybackThread) { + private void stop(final boolean quitPlaybackThread) { playbackHandler.post(new Runnable() { @Override public void run () { From c1c892f5ec5546257d1da1b125fc4c3466daede2 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Mon, 27 Nov 2017 06:29:47 -0800 Subject: [PATCH 0893/2472] Support undefined text track language when preferred is not available Also slightly improve language normalization/documentation. For this CL, it is assumed that null and "und" languages are different entities. Once we fully tackle language tag normalization, we can decide whether to normalize the "undefined" language. Issue:#2867 Issue:#2980 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177008509 --- RELEASENOTES.md | 3 + .../java/com/google/android/exoplayer2/C.java | 5 + .../trackselection/DefaultTrackSelector.java | 149 ++++++++++++------ .../google/android/exoplayer2/util/Util.java | 14 +- .../DefaultTrackSelectorTest.java | 64 ++++++++ 5 files changed, 180 insertions(+), 55 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index f772bc9f19..ae5bc0fb95 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -8,6 +8,9 @@ use this with `FfmpegAudioRenderer`. * Support extraction and decoding of Dolby Atmos ([#2465](https://github.com/google/ExoPlayer/issues/2465)). +* DefaultTrackSelector: Support undefined language text track selection when the + preferred language is not available + ([#2980](https://github.com/google/ExoPlayer/issues/2980)). ### 2.6.0 ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/C.java b/library/core/src/main/java/com/google/android/exoplayer2/C.java index 592589e221..6a35c0c5e8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/C.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/C.java @@ -424,6 +424,11 @@ public final class C { */ public static final int SELECTION_FLAG_AUTOSELECT = 4; + /** + * Represents an undetermined language as an ISO 639 alpha-3 language code. + */ + public static final String LANGUAGE_UNDETERMINED = "und"; + /** * Represents a streaming or other media type. */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index c789caded4..0029cdbd31 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -46,7 +46,7 @@ import java.util.concurrent.atomic.AtomicReference; * Parameters currentParameters = trackSelector.getParameters(); * // Generate new parameters to prefer German audio and impose a maximum video size constraint. * Parameters newParameters = currentParameters - * .withPreferredAudioLanguage("de") + * .withPreferredAudioLanguage("deu") * .withMaxVideoSize(1024, 768); * // Set the new parameters on the selector. * trackSelector.setParameters(newParameters);} @@ -81,17 +81,22 @@ public class DefaultTrackSelector extends MappingTrackSelector { // Audio /** - * The preferred language for audio, as well as for forced text tracks as defined by RFC 5646. + * The preferred language for audio, as well as for forced text tracks, as an ISO 639-2/T tag. * {@code null} selects the default track, or the first track if there's no default. */ public final String preferredAudioLanguage; // Text /** - * The preferred language for text tracks as defined by RFC 5646. {@code null} selects the + * The preferred language for text tracks as an ISO 639-2/T tag. {@code null} selects the * default track if there is one, or no track otherwise. */ public final String preferredTextLanguage; + /** + * Whether a text track with undetermined language should be selected if no track with + * {@link #preferredTextLanguage} is available, or if {@link #preferredTextLanguage} is unset. + */ + public final boolean selectUndeterminedTextLanguage; // Video /** @@ -150,6 +155,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { *

            *
          • No preferred audio language is set.
          • *
          • No preferred text language is set.
          • + *
          • Text tracks with undetermined language are not selected if no track with + * {@link #preferredTextLanguage} is available.
          • *
          • Lowest bitrate track selections are not forced.
          • *
          • Adaptation between different mime types is not allowed.
          • *
          • Non seamless adaptation is allowed.
          • @@ -161,13 +168,14 @@ public class DefaultTrackSelector extends MappingTrackSelector { *
          */ public Parameters() { - this(null, null, false, false, true, Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE, - true, true, Integer.MAX_VALUE, Integer.MAX_VALUE, true); + this(null, null, false, false, false, true, Integer.MAX_VALUE, Integer.MAX_VALUE, + Integer.MAX_VALUE, true, true, Integer.MAX_VALUE, Integer.MAX_VALUE, true); } /** * @param preferredAudioLanguage See {@link #preferredAudioLanguage} * @param preferredTextLanguage See {@link #preferredTextLanguage} + * @param selectUndeterminedTextLanguage See {@link #selectUndeterminedTextLanguage}. * @param forceLowestBitrate See {@link #forceLowestBitrate}. * @param allowMixedMimeAdaptiveness See {@link #allowMixedMimeAdaptiveness} * @param allowNonSeamlessAdaptiveness See {@link #allowNonSeamlessAdaptiveness} @@ -181,13 +189,14 @@ public class DefaultTrackSelector extends MappingTrackSelector { * @param viewportOrientationMayChange See {@link #viewportOrientationMayChange} */ public Parameters(String preferredAudioLanguage, String preferredTextLanguage, - boolean forceLowestBitrate, boolean allowMixedMimeAdaptiveness, - boolean allowNonSeamlessAdaptiveness, int maxVideoWidth, int maxVideoHeight, - int maxVideoBitrate, boolean exceedVideoConstraintsIfNecessary, + boolean selectUndeterminedTextLanguage, boolean forceLowestBitrate, + boolean allowMixedMimeAdaptiveness, boolean allowNonSeamlessAdaptiveness, int maxVideoWidth, + int maxVideoHeight, int maxVideoBitrate, boolean exceedVideoConstraintsIfNecessary, boolean exceedRendererCapabilitiesIfNecessary, int viewportWidth, int viewportHeight, boolean viewportOrientationMayChange) { this.preferredAudioLanguage = preferredAudioLanguage; this.preferredTextLanguage = preferredTextLanguage; + this.selectUndeterminedTextLanguage = selectUndeterminedTextLanguage; this.forceLowestBitrate = forceLowestBitrate; this.allowMixedMimeAdaptiveness = allowMixedMimeAdaptiveness; this.allowNonSeamlessAdaptiveness = allowNonSeamlessAdaptiveness; @@ -209,10 +218,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (TextUtils.equals(preferredAudioLanguage, this.preferredAudioLanguage)) { return this; } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, forceLowestBitrate, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, viewportOrientationMayChange); + return new Parameters(preferredAudioLanguage, preferredTextLanguage, + selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, + allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, + exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, + viewportHeight, viewportOrientationMayChange); } /** @@ -223,10 +233,26 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (TextUtils.equals(preferredTextLanguage, this.preferredTextLanguage)) { return this; } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, forceLowestBitrate, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, viewportOrientationMayChange); + return new Parameters(preferredAudioLanguage, preferredTextLanguage, + selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, + allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, + exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, + viewportHeight, viewportOrientationMayChange); + } + + /** + * Returns an instance with the provided {@link #selectUndeterminedTextLanguage}. + */ + public Parameters withSelectUndeterminedTextLanguageAsFallback( + boolean selectUndeterminedTextLanguage) { + if (selectUndeterminedTextLanguage == this.selectUndeterminedTextLanguage) { + return this; + } + return new Parameters(preferredAudioLanguage, preferredTextLanguage, + selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, + allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, + exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, + viewportHeight, viewportOrientationMayChange); } /** @@ -236,10 +262,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (forceLowestBitrate == this.forceLowestBitrate) { return this; } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, forceLowestBitrate, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, viewportOrientationMayChange); + return new Parameters(preferredAudioLanguage, preferredTextLanguage, + selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, + allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, + exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, + viewportHeight, viewportOrientationMayChange); } /** @@ -249,10 +276,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (allowMixedMimeAdaptiveness == this.allowMixedMimeAdaptiveness) { return this; } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, forceLowestBitrate, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, viewportOrientationMayChange); + return new Parameters(preferredAudioLanguage, preferredTextLanguage, + selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, + allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, + exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, + viewportHeight, viewportOrientationMayChange); } /** @@ -262,10 +290,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (allowNonSeamlessAdaptiveness == this.allowNonSeamlessAdaptiveness) { return this; } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, forceLowestBitrate, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, viewportOrientationMayChange); + return new Parameters(preferredAudioLanguage, preferredTextLanguage, + selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, + allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, + exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, + viewportHeight, viewportOrientationMayChange); } /** @@ -275,10 +304,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (maxVideoWidth == this.maxVideoWidth && maxVideoHeight == this.maxVideoHeight) { return this; } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, forceLowestBitrate, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, viewportOrientationMayChange); + return new Parameters(preferredAudioLanguage, preferredTextLanguage, + selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, + allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, + exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, + viewportHeight, viewportOrientationMayChange); } /** @@ -288,10 +318,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (maxVideoBitrate == this.maxVideoBitrate) { return this; } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, forceLowestBitrate, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, viewportOrientationMayChange); + return new Parameters(preferredAudioLanguage, preferredTextLanguage, + selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, + allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, + exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, + viewportHeight, viewportOrientationMayChange); } /** @@ -320,10 +351,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (exceedVideoConstraintsIfNecessary == this.exceedVideoConstraintsIfNecessary) { return this; } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, forceLowestBitrate, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, viewportOrientationMayChange); + return new Parameters(preferredAudioLanguage, preferredTextLanguage, + selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, + allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, + exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, + viewportHeight, viewportOrientationMayChange); } /** @@ -334,10 +366,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (exceedRendererCapabilitiesIfNecessary == this.exceedRendererCapabilitiesIfNecessary) { return this; } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, forceLowestBitrate, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, viewportOrientationMayChange); + return new Parameters(preferredAudioLanguage, preferredTextLanguage, + selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, + allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, + exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, + viewportHeight, viewportOrientationMayChange); } /** @@ -350,10 +383,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { && viewportOrientationMayChange == this.viewportOrientationMayChange) { return this; } - return new Parameters(preferredAudioLanguage, preferredTextLanguage, forceLowestBitrate, - allowMixedMimeAdaptiveness, allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, - maxVideoBitrate, exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, - viewportWidth, viewportHeight, viewportOrientationMayChange); + return new Parameters(preferredAudioLanguage, preferredTextLanguage, + selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, + allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, + exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, + viewportHeight, viewportOrientationMayChange); } /** @@ -880,17 +914,20 @@ public class DefaultTrackSelector extends MappingTrackSelector { boolean isDefault = (format.selectionFlags & C.SELECTION_FLAG_DEFAULT) != 0; boolean isForced = (format.selectionFlags & C.SELECTION_FLAG_FORCED) != 0; int trackScore; - if (formatHasLanguage(format, params.preferredTextLanguage)) { + boolean preferredLanguageFound = formatHasLanguage(format, params.preferredTextLanguage); + if (preferredLanguageFound + || (params.selectUndeterminedTextLanguage && formatHasNoLanguage(format))) { if (isDefault) { - trackScore = 6; + trackScore = 8; } else if (!isForced) { // Prefer non-forced to forced if a preferred text language has been specified. Where // both are provided the non-forced track will usually contain the forced subtitles as // a subset. - trackScore = 5; + trackScore = 6; } else { trackScore = 4; } + trackScore += preferredLanguageFound ? 1 : 0; } else if (isDefault) { trackScore = 3; } else if (isForced) { @@ -980,6 +1017,16 @@ public class DefaultTrackSelector extends MappingTrackSelector { && maskedSupport == RendererCapabilities.FORMAT_EXCEEDS_CAPABILITIES); } + /** + * Returns whether a {@link Format} does not define a language. + * + * @param format The {@link Format}. + * @return Whether the {@link Format} does not define a language. + */ + protected static boolean formatHasNoLanguage(Format format) { + return TextUtils.isEmpty(format.language) || formatHasLanguage(format, C.LANGUAGE_UNDETERMINED); + } + /** * Returns whether a {@link Format} specifies a particular language, or {@code false} if * {@code language} is null. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java index 5b2de1042e..24c5f4036d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -50,6 +50,7 @@ import java.util.Formatter; import java.util.GregorianCalendar; import java.util.List; import java.util.Locale; +import java.util.MissingResourceException; import java.util.TimeZone; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -238,13 +239,18 @@ public final class Util { } /** - * Returns a normalized RFC 5646 language code. + * Returns a normalized RFC 639-2/T code for {@code language}. * - * @param language A possibly non-normalized RFC 5646 language code. - * @return The normalized code, or null if the input was null. + * @param language A case-insensitive ISO 639 alpha-2 or alpha-3 language code. + * @return The all-lowercase normalized code, or null if the input was null, or + * {@code language.toLowerCase()} if the language could not be normalized. */ public static String normalizeLanguageCode(String language) { - return language == null ? null : new Locale(language).getLanguage(); + try { + return language == null ? null : new Locale(language).getISO3Language(); + } catch (MissingResourceException e) { + return language.toLowerCase(); + } } /** diff --git a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java index a0e499139c..b2b149b004 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java @@ -36,6 +36,8 @@ public final class DefaultTrackSelectorTest { private static final Parameters DEFAULT_PARAMETERS = new Parameters(); private static final RendererCapabilities ALL_AUDIO_FORMAT_SUPPORTED_RENDERER_CAPABILITIES = new FakeRendererCapabilities(C.TRACK_TYPE_AUDIO); + private static final RendererCapabilities ALL_TEXT_FORMAT_SUPPORTED_RENDERER_CAPABILITIES = + new FakeRendererCapabilities(C.TRACK_TYPE_TEXT); private static final RendererCapabilities ALL_AUDIO_FORMAT_EXCEEDED_RENDERER_CAPABILITIES = new FakeRendererCapabilities(C.TRACK_TYPE_AUDIO, FORMAT_EXCEEDS_CAPABILITIES); @@ -534,6 +536,60 @@ public final class DefaultTrackSelectorTest { .isEqualTo(lowerSampleRateHigherBitrateFormat); } + /** + * Tests that the default track selector will select a text track with undetermined language if no + * text track with the preferred language is available but + * {@link Parameters#selectUndeterminedTextLanguage} is true. + */ + @Test + public void testSelectUndeterminedTextLanguageAsFallback() throws ExoPlaybackException{ + Format spanish = Format.createTextContainerFormat("spanish", null, + MimeTypes.TEXT_VTT, null, Format.NO_VALUE, 0, "spa"); + Format german = Format.createTextContainerFormat("german", null, + MimeTypes.TEXT_VTT, null, Format.NO_VALUE, 0, "de"); + Format undeterminedUnd = Format.createTextContainerFormat("undeterminedUnd", null, + MimeTypes.TEXT_VTT, null, Format.NO_VALUE, 0, "und"); + Format undeterminedNull = Format.createTextContainerFormat("undeterminedNull", null, + MimeTypes.TEXT_VTT, null, Format.NO_VALUE, 0, null); + + RendererCapabilities[] textRendererCapabilites = + new RendererCapabilities[] {ALL_TEXT_FORMAT_SUPPORTED_RENDERER_CAPABILITIES}; + + TrackSelectorResult result; + + result = trackSelector.selectTracks(textRendererCapabilites, + wrapFormats(spanish, german, undeterminedUnd, undeterminedNull)); + assertThat(result.selections.get(0)).isNull(); + + trackSelector.setParameters( + DEFAULT_PARAMETERS.withSelectUndeterminedTextLanguageAsFallback(true)); + result = trackSelector.selectTracks(textRendererCapabilites, + wrapFormats(spanish, german, undeterminedUnd, undeterminedNull)); + assertThat(result.selections.get(0).getFormat(0)).isSameAs(undeterminedUnd); + + trackSelector.setParameters(DEFAULT_PARAMETERS.withPreferredTextLanguage("spa")); + result = trackSelector.selectTracks(textRendererCapabilites, + wrapFormats(spanish, german, undeterminedUnd, undeterminedNull)); + assertThat(result.selections.get(0).getFormat(0)).isSameAs(spanish); + + result = trackSelector.selectTracks(textRendererCapabilites, + wrapFormats(german, undeterminedUnd, undeterminedNull)); + assertThat(result.selections.get(0)).isNull(); + + trackSelector.setParameters( + trackSelector.getParameters().withSelectUndeterminedTextLanguageAsFallback(true)); + result = trackSelector.selectTracks(textRendererCapabilites, + wrapFormats(german, undeterminedUnd, undeterminedNull)); + assertThat(result.selections.get(0).getFormat(0)).isSameAs(undeterminedUnd); + + result = trackSelector.selectTracks(textRendererCapabilites, + wrapFormats(german, undeterminedNull)); + assertThat(result.selections.get(0).getFormat(0)).isSameAs(undeterminedNull); + + result = trackSelector.selectTracks(textRendererCapabilites, wrapFormats(german)); + assertThat(result.selections.get(0)).isNull(); + } + /** * Tests that track selector will select audio tracks with lower bitrate when {@link Parameters} * indicate lowest bitrate preference, even when tracks are within capabilities. @@ -562,6 +618,14 @@ public final class DefaultTrackSelectorTest { return new TrackGroupArray(new TrackGroup(formats)); } + private static TrackGroupArray wrapFormats(Format... formats) { + TrackGroup[] trackGroups = new TrackGroup[formats.length]; + for (int i = 0; i < trackGroups.length; i++) { + trackGroups[i] = new TrackGroup(formats[i]); + } + return new TrackGroupArray(trackGroups); + } + /** * A {@link RendererCapabilities} that advertises support for all formats of a given type using * a provided support value. For any format that does not have the given track type, From 1861aea2b3159537e11d7e5329210dede110e048 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 27 Nov 2017 07:02:33 -0800 Subject: [PATCH 0894/2472] Add throws IllegalSeekPositionException doc to seekTo(windowIndex, positionMs). ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177011497 --- .../src/main/java/com/google/android/exoplayer2/Player.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Player.java b/library/core/src/main/java/com/google/android/exoplayer2/Player.java index dc703f924a..d911f83392 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Player.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Player.java @@ -368,6 +368,8 @@ public interface Player { * @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. + * @throws IllegalSeekPositionException If the player has a non-empty timeline and the provided + * {@code windowIndex} is not within the bounds of the current timeline. */ void seekTo(int windowIndex, long positionMs); From 23a7f2d994007f59bd88106e35db45b93f78fcb5 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 27 Nov 2017 12:42:53 -0800 Subject: [PATCH 0895/2472] Force wrapping of HLS ID3 timestamp Merge of https://github.com/google/ExoPlayer/pull/3495 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177057183 --- .../com/google/android/exoplayer2/source/hls/HlsMediaChunk.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/hls/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 index 83167c152f..1ad5acc5c5 100644 --- a/library/hls/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 @@ -306,7 +306,7 @@ import java.util.concurrent.atomic.AtomicInteger; if (PRIV_TIMESTAMP_FRAME_OWNER.equals(privFrame.owner)) { System.arraycopy(privFrame.privateData, 0, id3Data.data, 0, 8 /* timestamp size */); id3Data.reset(8); - return id3Data.readLong() & ((1L << 33) - 1L); + return id3Data.readLong() & 0x1FFFFFFFFL; } } } From d3fd2d1b872d2584453a60b5aaf3fe8ef502f48e Mon Sep 17 00:00:00 2001 From: ojw28 Date: Tue, 28 Nov 2017 17:02:04 +0000 Subject: [PATCH 0896/2472] Update ISSUE_TEMPLATE --- ISSUE_TEMPLATE | 2 -- 1 file changed, 2 deletions(-) diff --git a/ISSUE_TEMPLATE b/ISSUE_TEMPLATE index 1b912312d1..e85c0c28c7 100644 --- a/ISSUE_TEMPLATE +++ b/ISSUE_TEMPLATE @@ -1,5 +1,3 @@ -*** ISSUES THAT IGNORE THIS TEMPLATE WILL BE CLOSED WITHOUT INVESTIGATION *** - Before filing an issue: ----------------------- - Search existing issues, including issues that are closed. From e175bf9e4271bae6c85bc49ff2d51897660de5c2 Mon Sep 17 00:00:00 2001 From: Pavel Stambrecht Date: Mon, 4 Dec 2017 15:45:54 +0100 Subject: [PATCH 0897/2472] Iso8601Parser improved to be able to parse timestamp offsets from UTC --- .../source/dash/DashMediaSource.java | 36 ++++++++++++++----- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/library/dash/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 index a82b5af583..edd941c1dd 100644 --- a/library/dash/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 @@ -934,19 +934,39 @@ public final class DashMediaSource implements MediaSource { private static final class Iso8601Parser implements ParsingLoadable.Parser { + private static final String ISO_8601_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'"; + private static final String ISO_8601_FORMAT_2 = "yyyy-MM-dd'T'HH:mm:ssZ"; + private static final String ISO_8601_FORMAT_3 = "yyyy-MM-dd'T'HH:mm:ssZ"; + private static final String ISO_8601_FORMAT_2_REGEX_PATTERN = ".*[+\\-]\\d{2}:\\d{2}$"; + private static final String ISO_8601_FORMAT_3_REGEX_PATTERN = ".*[+\\-]\\d{4}$"; + @Override public Long parse(Uri uri, InputStream inputStream) throws IOException { String firstLine = new BufferedReader(new InputStreamReader(inputStream)).readLine(); - try { - // TODO: It may be necessary to handle timestamp offsets from UTC. - SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US); - format.setTimeZone(TimeZone.getTimeZone("UTC")); - return format.parse(firstLine).getTime(); - } catch (ParseException e) { - throw new ParserException(e); + + if (firstLine != null) { + //determine format pattern + String formatPattern; + if (firstLine.matches(ISO_8601_FORMAT_2_REGEX_PATTERN)) { + formatPattern = ISO_8601_FORMAT_2; + } else if (firstLine.matches(ISO_8601_FORMAT_3_REGEX_PATTERN)) { + formatPattern = ISO_8601_FORMAT_3; + } else { + formatPattern = ISO_8601_FORMAT; + } + //parse + try { + SimpleDateFormat format = new SimpleDateFormat(formatPattern, Locale.US); + format.setTimeZone(TimeZone.getTimeZone("UTC")); + return format.parse(firstLine).getTime(); + } catch (ParseException e) { + throw new ParserException(e); + } + + } else { + throw new ParserException("Unable to parse ISO 8601. Input value is null"); } } - } } From ee05b60a19ef9bb1e43d9e7d337a41552f3f9f78 Mon Sep 17 00:00:00 2001 From: Pavel Stambrecht Date: Mon, 4 Dec 2017 15:52:12 +0100 Subject: [PATCH 0898/2472] Iso8601Parser improved to be able to parse timestamp offsets from UTC --- .../source/dash/DashMediaSource.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/library/dash/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 index edd941c1dd..f1ee813020 100644 --- a/library/dash/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 @@ -935,10 +935,9 @@ public final class DashMediaSource implements MediaSource { private static final class Iso8601Parser implements ParsingLoadable.Parser { private static final String ISO_8601_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'"; - private static final String ISO_8601_FORMAT_2 = "yyyy-MM-dd'T'HH:mm:ssZ"; - private static final String ISO_8601_FORMAT_3 = "yyyy-MM-dd'T'HH:mm:ssZ"; - private static final String ISO_8601_FORMAT_2_REGEX_PATTERN = ".*[+\\-]\\d{2}:\\d{2}$"; - private static final String ISO_8601_FORMAT_3_REGEX_PATTERN = ".*[+\\-]\\d{4}$"; + private static final String ISO_8601_WITH_OFFSET_FORMAT = "yyyy-MM-dd'T'HH:mm:ssZ"; + private static final String ISO_8601_WITH_OFFSET_FORMAT_REGEX_PATTERN = ".*[+\\-]\\d{2}:\\d{2}$"; + private static final String ISO_8601_WITH_OFFSET_FORMAT_REGEX_PATTERN_2 = ".*[+\\-]\\d{4}$"; @Override public Long parse(Uri uri, InputStream inputStream) throws IOException { @@ -947,10 +946,10 @@ public final class DashMediaSource implements MediaSource { if (firstLine != null) { //determine format pattern String formatPattern; - if (firstLine.matches(ISO_8601_FORMAT_2_REGEX_PATTERN)) { - formatPattern = ISO_8601_FORMAT_2; - } else if (firstLine.matches(ISO_8601_FORMAT_3_REGEX_PATTERN)) { - formatPattern = ISO_8601_FORMAT_3; + if (firstLine.matches(ISO_8601_WITH_OFFSET_FORMAT_REGEX_PATTERN)) { + formatPattern = ISO_8601_WITH_OFFSET_FORMAT; + } else if (firstLine.matches(ISO_8601_WITH_OFFSET_FORMAT_REGEX_PATTERN_2)) { + formatPattern = ISO_8601_WITH_OFFSET_FORMAT; } else { formatPattern = ISO_8601_FORMAT; } @@ -967,6 +966,7 @@ public final class DashMediaSource implements MediaSource { throw new ParserException("Unable to parse ISO 8601. Input value is null"); } } - } + } + } From 1d96492c1e370cf5c0792c987d106ea579285856 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 27 Nov 2017 13:32:44 -0800 Subject: [PATCH 0899/2472] Update moe equivalence ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177063576 --- .../com/google/android/exoplayer2/source/hls/HlsMediaChunk.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/library/hls/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 index 1ad5acc5c5..c4e54d4bd3 100644 --- a/library/hls/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 @@ -306,6 +306,8 @@ import java.util.concurrent.atomic.AtomicInteger; if (PRIV_TIMESTAMP_FRAME_OWNER.equals(privFrame.owner)) { System.arraycopy(privFrame.privateData, 0, id3Data.data, 0, 8 /* timestamp size */); id3Data.reset(8); + // The top 31 bits should be zeros, but explicitly zero them to wrap in the case that the + // streaming provider forgot. See: https://github.com/google/ExoPlayer/pull/3495. return id3Data.readLong() & 0x1FFFFFFFFL; } } From 7a031980281555edd99d353469a7fdc5ea7fec22 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 28 Nov 2017 03:58:30 -0800 Subject: [PATCH 0900/2472] Add some clarifications to MediaSource documentation ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177141094 --- .../android/exoplayer2/source/MediaSource.java | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java index 7288b39897..4a0d8e196d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java @@ -35,7 +35,8 @@ import java.io.IOException; * player to load and read the media. * * All methods are called on the player's internal playback thread, as described in the - * {@link ExoPlayer} Javadoc. + * {@link ExoPlayer} Javadoc. They should not be called directly from application code. Instances + * should not be re-used, meaning they should be passed to {@link ExoPlayer#prepare} at most once. */ public interface MediaSource { @@ -150,6 +151,8 @@ public interface MediaSource { /** * Starts preparation of the source. + *

          + * Should not be called directly from application code. * * @param player The player for which this source is being prepared. * @param isTopLevelSource Whether this source has been passed directly to @@ -162,6 +165,8 @@ public interface MediaSource { /** * Throws any pending error encountered while loading or refreshing source information. + *

          + * Should not be called directly from application code. */ void maybeThrowSourceInfoRefreshError() throws IOException; @@ -169,6 +174,8 @@ public interface MediaSource { * Returns a new {@link MediaPeriod} identified by {@code periodId}. This method may be called * multiple times with the same period identifier without an intervening call to * {@link #releasePeriod(MediaPeriod)}. + *

          + * Should not be called directly from application code. * * @param id The identifier of the period. * @param allocator An {@link Allocator} from which to obtain media buffer allocations. @@ -178,6 +185,8 @@ public interface MediaSource { /** * Releases the period. + *

          + * Should not be called directly from application code. * * @param mediaPeriod The period to release. */ @@ -186,8 +195,7 @@ public interface MediaSource { /** * Releases the source. *

          - * This method should be called when the source is no longer required. It may be called in any - * state. + * Should not be called directly from application code. */ void releaseSource(); From 0e0300b802bc10e210b0e36db18ecf35ecd30af2 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 28 Nov 2017 08:40:21 -0800 Subject: [PATCH 0901/2472] Extractor cleanup - Align class summary Javadoc - Fix ErrorProne + Style warnings ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177165593 --- .../extractor/flv/FlvExtractor.java | 46 ++++++++++++------- .../extractor/mkv/MatroskaExtractor.java | 2 +- .../extractor/mp3/Mp3Extractor.java | 2 +- .../exoplayer2/extractor/mp3/XingSeeker.java | 3 +- .../extractor/mp4/FragmentedMp4Extractor.java | 2 +- .../extractor/mp4/Mp4Extractor.java | 2 +- .../extractor/ogg/DefaultOggSeeker.java | 2 +- .../exoplayer2/extractor/ogg/FlacReader.java | 3 +- .../extractor/ogg/OggExtractor.java | 2 +- .../extractor/rawcc/RawCcExtractor.java | 2 +- .../exoplayer2/extractor/ts/Ac3Extractor.java | 2 +- .../extractor/ts/AdtsExtractor.java | 2 +- .../exoplayer2/extractor/ts/PsExtractor.java | 2 +- .../exoplayer2/extractor/ts/TsExtractor.java | 2 +- .../extractor/wav/WavExtractor.java | 4 +- 15 files changed, 47 insertions(+), 31 deletions(-) diff --git a/library/core/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 index 218e6ffd82..30b66d65fd 100644 --- a/library/core/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 @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor.flv; +import android.support.annotation.IntDef; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; @@ -25,9 +26,11 @@ import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; /** - * Facilitates the extraction of data from the FLV container format. + * Extracts data from the FLV container format. */ public final class FlvExtractor implements Extractor, SeekMap { @@ -43,16 +46,22 @@ public final class FlvExtractor implements Extractor, SeekMap { }; - // Header sizes. - private static final int FLV_HEADER_SIZE = 9; - private static final int FLV_TAG_HEADER_SIZE = 11; - - // Parser states. + /** + * Extractor states. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({STATE_READING_FLV_HEADER, STATE_SKIPPING_TO_TAG_HEADER, STATE_READING_TAG_HEADER, + STATE_READING_TAG_DATA}) + private @interface States {} private static final int STATE_READING_FLV_HEADER = 1; private static final int STATE_SKIPPING_TO_TAG_HEADER = 2; private static final int STATE_READING_TAG_HEADER = 3; private static final int STATE_READING_TAG_DATA = 4; + // Header sizes. + private static final int FLV_HEADER_SIZE = 9; + private static final int FLV_TAG_HEADER_SIZE = 11; + // Tag types. private static final int TAG_TYPE_AUDIO = 8; private static final int TAG_TYPE_VIDEO = 9; @@ -71,11 +80,11 @@ public final class FlvExtractor implements Extractor, SeekMap { private ExtractorOutput extractorOutput; // State variables. - private int parserState; + private @States int state; private int bytesToNextTagHeader; - public int tagType; - public int tagDataSize; - public long tagTimestampUs; + private int tagType; + private int tagDataSize; + private long tagTimestampUs; // Tags readers. private AudioTagPayloadReader audioReader; @@ -87,7 +96,7 @@ public final class FlvExtractor implements Extractor, SeekMap { headerBuffer = new ParsableByteArray(FLV_HEADER_SIZE); tagHeaderBuffer = new ParsableByteArray(FLV_TAG_HEADER_SIZE); tagData = new ParsableByteArray(); - parserState = STATE_READING_FLV_HEADER; + state = STATE_READING_FLV_HEADER; } @Override @@ -128,7 +137,7 @@ public final class FlvExtractor implements Extractor, SeekMap { @Override public void seek(long position, long timeUs) { - parserState = STATE_READING_FLV_HEADER; + state = STATE_READING_FLV_HEADER; bytesToNextTagHeader = 0; } @@ -141,7 +150,7 @@ public final class FlvExtractor implements Extractor, SeekMap { public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException, InterruptedException { while (true) { - switch (parserState) { + switch (state) { case STATE_READING_FLV_HEADER: if (!readFlvHeader(input)) { return RESULT_END_OF_INPUT; @@ -160,6 +169,9 @@ public final class FlvExtractor implements Extractor, SeekMap { return RESULT_CONTINUE; } break; + default: + // Never happens. + throw new IllegalStateException(); } } } @@ -199,7 +211,7 @@ public final class FlvExtractor implements Extractor, SeekMap { // We need to skip any additional content in the FLV header, plus the 4 byte previous tag size. bytesToNextTagHeader = headerBuffer.readInt() - FLV_HEADER_SIZE + 4; - parserState = STATE_SKIPPING_TO_TAG_HEADER; + state = STATE_SKIPPING_TO_TAG_HEADER; return true; } @@ -213,7 +225,7 @@ public final class FlvExtractor implements Extractor, SeekMap { private void skipToTagHeader(ExtractorInput input) throws IOException, InterruptedException { input.skipFully(bytesToNextTagHeader); bytesToNextTagHeader = 0; - parserState = STATE_READING_TAG_HEADER; + state = STATE_READING_TAG_HEADER; } /** @@ -236,7 +248,7 @@ public final class FlvExtractor implements Extractor, SeekMap { tagTimestampUs = tagHeaderBuffer.readUnsignedInt24(); tagTimestampUs = ((tagHeaderBuffer.readUnsignedByte() << 24) | tagTimestampUs) * 1000L; tagHeaderBuffer.skipBytes(3); // streamId - parserState = STATE_READING_TAG_DATA; + state = STATE_READING_TAG_DATA; return true; } @@ -261,7 +273,7 @@ public final class FlvExtractor implements Extractor, SeekMap { wasConsumed = false; } bytesToNextTagHeader = 4; // There's a 4 byte previous tag size before the next header. - parserState = STATE_SKIPPING_TO_TAG_HEADER; + state = STATE_SKIPPING_TO_TAG_HEADER; return wasConsumed; } diff --git a/library/core/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 index 5aefd041c4..4b0bbda275 100644 --- a/library/core/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 @@ -53,7 +53,7 @@ import java.util.Locale; import java.util.UUID; /** - * Extracts data from a Matroska or WebM file. + * Extracts data from the Matroska and WebM container formats. */ public final class MatroskaExtractor implements Extractor { diff --git a/library/core/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 index a4349ada09..dc7d21851a 100644 --- a/library/core/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 @@ -38,7 +38,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; /** - * Extracts data from an MP3 file. + * Extracts data from the MP3 container format. */ public final class Mp3Extractor implements Extractor { diff --git a/library/core/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 index 5e8d72f18d..9b1158dfa8 100644 --- a/library/core/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 @@ -113,12 +113,13 @@ import com.google.android.exoplayer2.util.Util; fx = 256f; } else { int a = (int) percent; - float fa, fb; + float fa; if (a == 0) { fa = 0f; } else { fa = tableOfContents[a - 1]; } + float fb; if (a < 99) { fb = tableOfContents[a]; } else { diff --git a/library/core/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 index e86157dd92..4bc1b04418 100644 --- a/library/core/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 @@ -53,7 +53,7 @@ import java.util.Stack; import java.util.UUID; /** - * Facilitates the extraction of data from the fragmented mp4 container format. + * Extracts data from the FMP4 container format. */ public final class FragmentedMp4Extractor implements Extractor { diff --git a/library/core/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 index f23af98e7f..f2412bf4ba 100644 --- a/library/core/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 @@ -41,7 +41,7 @@ import java.util.List; import java.util.Stack; /** - * Extracts data from an unfragmented MP4 file. + * Extracts data from the MP4 container format. */ public final class Mp4Extractor implements Extractor, SeekMap { diff --git a/library/core/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 index 5470e2badc..77def57275 100644 --- a/library/core/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 @@ -186,7 +186,7 @@ import java.io.IOException; return start; } - long offset = pageSize * (granuleDistance <= 0 ? 2 : 1); + long offset = pageSize * (granuleDistance <= 0 ? 2L : 1L); long nextPosition = input.getPosition() - offset + (granuleDistance * (end - start) / (endGranule - startGranule)); diff --git a/library/core/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 index f4da6e3960..304fb3dd96 100644 --- a/library/core/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 @@ -118,8 +118,9 @@ import java.util.List; case 14: case 15: return 256 << (blockSizeCode - 8); + default: + return -1; } - return -1; } private class FlacOggSeeker implements OggSeeker, SeekMap { diff --git a/library/core/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 index 54e168c665..a4d8f97d5b 100644 --- a/library/core/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 @@ -27,7 +27,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray; import java.io.IOException; /** - * Ogg {@link Extractor}. + * Extracts data from the Ogg container format. */ public class OggExtractor implements Extractor { diff --git a/library/core/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 index 7840eafce6..aa77aba30e 100644 --- a/library/core/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 @@ -29,7 +29,7 @@ import com.google.android.exoplayer2.util.Util; import java.io.IOException; /** - * Extracts CEA data from a RawCC file. + * Extracts data from the RawCC container format. */ public final class RawCcExtractor implements Extractor { diff --git a/library/core/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 index 4d54600c6d..bc37277c57 100644 --- a/library/core/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 @@ -29,7 +29,7 @@ import com.google.android.exoplayer2.util.Util; import java.io.IOException; /** - * Extracts samples from (E-)AC-3 bitstreams. + * Extracts data from (E-)AC-3 bitstreams. */ public final class Ac3Extractor implements Extractor { diff --git a/library/core/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 index 5ce15952a5..a0a748660e 100644 --- a/library/core/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 @@ -29,7 +29,7 @@ import com.google.android.exoplayer2.util.Util; import java.io.IOException; /** - * Extracts samples from AAC bit streams with ADTS framing. + * Extracts data from AAC bit streams with ADTS framing. */ public final class AdtsExtractor implements Extractor { diff --git a/library/core/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 index 69c5745eaa..f3aad6ba6b 100644 --- a/library/core/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 @@ -31,7 +31,7 @@ import com.google.android.exoplayer2.util.TimestampAdjuster; import java.io.IOException; /** - * Facilitates the extraction of data from the MPEG-2 PS container format. + * Extracts data from the MPEG-2 PS container format. */ public final class PsExtractor implements Extractor { diff --git a/library/core/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 index 213d30d47d..13e669da23 100644 --- a/library/core/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 @@ -45,7 +45,7 @@ import java.util.Collections; import java.util.List; /** - * Facilitates the extraction of data from the MPEG-2 TS container format. + * Extracts data from the MPEG-2 TS container format. */ public final class TsExtractor implements Extractor { diff --git a/library/core/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 index cb46aa5519..cb9a2653d7 100644 --- a/library/core/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 @@ -28,7 +28,9 @@ import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.util.MimeTypes; import java.io.IOException; -/** {@link Extractor} to extract samples from a WAV byte stream. */ +/** + * Extracts data from WAV byte streams. + */ public final class WavExtractor implements Extractor, SeekMap { /** From a8d4ad94f4011ec310e3d3359d06d405bd9ef37e Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 28 Nov 2017 09:22:32 -0800 Subject: [PATCH 0902/2472] Fix DefaultTrackSelector#Parameter withSelectUndeterminedTextLanguage ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177170994 --- .../exoplayer2/trackselection/DefaultTrackSelector.java | 3 +-- .../exoplayer2/trackselection/DefaultTrackSelectorTest.java | 5 ++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index 0029cdbd31..49b8e8964b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -243,8 +243,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { /** * Returns an instance with the provided {@link #selectUndeterminedTextLanguage}. */ - public Parameters withSelectUndeterminedTextLanguageAsFallback( - boolean selectUndeterminedTextLanguage) { + public Parameters withSelectUndeterminedTextLanguage(boolean selectUndeterminedTextLanguage) { if (selectUndeterminedTextLanguage == this.selectUndeterminedTextLanguage) { return this; } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java index b2b149b004..6b14d139ae 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java @@ -561,8 +561,7 @@ public final class DefaultTrackSelectorTest { wrapFormats(spanish, german, undeterminedUnd, undeterminedNull)); assertThat(result.selections.get(0)).isNull(); - trackSelector.setParameters( - DEFAULT_PARAMETERS.withSelectUndeterminedTextLanguageAsFallback(true)); + trackSelector.setParameters(DEFAULT_PARAMETERS.withSelectUndeterminedTextLanguage(true)); result = trackSelector.selectTracks(textRendererCapabilites, wrapFormats(spanish, german, undeterminedUnd, undeterminedNull)); assertThat(result.selections.get(0).getFormat(0)).isSameAs(undeterminedUnd); @@ -577,7 +576,7 @@ public final class DefaultTrackSelectorTest { assertThat(result.selections.get(0)).isNull(); trackSelector.setParameters( - trackSelector.getParameters().withSelectUndeterminedTextLanguageAsFallback(true)); + trackSelector.getParameters().withSelectUndeterminedTextLanguage(true)); result = trackSelector.selectTracks(textRendererCapabilites, wrapFormats(german, undeterminedUnd, undeterminedNull)); assertThat(result.selections.get(0).getFormat(0)).isSameAs(undeterminedUnd); From 9eed1150e083a224a732506ed9db66bd3699f989 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 28 Nov 2017 09:43:08 -0800 Subject: [PATCH 0903/2472] Clean up some extrator SeekMap implementations ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177173618 --- .../extractor/flv/FlvExtractor.java | 45 +++++------ .../extractor/flv/ScriptTagPayloadReader.java | 8 +- .../extractor/wav/WavExtractor.java | 21 +---- .../exoplayer2/extractor/wav/WavHeader.java | 76 ++++++++++++------- 4 files changed, 69 insertions(+), 81 deletions(-) diff --git a/library/core/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 index 30b66d65fd..2da075ff53 100644 --- a/library/core/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 @@ -32,7 +32,7 @@ import java.lang.annotation.RetentionPolicy; /** * Extracts data from the FLV container format. */ -public final class FlvExtractor implements Extractor, SeekMap { +public final class FlvExtractor implements Extractor { /** * Factory for {@link FlvExtractor} instances. @@ -70,32 +70,28 @@ public final class FlvExtractor implements Extractor, SeekMap { // FLV container identifier. private static final int FLV_TAG = Util.getIntegerCodeForString("FLV"); - // Temporary buffers. private final ParsableByteArray scratch; private final ParsableByteArray headerBuffer; private final ParsableByteArray tagHeaderBuffer; private final ParsableByteArray tagData; + private final ScriptTagPayloadReader metadataReader; - // Extractor outputs. private ExtractorOutput extractorOutput; - - // State variables. private @States int state; private int bytesToNextTagHeader; private int tagType; private int tagDataSize; private long tagTimestampUs; - - // Tags readers. + private boolean outputSeekMap; private AudioTagPayloadReader audioReader; private VideoTagPayloadReader videoReader; - private ScriptTagPayloadReader metadataReader; public FlvExtractor() { scratch = new ParsableByteArray(4); headerBuffer = new ParsableByteArray(FLV_HEADER_SIZE); tagHeaderBuffer = new ParsableByteArray(FLV_TAG_HEADER_SIZE); tagData = new ParsableByteArray(); + metadataReader = new ScriptTagPayloadReader(); state = STATE_READING_FLV_HEADER; } @@ -203,11 +199,7 @@ public final class FlvExtractor implements Extractor, SeekMap { videoReader = new VideoTagPayloadReader( extractorOutput.track(TAG_TYPE_VIDEO, C.TRACK_TYPE_VIDEO)); } - if (metadataReader == null) { - metadataReader = new ScriptTagPayloadReader(null); - } extractorOutput.endTracks(); - extractorOutput.seekMap(this); // We need to skip any additional content in the FLV header, plus the 4 byte previous tag size. bytesToNextTagHeader = headerBuffer.readInt() - FLV_HEADER_SIZE + 4; @@ -263,11 +255,18 @@ public final class FlvExtractor implements Extractor, SeekMap { private boolean readTagData(ExtractorInput input) throws IOException, InterruptedException { boolean wasConsumed = true; if (tagType == TAG_TYPE_AUDIO && audioReader != null) { + ensureOutputSeekMap(); audioReader.consume(prepareTagData(input), tagTimestampUs); } else if (tagType == TAG_TYPE_VIDEO && videoReader != null) { + ensureOutputSeekMap(); videoReader.consume(prepareTagData(input), tagTimestampUs); - } else if (tagType == TAG_TYPE_SCRIPT_DATA && metadataReader != null) { + } else if (tagType == TAG_TYPE_SCRIPT_DATA && !outputSeekMap) { metadataReader.consume(prepareTagData(input), tagTimestampUs); + long durationUs = metadataReader.getDurationUs(); + if (durationUs != C.TIME_UNSET) { + extractorOutput.seekMap(new SeekMap.Unseekable(durationUs)); + outputSeekMap = true; + } } else { input.skipFully(tagDataSize); wasConsumed = false; @@ -289,21 +288,11 @@ public final class FlvExtractor implements Extractor, SeekMap { return tagData; } - // SeekMap implementation. - - @Override - public boolean isSeekable() { - return false; - } - - @Override - public long getDurationUs() { - return metadataReader.getDurationUs(); - } - - @Override - public long getPosition(long timeUs) { - return 0; + private void ensureOutputSeekMap() { + if (!outputSeekMap) { + extractorOutput.seekMap(new SeekMap.Unseekable(C.TIME_UNSET)); + outputSeekMap = true; + } } } diff --git a/library/core/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 index 1a4f8f3e88..2dec85ffcc 100644 --- a/library/core/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 @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.extractor.flv; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; -import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.util.ParsableByteArray; import java.util.ArrayList; import java.util.Date; @@ -44,11 +43,8 @@ import java.util.Map; private long durationUs; - /** - * @param output A {@link TrackOutput} to which samples should be written. - */ - public ScriptTagPayloadReader(TrackOutput output) { - super(output); + public ScriptTagPayloadReader() { + super(null); durationUs = C.TIME_UNSET; } diff --git a/library/core/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 index cb9a2653d7..4f2be71a69 100644 --- a/library/core/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 @@ -23,7 +23,6 @@ import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorOutput; 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.util.MimeTypes; import java.io.IOException; @@ -31,7 +30,7 @@ import java.io.IOException; /** * Extracts data from WAV byte streams. */ -public final class WavExtractor implements Extractor, SeekMap { +public final class WavExtractor implements Extractor { /** * Factory for {@link WavExtractor} instances. @@ -95,7 +94,7 @@ public final class WavExtractor implements Extractor, SeekMap { if (!wavHeader.hasDataBounds()) { WavHeaderReader.skipToData(input, wavHeader); - extractorOutput.seekMap(this); + extractorOutput.seekMap(wavHeader); } int bytesAppended = trackOutput.sampleData(input, MAX_INPUT_SIZE - pendingBytes, true); @@ -115,20 +114,4 @@ public final class WavExtractor implements Extractor, SeekMap { return bytesAppended == RESULT_END_OF_INPUT ? RESULT_END_OF_INPUT : RESULT_CONTINUE; } - // SeekMap implementation. - - @Override - public long getDurationUs() { - return wavHeader.getDurationUs(); - } - - @Override - public boolean isSeekable() { - return true; - } - - @Override - public long getPosition(long timeUs) { - return wavHeader.getPosition(timeUs); - } } diff --git a/library/core/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 index a57060f604..1c1fc97a22 100644 --- a/library/core/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 @@ -16,9 +16,10 @@ package com.google.android.exoplayer2.extractor.wav; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.extractor.SeekMap; /** Header for a WAV file. */ -/*package*/ final class WavHeader { +/* package */ final class WavHeader implements SeekMap { /** Number of audio chanels. */ private final int numChannels; @@ -49,12 +50,56 @@ import com.google.android.exoplayer2.C; this.encoding = encoding; } - /** Returns the duration in microseconds of this WAV. */ + // Setting bounds. + + /** + * Sets the data start position and size in bytes of sample data in this WAV. + * + * @param dataStartPosition The data start position in bytes. + * @param dataSize The data size in bytes. + */ + public void setDataBounds(long dataStartPosition, long dataSize) { + this.dataStartPosition = dataStartPosition; + this.dataSize = dataSize; + } + + /** Returns whether the data start position and size have been set. */ + public boolean hasDataBounds() { + return dataStartPosition != 0 && dataSize != 0; + } + + // SeekMap implementation. + + @Override + public boolean isSeekable() { + return true; + } + + @Override public long getDurationUs() { long numFrames = dataSize / blockAlignment; return (numFrames * C.MICROS_PER_SECOND) / sampleRateHz; } + @Override + public long getPosition(long timeUs) { + long unroundedPosition = (timeUs * averageBytesPerSecond) / C.MICROS_PER_SECOND; + // Round down to nearest frame. + long position = (unroundedPosition / blockAlignment) * blockAlignment; + return Math.min(position, dataSize - blockAlignment) + dataStartPosition; + } + + // Misc getters. + + /** + * Returns the time in microseconds for the given position in bytes. + * + * @param position The position in bytes. + */ + public long getTimeUs(long position) { + return position * C.MICROS_PER_SECOND / averageBytesPerSecond; + } + /** Returns the bytes per frame of this WAV. */ public int getBytesPerFrame() { return blockAlignment; @@ -75,33 +120,8 @@ import com.google.android.exoplayer2.C; return numChannels; } - /** Returns the position in bytes in this WAV for the given time in microseconds. */ - public long getPosition(long timeUs) { - long unroundedPosition = (timeUs * averageBytesPerSecond) / C.MICROS_PER_SECOND; - // Round down to nearest frame. - long position = (unroundedPosition / blockAlignment) * blockAlignment; - return Math.min(position, dataSize - blockAlignment) + dataStartPosition; - } - - /** Returns the time in microseconds for the given position in bytes in this WAV. */ - public long getTimeUs(long position) { - return position * C.MICROS_PER_SECOND / averageBytesPerSecond; - } - - /** Returns true if the data start position and size have been set. */ - public boolean hasDataBounds() { - return dataStartPosition != 0 && dataSize != 0; - } - - /** Sets the start position and size in bytes of sample data in this WAV. */ - public void setDataBounds(long dataStartPosition, long dataSize) { - this.dataStartPosition = dataStartPosition; - this.dataSize = dataSize; - } - /** Returns the PCM encoding. **/ - @C.PcmEncoding - public int getEncoding() { + public @C.PcmEncoding int getEncoding() { return encoding; } From c12349e2c937ed6bd9fcea6b9ac47b9031f62bda Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 28 Nov 2017 09:53:30 -0800 Subject: [PATCH 0904/2472] Allow setting supported formats on AdsLoaders ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177175377 --- .../exoplayer2/ext/ima/ImaAdsLoader.java | 24 +++++++++++++++++++ .../exoplayer2/source/ads/AdsLoader.java | 10 ++++++++ .../exoplayer2/source/ads/AdsMediaSource.java | 1 + .../android/exoplayer2/util/MimeTypes.java | 3 +++ .../source/dash/DashMediaSource.java | 3 +-- 5 files changed, 39 insertions(+), 2 deletions(-) diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index 5b61db0264..cf8b8a3f6d 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -49,10 +49,13 @@ import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.ads.AdPlaybackState; import com.google.android.exoplayer2.source.ads.AdsLoader; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.MimeTypes; import java.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Map; @@ -117,6 +120,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A private final AdDisplayContainer adDisplayContainer; private final com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader; + private List supportedMimeTypes; private EventListener eventListener; private Player player; private ViewGroup adUiViewGroup; @@ -238,6 +242,25 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A // AdsLoader implementation. + @Override + public void setSupportedContentTypes(@C.ContentType int... contentTypes) { + List supportedMimeTypes = new ArrayList<>(); + for (@C.ContentType int contentType : contentTypes) { + if (contentType == C.TYPE_DASH) { + supportedMimeTypes.add(MimeTypes.APPLICATION_MPD); + } else if (contentType == C.TYPE_HLS) { + supportedMimeTypes.add(MimeTypes.APPLICATION_M3U8); + } else if (contentType == C.TYPE_OTHER) { + supportedMimeTypes.addAll(Arrays.asList( + MimeTypes.VIDEO_MP4, MimeTypes.VIDEO_WEBM, MimeTypes.VIDEO_H263, MimeTypes.VIDEO_MPEG, + MimeTypes.AUDIO_MP4, MimeTypes.AUDIO_MPEG)); + } else if (contentType == C.TYPE_SS) { + // IMA does not support SmoothStreaming ad media. + } + } + this.supportedMimeTypes = Collections.unmodifiableList(supportedMimeTypes); + } + @Override public void attachPlayer(ExoPlayer player, EventListener eventListener, ViewGroup adUiViewGroup) { this.player = player; @@ -296,6 +319,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A ImaSdkFactory imaSdkFactory = ImaSdkFactory.getInstance(); AdsRenderingSettings adsRenderingSettings = imaSdkFactory.createAdsRenderingSettings(); adsRenderingSettings.setEnablePreloading(true); + adsRenderingSettings.setMimeTypes(supportedMimeTypes); adsManager.init(adsRenderingSettings); if (DEBUG) { Log.d(TAG, "Initialized with preloading"); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsLoader.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsLoader.java index 241750a21f..99feccd2f3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsLoader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsLoader.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.source.ads; import android.view.ViewGroup; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import java.io.IOException; @@ -71,6 +72,15 @@ public interface AdsLoader { } + /** + * Sets the supported content types for ad media. Must be called before the first call to + * {@link #attachPlayer(ExoPlayer, EventListener, ViewGroup)}. Subsequent calls may be ignored. + * + * @param contentTypes The supported content types for ad media. Each element must be one of + * {@link C#TYPE_DASH}, {@link C#TYPE_HLS}, {@link C#TYPE_SS} and {@link C#TYPE_OTHER}. + */ + void setSupportedContentTypes(@C.ContentType int... contentTypes); + /** * Attaches a player that will play ads loaded using this instance. Called on the main thread by * {@link AdsMediaSource}. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java index 397b8effd3..202e31cba1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java @@ -132,6 +132,7 @@ public final class AdsMediaSource implements MediaSource { period = new Timeline.Period(); adGroupMediaSources = new MediaSource[0][]; adDurationsUs = new long[0][]; + adsLoader.setSupportedContentTypes(C.TYPE_OTHER); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java b/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java index a68e0142d6..8307e998a0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java @@ -36,6 +36,7 @@ public final class MimeTypes { public static final String VIDEO_VP8 = BASE_TYPE_VIDEO + "/x-vnd.on2.vp8"; public static final String VIDEO_VP9 = BASE_TYPE_VIDEO + "/x-vnd.on2.vp9"; public static final String VIDEO_MP4V = BASE_TYPE_VIDEO + "/mp4v-es"; + public static final String VIDEO_MPEG = BASE_TYPE_VIDEO + "/mpeg"; public static final String VIDEO_MPEG2 = BASE_TYPE_VIDEO + "/mpeg2"; public static final String VIDEO_VC1 = BASE_TYPE_VIDEO + "/wvc1"; public static final String VIDEO_UNKNOWN = BASE_TYPE_VIDEO + "/x-unknown"; @@ -70,7 +71,9 @@ public final class MimeTypes { public static final String APPLICATION_MP4 = BASE_TYPE_APPLICATION + "/mp4"; public static final String APPLICATION_WEBM = BASE_TYPE_APPLICATION + "/webm"; + public static final String APPLICATION_MPD = BASE_TYPE_APPLICATION + "/dash+xml"; public static final String APPLICATION_M3U8 = BASE_TYPE_APPLICATION + "/x-mpegURL"; + public static final String APPLICATION_SS = BASE_TYPE_APPLICATION + "/vnd.ms-sstr+xml"; public static final String APPLICATION_ID3 = BASE_TYPE_APPLICATION + "/id3"; public static final String APPLICATION_CEA608 = BASE_TYPE_APPLICATION + "/cea-608"; public static final String APPLICATION_CEA708 = BASE_TYPE_APPLICATION + "/cea-708"; diff --git a/library/dash/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 index f1ee813020..c5fbafb84e 100644 --- a/library/dash/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 @@ -877,8 +877,7 @@ public final class DashMediaSource implements MediaSource { } - private final class ManifestCallback implements - Loader.Callback> { + private final class ManifestCallback implements Loader.Callback> { @Override public void onLoadCompleted(ParsingLoadable loadable, From 63dbf56b6c0bcd69d8e218a7aa55e3214c94b130 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 28 Nov 2017 10:45:38 -0800 Subject: [PATCH 0905/2472] Allow multiple video and audio debug listeners in SimpleExoPlayer. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177184331 --- .../exoplayer2/demo/PlayerActivity.java | 4 +- .../android/exoplayer2/SimpleExoPlayer.java | 82 +++++++++++++++---- .../exoplayer2/testutil/ExoHostedTest.java | 32 +++++++- 3 files changed, 97 insertions(+), 21 deletions(-) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index efde775176..cf0f8b8dc8 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -298,8 +298,8 @@ public class PlayerActivity extends Activity implements OnClickListener, player.addListener(new PlayerEventListener()); player.addListener(eventLogger); player.addMetadataOutput(eventLogger); - player.setAudioDebugListener(eventLogger); - player.setVideoDebugListener(eventLogger); + player.addAudioDebugListener(eventLogger); + player.addVideoDebugListener(eventLogger); simpleExoPlayerView.setPlayer(player); player.setPlayWhenReady(shouldAutoPlay); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 5a5a948d58..1374b73709 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -91,6 +91,8 @@ public class SimpleExoPlayer implements ExoPlayer { private final CopyOnWriteArraySet videoListeners; private final CopyOnWriteArraySet textOutputs; private final CopyOnWriteArraySet metadataOutputs; + private final CopyOnWriteArraySet videoDebugListeners; + private final CopyOnWriteArraySet audioDebugListeners; private final int videoRendererCount; private final int audioRendererCount; @@ -103,8 +105,6 @@ public class SimpleExoPlayer implements ExoPlayer { private int videoScalingMode; private SurfaceHolder surfaceHolder; private TextureView textureView; - private AudioRendererEventListener audioDebugListener; - private VideoRendererEventListener videoDebugListener; private DecoderCounters videoDecoderCounters; private DecoderCounters audioDecoderCounters; private int audioSessionId; @@ -117,6 +117,8 @@ public class SimpleExoPlayer implements ExoPlayer { videoListeners = new CopyOnWriteArraySet<>(); textOutputs = new CopyOnWriteArraySet<>(); metadataOutputs = new CopyOnWriteArraySet<>(); + videoDebugListeners = new CopyOnWriteArraySet<>(); + audioDebugListeners = new CopyOnWriteArraySet<>(); Looper eventLooper = Looper.myLooper() != null ? Looper.myLooper() : Looper.getMainLooper(); Handler eventHandler = new Handler(eventLooper); renderers = renderersFactory.createRenderers(eventHandler, componentListener, componentListener, @@ -576,18 +578,64 @@ public class SimpleExoPlayer implements ExoPlayer { * Sets a listener to receive debug events from the video renderer. * * @param listener The listener. + * @deprecated Use {@link #addVideoDebugListener(VideoRendererEventListener)}. */ + @Deprecated public void setVideoDebugListener(VideoRendererEventListener listener) { - videoDebugListener = listener; + videoDebugListeners.clear(); + if (listener != null) { + addVideoDebugListener(listener); + } + } + + /** + * Adds a listener to receive debug events from the video renderer. + * + * @param listener The listener. + */ + public void addVideoDebugListener(VideoRendererEventListener listener) { + videoDebugListeners.add(listener); + } + + /** + * Removes a listener to receive debug events from the video renderer. + * + * @param listener The listener. + */ + public void removeVideoDebugListener(VideoRendererEventListener listener) { + videoDebugListeners.remove(listener); } /** * Sets a listener to receive debug events from the audio renderer. * * @param listener The listener. + * @deprecated Use {@link #addAudioDebugListener(AudioRendererEventListener)}. */ + @Deprecated public void setAudioDebugListener(AudioRendererEventListener listener) { - audioDebugListener = listener; + audioDebugListeners.clear(); + if (listener != null) { + addAudioDebugListener(listener); + } + } + + /** + * Adds a listener to receive debug events from the audio renderer. + * + * @param listener The listener. + */ + public void addAudioDebugListener(AudioRendererEventListener listener) { + audioDebugListeners.add(listener); + } + + /** + * Removes a listener to receive debug events from the audio renderer. + * + * @param listener The listener. + */ + public void removeAudioDebugListener(AudioRendererEventListener listener) { + audioDebugListeners.remove(listener); } // ExoPlayer implementation @@ -877,7 +925,7 @@ public class SimpleExoPlayer implements ExoPlayer { @Override public void onVideoEnabled(DecoderCounters counters) { videoDecoderCounters = counters; - if (videoDebugListener != null) { + for (VideoRendererEventListener videoDebugListener : videoDebugListeners) { videoDebugListener.onVideoEnabled(counters); } } @@ -885,7 +933,7 @@ public class SimpleExoPlayer implements ExoPlayer { @Override public void onVideoDecoderInitialized(String decoderName, long initializedTimestampMs, long initializationDurationMs) { - if (videoDebugListener != null) { + for (VideoRendererEventListener videoDebugListener : videoDebugListeners) { videoDebugListener.onVideoDecoderInitialized(decoderName, initializedTimestampMs, initializationDurationMs); } @@ -894,14 +942,14 @@ public class SimpleExoPlayer implements ExoPlayer { @Override public void onVideoInputFormatChanged(Format format) { videoFormat = format; - if (videoDebugListener != null) { + for (VideoRendererEventListener videoDebugListener : videoDebugListeners) { videoDebugListener.onVideoInputFormatChanged(format); } } @Override public void onDroppedFrames(int count, long elapsed) { - if (videoDebugListener != null) { + for (VideoRendererEventListener videoDebugListener : videoDebugListeners) { videoDebugListener.onDroppedFrames(count, elapsed); } } @@ -913,7 +961,7 @@ public class SimpleExoPlayer implements ExoPlayer { videoListener.onVideoSizeChanged(width, height, unappliedRotationDegrees, pixelWidthHeightRatio); } - if (videoDebugListener != null) { + for (VideoRendererEventListener videoDebugListener : videoDebugListeners) { videoDebugListener.onVideoSizeChanged(width, height, unappliedRotationDegrees, pixelWidthHeightRatio); } @@ -926,14 +974,14 @@ public class SimpleExoPlayer implements ExoPlayer { videoListener.onRenderedFirstFrame(); } } - if (videoDebugListener != null) { + for (VideoRendererEventListener videoDebugListener : videoDebugListeners) { videoDebugListener.onRenderedFirstFrame(surface); } } @Override public void onVideoDisabled(DecoderCounters counters) { - if (videoDebugListener != null) { + for (VideoRendererEventListener videoDebugListener : videoDebugListeners) { videoDebugListener.onVideoDisabled(counters); } videoFormat = null; @@ -945,7 +993,7 @@ public class SimpleExoPlayer implements ExoPlayer { @Override public void onAudioEnabled(DecoderCounters counters) { audioDecoderCounters = counters; - if (audioDebugListener != null) { + for (AudioRendererEventListener audioDebugListener : audioDebugListeners) { audioDebugListener.onAudioEnabled(counters); } } @@ -953,7 +1001,7 @@ public class SimpleExoPlayer implements ExoPlayer { @Override public void onAudioSessionId(int sessionId) { audioSessionId = sessionId; - if (audioDebugListener != null) { + for (AudioRendererEventListener audioDebugListener : audioDebugListeners) { audioDebugListener.onAudioSessionId(sessionId); } } @@ -961,7 +1009,7 @@ public class SimpleExoPlayer implements ExoPlayer { @Override public void onAudioDecoderInitialized(String decoderName, long initializedTimestampMs, long initializationDurationMs) { - if (audioDebugListener != null) { + for (AudioRendererEventListener audioDebugListener : audioDebugListeners) { audioDebugListener.onAudioDecoderInitialized(decoderName, initializedTimestampMs, initializationDurationMs); } @@ -970,7 +1018,7 @@ public class SimpleExoPlayer implements ExoPlayer { @Override public void onAudioInputFormatChanged(Format format) { audioFormat = format; - if (audioDebugListener != null) { + for (AudioRendererEventListener audioDebugListener : audioDebugListeners) { audioDebugListener.onAudioInputFormatChanged(format); } } @@ -978,14 +1026,14 @@ public class SimpleExoPlayer implements ExoPlayer { @Override public void onAudioSinkUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) { - if (audioDebugListener != null) { + for (AudioRendererEventListener audioDebugListener : audioDebugListeners) { audioDebugListener.onAudioSinkUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs); } } @Override public void onAudioDisabled(DecoderCounters counters) { - if (audioDebugListener != null) { + for (AudioRendererEventListener audioDebugListener : audioDebugListeners) { audioDebugListener.onAudioDisabled(counters); } audioFormat = null; diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java index ee4018ba0e..5ff0533f71 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java @@ -78,6 +78,8 @@ public abstract class ExoHostedTest extends Player.DefaultEventListener implemen private Surface surface; private ExoPlaybackException playerError; private Player.EventListener playerEventListener; + private VideoRendererEventListener videoDebugListener; + private AudioRendererEventListener audioDebugListener; private boolean playerWasPrepared; private boolean playing; @@ -140,6 +142,26 @@ public abstract class ExoHostedTest extends Player.DefaultEventListener implemen } } + /** + * Sets an {@link VideoRendererEventListener} to listen for video debug events during the test. + */ + public final void setVideoDebugListener(VideoRendererEventListener videoDebugListener) { + this.videoDebugListener = videoDebugListener; + if (player != null) { + player.addVideoDebugListener(videoDebugListener); + } + } + + /** + * Sets an {@link AudioRendererEventListener} to listen for audio debug events during the test. + */ + public final void setAudioDebugListener(AudioRendererEventListener audioDebugListener) { + this.audioDebugListener = audioDebugListener; + if (player != null) { + player.addAudioDebugListener(audioDebugListener); + } + } + // HostedTest implementation @Override @@ -155,9 +177,15 @@ public abstract class ExoHostedTest extends Player.DefaultEventListener implemen if (playerEventListener != null) { player.addListener(playerEventListener); } + if (videoDebugListener != null) { + player.addVideoDebugListener(videoDebugListener); + } + if (audioDebugListener != null) { + player.addAudioDebugListener(audioDebugListener); + } player.addListener(this); - player.setAudioDebugListener(this); - player.setVideoDebugListener(this); + player.addAudioDebugListener(this); + player.addVideoDebugListener(this); player.setPlayWhenReady(true); actionHandler = new Handler(); // Schedule any pending actions. From 91b324f6b970c8b036289872e03841ba1754ab64 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 29 Nov 2017 08:53:27 -0800 Subject: [PATCH 0906/2472] Fix weird XingSeeker indexing There are still things broken about the seeker, but this cleans up some of the weird bits. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177315136 --- .../exoplayer2/extractor/mp3/XingSeeker.java | 43 +++++++------------ 1 file changed, 16 insertions(+), 27 deletions(-) diff --git a/library/core/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 index 9b1158dfa8..55888066e7 100644 --- a/library/core/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 @@ -58,9 +58,8 @@ import com.google.android.exoplayer2.util.Util; } long sizeBytes = frame.readUnsignedIntToInt(); - frame.skipBytes(1); - long[] tableOfContents = new long[99]; - for (int i = 0; i < 99; i++) { + long[] tableOfContents = new long[100]; + for (int i = 0; i < 100; i++) { tableOfContents[i] = frame.readUnsignedByte(); } @@ -105,30 +104,20 @@ import com.google.android.exoplayer2.util.Util; if (!isSeekable()) { return firstFramePosition; } - float percent = timeUs * 100f / durationUs; - float fx; - if (percent <= 0f) { - fx = 0f; - } else if (percent >= 100f) { - fx = 256f; + double percent = (timeUs * 100d) / durationUs; + double fx; + if (percent <= 0) { + fx = 0; + } else if (percent >= 100) { + fx = 256; } else { int a = (int) percent; - float fa; - if (a == 0) { - fa = 0f; - } else { - fa = tableOfContents[a - 1]; - } - float fb; - if (a < 99) { - fb = tableOfContents[a]; - } else { - fb = 256f; - } + float fa = tableOfContents[a]; + float fb = a == 99 ? 256 : tableOfContents[a + 1]; fx = fa + (fb - fa) * (percent - a); } - long position = Math.round((1.0 / 256) * fx * sizeBytes) + firstFramePosition; + long position = Math.round((fx / 256) * sizeBytes) + firstFramePosition; long maximumPosition = inputLength != C.LENGTH_UNSET ? inputLength - 1 : firstFramePosition - headerSize + sizeBytes - 1; return Math.min(position, maximumPosition); @@ -139,14 +128,14 @@ import com.google.android.exoplayer2.util.Util; if (!isSeekable() || position < firstFramePosition) { return 0L; } - double offsetByte = 256.0 * (position - firstFramePosition) / sizeBytes; + double offsetByte = (256d * (position - firstFramePosition)) / sizeBytes; int previousTocPosition = - Util.binarySearchFloor(tableOfContents, (long) offsetByte, true, false) + 1; + Util.binarySearchFloor(tableOfContents, (long) offsetByte, true, true); long previousTime = getTimeUsForTocPosition(previousTocPosition); // Linearly interpolate the time taking into account the next entry. - long previousByte = previousTocPosition == 0 ? 0 : tableOfContents[previousTocPosition - 1]; - long nextByte = previousTocPosition == 99 ? 256 : tableOfContents[previousTocPosition]; + long previousByte = tableOfContents[previousTocPosition]; + long nextByte = previousTocPosition == 99 ? 256 : tableOfContents[previousTocPosition + 1]; long nextTime = getTimeUsForTocPosition(previousTocPosition + 1); long timeOffset = nextByte == previousByte ? 0 : (long) ((nextTime - previousTime) * (offsetByte - previousByte) / (nextByte - previousByte)); @@ -163,7 +152,7 @@ import com.google.android.exoplayer2.util.Util; * interpreted as a percentage of the stream's duration between 0 and 100. */ private long getTimeUsForTocPosition(int tocPosition) { - return durationUs * tocPosition / 100; + return (durationUs * tocPosition) / 100; } } From 800cfeea3d18d2f7d1e8198c2a483460413fa08f Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 29 Nov 2017 09:09:17 -0800 Subject: [PATCH 0907/2472] Optimize seeking for unseekable SeekMaps - Avoid re-downloading data prior to the first mdat box when seeking back to the start of an unseekable FMP4. - Avoid re-downloading data prior to the first frame for constant bitrate MP3. - Update SeekMap.getPosition documentation to allow a non-zero position for the unseekable case. Note that XingSeeker was already returning a non-zero position if unseekable. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177317256 --- .../android/exoplayer2/extractor/SeekMap.java | 16 ++++++++++++++-- .../extractor/mp3/ConstantBitrateSeeker.java | 2 +- .../extractor/mp4/FragmentedMp4Extractor.java | 3 ++- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekMap.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekMap.java index 778aa4d715..964c43a45a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekMap.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/SeekMap.java @@ -28,13 +28,24 @@ public interface SeekMap { final class Unseekable implements SeekMap { private final long durationUs; + private final long startPosition; /** * @param durationUs The duration of the stream in microseconds, or {@link C#TIME_UNSET} if * the duration is unknown. */ public Unseekable(long durationUs) { + this(durationUs, 0); + } + + /** + * @param durationUs The duration of the stream in microseconds, or {@link C#TIME_UNSET} if + * the duration is unknown. + * @param startPosition The position (byte offset) of the start of the media. + */ + public Unseekable(long durationUs, long startPosition) { this.durationUs = durationUs; + this.startPosition = startPosition; } @Override @@ -49,7 +60,7 @@ public interface SeekMap { @Override public long getPosition(long timeUs) { - return 0; + return startPosition; } } @@ -78,7 +89,8 @@ public interface SeekMap { * * @param timeUs A seek position in microseconds. * @return The corresponding position (byte offset) in the stream from which data can be provided - * to the extractor, or 0 if {@code #isSeekable()} returns false. + * to the extractor. If {@link #isSeekable()} returns false then the returned value will be + * independent of {@code timeUs}, and will indicate the start of the media in the stream. */ long getPosition(long timeUs); diff --git a/library/core/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 index df7748a910..47e12161a8 100644 --- a/library/core/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 @@ -43,7 +43,7 @@ import com.google.android.exoplayer2.util.Util; @Override public long getPosition(long timeUs) { if (durationUs == C.TIME_UNSET) { - return 0; + return firstFramePosition; } timeUs = Util.constrainValue(timeUs, 0, durationUs); return firstFramePosition + (timeUs * bitrate) / (C.MICROS_PER_SECOND * BITS_PER_BYTE); diff --git a/library/core/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 index 4bc1b04418..28a1ffaa7b 100644 --- a/library/core/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 @@ -345,7 +345,8 @@ public final class FragmentedMp4Extractor implements Extractor { currentTrackBundle = null; endOfMdatPosition = atomPosition + atomSize; if (!haveOutputSeekMap) { - extractorOutput.seekMap(new SeekMap.Unseekable(durationUs)); + // This must be the first mdat in the stream. + extractorOutput.seekMap(new SeekMap.Unseekable(durationUs, atomPosition)); haveOutputSeekMap = true; } parserState = STATE_READING_ENCRYPTION_DATA; From f0f726dfa9a6cdcb01a33d6ed24140fbc471ab44 Mon Sep 17 00:00:00 2001 From: mdoucleff Date: Wed, 29 Nov 2017 16:59:41 -0800 Subject: [PATCH 0908/2472] Add manifestless captions support. This code fits into the pre-existing captions fetcher architecture. 1. ManifestlessCaptionsMetadata Other captions fetchers must first fetch a manifest (HLS or manifest) to discover captions tracks. This process does not exist for manifestless. All we need to do is scan the FormatStream's for the right itag, so this is an all-static class. 2. ManifestlessSubtitleWindowProvider Once a captions track is selected, a subtitles provider is instantiated. This is the main interface used by the player to retrieve captions according to playback position. This class stores fetched captions in a tree index by time for efficient lookups. Background captions fetches are used to populate the tree. 3. ManifestlessCaptionsFetch Captions are fetched one segment at a time. One instance of this object is required per fetch. It performs a blocking fetch on call(), and is intended to be submitted to a background-thread executor. 4. ManifestlessCaptionsFetch.CaptionSegment This is the result of the caption fetch. These values are used to populate the captions tree. Manifestlessness The initial request is always a headm request. There is a separate tree of every segment indexed by start time. This tree is used to improve manifestless sequence number calculation. Once we have data for the current timestamp, we walk forward through the tree to find the next unfetched sequence number, and fetch that. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177385094 --- .../google/android/exoplayer2/text/webvtt/WebvttCssStyle.java | 4 +++- .../com/google/android/exoplayer2/text/webvtt/WebvttCue.java | 4 ++-- .../android/exoplayer2/text/webvtt/WebvttCueParser.java | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/library/core/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 index 10c17e2888..a78c5afa78 100644 --- a/library/core/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 @@ -31,10 +31,11 @@ import java.util.List; * @see W3C specification - Apply * CSS properties */ -/* package */ final class WebvttCssStyle { +public final class WebvttCssStyle { public static final int UNSPECIFIED = -1; + /** Style flag enum */ @Retention(RetentionPolicy.SOURCE) @IntDef(flag = true, value = {UNSPECIFIED, STYLE_NORMAL, STYLE_BOLD, STYLE_ITALIC, STYLE_BOLD_ITALIC}) @@ -44,6 +45,7 @@ import java.util.List; public static final int STYLE_ITALIC = Typeface.ITALIC; public static final int STYLE_BOLD_ITALIC = Typeface.BOLD_ITALIC; + /** Font size unit enum */ @Retention(RetentionPolicy.SOURCE) @IntDef({UNSPECIFIED, FONT_SIZE_UNIT_PIXEL, FONT_SIZE_UNIT_EM, FONT_SIZE_UNIT_PERCENT}) public @interface FontSizeUnit {} diff --git a/library/core/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 index 295fdc656f..e16b231f7e 100644 --- a/library/core/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 @@ -23,7 +23,7 @@ import com.google.android.exoplayer2.text.Cue; /** * A representation of a WebVTT cue. */ -/* package */ final class WebvttCue extends Cue { +public final class WebvttCue extends Cue { public final long startTime; public final long endTime; @@ -59,7 +59,7 @@ import com.google.android.exoplayer2.text.Cue; * Builder for WebVTT cues. */ @SuppressWarnings("hiding") - public static final class Builder { + public static class Builder { private static final String TAG = "WebvttCueBuilder"; diff --git a/library/core/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 index 54af4dbf63..80ebecdc0e 100644 --- a/library/core/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 @@ -45,7 +45,7 @@ import java.util.regex.Pattern; /** * Parser for WebVTT cues. (https://w3c.github.io/webvtt/#cues) */ -/* package */ final class WebvttCueParser { +public final class WebvttCueParser { public static final Pattern CUE_HEADER_PATTERN = Pattern .compile("^(\\S+)\\s+-->\\s+(\\S+)(.*)?$"); @@ -90,7 +90,7 @@ import java.util.regex.Pattern; * @param styles List of styles defined by the CSS style blocks preceeding the cues. * @return Whether a valid Cue was found. */ - /* package */ boolean parseCue(ParsableByteArray webvttData, WebvttCue.Builder builder, + public boolean parseCue(ParsableByteArray webvttData, WebvttCue.Builder builder, List styles) { String firstLine = webvttData.readLine(); if (firstLine == null) { From 2567bf51fc10ee8466d4aefbe062f5c01e5add15 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 30 Nov 2017 00:33:10 -0800 Subject: [PATCH 0909/2472] Log load errors from AdsMediaSource in the demo app ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177419981 --- .../android/exoplayer2/demo/EventLogger.java | 21 ++++++++++++++++++- .../exoplayer2/demo/PlayerActivity.java | 3 ++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java index 9233b016f5..4819c28753 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java @@ -42,6 +42,7 @@ import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener; import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.trackselection.MappingTrackSelector; import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; import com.google.android.exoplayer2.trackselection.TrackSelection; @@ -57,7 +58,8 @@ import java.util.Locale; */ /* package */ final class EventLogger implements Player.EventListener, MetadataOutput, AudioRendererEventListener, VideoRendererEventListener, AdaptiveMediaSourceEventListener, - ExtractorMediaSource.EventListener, DefaultDrmSessionManager.EventListener { + ExtractorMediaSource.EventListener, AdsMediaSource.AdsListener, + DefaultDrmSessionManager.EventListener { private static final String TAG = "EventLogger"; private static final int MAX_TIMELINE_ITEM_LINES = 3; @@ -369,6 +371,23 @@ import java.util.Locale; // Do nothing. } + // AdsMediaSource.EventListener + + @Override + public void onAdLoadError(IOException error) { + printInternalError("loadError", error); + } + + @Override + public void onAdClicked() { + // Do nothing. + } + + @Override + public void onAdTapped() { + // Do nothing. + } + // Internal methods private void printInternalError(String type, Exception e) { diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index cf0f8b8dc8..7d0975a750 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -471,7 +471,8 @@ public class PlayerActivity extends Activity implements OnClickListener, // The demo app has a non-null overlay frame layout. simpleExoPlayerView.getOverlayFrameLayout().addView(adUiViewGroup); } - return new AdsMediaSource(mediaSource, mediaDataSourceFactory, adsLoader, adUiViewGroup); + return new AdsMediaSource(mediaSource, mediaDataSourceFactory, adsLoader, adUiViewGroup, + mainHandler, eventLogger); } private void releaseAdsLoader() { From 9526e8586adedc8022abf1b4b30883efe3c69ea5 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 30 Nov 2017 01:25:28 -0800 Subject: [PATCH 0910/2472] Use a MediaSource factory internally in AdsMediaSource Support ad MediaSources that aren't prepared immediately by using DeferredMediaPeriod, moved up from DynamicConcatenatingMediaSource. In a later change the new interfaces will be made public so that apps can provide their own MediaSource factories. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177424172 --- .../source/DeferredMediaPeriod.java | 139 +++++++++++++++++ .../DynamicConcatenatingMediaSource.java | 108 ------------- .../exoplayer2/source/ads/AdsMediaSource.java | 143 +++++++++++++++--- 3 files changed, 259 insertions(+), 131 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java new file mode 100644 index 0000000000..bc29b2fdf1 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java @@ -0,0 +1,139 @@ +/* + * 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; + +import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import com.google.android.exoplayer2.trackselection.TrackSelection; +import com.google.android.exoplayer2.upstream.Allocator; +import java.io.IOException; + +/** + * Media period that wraps a media source and defers calling its + * {@link MediaSource#createPeriod(MediaPeriodId, Allocator)} method until {@link #createPeriod()} + * has been called. This is useful if you need to return a media period immediately but the media + * source that should create it is not yet prepared. + */ +public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callback { + + public final MediaSource mediaSource; + + private final MediaPeriodId id; + private final Allocator allocator; + + private MediaPeriod mediaPeriod; + private Callback callback; + private long preparePositionUs; + + public DeferredMediaPeriod(MediaSource mediaSource, MediaPeriodId id, Allocator allocator) { + this.id = id; + this.allocator = allocator; + this.mediaSource = mediaSource; + } + + /** + * Calls {@link MediaSource#createPeriod(MediaPeriodId, Allocator)} on the wrapped source then + * prepares it if {@link #prepare(Callback, long)} has been called. Call {@link #releasePeriod()} + * to release the period. + */ + public void createPeriod() { + mediaPeriod = mediaSource.createPeriod(id, allocator); + if (callback != null) { + mediaPeriod.prepare(this, preparePositionUs); + } + } + + /** + * Releases the period. + */ + public void releasePeriod() { + if (mediaPeriod != null) { + mediaSource.releasePeriod(mediaPeriod); + } + } + + @Override + public void prepare(Callback callback, long preparePositionUs) { + this.callback = callback; + this.preparePositionUs = preparePositionUs; + if (mediaPeriod != null) { + mediaPeriod.prepare(this, preparePositionUs); + } + } + + @Override + public void maybeThrowPrepareError() throws IOException { + if (mediaPeriod != null) { + mediaPeriod.maybeThrowPrepareError(); + } else { + mediaSource.maybeThrowSourceInfoRefreshError(); + } + } + + @Override + public TrackGroupArray getTrackGroups() { + return mediaPeriod.getTrackGroups(); + } + + @Override + public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, + SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { + return mediaPeriod.selectTracks(selections, mayRetainStreamFlags, streams, streamResetFlags, + positionUs); + } + + @Override + public void discardBuffer(long positionUs, boolean toKeyframe) { + mediaPeriod.discardBuffer(positionUs, toKeyframe); + } + + @Override + public long readDiscontinuity() { + return mediaPeriod.readDiscontinuity(); + } + + @Override + public long getBufferedPositionUs() { + return mediaPeriod.getBufferedPositionUs(); + } + + @Override + public long seekToUs(long positionUs) { + return mediaPeriod.seekToUs(positionUs); + } + + @Override + public long getNextLoadPositionUs() { + return mediaPeriod.getNextLoadPositionUs(); + } + + @Override + public boolean continueLoading(long positionUs) { + return mediaPeriod != null && mediaPeriod.continueLoading(positionUs); + } + + @Override + public void onContinueLoadingRequested(MediaPeriod source) { + callback.onContinueLoadingRequested(this); + } + + // MediaPeriod.Callback implementation + + @Override + public void onPrepared(MediaPeriod mediaPeriod) { + callback.onPrepared(this); + } + +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java index e80abad3ef..b66e5ebe09 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java @@ -27,7 +27,6 @@ import com.google.android.exoplayer2.ExoPlayer.ExoPlayerComponent; import com.google.android.exoplayer2.ExoPlayer.ExoPlayerMessage; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder; -import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; @@ -758,111 +757,4 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl } - /** - * Media period used for periods created from unprepared media sources exposed through - * {@link DeferredTimeline}. Period preparation is postponed until the actual media source becomes - * available. - */ - private static final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callback { - - public final MediaSource mediaSource; - - private final MediaPeriodId id; - private final Allocator allocator; - - private MediaPeriod mediaPeriod; - private Callback callback; - private long preparePositionUs; - - public DeferredMediaPeriod(MediaSource mediaSource, MediaPeriodId id, Allocator allocator) { - this.id = id; - this.allocator = allocator; - this.mediaSource = mediaSource; - } - - public void createPeriod() { - mediaPeriod = mediaSource.createPeriod(id, allocator); - if (callback != null) { - mediaPeriod.prepare(this, preparePositionUs); - } - } - - public void releasePeriod() { - if (mediaPeriod != null) { - mediaSource.releasePeriod(mediaPeriod); - } - } - - @Override - public void prepare(Callback callback, long preparePositionUs) { - this.callback = callback; - this.preparePositionUs = preparePositionUs; - if (mediaPeriod != null) { - mediaPeriod.prepare(this, preparePositionUs); - } - } - - @Override - public void maybeThrowPrepareError() throws IOException { - if (mediaPeriod != null) { - mediaPeriod.maybeThrowPrepareError(); - } else { - mediaSource.maybeThrowSourceInfoRefreshError(); - } - } - - @Override - public TrackGroupArray getTrackGroups() { - return mediaPeriod.getTrackGroups(); - } - - @Override - public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, - SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { - return mediaPeriod.selectTracks(selections, mayRetainStreamFlags, streams, streamResetFlags, - positionUs); - } - - @Override - public void discardBuffer(long positionUs) { - mediaPeriod.discardBuffer(positionUs); - } - - @Override - public long readDiscontinuity() { - return mediaPeriod.readDiscontinuity(); - } - - @Override - public long getBufferedPositionUs() { - return mediaPeriod.getBufferedPositionUs(); - } - - @Override - public long seekToUs(long positionUs) { - return mediaPeriod.seekToUs(positionUs); - } - - @Override - public long getNextLoadPositionUs() { - return mediaPeriod.getNextLoadPositionUs(); - } - - @Override - public boolean continueLoading(long positionUs) { - return mediaPeriod != null && mediaPeriod.continueLoading(positionUs); - } - - @Override - public void onContinueLoadingRequested(MediaPeriod source) { - callback.onContinueLoadingRequested(this); - } - - @Override - public void onPrepared(MediaPeriod mediaPeriod) { - callback.onPrepared(this); - } - } - } - diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java index 202e31cba1..47a2540c38 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source.ads; +import android.net.Uri; import android.os.Handler; import android.os.Looper; import android.support.annotation.Nullable; @@ -23,15 +24,19 @@ import android.view.ViewGroup; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.DeferredMediaPeriod; import com.google.android.exoplayer2.source.ExtractorMediaSource; +import com.google.android.exoplayer2.source.ExtractorMediaSource.EventListener; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Map; /** @@ -68,12 +73,12 @@ public final class AdsMediaSource implements MediaSource { private static final String TAG = "AdsMediaSource"; private final MediaSource contentMediaSource; - private final DataSource.Factory dataSourceFactory; private final AdsLoader adsLoader; private final ViewGroup adUiViewGroup; private final Handler mainHandler; private final ComponentListener componentListener; - private final Map adMediaSourceByMediaPeriod; + private final AdMediaSourceFactory adMediaSourceFactory; + private final Map> deferredMediaPeriodByAdMediaSource; private final Timeline.Period period; @Nullable private final Handler eventHandler; @@ -95,6 +100,9 @@ public final class AdsMediaSource implements MediaSource { /** * Constructs a new source that inserts ads linearly with the content specified by * {@code contentMediaSource}. + *

          + * Ad media is loaded using {@link ExtractorMediaSource}. If {@code eventListener} is + * non-{@code null} it will be notified of both ad tag and ad media load errors. * * @param contentMediaSource The {@link MediaSource} providing the content to play. * @param dataSourceFactory Factory for data sources used to load ad media. @@ -109,6 +117,9 @@ public final class AdsMediaSource implements MediaSource { /** * Constructs a new source that inserts ads linearly with the content specified by * {@code contentMediaSource}. + *

          + * Ad media is loaded using {@link ExtractorMediaSource}. If {@code eventListener} is + * non-{@code null} it will be notified of both ad tag and ad media load errors. * * @param contentMediaSource The {@link MediaSource} providing the content to play. * @param dataSourceFactory Factory for data sources used to load ad media. @@ -121,18 +132,18 @@ public final class AdsMediaSource implements MediaSource { AdsLoader adsLoader, ViewGroup adUiViewGroup, @Nullable Handler eventHandler, @Nullable AdsListener eventListener) { this.contentMediaSource = contentMediaSource; - this.dataSourceFactory = dataSourceFactory; this.adsLoader = adsLoader; this.adUiViewGroup = adUiViewGroup; this.eventHandler = eventHandler; this.eventListener = eventListener; mainHandler = new Handler(Looper.getMainLooper()); componentListener = new ComponentListener(); - adMediaSourceByMediaPeriod = new HashMap<>(); + adMediaSourceFactory = new ExtractorAdMediaSourceFactory(dataSourceFactory); + deferredMediaPeriodByAdMediaSource = new HashMap<>(); period = new Timeline.Period(); adGroupMediaSources = new MediaSource[0][]; adDurationsUs = new long[0][]; - adsLoader.setSupportedContentTypes(C.TYPE_OTHER); + adsLoader.setSupportedContentTypes(adMediaSourceFactory.getSupportedTypes()); } @Override @@ -173,10 +184,9 @@ public final class AdsMediaSource implements MediaSource { final int adGroupIndex = id.adGroupIndex; final int adIndexInAdGroup = id.adIndexInAdGroup; if (adGroupMediaSources[adGroupIndex].length <= adIndexInAdGroup) { - MediaSource adMediaSource = new ExtractorMediaSource.Builder( - adPlaybackState.adUris[id.adGroupIndex][id.adIndexInAdGroup], dataSourceFactory) - .setEventListener(mainHandler, componentListener) - .build(); + Uri adUri = adPlaybackState.adUris[id.adGroupIndex][id.adIndexInAdGroup]; + final MediaSource adMediaSource = + adMediaSourceFactory.createAdMediaSource(adUri, mainHandler, componentListener); int oldAdCount = adGroupMediaSources[id.adGroupIndex].length; if (adIndexInAdGroup >= oldAdCount) { int adCount = adIndexInAdGroup + 1; @@ -186,30 +196,37 @@ public final class AdsMediaSource implements MediaSource { Arrays.fill(adDurationsUs[adGroupIndex], oldAdCount, adCount, C.TIME_UNSET); } adGroupMediaSources[adGroupIndex][adIndexInAdGroup] = adMediaSource; - adMediaSource.prepareSource(player, false, new Listener() { + deferredMediaPeriodByAdMediaSource.put(adMediaSource, new ArrayList()); + adMediaSource.prepareSource(player, false, new MediaSource.Listener() { @Override public void onSourceInfoRefreshed(MediaSource source, Timeline timeline, - Object manifest) { - onAdSourceInfoRefreshed(adGroupIndex, adIndexInAdGroup, timeline); + @Nullable Object manifest) { + onAdSourceInfoRefreshed(adMediaSource, adGroupIndex, adIndexInAdGroup, timeline); } }); } MediaSource mediaSource = adGroupMediaSources[adGroupIndex][adIndexInAdGroup]; - MediaPeriod mediaPeriod = mediaSource.createPeriod(new MediaPeriodId(0), allocator); - adMediaSourceByMediaPeriod.put(mediaPeriod, mediaSource); - return mediaPeriod; + DeferredMediaPeriod deferredMediaPeriod = + new DeferredMediaPeriod(mediaSource, new MediaPeriodId(0), allocator); + List mediaPeriods = deferredMediaPeriodByAdMediaSource.get(mediaSource); + if (mediaPeriods == null) { + deferredMediaPeriod.createPeriod(); + } else { + // Keep track of the deferred media period so it can be populated with the real media period + // when the source's info becomes available. + mediaPeriods.add(deferredMediaPeriod); + } + return deferredMediaPeriod; } else { - return contentMediaSource.createPeriod(id, allocator); + DeferredMediaPeriod mediaPeriod = new DeferredMediaPeriod(contentMediaSource, id, allocator); + mediaPeriod.createPeriod(); + return mediaPeriod; } } @Override public void releasePeriod(MediaPeriod mediaPeriod) { - if (adMediaSourceByMediaPeriod.containsKey(mediaPeriod)) { - adMediaSourceByMediaPeriod.remove(mediaPeriod).releasePeriod(mediaPeriod); - } else { - contentMediaSource.releasePeriod(mediaPeriod); - } + ((DeferredMediaPeriod) mediaPeriod).releasePeriod(); } @Override @@ -264,9 +281,17 @@ public final class AdsMediaSource implements MediaSource { maybeUpdateSourceInfo(); } - private void onAdSourceInfoRefreshed(int adGroupIndex, int adIndexInAdGroup, Timeline timeline) { + private void onAdSourceInfoRefreshed(MediaSource mediaSource, int adGroupIndex, + int adIndexInAdGroup, Timeline timeline) { Assertions.checkArgument(timeline.getPeriodCount() == 1); adDurationsUs[adGroupIndex][adIndexInAdGroup] = timeline.getPeriod(0, period).getDurationUs(); + if (deferredMediaPeriodByAdMediaSource.containsKey(mediaSource)) { + List mediaPeriods = deferredMediaPeriodByAdMediaSource.get(mediaSource); + for (int i = 0; i < mediaPeriods.size(); i++) { + mediaPeriods.get(i).createPeriod(); + } + deferredMediaPeriodByAdMediaSource.remove(mediaSource); + } maybeUpdateSourceInfo(); } @@ -285,7 +310,7 @@ public final class AdsMediaSource implements MediaSource { * Listener for component events. All methods are called on the main thread. */ private final class ComponentListener implements AdsLoader.EventListener, - ExtractorMediaSource.EventListener { + AdMediaSourceLoadErrorListener { @Override public void onAdPlaybackState(final AdPlaybackState adPlaybackState) { @@ -349,4 +374,76 @@ public final class AdsMediaSource implements MediaSource { } + /** + * Listener for errors while loading an ad {@link MediaSource}. + */ + private interface AdMediaSourceLoadErrorListener { + + /** + * Called when an error occurs loading media data. + * + * @param error The load error. + */ + void onLoadError(IOException error); + + } + + /** + * Factory for {@link MediaSource}s for loading ad media. + */ + private interface AdMediaSourceFactory { + + /** + * Creates a new {@link MediaSource} for loading the ad media with the specified {@code uri}. + * + * @param uri The URI of the ad. + * @param handler A handler for listener events. + * @param listener A listener for ad load errors. To have ad media source load errors notified + * via the ads media source's listener, call this listener's onLoadError method from your + * new media source's load error listener using the specified {@code handler}. Otherwise, + * this parameter can be ignored. + * @return The new media source. + */ + MediaSource createAdMediaSource(Uri uri, Handler handler, + AdMediaSourceLoadErrorListener listener); + + /** + * Returns the content types supported by media sources created by this factory. Each element + * should be one of {@link C#TYPE_DASH}, {@link C#TYPE_SS}, {@link C#TYPE_HLS} or + * {@link C#TYPE_OTHER}. + * + * @return The content types supported by the factory. + */ + int[] getSupportedTypes(); + + } + + private static final class ExtractorAdMediaSourceFactory implements AdMediaSourceFactory { + + private final DataSource.Factory dataSourceFactory; + + public ExtractorAdMediaSourceFactory(DataSource.Factory dataSourceFactory) { + this.dataSourceFactory = dataSourceFactory; + } + + @Override + public MediaSource createAdMediaSource(Uri uri, Handler handler, + final AdMediaSourceLoadErrorListener listener) { + return new ExtractorMediaSource.Builder(uri, dataSourceFactory).setEventListener(handler, + new EventListener() { + @Override + public void onLoadError(IOException error) { + listener.onLoadError(error); + } + }).build(); + } + + @Override + public int[] getSupportedTypes() { + // Only ExtractorMediaSource is supported. + return new int[] {C.TYPE_OTHER}; + } + + } + } From 315a6c3558024038f3ca736844cb67534ab2806f Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 30 Nov 2017 01:27:07 -0800 Subject: [PATCH 0911/2472] Update getPosition(0) positions for FragmentedMp4Extractor ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177424314 --- .../src/androidTest/assets/mp4/sample_fragmented.mp4.0.dump | 2 +- .../src/androidTest/assets/mp4/sample_fragmented_sei.mp4.0.dump | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/library/core/src/androidTest/assets/mp4/sample_fragmented.mp4.0.dump b/library/core/src/androidTest/assets/mp4/sample_fragmented.mp4.0.dump index bf822d9db4..95f6528fd6 100644 --- a/library/core/src/androidTest/assets/mp4/sample_fragmented.mp4.0.dump +++ b/library/core/src/androidTest/assets/mp4/sample_fragmented.mp4.0.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = false duration = UNSET TIME - getPosition(0) = 0 + getPosition(0) = 1828 numberOfTracks = 2 track 0: format: diff --git a/library/core/src/androidTest/assets/mp4/sample_fragmented_sei.mp4.0.dump b/library/core/src/androidTest/assets/mp4/sample_fragmented_sei.mp4.0.dump index 9d3755b23b..ebd33133e2 100644 --- a/library/core/src/androidTest/assets/mp4/sample_fragmented_sei.mp4.0.dump +++ b/library/core/src/androidTest/assets/mp4/sample_fragmented_sei.mp4.0.dump @@ -1,7 +1,7 @@ seekMap: isSeekable = false duration = UNSET TIME - getPosition(0) = 0 + getPosition(0) = 1828 numberOfTracks = 3 track 0: format: From 4e8b9282f518ea2568290c1a9b8655b0b8ca1d92 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 30 Nov 2017 02:57:07 -0800 Subject: [PATCH 0912/2472] Add a notice that NDK <= version 15c is required for VP9 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177430827 --- extensions/vp9/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/extensions/vp9/README.md b/extensions/vp9/README.md index 941b413c09..649e4a6ee2 100644 --- a/extensions/vp9/README.md +++ b/extensions/vp9/README.md @@ -28,7 +28,8 @@ EXOPLAYER_ROOT="$(pwd)" VP9_EXT_PATH="${EXOPLAYER_ROOT}/extensions/vp9/src/main" ``` -* Download the [Android NDK][] and set its location in an environment variable: +* Download the [Android NDK][] and set its location in an environment variable. +Only versions up to NDK 15c are supported currently (see [#3520][]). ``` NDK_PATH="" @@ -70,6 +71,7 @@ ${NDK_PATH}/ndk-build APP_ABI=all -j4 [top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md [Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html +[#3520]: https://github.com/google/ExoPlayer/issues/3520 ## Notes ## From 8fbc2a5c9b0f9904eef67fb77792e3b42cf904b3 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 30 Nov 2017 05:34:51 -0800 Subject: [PATCH 0913/2472] Snap to frame boundary in ConstantBitrateSeeker - This change snaps the seek position for constant bitrate MP3s to the nearest frame boundary, avoiding the need to skip one byte at a time to re-synchronize (this may still happen if the MP3 does not really have fixed size frames). - Tweaked both ConstantBitrateSeeker and WavHeader to ensure the returned positions are valid. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177441798 --- .../assets/mp3/play-trimmed.mp3.1.dump | 6 +++- .../assets/mp3/play-trimmed.mp3.2.dump | 6 +++- .../assets/mp3/play-trimmed.mp3.3.dump | 6 +++- .../extractor/mp3/ConstantBitrateSeeker.java | 32 +++++++++++++++---- .../extractor/mp3/Mp3Extractor.java | 4 +-- .../exoplayer2/extractor/wav/WavHeader.java | 11 ++++--- 6 files changed, 50 insertions(+), 15 deletions(-) diff --git a/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.1.dump b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.1.dump index 0b6516ccdb..37a04215ee 100644 --- a/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.1.dump +++ b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.1.dump @@ -25,5 +25,9 @@ track 0: language = null drmInitData = - initializationData: - sample count = 0 + sample count = 1 + sample 0: + time = 0 + flags = 1 + data = length 418, hash B819987 tracksEnded = true diff --git a/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.2.dump b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.2.dump index 0b6516ccdb..37a04215ee 100644 --- a/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.2.dump +++ b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.2.dump @@ -25,5 +25,9 @@ track 0: language = null drmInitData = - initializationData: - sample count = 0 + sample count = 1 + sample 0: + time = 0 + flags = 1 + data = length 418, hash B819987 tracksEnded = true diff --git a/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.3.dump b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.3.dump index 0b6516ccdb..37a04215ee 100644 --- a/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.3.dump +++ b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.3.dump @@ -25,5 +25,9 @@ track 0: language = null drmInitData = - initializationData: - sample count = 0 + sample count = 1 + sample 0: + time = 0 + flags = 1 + data = length 418, hash B819987 tracksEnded = true diff --git a/library/core/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 index 47e12161a8..e02e99e139 100644 --- a/library/core/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 @@ -26,27 +26,47 @@ import com.google.android.exoplayer2.util.Util; private static final int BITS_PER_BYTE = 8; private final long firstFramePosition; + private final long dataSize; + private final int frameSize; private final int bitrate; private final long durationUs; - public ConstantBitrateSeeker(long firstFramePosition, int bitrate, long inputLength) { + /** + * @param firstFramePosition The position (byte offset) of the first frame. + * @param inputLength The length of the stream. + * @param frameSize The size of a single frame in the stream. + * @param bitrate The stream's bitrate. + */ + public ConstantBitrateSeeker(long firstFramePosition, long inputLength, int frameSize, + int bitrate) { this.firstFramePosition = firstFramePosition; + this.frameSize = frameSize; this.bitrate = bitrate; - durationUs = inputLength == C.LENGTH_UNSET ? C.TIME_UNSET : getTimeUs(inputLength); + if (inputLength == C.LENGTH_UNSET) { + dataSize = C.LENGTH_UNSET; + durationUs = C.TIME_UNSET; + } else { + dataSize = inputLength - firstFramePosition; + durationUs = getTimeUs(inputLength); + } } @Override public boolean isSeekable() { - return durationUs != C.TIME_UNSET; + return dataSize != C.LENGTH_UNSET; } @Override public long getPosition(long timeUs) { - if (durationUs == C.TIME_UNSET) { + if (dataSize == C.LENGTH_UNSET) { return firstFramePosition; } - timeUs = Util.constrainValue(timeUs, 0, durationUs); - return firstFramePosition + (timeUs * bitrate) / (C.MICROS_PER_SECOND * BITS_PER_BYTE); + long positionOffset = (timeUs * bitrate) / (C.MICROS_PER_SECOND * BITS_PER_BYTE); + // Constrain to nearest preceding frame offset. + positionOffset = (positionOffset / frameSize) * frameSize; + positionOffset = Util.constrainValue(positionOffset, 0, dataSize - frameSize); + // Add data start position. + return firstFramePosition + positionOffset; } @Override diff --git a/library/core/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 index dc7d21851a..7c579504c3 100644 --- a/library/core/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 @@ -393,8 +393,8 @@ public final class Mp3Extractor implements Extractor { input.peekFully(scratch.data, 0, 4); scratch.setPosition(0); MpegAudioHeader.populateHeader(scratch.readInt(), synchronizedHeader); - return new ConstantBitrateSeeker(input.getPosition(), synchronizedHeader.bitrate, - input.getLength()); + return new ConstantBitrateSeeker(input.getPosition(), input.getLength(), + synchronizedHeader.frameSize, synchronizedHeader.bitrate); } /** diff --git a/library/core/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 index 1c1fc97a22..2cdd31cb6f 100644 --- a/library/core/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 @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.extractor.wav; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.SeekMap; +import com.google.android.exoplayer2.util.Util; /** Header for a WAV file. */ /* package */ final class WavHeader implements SeekMap { @@ -83,10 +84,12 @@ import com.google.android.exoplayer2.extractor.SeekMap; @Override public long getPosition(long timeUs) { - long unroundedPosition = (timeUs * averageBytesPerSecond) / C.MICROS_PER_SECOND; - // Round down to nearest frame. - long position = (unroundedPosition / blockAlignment) * blockAlignment; - return Math.min(position, dataSize - blockAlignment) + dataStartPosition; + long positionOffset = (timeUs * averageBytesPerSecond) / C.MICROS_PER_SECOND; + // Constrain to nearest preceding frame offset. + positionOffset = (positionOffset / blockAlignment) * blockAlignment; + positionOffset = Util.constrainValue(positionOffset, 0, dataSize - blockAlignment); + // Add data start position. + return dataStartPosition + positionOffset; } // Misc getters. From 022b85a625468235075de679cc19374fca823d8a Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 30 Nov 2017 05:39:50 -0800 Subject: [PATCH 0914/2472] Move resetting audio processors to initialize() The set of active audio processors was only updated on reconfiguration and when draining playback parameters completed. Draining playback parameters are cleared in reset(), so if parameters were set while paused then the sink was quickly reset, without draining completing, the set of active audio processors wouldn't be updated. This means that a switch to or from speed or pitch = 1 would not be handled correctly if made while paused and followed by a seek. Move resetting active audio processors from configure (where if the active audio processors were reset we'd always initialize a new AudioTrack) to initialize(). ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177442098 --- RELEASENOTES.md | 2 ++ .../google/android/exoplayer2/audio/DefaultAudioSink.java | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index ae5bc0fb95..a123c78323 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -11,6 +11,8 @@ * DefaultTrackSelector: Support undefined language text track selection when the preferred language is not available ([#2980](https://github.com/google/ExoPlayer/issues/2980)). +* Fix handling of playback parameters changes while paused when followed by a + seek. ### 2.6.0 ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java index ba62ac126e..eb27c0fe55 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java @@ -364,9 +364,6 @@ public final class DefaultAudioSink implements AudioSink { encoding = audioProcessor.getOutputEncoding(); } } - if (flush) { - resetAudioProcessors(); - } } int channelConfig; @@ -492,6 +489,9 @@ public final class DefaultAudioSink implements AudioSink { // The old playback parameters may no longer be applicable so try to reset them now. setPlaybackParameters(playbackParameters); + // Flush and reset active audio processors. + resetAudioProcessors(); + int audioSessionId = audioTrack.getAudioSessionId(); if (enablePreV21AudioSessionWorkaround) { if (Util.SDK_INT < 21) { From 0ea8c8bfa0db99fb2322e5ed2b3c9fb66c2bf9f7 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 30 Nov 2017 07:20:45 -0800 Subject: [PATCH 0915/2472] Fix VBRI and XING seekers - Remove skipping of the VBRI/XING frame before calculating position offsets. This was incorrect. Instead, a constraint is used to ensure we don't return positions within these frames, the difference being that the constraint adjusts only positions that would fall within the frames, where-as the previous approach shifted positions through the whole stream. - Excluded last entry in the VBRI table because it has an invalid position (the length of the stream). - Give variables in XingSeeker descriptive names. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177451295 --- .../androidTest/assets/mp3/bear.mp3.1.dump | 308 +++++++++--------- .../androidTest/assets/mp3/bear.mp3.2.dump | 76 ++--- .../extractor/mp3/ConstantBitrateSeeker.java | 18 +- .../extractor/mp3/Mp3Extractor.java | 7 +- .../exoplayer2/extractor/mp3/VbriSeeker.java | 33 +- .../exoplayer2/extractor/mp3/XingSeeker.java | 112 ++++--- .../extractor/mp3/XingSeekerTest.java | 26 +- 7 files changed, 298 insertions(+), 282 deletions(-) diff --git a/library/core/src/androidTest/assets/mp3/bear.mp3.1.dump b/library/core/src/androidTest/assets/mp3/bear.mp3.1.dump index 2e0b21050c..7b6fe9db37 100644 --- a/library/core/src/androidTest/assets/mp3/bear.mp3.1.dump +++ b/library/core/src/androidTest/assets/mp3/bear.mp3.1.dump @@ -25,309 +25,313 @@ track 0: language = null drmInitData = - initializationData: - sample count = 76 + sample count = 77 sample 0: - time = 945782 + time = 928567 + flags = 1 + data = length 384, hash F7E344F4 + sample 1: + time = 952567 flags = 1 data = length 384, hash 14EF6AFD - sample 1: - time = 969782 + sample 2: + time = 976567 flags = 1 data = length 384, hash 61C9B92C - sample 2: - time = 993782 + sample 3: + time = 1000567 flags = 1 data = length 384, hash ABE1368 - sample 3: - time = 1017782 + sample 4: + time = 1024567 flags = 1 data = length 384, hash 6A3B8547 - sample 4: - time = 1041782 + sample 5: + time = 1048567 flags = 1 data = length 384, hash 30E905FA - sample 5: - time = 1065782 + sample 6: + time = 1072567 flags = 1 data = length 384, hash 21A267CD - sample 6: - time = 1089782 + sample 7: + time = 1096567 flags = 1 data = length 384, hash D96A2651 - sample 7: - time = 1113782 + sample 8: + time = 1120567 flags = 1 data = length 384, hash 72340177 - sample 8: - time = 1137782 + sample 9: + time = 1144567 flags = 1 data = length 384, hash 9345E744 - sample 9: - time = 1161782 + sample 10: + time = 1168567 flags = 1 data = length 384, hash FDE39E3A - sample 10: - time = 1185782 + sample 11: + time = 1192567 flags = 1 data = length 384, hash F0B7465 - sample 11: - time = 1209782 + sample 12: + time = 1216567 flags = 1 data = length 384, hash 3693AB86 - sample 12: - time = 1233782 + sample 13: + time = 1240567 flags = 1 data = length 384, hash F39719B1 - sample 13: - time = 1257782 + sample 14: + time = 1264567 flags = 1 data = length 384, hash DA3958DC - sample 14: - time = 1281782 + sample 15: + time = 1288567 flags = 1 data = length 384, hash FDC7599F - sample 15: - time = 1305782 + sample 16: + time = 1312567 flags = 1 data = length 384, hash AEFF8471 - sample 16: - time = 1329782 + sample 17: + time = 1336567 flags = 1 data = length 384, hash 89C92C19 - sample 17: - time = 1353782 + sample 18: + time = 1360567 flags = 1 data = length 384, hash 5C786A4B - sample 18: - time = 1377782 + sample 19: + time = 1384567 flags = 1 data = length 384, hash 5ACA8B - sample 19: - time = 1401782 + sample 20: + time = 1408567 flags = 1 data = length 384, hash 7755974C - sample 20: - time = 1425782 + sample 21: + time = 1432567 flags = 1 data = length 384, hash 3934B73C - sample 21: - time = 1449782 + sample 22: + time = 1456567 flags = 1 data = length 384, hash DDD70A2F - sample 22: - time = 1473782 + sample 23: + time = 1480567 flags = 1 data = length 384, hash 8FACE2EF - sample 23: - time = 1497782 + sample 24: + time = 1504567 flags = 1 data = length 384, hash 4A602591 - sample 24: - time = 1521782 + sample 25: + time = 1528567 flags = 1 data = length 384, hash D019AA2D - sample 25: - time = 1545782 + sample 26: + time = 1552567 flags = 1 data = length 384, hash 8A680B9D - sample 26: - time = 1569782 + sample 27: + time = 1576567 flags = 1 data = length 384, hash B655C959 - sample 27: - time = 1593782 + sample 28: + time = 1600567 flags = 1 data = length 384, hash 2168336B - sample 28: - time = 1617782 + sample 29: + time = 1624567 flags = 1 data = length 384, hash D77F6D31 - sample 29: - time = 1641782 + sample 30: + time = 1648567 flags = 1 data = length 384, hash 524B4B2F - sample 30: - time = 1665782 + sample 31: + time = 1672567 flags = 1 data = length 384, hash 4752DDFC - sample 31: - time = 1689782 + sample 32: + time = 1696567 flags = 1 data = length 384, hash E786727F - sample 32: - time = 1713782 + sample 33: + time = 1720567 flags = 1 data = length 384, hash 5DA6FB8C - sample 33: - time = 1737782 + sample 34: + time = 1744567 flags = 1 data = length 384, hash 92F24269 - sample 34: - time = 1761782 + sample 35: + time = 1768567 flags = 1 data = length 384, hash CD0A3BA1 - sample 35: - time = 1785782 + sample 36: + time = 1792567 flags = 1 data = length 384, hash 7D00409F - sample 36: - time = 1809782 + sample 37: + time = 1816567 flags = 1 data = length 384, hash D7ADB5FA - sample 37: - time = 1833782 + sample 38: + time = 1840567 flags = 1 data = length 384, hash 4A140209 - sample 38: - time = 1857782 + sample 39: + time = 1864567 flags = 1 data = length 384, hash E801184A - sample 39: - time = 1881782 + sample 40: + time = 1888567 flags = 1 data = length 384, hash 53C6CF9C - sample 40: - time = 1905782 + sample 41: + time = 1912567 flags = 1 data = length 384, hash 19A8D99F - sample 41: - time = 1929782 + sample 42: + time = 1936567 flags = 1 data = length 384, hash E47EB43F - sample 42: - time = 1953782 + sample 43: + time = 1960567 flags = 1 data = length 384, hash 4EA329E7 - sample 43: - time = 1977782 + sample 44: + time = 1984567 flags = 1 data = length 384, hash 1CCAAE62 - sample 44: - time = 2001782 + sample 45: + time = 2008567 flags = 1 data = length 384, hash ED3F8C66 - sample 45: - time = 2025782 + sample 46: + time = 2032567 flags = 1 data = length 384, hash D3D646B6 - sample 46: - time = 2049782 + sample 47: + time = 2056567 flags = 1 data = length 384, hash 68CD1574 - sample 47: - time = 2073782 + sample 48: + time = 2080567 flags = 1 data = length 384, hash 8CEAB382 - sample 48: - time = 2097782 + sample 49: + time = 2104567 flags = 1 data = length 384, hash D54B1C48 - sample 49: - time = 2121782 + sample 50: + time = 2128567 flags = 1 data = length 384, hash FFE2EE90 - sample 50: - time = 2145782 + sample 51: + time = 2152567 flags = 1 data = length 384, hash BFE8A673 - sample 51: - time = 2169782 + sample 52: + time = 2176567 flags = 1 data = length 384, hash 978B1C92 - sample 52: - time = 2193782 + sample 53: + time = 2200567 flags = 1 data = length 384, hash 810CC71E - sample 53: - time = 2217782 + sample 54: + time = 2224567 flags = 1 data = length 384, hash 44FE42D9 - sample 54: - time = 2241782 + sample 55: + time = 2248567 flags = 1 data = length 384, hash 2F5BB02C - sample 55: - time = 2265782 + sample 56: + time = 2272567 flags = 1 data = length 384, hash 77DDB90 - sample 56: - time = 2289782 + sample 57: + time = 2296567 flags = 1 data = length 384, hash 24FB5EDA - sample 57: - time = 2313782 + sample 58: + time = 2320567 flags = 1 data = length 384, hash E73203C6 - sample 58: - time = 2337782 + sample 59: + time = 2344567 flags = 1 data = length 384, hash 14B525F1 - sample 59: - time = 2361782 + sample 60: + time = 2368567 flags = 1 data = length 384, hash 5E0F4E2E - sample 60: - time = 2385782 + sample 61: + time = 2392567 flags = 1 data = length 384, hash 67EE4E31 - sample 61: - time = 2409782 + sample 62: + time = 2416567 flags = 1 data = length 384, hash 2E04EC4C - sample 62: - time = 2433782 + sample 63: + time = 2440567 flags = 1 data = length 384, hash 852CABA7 - sample 63: - time = 2457782 + sample 64: + time = 2464567 flags = 1 data = length 384, hash 19928903 - sample 64: - time = 2481782 + sample 65: + time = 2488567 flags = 1 data = length 384, hash 5DA42021 - sample 65: - time = 2505782 + sample 66: + time = 2512567 flags = 1 data = length 384, hash 45B20B7C - sample 66: - time = 2529782 + sample 67: + time = 2536567 flags = 1 data = length 384, hash D108A215 - sample 67: - time = 2553782 + sample 68: + time = 2560567 flags = 1 data = length 384, hash BD25DB7C - sample 68: - time = 2577782 + sample 69: + time = 2584567 flags = 1 data = length 384, hash DA7F9861 - sample 69: - time = 2601782 + sample 70: + time = 2608567 flags = 1 data = length 384, hash CCD576F - sample 70: - time = 2625782 + sample 71: + time = 2632567 flags = 1 data = length 384, hash 405C1EB5 - sample 71: - time = 2649782 + sample 72: + time = 2656567 flags = 1 data = length 384, hash 6640B74E - sample 72: - time = 2673782 + sample 73: + time = 2680567 flags = 1 data = length 384, hash B4E5937A - sample 73: - time = 2697782 + sample 74: + time = 2704567 flags = 1 data = length 384, hash CEE17733 - sample 74: - time = 2721782 + sample 75: + time = 2728567 flags = 1 data = length 384, hash 2A0DA733 - sample 75: - time = 2745782 + sample 76: + time = 2752567 flags = 1 data = length 384, hash 97F4129B tracksEnded = true diff --git a/library/core/src/androidTest/assets/mp3/bear.mp3.2.dump b/library/core/src/androidTest/assets/mp3/bear.mp3.2.dump index b3cb117cb2..3f393e768e 100644 --- a/library/core/src/androidTest/assets/mp3/bear.mp3.2.dump +++ b/library/core/src/androidTest/assets/mp3/bear.mp3.2.dump @@ -27,155 +27,155 @@ track 0: initializationData: sample count = 38 sample 0: - time = 1858196 + time = 1871586 flags = 1 data = length 384, hash E801184A sample 1: - time = 1882196 + time = 1895586 flags = 1 data = length 384, hash 53C6CF9C sample 2: - time = 1906196 + time = 1919586 flags = 1 data = length 384, hash 19A8D99F sample 3: - time = 1930196 + time = 1943586 flags = 1 data = length 384, hash E47EB43F sample 4: - time = 1954196 + time = 1967586 flags = 1 data = length 384, hash 4EA329E7 sample 5: - time = 1978196 + time = 1991586 flags = 1 data = length 384, hash 1CCAAE62 sample 6: - time = 2002196 + time = 2015586 flags = 1 data = length 384, hash ED3F8C66 sample 7: - time = 2026196 + time = 2039586 flags = 1 data = length 384, hash D3D646B6 sample 8: - time = 2050196 + time = 2063586 flags = 1 data = length 384, hash 68CD1574 sample 9: - time = 2074196 + time = 2087586 flags = 1 data = length 384, hash 8CEAB382 sample 10: - time = 2098196 + time = 2111586 flags = 1 data = length 384, hash D54B1C48 sample 11: - time = 2122196 + time = 2135586 flags = 1 data = length 384, hash FFE2EE90 sample 12: - time = 2146196 + time = 2159586 flags = 1 data = length 384, hash BFE8A673 sample 13: - time = 2170196 + time = 2183586 flags = 1 data = length 384, hash 978B1C92 sample 14: - time = 2194196 + time = 2207586 flags = 1 data = length 384, hash 810CC71E sample 15: - time = 2218196 + time = 2231586 flags = 1 data = length 384, hash 44FE42D9 sample 16: - time = 2242196 + time = 2255586 flags = 1 data = length 384, hash 2F5BB02C sample 17: - time = 2266196 + time = 2279586 flags = 1 data = length 384, hash 77DDB90 sample 18: - time = 2290196 + time = 2303586 flags = 1 data = length 384, hash 24FB5EDA sample 19: - time = 2314196 + time = 2327586 flags = 1 data = length 384, hash E73203C6 sample 20: - time = 2338196 + time = 2351586 flags = 1 data = length 384, hash 14B525F1 sample 21: - time = 2362196 + time = 2375586 flags = 1 data = length 384, hash 5E0F4E2E sample 22: - time = 2386196 + time = 2399586 flags = 1 data = length 384, hash 67EE4E31 sample 23: - time = 2410196 + time = 2423586 flags = 1 data = length 384, hash 2E04EC4C sample 24: - time = 2434196 + time = 2447586 flags = 1 data = length 384, hash 852CABA7 sample 25: - time = 2458196 + time = 2471586 flags = 1 data = length 384, hash 19928903 sample 26: - time = 2482196 + time = 2495586 flags = 1 data = length 384, hash 5DA42021 sample 27: - time = 2506196 + time = 2519586 flags = 1 data = length 384, hash 45B20B7C sample 28: - time = 2530196 + time = 2543586 flags = 1 data = length 384, hash D108A215 sample 29: - time = 2554196 + time = 2567586 flags = 1 data = length 384, hash BD25DB7C sample 30: - time = 2578196 + time = 2591586 flags = 1 data = length 384, hash DA7F9861 sample 31: - time = 2602196 + time = 2615586 flags = 1 data = length 384, hash CCD576F sample 32: - time = 2626196 + time = 2639586 flags = 1 data = length 384, hash 405C1EB5 sample 33: - time = 2650196 + time = 2663586 flags = 1 data = length 384, hash 6640B74E sample 34: - time = 2674196 + time = 2687586 flags = 1 data = length 384, hash B4E5937A sample 35: - time = 2698196 + time = 2711586 flags = 1 data = length 384, hash CEE17733 sample 36: - time = 2722196 + time = 2735586 flags = 1 data = length 384, hash 2A0DA733 sample 37: - time = 2746196 + time = 2759586 flags = 1 data = length 384, hash 97F4129B tracksEnded = true diff --git a/library/core/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 index e02e99e139..442e62deca 100644 --- a/library/core/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 @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.extractor.mp3; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.extractor.MpegAudioHeader; import com.google.android.exoplayer2.util.Util; /** @@ -26,22 +27,21 @@ import com.google.android.exoplayer2.util.Util; private static final int BITS_PER_BYTE = 8; private final long firstFramePosition; - private final long dataSize; private final int frameSize; + private final long dataSize; private final int bitrate; private final long durationUs; /** - * @param firstFramePosition The position (byte offset) of the first frame. - * @param inputLength The length of the stream. - * @param frameSize The size of a single frame in the stream. - * @param bitrate The stream's bitrate. + * @param inputLength The length of the stream in bytes, or {@link C#LENGTH_UNSET} if unknown. + * @param firstFramePosition The position of the first frame in the stream. + * @param mpegAudioHeader The MPEG audio header associated with the first frame. */ - public ConstantBitrateSeeker(long firstFramePosition, long inputLength, int frameSize, - int bitrate) { + public ConstantBitrateSeeker(long inputLength, long firstFramePosition, + MpegAudioHeader mpegAudioHeader) { this.firstFramePosition = firstFramePosition; - this.frameSize = frameSize; - this.bitrate = bitrate; + this.frameSize = mpegAudioHeader.frameSize; + this.bitrate = mpegAudioHeader.bitrate; if (inputLength == C.LENGTH_UNSET) { dataSize = C.LENGTH_UNSET; durationUs = C.TIME_UNSET; diff --git a/library/core/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 index 7c579504c3..5c56dc460a 100644 --- a/library/core/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 @@ -360,7 +360,7 @@ public final class Mp3Extractor implements Extractor { int seekHeader = getSeekFrameHeader(frame, xingBase); Seeker seeker; if (seekHeader == SEEK_HEADER_XING || seekHeader == SEEK_HEADER_INFO) { - seeker = XingSeeker.create(synchronizedHeader, frame, input.getPosition(), input.getLength()); + seeker = XingSeeker.create(input.getLength(), input.getPosition(), synchronizedHeader, frame); if (seeker != null && !gaplessInfoHolder.hasGaplessInfo()) { // If there is a Xing header, read gapless playback metadata at a fixed offset. input.resetPeekPosition(); @@ -375,7 +375,7 @@ public final class Mp3Extractor implements Extractor { return getConstantBitrateSeeker(input); } } else if (seekHeader == SEEK_HEADER_VBRI) { - seeker = VbriSeeker.create(synchronizedHeader, frame, input.getPosition(), input.getLength()); + seeker = VbriSeeker.create(input.getLength(), input.getPosition(), synchronizedHeader, frame); input.skipFully(synchronizedHeader.frameSize); } else { // seekerHeader == SEEK_HEADER_UNSET // This frame doesn't contain seeking information, so reset the peek position. @@ -393,8 +393,7 @@ public final class Mp3Extractor implements Extractor { input.peekFully(scratch.data, 0, 4); scratch.setPosition(0); MpegAudioHeader.populateHeader(scratch.readInt(), synchronizedHeader); - return new ConstantBitrateSeeker(input.getPosition(), input.getLength(), - synchronizedHeader.frameSize, synchronizedHeader.bitrate); + return new ConstantBitrateSeeker(input.getLength(), input.getPosition(), synchronizedHeader); } /** diff --git a/library/core/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 index c43f065592..cc631d9f7e 100644 --- a/library/core/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 @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor.mp3; +import android.util.Log; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.MpegAudioHeader; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -25,21 +26,23 @@ import com.google.android.exoplayer2.util.Util; */ /* package */ final class VbriSeeker implements Mp3Extractor.Seeker { + private static final String TAG = "VbriSeeker"; + /** * Returns a {@link VbriSeeker} for seeking in the stream, if required information is present. * Returns {@code null} if not. On returning, {@code frame}'s position is not specified so the * caller should reset it. * + * @param inputLength The length of the stream in bytes, or {@link C#LENGTH_UNSET} if unknown. + * @param position The position of the start of this frame in the stream. * @param mpegAudioHeader The MPEG audio header associated with the frame. * @param frame The data in this audio frame, with its position set to immediately after the * 'VBRI' tag. - * @param position The position (byte offset) of the start of this frame in the stream. - * @param inputLength The length of the stream in bytes. * @return A {@link VbriSeeker} for seeking in the stream, or {@code null} if the required * information is not present. */ - public static VbriSeeker create(MpegAudioHeader mpegAudioHeader, ParsableByteArray frame, - long position, long inputLength) { + public static VbriSeeker create(long inputLength, long position, MpegAudioHeader mpegAudioHeader, + ParsableByteArray frame) { frame.skipBytes(10); int numFrames = frame.readInt(); if (numFrames <= 0) { @@ -53,15 +56,15 @@ import com.google.android.exoplayer2.util.Util; int entrySize = frame.readUnsignedShort(); frame.skipBytes(2); - // Skip the frame containing the VBRI header. - position += mpegAudioHeader.frameSize; - + long minPosition = position + mpegAudioHeader.frameSize; // Read table of contents entries. - long[] timesUs = new long[entryCount + 1]; - long[] positions = new long[entryCount + 1]; - timesUs[0] = 0L; - positions[0] = position; - for (int index = 1; index < timesUs.length; index++) { + long[] timesUs = new long[entryCount]; + long[] positions = new long[entryCount]; + for (int index = 0; index < entryCount; index++) { + timesUs[index] = (index * durationUs) / entryCount; + // Ensure positions do not fall within the frame containing the VBRI header. This constraint + // will normally only apply to the first entry in the table. + positions[index] = Math.max(position, minPosition); int segmentSize; switch (entrySize) { case 1: @@ -80,9 +83,9 @@ import com.google.android.exoplayer2.util.Util; return null; } position += segmentSize * scale; - timesUs[index] = index * durationUs / entryCount; - positions[index] = - inputLength == C.LENGTH_UNSET ? position : Math.min(inputLength, position); + } + if (inputLength != C.LENGTH_UNSET && inputLength != position) { + Log.w(TAG, "VBRI data size mismatch: " + inputLength + ", " + position); } return new VbriSeeker(timesUs, positions, durationUs); } diff --git a/library/core/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 index 55888066e7..e532249a64 100644 --- a/library/core/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 @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor.mp3; +import android.util.Log; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.MpegAudioHeader; import com.google.android.exoplayer2.util.ParsableByteArray; @@ -25,24 +26,25 @@ import com.google.android.exoplayer2.util.Util; */ /* package */ final class XingSeeker implements Mp3Extractor.Seeker { + private static final String TAG = "XingSeeker"; + /** * Returns a {@link XingSeeker} for seeking in the stream, if required information is present. * Returns {@code null} if not. On returning, {@code frame}'s position is not specified so the * caller should reset it. * + * @param inputLength The length of the stream in bytes, or {@link C#LENGTH_UNSET} if unknown. + * @param position The position of the start of this frame in the stream. * @param mpegAudioHeader The MPEG audio header associated with the frame. * @param frame The data in this audio frame, with its position set to immediately after the * 'Xing' or 'Info' tag. - * @param position The position (byte offset) of the start of this frame in the stream. - * @param inputLength The length of the stream in bytes. * @return A {@link XingSeeker} for seeking in the stream, or {@code null} if the required * information is not present. */ - public static XingSeeker create(MpegAudioHeader mpegAudioHeader, ParsableByteArray frame, - long position, long inputLength) { + public static XingSeeker create(long inputLength, long position, MpegAudioHeader mpegAudioHeader, + ParsableByteArray frame) { int samplesPerFrame = mpegAudioHeader.samplesPerFrame; int sampleRate = mpegAudioHeader.sampleRate; - long firstFramePosition = position + mpegAudioHeader.frameSize; int flags = frame.readInt(); int frameCount; @@ -54,10 +56,10 @@ import com.google.android.exoplayer2.util.Util; sampleRate); if ((flags & 0x06) != 0x06) { // If the size in bytes or table of contents is missing, the stream is not seekable. - return new XingSeeker(firstFramePosition, durationUs, inputLength); + return new XingSeeker(position, mpegAudioHeader.frameSize, durationUs); } - long sizeBytes = frame.readUnsignedIntToInt(); + long dataSize = frame.readUnsignedIntToInt(); long[] tableOfContents = new long[100]; for (int i = 0; i < 100; i++) { tableOfContents[i] = frame.readUnsignedByte(); @@ -66,32 +68,37 @@ import com.google.android.exoplayer2.util.Util; // TODO: Handle encoder delay and padding in 3 bytes offset by xingBase + 213 bytes: // delay = (frame.readUnsignedByte() << 4) + (frame.readUnsignedByte() >> 4); // padding = ((frame.readUnsignedByte() & 0x0F) << 8) + frame.readUnsignedByte(); - return new XingSeeker(firstFramePosition, durationUs, inputLength, tableOfContents, - sizeBytes, mpegAudioHeader.frameSize); + + if (inputLength != C.LENGTH_UNSET && inputLength != position + dataSize) { + Log.w(TAG, "XING data size mismatch: " + inputLength + ", " + (position + dataSize)); + } + return new XingSeeker(position, mpegAudioHeader.frameSize, durationUs, dataSize, + tableOfContents); } - private final long firstFramePosition; + private final long dataStartPosition; + private final int xingFrameSize; private final long durationUs; - private final long inputLength; + /** + * Data size, including the XING frame. + */ + private final long dataSize; /** * Entries are in the range [0, 255], but are stored as long integers for convenience. */ private final long[] tableOfContents; - private final long sizeBytes; - private final int headerSize; - private XingSeeker(long firstFramePosition, long durationUs, long inputLength) { - this(firstFramePosition, durationUs, inputLength, null, 0, 0); + private XingSeeker(long dataStartPosition, int xingFrameSize, long durationUs) { + this(dataStartPosition, xingFrameSize, durationUs, C.LENGTH_UNSET, null); } - private XingSeeker(long firstFramePosition, long durationUs, long inputLength, - long[] tableOfContents, long sizeBytes, int headerSize) { - this.firstFramePosition = firstFramePosition; + private XingSeeker(long dataStartPosition, int xingFrameSize, long durationUs, long dataSize, + long[] tableOfContents) { + this.dataStartPosition = dataStartPosition; + this.xingFrameSize = xingFrameSize; this.durationUs = durationUs; - this.inputLength = inputLength; + this.dataSize = dataSize; this.tableOfContents = tableOfContents; - this.sizeBytes = sizeBytes; - this.headerSize = headerSize; } @Override @@ -102,44 +109,45 @@ import com.google.android.exoplayer2.util.Util; @Override public long getPosition(long timeUs) { if (!isSeekable()) { - return firstFramePosition; + return dataStartPosition + xingFrameSize; } double percent = (timeUs * 100d) / durationUs; - double fx; + double scaledPosition; if (percent <= 0) { - fx = 0; + scaledPosition = 0; } else if (percent >= 100) { - fx = 256; + scaledPosition = 256; } else { - int a = (int) percent; - float fa = tableOfContents[a]; - float fb = a == 99 ? 256 : tableOfContents[a + 1]; - fx = fa + (fb - fa) * (percent - a); + int prevTableIndex = (int) percent; + double prevScaledPosition = tableOfContents[prevTableIndex]; + double nextScaledPosition = prevTableIndex == 99 ? 256 : tableOfContents[prevTableIndex + 1]; + // Linearly interpolate between the two scaled positions. + double interpolateFraction = percent - prevTableIndex; + scaledPosition = prevScaledPosition + + (interpolateFraction * (nextScaledPosition - prevScaledPosition)); } - - long position = Math.round((fx / 256) * sizeBytes) + firstFramePosition; - long maximumPosition = inputLength != C.LENGTH_UNSET ? inputLength - 1 - : firstFramePosition - headerSize + sizeBytes - 1; - return Math.min(position, maximumPosition); + long positionOffset = Math.round((scaledPosition / 256) * dataSize); + // Ensure returned positions skip the frame containing the XING header. + positionOffset = Util.constrainValue(positionOffset, xingFrameSize, dataSize - 1); + return dataStartPosition + positionOffset; } @Override public long getTimeUs(long position) { - if (!isSeekable() || position < firstFramePosition) { + long positionOffset = position - dataStartPosition; + if (!isSeekable() || positionOffset <= xingFrameSize) { return 0L; } - double offsetByte = (256d * (position - firstFramePosition)) / sizeBytes; - int previousTocPosition = - Util.binarySearchFloor(tableOfContents, (long) offsetByte, true, true); - long previousTime = getTimeUsForTocPosition(previousTocPosition); - - // Linearly interpolate the time taking into account the next entry. - long previousByte = tableOfContents[previousTocPosition]; - long nextByte = previousTocPosition == 99 ? 256 : tableOfContents[previousTocPosition + 1]; - long nextTime = getTimeUsForTocPosition(previousTocPosition + 1); - long timeOffset = nextByte == previousByte ? 0 : (long) ((nextTime - previousTime) - * (offsetByte - previousByte) / (nextByte - previousByte)); - return previousTime + timeOffset; + double scaledPosition = (positionOffset * 256d) / dataSize; + int prevTableIndex = Util.binarySearchFloor(tableOfContents, (long) scaledPosition, true, true); + long prevTimeUs = getTimeUsForTableIndex(prevTableIndex); + long prevScaledPosition = tableOfContents[prevTableIndex]; + long nextTimeUs = getTimeUsForTableIndex(prevTableIndex + 1); + long nextScaledPosition = prevTableIndex == 99 ? 256 : tableOfContents[prevTableIndex + 1]; + // Linearly interpolate between the two table entries. + double interpolateFraction = prevScaledPosition == nextScaledPosition ? 0 + : ((scaledPosition - prevScaledPosition) / (nextScaledPosition - prevScaledPosition)); + return prevTimeUs + Math.round(interpolateFraction * (nextTimeUs - prevTimeUs)); } @Override @@ -148,11 +156,13 @@ import com.google.android.exoplayer2.util.Util; } /** - * Returns the time in microseconds corresponding to a table of contents position, which is - * interpreted as a percentage of the stream's duration between 0 and 100. + * Returns the time in microseconds for a given table index. + * + * @param tableIndex A table index in the range [0, 100]. + * @return The corresponding time in microseconds. */ - private long getTimeUsForTocPosition(int tocPosition) { - return (durationUs * tocPosition) / 100; + private long getTimeUsForTableIndex(int tableIndex) { + return (durationUs * tableIndex) / 100; } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp3/XingSeekerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp3/XingSeekerTest.java index b43949b7c2..e644abc7ef 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp3/XingSeekerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp3/XingSeekerTest.java @@ -43,17 +43,17 @@ public final class XingSeekerTest { private static final int XING_FRAME_POSITION = 157; /** - * Size of the audio stream, encoded in {@link #XING_FRAME_PAYLOAD}. + * Data size, as encoded in {@link #XING_FRAME_PAYLOAD}. */ - private static final int STREAM_SIZE_BYTES = 948505; + private static final int DATA_SIZE_BYTES = 948505; /** * Duration of the audio stream in microseconds, encoded in {@link #XING_FRAME_PAYLOAD}. */ private static final int STREAM_DURATION_US = 59271836; /** - * The length of the file in bytes. + * The length of the stream in bytes. */ - private static final int INPUT_LENGTH = 948662; + private static final int STREAM_LENGTH = XING_FRAME_POSITION + DATA_SIZE_BYTES; private XingSeeker seeker; private XingSeeker seekerWithInputLength; @@ -63,10 +63,10 @@ public final class XingSeekerTest { public void setUp() throws Exception { MpegAudioHeader xingFrameHeader = new MpegAudioHeader(); MpegAudioHeader.populateHeader(XING_FRAME_HEADER_DATA, xingFrameHeader); - seeker = XingSeeker.create(xingFrameHeader, new ParsableByteArray(XING_FRAME_PAYLOAD), - XING_FRAME_POSITION, C.LENGTH_UNSET); - seekerWithInputLength = XingSeeker.create(xingFrameHeader, - new ParsableByteArray(XING_FRAME_PAYLOAD), XING_FRAME_POSITION, INPUT_LENGTH); + seeker = XingSeeker.create(C.LENGTH_UNSET, XING_FRAME_POSITION, xingFrameHeader, + new ParsableByteArray(XING_FRAME_PAYLOAD)); + seekerWithInputLength = XingSeeker.create(STREAM_LENGTH, + XING_FRAME_POSITION, xingFrameHeader, new ParsableByteArray(XING_FRAME_PAYLOAD)); xingFrameSize = xingFrameHeader.frameSize; } @@ -84,10 +84,10 @@ public final class XingSeekerTest { @Test public void testGetTimeUsAtEndOfStream() { - assertThat(seeker.getTimeUs(XING_FRAME_POSITION + xingFrameSize + STREAM_SIZE_BYTES)) + assertThat(seeker.getTimeUs(STREAM_LENGTH)) .isEqualTo(STREAM_DURATION_US); assertThat( - seekerWithInputLength.getTimeUs(XING_FRAME_POSITION + xingFrameSize + STREAM_SIZE_BYTES)) + seekerWithInputLength.getTimeUs(STREAM_LENGTH)) .isEqualTo(STREAM_DURATION_US); } @@ -100,14 +100,14 @@ public final class XingSeekerTest { @Test public void testGetPositionAtEndOfStream() { assertThat(seeker.getPosition(STREAM_DURATION_US)) - .isEqualTo(XING_FRAME_POSITION + STREAM_SIZE_BYTES - 1); + .isEqualTo(STREAM_LENGTH - 1); assertThat(seekerWithInputLength.getPosition(STREAM_DURATION_US)) - .isEqualTo(XING_FRAME_POSITION + STREAM_SIZE_BYTES - 1); + .isEqualTo(STREAM_LENGTH - 1); } @Test public void testGetTimeForAllPositions() { - for (int offset = xingFrameSize; offset < STREAM_SIZE_BYTES; offset++) { + for (int offset = xingFrameSize; offset < DATA_SIZE_BYTES; offset++) { int position = XING_FRAME_POSITION + offset; long timeUs = seeker.getTimeUs(position); assertThat(seeker.getPosition(timeUs)).isEqualTo(position); From b61d09c4166c798bd242cb3fa573de77b579ff88 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 30 Nov 2017 08:36:03 -0800 Subject: [PATCH 0916/2472] Fix mp3 extractor test ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177458840 --- .../androidTest/assets/mp3/bear.mp3.1.dump | 154 +++++++++--------- 1 file changed, 77 insertions(+), 77 deletions(-) diff --git a/library/core/src/androidTest/assets/mp3/bear.mp3.1.dump b/library/core/src/androidTest/assets/mp3/bear.mp3.1.dump index 7b6fe9db37..a57894e81e 100644 --- a/library/core/src/androidTest/assets/mp3/bear.mp3.1.dump +++ b/library/core/src/androidTest/assets/mp3/bear.mp3.1.dump @@ -27,311 +27,311 @@ track 0: initializationData: sample count = 77 sample 0: - time = 928567 + time = 928568 flags = 1 data = length 384, hash F7E344F4 sample 1: - time = 952567 + time = 952568 flags = 1 data = length 384, hash 14EF6AFD sample 2: - time = 976567 + time = 976568 flags = 1 data = length 384, hash 61C9B92C sample 3: - time = 1000567 + time = 1000568 flags = 1 data = length 384, hash ABE1368 sample 4: - time = 1024567 + time = 1024568 flags = 1 data = length 384, hash 6A3B8547 sample 5: - time = 1048567 + time = 1048568 flags = 1 data = length 384, hash 30E905FA sample 6: - time = 1072567 + time = 1072568 flags = 1 data = length 384, hash 21A267CD sample 7: - time = 1096567 + time = 1096568 flags = 1 data = length 384, hash D96A2651 sample 8: - time = 1120567 + time = 1120568 flags = 1 data = length 384, hash 72340177 sample 9: - time = 1144567 + time = 1144568 flags = 1 data = length 384, hash 9345E744 sample 10: - time = 1168567 + time = 1168568 flags = 1 data = length 384, hash FDE39E3A sample 11: - time = 1192567 + time = 1192568 flags = 1 data = length 384, hash F0B7465 sample 12: - time = 1216567 + time = 1216568 flags = 1 data = length 384, hash 3693AB86 sample 13: - time = 1240567 + time = 1240568 flags = 1 data = length 384, hash F39719B1 sample 14: - time = 1264567 + time = 1264568 flags = 1 data = length 384, hash DA3958DC sample 15: - time = 1288567 + time = 1288568 flags = 1 data = length 384, hash FDC7599F sample 16: - time = 1312567 + time = 1312568 flags = 1 data = length 384, hash AEFF8471 sample 17: - time = 1336567 + time = 1336568 flags = 1 data = length 384, hash 89C92C19 sample 18: - time = 1360567 + time = 1360568 flags = 1 data = length 384, hash 5C786A4B sample 19: - time = 1384567 + time = 1384568 flags = 1 data = length 384, hash 5ACA8B sample 20: - time = 1408567 + time = 1408568 flags = 1 data = length 384, hash 7755974C sample 21: - time = 1432567 + time = 1432568 flags = 1 data = length 384, hash 3934B73C sample 22: - time = 1456567 + time = 1456568 flags = 1 data = length 384, hash DDD70A2F sample 23: - time = 1480567 + time = 1480568 flags = 1 data = length 384, hash 8FACE2EF sample 24: - time = 1504567 + time = 1504568 flags = 1 data = length 384, hash 4A602591 sample 25: - time = 1528567 + time = 1528568 flags = 1 data = length 384, hash D019AA2D sample 26: - time = 1552567 + time = 1552568 flags = 1 data = length 384, hash 8A680B9D sample 27: - time = 1576567 + time = 1576568 flags = 1 data = length 384, hash B655C959 sample 28: - time = 1600567 + time = 1600568 flags = 1 data = length 384, hash 2168336B sample 29: - time = 1624567 + time = 1624568 flags = 1 data = length 384, hash D77F6D31 sample 30: - time = 1648567 + time = 1648568 flags = 1 data = length 384, hash 524B4B2F sample 31: - time = 1672567 + time = 1672568 flags = 1 data = length 384, hash 4752DDFC sample 32: - time = 1696567 + time = 1696568 flags = 1 data = length 384, hash E786727F sample 33: - time = 1720567 + time = 1720568 flags = 1 data = length 384, hash 5DA6FB8C sample 34: - time = 1744567 + time = 1744568 flags = 1 data = length 384, hash 92F24269 sample 35: - time = 1768567 + time = 1768568 flags = 1 data = length 384, hash CD0A3BA1 sample 36: - time = 1792567 + time = 1792568 flags = 1 data = length 384, hash 7D00409F sample 37: - time = 1816567 + time = 1816568 flags = 1 data = length 384, hash D7ADB5FA sample 38: - time = 1840567 + time = 1840568 flags = 1 data = length 384, hash 4A140209 sample 39: - time = 1864567 + time = 1864568 flags = 1 data = length 384, hash E801184A sample 40: - time = 1888567 + time = 1888568 flags = 1 data = length 384, hash 53C6CF9C sample 41: - time = 1912567 + time = 1912568 flags = 1 data = length 384, hash 19A8D99F sample 42: - time = 1936567 + time = 1936568 flags = 1 data = length 384, hash E47EB43F sample 43: - time = 1960567 + time = 1960568 flags = 1 data = length 384, hash 4EA329E7 sample 44: - time = 1984567 + time = 1984568 flags = 1 data = length 384, hash 1CCAAE62 sample 45: - time = 2008567 + time = 2008568 flags = 1 data = length 384, hash ED3F8C66 sample 46: - time = 2032567 + time = 2032568 flags = 1 data = length 384, hash D3D646B6 sample 47: - time = 2056567 + time = 2056568 flags = 1 data = length 384, hash 68CD1574 sample 48: - time = 2080567 + time = 2080568 flags = 1 data = length 384, hash 8CEAB382 sample 49: - time = 2104567 + time = 2104568 flags = 1 data = length 384, hash D54B1C48 sample 50: - time = 2128567 + time = 2128568 flags = 1 data = length 384, hash FFE2EE90 sample 51: - time = 2152567 + time = 2152568 flags = 1 data = length 384, hash BFE8A673 sample 52: - time = 2176567 + time = 2176568 flags = 1 data = length 384, hash 978B1C92 sample 53: - time = 2200567 + time = 2200568 flags = 1 data = length 384, hash 810CC71E sample 54: - time = 2224567 + time = 2224568 flags = 1 data = length 384, hash 44FE42D9 sample 55: - time = 2248567 + time = 2248568 flags = 1 data = length 384, hash 2F5BB02C sample 56: - time = 2272567 + time = 2272568 flags = 1 data = length 384, hash 77DDB90 sample 57: - time = 2296567 + time = 2296568 flags = 1 data = length 384, hash 24FB5EDA sample 58: - time = 2320567 + time = 2320568 flags = 1 data = length 384, hash E73203C6 sample 59: - time = 2344567 + time = 2344568 flags = 1 data = length 384, hash 14B525F1 sample 60: - time = 2368567 + time = 2368568 flags = 1 data = length 384, hash 5E0F4E2E sample 61: - time = 2392567 + time = 2392568 flags = 1 data = length 384, hash 67EE4E31 sample 62: - time = 2416567 + time = 2416568 flags = 1 data = length 384, hash 2E04EC4C sample 63: - time = 2440567 + time = 2440568 flags = 1 data = length 384, hash 852CABA7 sample 64: - time = 2464567 + time = 2464568 flags = 1 data = length 384, hash 19928903 sample 65: - time = 2488567 + time = 2488568 flags = 1 data = length 384, hash 5DA42021 sample 66: - time = 2512567 + time = 2512568 flags = 1 data = length 384, hash 45B20B7C sample 67: - time = 2536567 + time = 2536568 flags = 1 data = length 384, hash D108A215 sample 68: - time = 2560567 + time = 2560568 flags = 1 data = length 384, hash BD25DB7C sample 69: - time = 2584567 + time = 2584568 flags = 1 data = length 384, hash DA7F9861 sample 70: - time = 2608567 + time = 2608568 flags = 1 data = length 384, hash CCD576F sample 71: - time = 2632567 + time = 2632568 flags = 1 data = length 384, hash 405C1EB5 sample 72: - time = 2656567 + time = 2656568 flags = 1 data = length 384, hash 6640B74E sample 73: - time = 2680567 + time = 2680568 flags = 1 data = length 384, hash B4E5937A sample 74: - time = 2704567 + time = 2704568 flags = 1 data = length 384, hash CEE17733 sample 75: - time = 2728567 + time = 2728568 flags = 1 data = length 384, hash 2A0DA733 sample 76: - time = 2752567 + time = 2752568 flags = 1 data = length 384, hash 97F4129B tracksEnded = true From 393a7625630e50dcbb84b7c653d4d61f1168725e Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 4 Dec 2017 01:18:58 -0800 Subject: [PATCH 0917/2472] Use AdaptiveMediaSourceEventListener for ExtractorMediaSource This is a step towards harmonizing the MediaSource Builders and (potentially) providing MediaSource factories. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177783157 --- RELEASENOTES.md | 2 + .../android/exoplayer2/demo/EventLogger.java | 24 +- .../exoplayer2/ext/ima/ImaAdsMediaSource.java | 14 +- .../AdaptiveMediaSourceEventListener.java | 305 +---------- .../source/ExtractorMediaPeriod.java | 62 ++- .../source/ExtractorMediaSource.java | 159 +++++- .../source/MediaSourceEventListener.java | 487 ++++++++++++++++++ .../exoplayer2/source/ads/AdsMediaSource.java | 81 ++- .../android/exoplayer2/upstream/DataSpec.java | 19 +- 9 files changed, 753 insertions(+), 400 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java diff --git a/RELEASENOTES.md b/RELEASENOTES.md index a123c78323..3a73a9e716 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -13,6 +13,8 @@ ([#2980](https://github.com/google/ExoPlayer/issues/2980)). * Fix handling of playback parameters changes while paused when followed by a seek. +* Use the same listener `MediaSourceEventListener` for all MediaSource + implementations. ### 2.6.0 ### diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java index 4819c28753..b9be8f3846 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java @@ -39,7 +39,6 @@ import com.google.android.exoplayer2.metadata.id3.PrivFrame; import com.google.android.exoplayer2.metadata.id3.TextInformationFrame; import com.google.android.exoplayer2.metadata.id3.UrlLinkFrame; import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener; -import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.ads.AdsMediaSource; @@ -53,13 +52,15 @@ import java.io.IOException; import java.text.NumberFormat; import java.util.Locale; -/** - * Logs player events using {@link Log}. - */ -/* package */ final class EventLogger implements Player.EventListener, MetadataOutput, - AudioRendererEventListener, VideoRendererEventListener, AdaptiveMediaSourceEventListener, - ExtractorMediaSource.EventListener, AdsMediaSource.AdsListener, - DefaultDrmSessionManager.EventListener { +/** Logs player events using {@link Log}. */ +/* package */ final class EventLogger + implements Player.EventListener, + MetadataOutput, + AudioRendererEventListener, + VideoRendererEventListener, + AdaptiveMediaSourceEventListener, + AdsMediaSource.EventListener, + DefaultDrmSessionManager.EventListener { private static final String TAG = "EventLogger"; private static final int MAX_TIMELINE_ITEM_LINES = 3; @@ -322,13 +323,6 @@ import java.util.Locale; Log.d(TAG, "drmKeysLoaded [" + getSessionTimeString() + "]"); } - // ExtractorMediaSource.EventListener - - @Override - public void onLoadError(IOException error) { - printInternalError("loadError", error); - } - // AdaptiveMediaSourceEventListener @Override diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java index 02aa4807a5..cd646daf42 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java @@ -52,8 +52,8 @@ public final class ImaAdsMediaSource implements MediaSource { } /** - * Constructs a new source that inserts ads linearly with the content specified by - * {@code contentMediaSource}. + * Constructs a new source that inserts ads linearly with the content specified by {@code + * contentMediaSource}. * * @param contentMediaSource The {@link MediaSource} providing the content to play. * @param dataSourceFactory Factory for data sources used to load ad media. @@ -62,9 +62,13 @@ public final class ImaAdsMediaSource implements MediaSource { * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. */ - public ImaAdsMediaSource(MediaSource contentMediaSource, DataSource.Factory dataSourceFactory, - ImaAdsLoader imaAdsLoader, ViewGroup adUiViewGroup, @Nullable Handler eventHandler, - @Nullable AdsMediaSource.AdsListener eventListener) { + public ImaAdsMediaSource( + MediaSource contentMediaSource, + DataSource.Factory dataSourceFactory, + ImaAdsLoader imaAdsLoader, + ViewGroup adUiViewGroup, + @Nullable Handler eventHandler, + @Nullable AdsMediaSource.EventListener eventListener) { adsMediaSource = new AdsMediaSource(contentMediaSource, dataSourceFactory, imaAdsLoader, adUiViewGroup, eventHandler, eventListener); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/AdaptiveMediaSourceEventListener.java b/library/core/src/main/java/com/google/android/exoplayer2/source/AdaptiveMediaSourceEventListener.java index be07cbb5dc..2bc9d48726 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/AdaptiveMediaSourceEventListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/AdaptiveMediaSourceEventListener.java @@ -16,306 +16,39 @@ package com.google.android.exoplayer2.source; import android.os.Handler; -import android.os.SystemClock; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.upstream.DataSpec; -import com.google.android.exoplayer2.util.Assertions; -import java.io.IOException; +import android.support.annotation.Nullable; /** - * Interface for callbacks to be notified of adaptive {@link MediaSource} events. + * Interface for callbacks to be notified of {@link MediaSource} events. + * + * @deprecated Use {@link MediaSourceEventListener} */ -public interface AdaptiveMediaSourceEventListener { +@Deprecated +public interface AdaptiveMediaSourceEventListener extends MediaSourceEventListener { - /** - * Called when a load begins. - * - * @param dataSpec Defines the data being loaded. - * @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data - * being loaded. - * @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds - * to media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise. - * @param trackFormat The format of the track to which the data belongs. Null if the data does - * not belong to a track. - * @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the - * data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise. - * @param trackSelectionData Optional data associated with the selection of the track to which the - * data belongs. Null if the data does not belong to a track. - * @param mediaStartTimeMs The start time of the media being loaded, or {@link C#TIME_UNSET} if - * the load is not for media data. - * @param mediaEndTimeMs The end time of the media being loaded, or {@link C#TIME_UNSET} if the - * load is not for media data. - * @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} when the load began. - */ - void onLoadStarted(DataSpec dataSpec, int dataType, int trackType, Format trackFormat, - int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs, - long mediaEndTimeMs, long elapsedRealtimeMs); - - /** - * Called when a load ends. - * - * @param dataSpec Defines the data being loaded. - * @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data - * being loaded. - * @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds - * to media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise. - * @param trackFormat The format of the track to which the data belongs. Null if the data does - * not belong to a track. - * @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the - * data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise. - * @param trackSelectionData Optional data associated with the selection of the track to which the - * data belongs. Null if the data does not belong to a track. - * @param mediaStartTimeMs The start time of the media being loaded, or {@link C#TIME_UNSET} if - * the load is not for media data. - * @param mediaEndTimeMs The end time of the media being loaded, or {@link C#TIME_UNSET} if the - * load is not for media data. - * @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} when the load ended. - * @param loadDurationMs The duration of the load. - * @param bytesLoaded The number of bytes that were loaded. - */ - void onLoadCompleted(DataSpec dataSpec, int dataType, int trackType, Format trackFormat, - int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs, - long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs, long bytesLoaded); - - /** - * Called when a load is canceled. - * - * @param dataSpec Defines the data being loaded. - * @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data - * being loaded. - * @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds - * to media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise. - * @param trackFormat The format of the track to which the data belongs. Null if the data does - * not belong to a track. - * @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the - * data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise. - * @param trackSelectionData Optional data associated with the selection of the track to which the - * data belongs. Null if the data does not belong to a track. - * @param mediaStartTimeMs The start time of the media being loaded, or {@link C#TIME_UNSET} if - * the load is not for media data. - * @param mediaEndTimeMs The end time of the media being loaded, or {@link C#TIME_UNSET} if the - * load is not for media data. - * @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} when the load was - * canceled. - * @param loadDurationMs The duration of the load up to the point at which it was canceled. - * @param bytesLoaded The number of bytes that were loaded prior to cancelation. - */ - void onLoadCanceled(DataSpec dataSpec, int dataType, int trackType, Format trackFormat, - int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs, - long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs, long bytesLoaded); - - /** - * Called when a load error occurs. - *

          - * The error may or may not have resulted in the load being canceled, as indicated by the - * {@code wasCanceled} parameter. If the load was canceled, {@link #onLoadCanceled} will - * not be called in addition to this method. - *

          - * This method being called does not indicate that playback has failed, or that it will fail. The - * player may be able to recover from the error and continue. Hence applications should - * not implement this method to display a user visible error or initiate an application - * level retry ({@link Player.EventListener#onPlayerError} is the appropriate place to implement - * such behavior). This method is called to provide the application with an opportunity to log the - * error if it wishes to do so. - * - * @param dataSpec Defines the data being loaded. - * @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data - * being loaded. - * @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds - * to media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise. - * @param trackFormat The format of the track to which the data belongs. Null if the data does - * not belong to a track. - * @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the - * data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise. - * @param trackSelectionData Optional data associated with the selection of the track to which the - * data belongs. Null if the data does not belong to a track. - * @param mediaStartTimeMs The start time of the media being loaded, or {@link C#TIME_UNSET} if - * the load is not for media data. - * @param mediaEndTimeMs The end time of the media being loaded, or {@link C#TIME_UNSET} if the - * load is not for media data. - * @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} when the error - * occurred. - * @param loadDurationMs The duration of the load up to the point at which the error occurred. - * @param bytesLoaded The number of bytes that were loaded prior to the error. - * @param error The load error. - * @param wasCanceled Whether the load was canceled as a result of the error. - */ - void onLoadError(DataSpec dataSpec, int dataType, int trackType, Format trackFormat, - int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs, - long mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs, long bytesLoaded, - IOException error, boolean wasCanceled); - - /** - * Called when data is removed from the back of a media buffer, typically so that it can be - * re-buffered in a different format. - * - * @param trackType The type of the media. One of the {@link C} {@code TRACK_TYPE_*} constants. - * @param mediaStartTimeMs The start time of the media being discarded. - * @param mediaEndTimeMs The end time of the media being discarded. - */ - void onUpstreamDiscarded(int trackType, long mediaStartTimeMs, long mediaEndTimeMs); - - /** - * Called when a downstream format change occurs (i.e. when the format of the media being read - * from one or more {@link SampleStream}s provided by the source changes). - * - * @param trackType The type of the media. One of the {@link C} {@code TRACK_TYPE_*} constants. - * @param trackFormat The format of the track to which the data belongs. Null if the data does - * not belong to a track. - * @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the - * data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise. - * @param trackSelectionData Optional data associated with the selection of the track to which the - * data belongs. Null if the data does not belong to a track. - * @param mediaTimeMs The media time at which the change occurred. - */ - void onDownstreamFormatChanged(int trackType, Format trackFormat, int trackSelectionReason, - Object trackSelectionData, long mediaTimeMs); - - /** - * Dispatches events to a {@link AdaptiveMediaSourceEventListener}. - */ - final class EventDispatcher { + /** Dispatches events to a {@link MediaSourceEventListener}. */ + final class EventDispatcher extends MediaSourceEventListener.EventDispatcher { private final Handler handler; - private final AdaptiveMediaSourceEventListener listener; - private final long mediaTimeOffsetMs; + private final MediaSourceEventListener listener; - public EventDispatcher(Handler handler, AdaptiveMediaSourceEventListener listener) { + public EventDispatcher(@Nullable Handler handler, @Nullable MediaSourceEventListener listener) { this(handler, listener, 0); } - public EventDispatcher(Handler handler, AdaptiveMediaSourceEventListener listener, + public EventDispatcher( + @Nullable Handler handler, + @Nullable MediaSourceEventListener listener, long mediaTimeOffsetMs) { - this.handler = listener != null ? Assertions.checkNotNull(handler) : null; + super(handler, listener, mediaTimeOffsetMs); + this.handler = handler; this.listener = listener; - this.mediaTimeOffsetMs = mediaTimeOffsetMs; } - public EventDispatcher copyWithMediaTimeOffsetMs(long mediaTimeOffsetMs) { - return new EventDispatcher(handler, listener, mediaTimeOffsetMs); - } - - public void loadStarted(DataSpec dataSpec, int dataType, long elapsedRealtimeMs) { - loadStarted(dataSpec, dataType, C.TRACK_TYPE_UNKNOWN, null, C.SELECTION_REASON_UNKNOWN, - null, C.TIME_UNSET, C.TIME_UNSET, elapsedRealtimeMs); - } - - public void loadStarted(final DataSpec dataSpec, final int dataType, final int trackType, - final Format trackFormat, final int trackSelectionReason, final Object trackSelectionData, - final long mediaStartTimeUs, final long mediaEndTimeUs, final long elapsedRealtimeMs) { - if (listener != null) { - handler.post(new Runnable() { - @Override - public void run() { - listener.onLoadStarted(dataSpec, dataType, trackType, trackFormat, trackSelectionReason, - trackSelectionData, adjustMediaTime(mediaStartTimeUs), - adjustMediaTime(mediaEndTimeUs), elapsedRealtimeMs); - } - }); - } - } - - public void loadCompleted(DataSpec dataSpec, int dataType, long elapsedRealtimeMs, - long loadDurationMs, long bytesLoaded) { - loadCompleted(dataSpec, dataType, C.TRACK_TYPE_UNKNOWN, null, C.SELECTION_REASON_UNKNOWN, - null, C.TIME_UNSET, C.TIME_UNSET, elapsedRealtimeMs, loadDurationMs, bytesLoaded); - } - - public void loadCompleted(final DataSpec dataSpec, final int dataType, final int trackType, - final Format trackFormat, final int trackSelectionReason, final Object trackSelectionData, - final long mediaStartTimeUs, final long mediaEndTimeUs, final long elapsedRealtimeMs, - final long loadDurationMs, final long bytesLoaded) { - if (listener != null) { - handler.post(new Runnable() { - @Override - public void run() { - listener.onLoadCompleted(dataSpec, dataType, trackType, trackFormat, - trackSelectionReason, trackSelectionData, adjustMediaTime(mediaStartTimeUs), - adjustMediaTime(mediaEndTimeUs), elapsedRealtimeMs, loadDurationMs, bytesLoaded); - } - }); - } - } - - public void loadCanceled(DataSpec dataSpec, int dataType, long elapsedRealtimeMs, - long loadDurationMs, long bytesLoaded) { - loadCanceled(dataSpec, dataType, C.TRACK_TYPE_UNKNOWN, null, C.SELECTION_REASON_UNKNOWN, - null, C.TIME_UNSET, C.TIME_UNSET, elapsedRealtimeMs, loadDurationMs, bytesLoaded); - } - - public void loadCanceled(final DataSpec dataSpec, final int dataType, final int trackType, - final Format trackFormat, final int trackSelectionReason, final Object trackSelectionData, - final long mediaStartTimeUs, final long mediaEndTimeUs, final long elapsedRealtimeMs, - final long loadDurationMs, final long bytesLoaded) { - if (listener != null) { - handler.post(new Runnable() { - @Override - public void run() { - listener.onLoadCanceled(dataSpec, dataType, trackType, trackFormat, - trackSelectionReason, trackSelectionData, adjustMediaTime(mediaStartTimeUs), - adjustMediaTime(mediaEndTimeUs), elapsedRealtimeMs, loadDurationMs, bytesLoaded); - } - }); - } - } - - public void loadError(DataSpec dataSpec, int dataType, long elapsedRealtimeMs, - long loadDurationMs, long bytesLoaded, IOException error, boolean wasCanceled) { - loadError(dataSpec, dataType, C.TRACK_TYPE_UNKNOWN, null, C.SELECTION_REASON_UNKNOWN, - null, C.TIME_UNSET, C.TIME_UNSET, elapsedRealtimeMs, loadDurationMs, bytesLoaded, - error, wasCanceled); - } - - public void loadError(final DataSpec dataSpec, final int dataType, final int trackType, - final Format trackFormat, final int trackSelectionReason, final Object trackSelectionData, - final long mediaStartTimeUs, final long mediaEndTimeUs, final long elapsedRealtimeMs, - final long loadDurationMs, final long bytesLoaded, final IOException error, - final boolean wasCanceled) { - if (listener != null) { - handler.post(new Runnable() { - @Override - public void run() { - listener.onLoadError(dataSpec, dataType, trackType, trackFormat, trackSelectionReason, - trackSelectionData, adjustMediaTime(mediaStartTimeUs), - adjustMediaTime(mediaEndTimeUs), elapsedRealtimeMs, loadDurationMs, bytesLoaded, - error, wasCanceled); - } - }); - } - } - - public void upstreamDiscarded(final int trackType, final long mediaStartTimeUs, - final long mediaEndTimeUs) { - if (listener != null) { - handler.post(new Runnable() { - @Override - public void run() { - listener.onUpstreamDiscarded(trackType, adjustMediaTime(mediaStartTimeUs), - adjustMediaTime(mediaEndTimeUs)); - } - }); - } - } - - public void downstreamFormatChanged(final int trackType, final Format trackFormat, - final int trackSelectionReason, final Object trackSelectionData, - final long mediaTimeUs) { - if (listener != null) { - handler.post(new Runnable() { - @Override - public void run() { - listener.onDownstreamFormatChanged(trackType, trackFormat, trackSelectionReason, - trackSelectionData, adjustMediaTime(mediaTimeUs)); - } - }); - } - } - - private long adjustMediaTime(long mediaTimeUs) { - long mediaTimeMs = C.usToMs(mediaTimeUs); - return mediaTimeMs == C.TIME_UNSET ? C.TIME_UNSET : mediaTimeOffsetMs + mediaTimeMs; + public AdaptiveMediaSourceEventListener.EventDispatcher copyWithMediaTimeOffsetMs( + long mediaTimeOffsetMs) { + return new AdaptiveMediaSourceEventListener.EventDispatcher( + handler, listener, mediaTimeOffsetMs); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java index d43b2d87b2..e3c9012dbc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source; import android.net.Uri; import android.os.Handler; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; @@ -28,6 +29,7 @@ import com.google.android.exoplayer2.extractor.ExtractorOutput; 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.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SampleQueue.UpstreamFormatChangedListener; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.Allocator; @@ -74,11 +76,10 @@ import java.util.Arrays; private final Uri uri; private final DataSource dataSource; private final int minLoadableRetryCount; - private final Handler eventHandler; - private final ExtractorMediaSource.EventListener eventListener; + private final EventDispatcher eventDispatcher; private final Listener listener; private final Allocator allocator; - private final String customCacheKey; + @Nullable private final String customCacheKey; private final long continueLoadingCheckIntervalBytes; private final Loader loader; private final ExtractorHolder extractorHolder; @@ -117,8 +118,7 @@ import java.util.Arrays; * @param dataSource The data source to read the media. * @param extractors The extractors to use to read the data source. * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. - * @param eventHandler A handler for events. May be null if delivery of events is not required. - * @param eventListener A listener of events. May be null if delivery of events is not required. + * @param eventDispatcher A dispatcher to notify of events. * @param listener A listener to notify when information about the period changes. * @param allocator An {@link Allocator} from which to obtain media buffer allocations. * @param customCacheKey A custom key that uniquely identifies the original stream. Used for cache @@ -126,15 +126,20 @@ import java.util.Arrays; * @param continueLoadingCheckIntervalBytes The number of bytes that should be loaded between each * invocation of {@link Callback#onContinueLoadingRequested(SequenceableLoader)}. */ - public ExtractorMediaPeriod(Uri uri, DataSource dataSource, Extractor[] extractors, - int minLoadableRetryCount, Handler eventHandler, - ExtractorMediaSource.EventListener eventListener, Listener listener, - Allocator allocator, String customCacheKey, int continueLoadingCheckIntervalBytes) { + public ExtractorMediaPeriod( + Uri uri, + DataSource dataSource, + Extractor[] extractors, + int minLoadableRetryCount, + EventDispatcher eventDispatcher, + Listener listener, + Allocator allocator, + @Nullable String customCacheKey, + int continueLoadingCheckIntervalBytes) { this.uri = uri; this.dataSource = dataSource; this.minLoadableRetryCount = minLoadableRetryCount; - this.eventHandler = eventHandler; - this.eventListener = eventListener; + this.eventDispatcher = eventDispatcher; this.listener = listener; this.allocator = allocator; this.customCacheKey = customCacheKey; @@ -430,8 +435,22 @@ import java.util.Arrays; public int onLoadError(ExtractingLoadable loadable, long elapsedRealtimeMs, long loadDurationMs, IOException error) { copyLengthFromLoader(loadable); - notifyLoadError(error); - if (isLoadableExceptionFatal(error)) { + boolean isErrorFatal = isLoadableExceptionFatal(error); + eventDispatcher.loadError( + loadable.dataSpec, + C.DATA_TYPE_MEDIA, + C.TRACK_TYPE_UNKNOWN, + /* trackFormat= */ null, + C.SELECTION_REASON_UNKNOWN, + /* trackSelectionData= */ null, + /* mediaStartTimeUs= */ 0, + durationUs, + elapsedRealtimeMs, + loadDurationMs, + loadable.bytesLoaded, + error, + /* wasCanceled= */ isErrorFatal); + if (isErrorFatal) { return Loader.DONT_RETRY_FATAL; } int extractedSamplesCount = getExtractedSamplesCount(); @@ -607,17 +626,6 @@ import java.util.Arrays; return e instanceof UnrecognizedInputFormatException; } - private void notifyLoadError(final IOException error) { - if (eventHandler != null && eventListener != null) { - eventHandler.post(new Runnable() { - @Override - public void run() { - eventListener.onLoadError(error); - } - }); - } - } - private final class SampleStreamImpl implements SampleStream { private final int track; @@ -664,7 +672,9 @@ import java.util.Arrays; private boolean pendingExtractorSeek; private long seekTimeUs; + private DataSpec dataSpec; private long length; + private long bytesLoaded; public ExtractingLoadable(Uri uri, DataSource dataSource, ExtractorHolder extractorHolder, ConditionVariable loadCondition) { @@ -700,7 +710,8 @@ import java.util.Arrays; ExtractorInput input = null; try { long position = positionHolder.position; - length = dataSource.open(new DataSpec(uri, position, C.LENGTH_UNSET, customCacheKey)); + dataSpec = new DataSpec(uri, position, C.LENGTH_UNSET, customCacheKey); + length = dataSource.open(dataSpec); if (length != C.LENGTH_UNSET) { length += position; } @@ -724,6 +735,7 @@ import java.util.Arrays; result = Extractor.RESULT_CONTINUE; } else if (input != null) { positionHolder.position = input.getPosition(); + bytesLoaded = positionHolder.position - dataSpec.absoluteStreamPosition; } Util.closeQuietly(dataSource); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java index 066953b998..3ab2609c0e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java @@ -17,14 +17,18 @@ package com.google.android.exoplayer2.source; import android.net.Uri; import android.os.Handler; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; +import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorsFactory; +import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; @@ -40,10 +44,12 @@ import java.io.IOException; * Note that the built-in extractors for AAC, MPEG PS/TS and FLV streams do not support seeking. */ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPeriod.Listener { - /** * Listener of {@link ExtractorMediaSource} events. + * + * @deprecated Use {@link MediaSourceEventListener}. */ + @Deprecated public interface EventListener { /** @@ -89,8 +95,7 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe private final DataSource.Factory dataSourceFactory; private final ExtractorsFactory extractorsFactory; private final int minLoadableRetryCount; - private final Handler eventHandler; - private final EventListener eventListener; + private final EventDispatcher eventDispatcher; private final String customCacheKey; private final int continueLoadingCheckIntervalBytes; @@ -108,9 +113,9 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe private ExtractorsFactory extractorsFactory; private int minLoadableRetryCount; - private Handler eventHandler; - private EventListener eventListener; - private String customCacheKey; + @Nullable private Handler eventHandler; + @Nullable private MediaSourceEventListener eventListener; + @Nullable private String customCacheKey; private int continueLoadingCheckIntervalBytes; private boolean isBuildCalled; @@ -187,8 +192,24 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe * @param eventHandler A handler for events. * @param eventListener A listener of events. * @return This builder. + * @deprecated Use {@link #setEventListener(Handler, MediaSourceEventListener)}. */ + @Deprecated public Builder setEventListener(Handler eventHandler, EventListener eventListener) { + this.eventHandler = eventHandler; + this.eventListener = eventListener == null ? null : new EventListenerWrapper(eventListener); + return this; + } + + /** + * Sets the listener to respond to {@link ExtractorMediaSource} events and the handler to + * deliver these events. + * + * @param eventHandler A handler for events. + * @param eventListener A listener of events. + * @return This builder. + */ + public Builder setEventListener(Handler eventHandler, MediaSourceEventListener eventListener) { this.eventHandler = eventHandler; this.eventListener = eventListener; return this; @@ -270,12 +291,31 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe public ExtractorMediaSource(Uri uri, DataSource.Factory dataSourceFactory, ExtractorsFactory extractorsFactory, int minLoadableRetryCount, Handler eventHandler, EventListener eventListener, String customCacheKey, int continueLoadingCheckIntervalBytes) { + this( + uri, + dataSourceFactory, + extractorsFactory, + minLoadableRetryCount, + eventHandler, + eventListener == null ? null : new EventListenerWrapper(eventListener), + customCacheKey, + continueLoadingCheckIntervalBytes); + } + + private ExtractorMediaSource( + Uri uri, + DataSource.Factory dataSourceFactory, + ExtractorsFactory extractorsFactory, + int minLoadableRetryCount, + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener, + @Nullable String customCacheKey, + int continueLoadingCheckIntervalBytes) { this.uri = uri; this.dataSourceFactory = dataSourceFactory; this.extractorsFactory = extractorsFactory; this.minLoadableRetryCount = minLoadableRetryCount; - this.eventHandler = eventHandler; - this.eventListener = eventListener; + this.eventDispatcher = new EventDispatcher(eventHandler, eventListener); this.customCacheKey = customCacheKey; this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes; } @@ -294,9 +334,16 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe @Override public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { Assertions.checkArgument(id.periodIndex == 0); - return new ExtractorMediaPeriod(uri, dataSourceFactory.createDataSource(), - extractorsFactory.createExtractors(), minLoadableRetryCount, eventHandler, eventListener, - this, allocator, customCacheKey, continueLoadingCheckIntervalBytes); + return new ExtractorMediaPeriod( + uri, + dataSourceFactory.createDataSource(), + extractorsFactory.createExtractors(), + minLoadableRetryCount, + eventDispatcher, + this, + allocator, + customCacheKey, + continueLoadingCheckIntervalBytes); } @Override @@ -331,4 +378,94 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe this, new SinglePeriodTimeline(timelineDurationUs, timelineIsSeekable), null); } + /** + * Wraps a deprecated {@link EventListener}, invoking its callback from the equivalent callback in + * {@link MediaSourceEventListener}. + */ + private static final class EventListenerWrapper implements MediaSourceEventListener { + private final EventListener eventListener; + + public EventListenerWrapper(EventListener eventListener) { + this.eventListener = Assertions.checkNotNull(eventListener); + } + + @Override + public void onLoadStarted( + DataSpec dataSpec, + int dataType, + int trackType, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + long mediaStartTimeMs, + long mediaEndTimeMs, + long elapsedRealtimeMs) { + // Do nothing. + } + + @Override + public void onLoadCompleted( + DataSpec dataSpec, + int dataType, + int trackType, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + long mediaStartTimeMs, + long mediaEndTimeMs, + long elapsedRealtimeMs, + long loadDurationMs, + long bytesLoaded) { + // Do nothing. + } + + @Override + public void onLoadCanceled( + DataSpec dataSpec, + int dataType, + int trackType, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + long mediaStartTimeMs, + long mediaEndTimeMs, + long elapsedRealtimeMs, + long loadDurationMs, + long bytesLoaded) { + // Do nothing. + } + + @Override + public void onLoadError( + DataSpec dataSpec, + int dataType, + int trackType, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + long mediaStartTimeMs, + long mediaEndTimeMs, + long elapsedRealtimeMs, + long loadDurationMs, + long bytesLoaded, + IOException error, + boolean wasCanceled) { + eventListener.onLoadError(error); + } + + @Override + public void onUpstreamDiscarded(int trackType, long mediaStartTimeMs, long mediaEndTimeMs) { + // Do nothing. + } + + @Override + public void onDownstreamFormatChanged( + int trackType, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + long mediaTimeMs) { + // Do nothing. + } + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java new file mode 100644 index 0000000000..82e8781d70 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java @@ -0,0 +1,487 @@ +/* + * 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; + +import android.os.Handler; +import android.os.SystemClock; +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.util.Assertions; +import java.io.IOException; + +/** Interface for callbacks to be notified of {@link MediaSource} events. */ +public interface MediaSourceEventListener { + /** + * Called when a load begins. + * + * @param dataSpec Defines the data being loaded. + * @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data + * being loaded. + * @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds to + * media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise. + * @param trackFormat The format of the track to which the data belongs. Null if the data does not + * belong to a track. + * @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the + * data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise. + * @param trackSelectionData Optional data associated with the selection of the track to which the + * data belongs. Null if the data does not belong to a track. + * @param mediaStartTimeMs The start time of the media being loaded, or {@link C#TIME_UNSET} if + * the load is not for media data. + * @param mediaEndTimeMs The end time of the media being loaded, or {@link C#TIME_UNSET} if the + * load is not for media data. + * @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} when the load began. + */ + void onLoadStarted( + DataSpec dataSpec, + int dataType, + int trackType, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + long mediaStartTimeMs, + long mediaEndTimeMs, + long elapsedRealtimeMs); + + /** + * Called when a load ends. + * + * @param dataSpec Defines the data being loaded. + * @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data + * being loaded. + * @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds to + * media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise. + * @param trackFormat The format of the track to which the data belongs. Null if the data does not + * belong to a track. + * @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the + * data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise. + * @param trackSelectionData Optional data associated with the selection of the track to which the + * data belongs. Null if the data does not belong to a track. + * @param mediaStartTimeMs The start time of the media being loaded, or {@link C#TIME_UNSET} if + * the load is not for media data. + * @param mediaEndTimeMs The end time of the media being loaded, or {@link C#TIME_UNSET} if the + * load is not for media data. + * @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} when the load ended. + * @param loadDurationMs The duration of the load. + * @param bytesLoaded The number of bytes that were loaded. + */ + void onLoadCompleted( + DataSpec dataSpec, + int dataType, + int trackType, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + long mediaStartTimeMs, + long mediaEndTimeMs, + long elapsedRealtimeMs, + long loadDurationMs, + long bytesLoaded); + + /** + * Called when a load is canceled. + * + * @param dataSpec Defines the data being loaded. + * @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data + * being loaded. + * @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds to + * media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise. + * @param trackFormat The format of the track to which the data belongs. Null if the data does not + * belong to a track. + * @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the + * data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise. + * @param trackSelectionData Optional data associated with the selection of the track to which the + * data belongs. Null if the data does not belong to a track. + * @param mediaStartTimeMs The start time of the media being loaded, or {@link C#TIME_UNSET} if + * the load is not for media data. + * @param mediaEndTimeMs The end time of the media being loaded, or {@link C#TIME_UNSET} if the + * load is not for media data. + * @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} when the load was + * canceled. + * @param loadDurationMs The duration of the load up to the point at which it was canceled. + * @param bytesLoaded The number of bytes that were loaded prior to cancelation. + */ + void onLoadCanceled( + DataSpec dataSpec, + int dataType, + int trackType, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + long mediaStartTimeMs, + long mediaEndTimeMs, + long elapsedRealtimeMs, + long loadDurationMs, + long bytesLoaded); + + /** + * Called when a load error occurs. + * + *

          The error may or may not have resulted in the load being canceled, as indicated by the + * {@code wasCanceled} parameter. If the load was canceled, {@link #onLoadCanceled} will + * not be called in addition to this method. + * + *

          This method being called does not indicate that playback has failed, or that it will fail. + * The player may be able to recover from the error and continue. Hence applications should + * not implement this method to display a user visible error or initiate an application + * level retry ({@link Player.EventListener#onPlayerError} is the appropriate place to implement + * such behavior). This method is called to provide the application with an opportunity to log the + * error if it wishes to do so. + * + * @param dataSpec Defines the data being loaded. + * @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data + * being loaded. + * @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds to + * media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise. + * @param trackFormat The format of the track to which the data belongs. Null if the data does not + * belong to a track. + * @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the + * data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise. + * @param trackSelectionData Optional data associated with the selection of the track to which the + * data belongs. Null if the data does not belong to a track. + * @param mediaStartTimeMs The start time of the media being loaded, or {@link C#TIME_UNSET} if + * the load is not for media data. + * @param mediaEndTimeMs The end time of the media being loaded, or {@link C#TIME_UNSET} if the + * load is not for media data. + * @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} when the error + * occurred. + * @param loadDurationMs The duration of the load up to the point at which the error occurred. + * @param bytesLoaded The number of bytes that were loaded prior to the error. + * @param error The load error. + * @param wasCanceled Whether the load was canceled as a result of the error. + */ + void onLoadError( + DataSpec dataSpec, + int dataType, + int trackType, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + long mediaStartTimeMs, + long mediaEndTimeMs, + long elapsedRealtimeMs, + long loadDurationMs, + long bytesLoaded, + IOException error, + boolean wasCanceled); + + /** + * Called when data is removed from the back of a media buffer, typically so that it can be + * re-buffered in a different format. + * + * @param trackType The type of the media. One of the {@link C} {@code TRACK_TYPE_*} constants. + * @param mediaStartTimeMs The start time of the media being discarded. + * @param mediaEndTimeMs The end time of the media being discarded. + */ + void onUpstreamDiscarded(int trackType, long mediaStartTimeMs, long mediaEndTimeMs); + + /** + * Called when a downstream format change occurs (i.e. when the format of the media being read + * from one or more {@link SampleStream}s provided by the source changes). + * + * @param trackType The type of the media. One of the {@link C} {@code TRACK_TYPE_*} constants. + * @param trackFormat The format of the track to which the data belongs. Null if the data does not + * belong to a track. + * @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the + * data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise. + * @param trackSelectionData Optional data associated with the selection of the track to which the + * data belongs. Null if the data does not belong to a track. + * @param mediaTimeMs The media time at which the change occurred. + */ + void onDownstreamFormatChanged( + int trackType, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + long mediaTimeMs); + + /** Dispatches events to a {@link MediaSourceEventListener}. */ + class EventDispatcher { + + @Nullable private final Handler handler; + @Nullable private final MediaSourceEventListener listener; + private final long mediaTimeOffsetMs; + + public EventDispatcher(@Nullable Handler handler, @Nullable MediaSourceEventListener listener) { + this(handler, listener, 0); + } + + public EventDispatcher( + @Nullable Handler handler, + @Nullable MediaSourceEventListener listener, + long mediaTimeOffsetMs) { + this.handler = listener != null ? Assertions.checkNotNull(handler) : null; + this.listener = listener; + this.mediaTimeOffsetMs = mediaTimeOffsetMs; + } + + public void loadStarted(DataSpec dataSpec, int dataType, long elapsedRealtimeMs) { + loadStarted( + dataSpec, + dataType, + C.TRACK_TYPE_UNKNOWN, + null, + C.SELECTION_REASON_UNKNOWN, + null, + C.TIME_UNSET, + C.TIME_UNSET, + elapsedRealtimeMs); + } + + public void loadStarted( + final DataSpec dataSpec, + final int dataType, + final int trackType, + final Format trackFormat, + final int trackSelectionReason, + final Object trackSelectionData, + final long mediaStartTimeUs, + final long mediaEndTimeUs, + final long elapsedRealtimeMs) { + if (listener != null && handler != null) { + handler.post( + new Runnable() { + @Override + public void run() { + listener.onLoadStarted( + dataSpec, + dataType, + trackType, + trackFormat, + trackSelectionReason, + trackSelectionData, + adjustMediaTime(mediaStartTimeUs), + adjustMediaTime(mediaEndTimeUs), + elapsedRealtimeMs); + } + }); + } + } + + public void loadCompleted( + DataSpec dataSpec, + int dataType, + long elapsedRealtimeMs, + long loadDurationMs, + long bytesLoaded) { + loadCompleted( + dataSpec, + dataType, + C.TRACK_TYPE_UNKNOWN, + null, + C.SELECTION_REASON_UNKNOWN, + null, + C.TIME_UNSET, + C.TIME_UNSET, + elapsedRealtimeMs, + loadDurationMs, + bytesLoaded); + } + + public void loadCompleted( + final DataSpec dataSpec, + final int dataType, + final int trackType, + final Format trackFormat, + final int trackSelectionReason, + final Object trackSelectionData, + final long mediaStartTimeUs, + final long mediaEndTimeUs, + final long elapsedRealtimeMs, + final long loadDurationMs, + final long bytesLoaded) { + if (listener != null && handler != null) { + handler.post( + new Runnable() { + @Override + public void run() { + listener.onLoadCompleted( + dataSpec, + dataType, + trackType, + trackFormat, + trackSelectionReason, + trackSelectionData, + adjustMediaTime(mediaStartTimeUs), + adjustMediaTime(mediaEndTimeUs), + elapsedRealtimeMs, + loadDurationMs, + bytesLoaded); + } + }); + } + } + + public void loadCanceled( + DataSpec dataSpec, + int dataType, + long elapsedRealtimeMs, + long loadDurationMs, + long bytesLoaded) { + loadCanceled( + dataSpec, + dataType, + C.TRACK_TYPE_UNKNOWN, + null, + C.SELECTION_REASON_UNKNOWN, + null, + C.TIME_UNSET, + C.TIME_UNSET, + elapsedRealtimeMs, + loadDurationMs, + bytesLoaded); + } + + public void loadCanceled( + final DataSpec dataSpec, + final int dataType, + final int trackType, + final Format trackFormat, + final int trackSelectionReason, + final Object trackSelectionData, + final long mediaStartTimeUs, + final long mediaEndTimeUs, + final long elapsedRealtimeMs, + final long loadDurationMs, + final long bytesLoaded) { + if (listener != null && handler != null) { + handler.post( + new Runnable() { + @Override + public void run() { + listener.onLoadCanceled( + dataSpec, + dataType, + trackType, + trackFormat, + trackSelectionReason, + trackSelectionData, + adjustMediaTime(mediaStartTimeUs), + adjustMediaTime(mediaEndTimeUs), + elapsedRealtimeMs, + loadDurationMs, + bytesLoaded); + } + }); + } + } + + public void loadError( + DataSpec dataSpec, + int dataType, + long elapsedRealtimeMs, + long loadDurationMs, + long bytesLoaded, + IOException error, + boolean wasCanceled) { + loadError( + dataSpec, + dataType, + C.TRACK_TYPE_UNKNOWN, + null, + C.SELECTION_REASON_UNKNOWN, + null, + C.TIME_UNSET, + C.TIME_UNSET, + elapsedRealtimeMs, + loadDurationMs, + bytesLoaded, + error, + wasCanceled); + } + + public void loadError( + final DataSpec dataSpec, + final int dataType, + final int trackType, + final Format trackFormat, + final int trackSelectionReason, + final Object trackSelectionData, + final long mediaStartTimeUs, + final long mediaEndTimeUs, + final long elapsedRealtimeMs, + final long loadDurationMs, + final long bytesLoaded, + final IOException error, + final boolean wasCanceled) { + if (listener != null && handler != null) { + handler.post( + new Runnable() { + @Override + public void run() { + listener.onLoadError( + dataSpec, + dataType, + trackType, + trackFormat, + trackSelectionReason, + trackSelectionData, + adjustMediaTime(mediaStartTimeUs), + adjustMediaTime(mediaEndTimeUs), + elapsedRealtimeMs, + loadDurationMs, + bytesLoaded, + error, + wasCanceled); + } + }); + } + } + + public void upstreamDiscarded( + final int trackType, final long mediaStartTimeUs, final long mediaEndTimeUs) { + if (listener != null && handler != null) { + handler.post( + new Runnable() { + @Override + public void run() { + listener.onUpstreamDiscarded( + trackType, adjustMediaTime(mediaStartTimeUs), adjustMediaTime(mediaEndTimeUs)); + } + }); + } + } + + public void downstreamFormatChanged( + final int trackType, + final Format trackFormat, + final int trackSelectionReason, + final Object trackSelectionData, + final long mediaTimeUs) { + if (listener != null && handler != null) { + handler.post( + new Runnable() { + @Override + public void run() { + listener.onDownstreamFormatChanged( + trackType, + trackFormat, + trackSelectionReason, + trackSelectionData, + adjustMediaTime(mediaTimeUs)); + } + }); + } + } + + private long adjustMediaTime(long mediaTimeUs) { + long mediaTimeMs = C.usToMs(mediaTimeUs); + return mediaTimeMs == C.TIME_UNSET ? C.TIME_UNSET : mediaTimeOffsetMs + mediaTimeMs; + } + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java index 47a2540c38..54a8fd96ae 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java @@ -26,9 +26,9 @@ import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.DeferredMediaPeriod; import com.google.android.exoplayer2.source.ExtractorMediaSource; -import com.google.android.exoplayer2.source.ExtractorMediaSource.EventListener; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.util.Assertions; @@ -44,10 +44,8 @@ import java.util.Map; */ public final class AdsMediaSource implements MediaSource { - /** - * Listener for events relating to ad loading. - */ - public interface AdsListener { + /** Listener for ads media source events. */ + public interface EventListener extends MediaSourceEventListener { /** * Called if there was an error loading ads. The media source will load the content without ads @@ -75,15 +73,13 @@ public final class AdsMediaSource implements MediaSource { private final MediaSource contentMediaSource; private final AdsLoader adsLoader; private final ViewGroup adUiViewGroup; + @Nullable private final Handler eventHandler; + @Nullable private final EventListener eventListener; private final Handler mainHandler; private final ComponentListener componentListener; private final AdMediaSourceFactory adMediaSourceFactory; private final Map> deferredMediaPeriodByAdMediaSource; private final Timeline.Period period; - @Nullable - private final Handler eventHandler; - @Nullable - private final AdsListener eventListener; private Handler playerHandler; private ExoPlayer player; @@ -115,10 +111,10 @@ public final class AdsMediaSource implements MediaSource { } /** - * Constructs a new source that inserts ads linearly with the content specified by - * {@code contentMediaSource}. - *

          - * Ad media is loaded using {@link ExtractorMediaSource}. If {@code eventListener} is + * Constructs a new source that inserts ads linearly with the content specified by {@code + * contentMediaSource}. + * + *

          Ad media is loaded using {@link ExtractorMediaSource}. If {@code eventListener} is * non-{@code null} it will be notified of both ad tag and ad media load errors. * * @param contentMediaSource The {@link MediaSource} providing the content to play. @@ -128,9 +124,13 @@ public final class AdsMediaSource implements MediaSource { * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. */ - public AdsMediaSource(MediaSource contentMediaSource, DataSource.Factory dataSourceFactory, - AdsLoader adsLoader, ViewGroup adUiViewGroup, @Nullable Handler eventHandler, - @Nullable AdsListener eventListener) { + public AdsMediaSource( + MediaSource contentMediaSource, + DataSource.Factory dataSourceFactory, + AdsLoader adsLoader, + ViewGroup adUiViewGroup, + @Nullable Handler eventHandler, + @Nullable EventListener eventListener) { this.contentMediaSource = contentMediaSource; this.adsLoader = adsLoader; this.adUiViewGroup = adUiViewGroup; @@ -186,7 +186,7 @@ public final class AdsMediaSource implements MediaSource { if (adGroupMediaSources[adGroupIndex].length <= adIndexInAdGroup) { Uri adUri = adPlaybackState.adUris[id.adGroupIndex][id.adIndexInAdGroup]; final MediaSource adMediaSource = - adMediaSourceFactory.createAdMediaSource(adUri, mainHandler, componentListener); + adMediaSourceFactory.createAdMediaSource(adUri, eventHandler, eventListener); int oldAdCount = adGroupMediaSources[id.adGroupIndex].length; if (adIndexInAdGroup >= oldAdCount) { int adCount = adIndexInAdGroup + 1; @@ -306,11 +306,8 @@ public final class AdsMediaSource implements MediaSource { } } - /** - * Listener for component events. All methods are called on the main thread. - */ - private final class ComponentListener implements AdsLoader.EventListener, - AdMediaSourceLoadErrorListener { + /** Listener for component events. All methods are called on the main thread. */ + private final class ComponentListener implements AdsLoader.EventListener { @Override public void onAdPlaybackState(final AdPlaybackState adPlaybackState) { @@ -374,20 +371,6 @@ public final class AdsMediaSource implements MediaSource { } - /** - * Listener for errors while loading an ad {@link MediaSource}. - */ - private interface AdMediaSourceLoadErrorListener { - - /** - * Called when an error occurs loading media data. - * - * @param error The load error. - */ - void onLoadError(IOException error); - - } - /** * Factory for {@link MediaSource}s for loading ad media. */ @@ -397,15 +380,13 @@ public final class AdsMediaSource implements MediaSource { * Creates a new {@link MediaSource} for loading the ad media with the specified {@code uri}. * * @param uri The URI of the ad. - * @param handler A handler for listener events. - * @param listener A listener for ad load errors. To have ad media source load errors notified - * via the ads media source's listener, call this listener's onLoadError method from your - * new media source's load error listener using the specified {@code handler}. Otherwise, - * this parameter can be ignored. + * @param handler A handler for listener events. May be null if delivery of events is not + * required. + * @param listener A listener for events. May be null if delivery of events is not required. * @return The new media source. */ - MediaSource createAdMediaSource(Uri uri, Handler handler, - AdMediaSourceLoadErrorListener listener); + MediaSource createAdMediaSource( + Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener); /** * Returns the content types supported by media sources created by this factory. Each element @@ -427,15 +408,11 @@ public final class AdsMediaSource implements MediaSource { } @Override - public MediaSource createAdMediaSource(Uri uri, Handler handler, - final AdMediaSourceLoadErrorListener listener) { - return new ExtractorMediaSource.Builder(uri, dataSourceFactory).setEventListener(handler, - new EventListener() { - @Override - public void onLoadError(IOException error) { - listener.onLoadError(error); - } - }).build(); + public MediaSource createAdMediaSource( + Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener) { + return new ExtractorMediaSource.Builder(uri, dataSourceFactory) + .setEventListener(handler, listener) + .build(); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java index ab1542c7a6..cbe971bc5d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.upstream; import android.net.Uri; import android.support.annotation.IntDef; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Assertions; import java.lang.annotation.Retention; @@ -79,7 +80,7 @@ public final class DataSpec { * A key that uniquely identifies the original stream. Used for cache indexing. May be null if the * {@link DataSpec} is not intended to be used in conjunction with a cache. */ - public final String key; + @Nullable public final String key; /** * Request flags. Currently {@link #FLAG_ALLOW_GZIP} and * {@link #FLAG_ALLOW_CACHING_UNKNOWN_LENGTH} are the only supported flags. @@ -113,7 +114,7 @@ public final class DataSpec { * @param length {@link #length}. * @param key {@link #key}. */ - public DataSpec(Uri uri, long absoluteStreamPosition, long length, String key) { + public DataSpec(Uri uri, long absoluteStreamPosition, long length, @Nullable String key) { this(uri, absoluteStreamPosition, absoluteStreamPosition, length, key, 0); } @@ -147,8 +148,8 @@ public final class DataSpec { } /** - * Construct a {@link DataSpec} where {@link #position} may differ from - * {@link #absoluteStreamPosition}. + * Construct a {@link DataSpec} where {@link #position} may differ from {@link + * #absoluteStreamPosition}. * * @param uri {@link #uri}. * @param postBody {@link #postBody}. @@ -158,8 +159,14 @@ public final class DataSpec { * @param key {@link #key}. * @param flags {@link #flags}. */ - public DataSpec(Uri uri, byte[] postBody, long absoluteStreamPosition, long position, long length, - String key, @Flags int flags) { + public DataSpec( + Uri uri, + byte[] postBody, + long absoluteStreamPosition, + long position, + long length, + @Nullable String key, + @Flags int flags) { Assertions.checkArgument(absoluteStreamPosition >= 0); Assertions.checkArgument(position >= 0); Assertions.checkArgument(length > 0 || length == C.LENGTH_UNSET); From e759462af8b2d6df4ff2f378cf216e409ae8e7b2 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 4 Dec 2017 02:06:42 -0800 Subject: [PATCH 0918/2472] Update internal usages of deprecated AdaptiveMediaSourceEventListener ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177786580 --- .../android/exoplayer2/demo/EventLogger.java | 21 +++++++---- .../AdaptiveMediaSourceEventListener.java | 36 ++----------------- .../source/ExtractorMediaSource.java | 2 +- .../source/MediaSourceEventListener.java | 6 +++- .../source/chunk/ChunkSampleStream.java | 3 +- .../source/dash/DashMediaPeriod.java | 2 +- .../source/dash/DashMediaSource.java | 2 +- .../exoplayer2/source/hls/HlsMediaPeriod.java | 2 +- .../exoplayer2/source/hls/HlsMediaSource.java | 2 +- .../source/hls/HlsSampleStreamWrapper.java | 2 +- .../hls/playlist/HlsPlaylistTracker.java | 2 +- .../source/smoothstreaming/SsMediaPeriod.java | 2 +- .../source/smoothstreaming/SsMediaSource.java | 2 +- .../testutil/FakeAdaptiveMediaPeriod.java | 2 +- .../testutil/FakeAdaptiveMediaSource.java | 14 +++++--- 15 files changed, 41 insertions(+), 59 deletions(-) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java index b9be8f3846..6fe0f15232 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java @@ -38,7 +38,7 @@ import com.google.android.exoplayer2.metadata.id3.Id3Frame; import com.google.android.exoplayer2.metadata.id3.PrivFrame; import com.google.android.exoplayer2.metadata.id3.TextInformationFrame; import com.google.android.exoplayer2.metadata.id3.UrlLinkFrame; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener; +import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.ads.AdsMediaSource; @@ -58,7 +58,7 @@ import java.util.Locale; MetadataOutput, AudioRendererEventListener, VideoRendererEventListener, - AdaptiveMediaSourceEventListener, + MediaSourceEventListener, AdsMediaSource.EventListener, DefaultDrmSessionManager.EventListener { @@ -323,12 +323,19 @@ import java.util.Locale; Log.d(TAG, "drmKeysLoaded [" + getSessionTimeString() + "]"); } - // AdaptiveMediaSourceEventListener + // MediaSourceEventListener @Override - public void onLoadStarted(DataSpec dataSpec, int dataType, int trackType, Format trackFormat, - int trackSelectionReason, Object trackSelectionData, long mediaStartTimeMs, - long mediaEndTimeMs, long elapsedRealtimeMs) { + public void onLoadStarted( + DataSpec dataSpec, + int dataType, + int trackType, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + long mediaStartTimeMs, + long mediaEndTimeMs, + long elapsedRealtimeMs) { // Do nothing. } @@ -369,7 +376,7 @@ import java.util.Locale; @Override public void onAdLoadError(IOException error) { - printInternalError("loadError", error); + printInternalError("adLoadError", error); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/AdaptiveMediaSourceEventListener.java b/library/core/src/main/java/com/google/android/exoplayer2/source/AdaptiveMediaSourceEventListener.java index 2bc9d48726..ccc3beac55 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/AdaptiveMediaSourceEventListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/AdaptiveMediaSourceEventListener.java @@ -15,42 +15,10 @@ */ package com.google.android.exoplayer2.source; -import android.os.Handler; -import android.support.annotation.Nullable; - /** * Interface for callbacks to be notified of {@link MediaSource} events. * - * @deprecated Use {@link MediaSourceEventListener} + * @deprecated Use {@link MediaSourceEventListener}. */ @Deprecated -public interface AdaptiveMediaSourceEventListener extends MediaSourceEventListener { - - /** Dispatches events to a {@link MediaSourceEventListener}. */ - final class EventDispatcher extends MediaSourceEventListener.EventDispatcher { - - private final Handler handler; - private final MediaSourceEventListener listener; - - public EventDispatcher(@Nullable Handler handler, @Nullable MediaSourceEventListener listener) { - this(handler, listener, 0); - } - - public EventDispatcher( - @Nullable Handler handler, - @Nullable MediaSourceEventListener listener, - long mediaTimeOffsetMs) { - super(handler, listener, mediaTimeOffsetMs); - this.handler = handler; - this.listener = listener; - } - - public AdaptiveMediaSourceEventListener.EventDispatcher copyWithMediaTimeOffsetMs( - long mediaTimeOffsetMs) { - return new AdaptiveMediaSourceEventListener.EventDispatcher( - handler, listener, mediaTimeOffsetMs); - } - - } - -} +public interface AdaptiveMediaSourceEventListener extends MediaSourceEventListener {} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java index 3ab2609c0e..247eacd519 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java @@ -25,7 +25,7 @@ import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorsFactory; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java index 82e8781d70..4d500f94bd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java @@ -211,7 +211,7 @@ public interface MediaSourceEventListener { long mediaTimeMs); /** Dispatches events to a {@link MediaSourceEventListener}. */ - class EventDispatcher { + final class EventDispatcher { @Nullable private final Handler handler; @Nullable private final MediaSourceEventListener listener; @@ -230,6 +230,10 @@ public interface MediaSourceEventListener { this.mediaTimeOffsetMs = mediaTimeOffsetMs; } + public EventDispatcher copyWithMediaTimeOffsetMs(long mediaTimeOffsetMs) { + return new EventDispatcher(handler, listener, mediaTimeOffsetMs); + } + public void loadStarted(DataSpec dataSpec, int dataType, long elapsedRealtimeMs) { loadStarted( dataSpec, diff --git a/library/core/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 index bb51ae074e..fa95269690 100644 --- a/library/core/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 @@ -16,12 +16,11 @@ package com.google.android.exoplayer2.source.chunk; import android.util.Log; - import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SampleQueue; import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.source.SequenceableLoader; diff --git a/library/dash/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 index 35f3c2e129..3c401624db 100644 --- a/library/dash/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 @@ -19,10 +19,10 @@ import android.util.Pair; import android.util.SparseIntArray; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.CompositeSequenceableLoader; import com.google.android.exoplayer2.source.EmptySampleStream; import com.google.android.exoplayer2.source.MediaPeriod; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.source.SequenceableLoader; import com.google.android.exoplayer2.source.TrackGroup; diff --git a/library/dash/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 index c5fbafb84e..498c7c6de3 100644 --- a/library/dash/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 @@ -27,9 +27,9 @@ import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; 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.UtcTimingElement; diff --git a/library/hls/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 index ea9e52e62e..bd73ad27f9 100644 --- a/library/hls/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 @@ -19,9 +19,9 @@ import android.os.Handler; import android.text.TextUtils; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.CompositeSequenceableLoader; import com.google.android.exoplayer2.source.MediaPeriod; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; diff --git a/library/hls/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 index 3f28981f0e..563f4da059 100644 --- a/library/hls/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 @@ -22,9 +22,9 @@ import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SinglePeriodTimeline; import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylist; diff --git a/library/hls/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 index ddd6689fa6..beaa84556b 100644 --- a/library/hls/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 @@ -23,7 +23,7 @@ import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.SeekMap; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SampleQueue; import com.google.android.exoplayer2.source.SampleQueue.UpstreamFormatChangedListener; import com.google.android.exoplayer2.source.SampleStream; diff --git a/library/hls/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 index 355a8575ca..0677ff7ca0 100644 --- a/library/hls/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 @@ -20,7 +20,7 @@ import android.os.Handler; import android.os.SystemClock; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ParserException; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.chunk.ChunkedTrackBlacklistUtil; import com.google.android.exoplayer2.source.hls.HlsDataSourceFactory; import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl; diff --git a/library/smoothstreaming/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 index 1cc2a6833d..9b664d8a61 100644 --- a/library/smoothstreaming/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 @@ -18,9 +18,9 @@ package com.google.android.exoplayer2.source.smoothstreaming; import android.util.Base64; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.extractor.mp4.TrackEncryptionBox; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.CompositeSequenceableLoader; import com.google.android.exoplayer2.source.MediaPeriod; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.source.SequenceableLoader; import com.google.android.exoplayer2.source.TrackGroup; diff --git a/library/smoothstreaming/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 index 5a93847428..5a26585874 100644 --- a/library/smoothstreaming/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 @@ -25,9 +25,9 @@ import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SinglePeriodTimeline; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest.StreamElement; diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java index 3dcf551943..1b2e1af9b7 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java @@ -15,9 +15,9 @@ */ package com.google.android.exoplayer2.testutil; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.CompositeSequenceableLoader; import com.google.android.exoplayer2.source.MediaPeriod; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.source.SequenceableLoader; import com.google.android.exoplayer2.source.TrackGroupArray; diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaSource.java index 59bcaf3e7c..fbb2a83027 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaSource.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaSource.java @@ -18,9 +18,9 @@ package com.google.android.exoplayer2.testutil; import android.os.Handler; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline.Period; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSourceEventListener; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.upstream.Allocator; @@ -33,9 +33,13 @@ public class FakeAdaptiveMediaSource extends FakeMediaSource { private final EventDispatcher eventDispatcher; private final FakeChunkSource.Factory chunkSourceFactory; - public FakeAdaptiveMediaSource(Timeline timeline, Object manifest, - TrackGroupArray trackGroupArray, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener, FakeChunkSource.Factory chunkSourceFactory) { + public FakeAdaptiveMediaSource( + Timeline timeline, + Object manifest, + TrackGroupArray trackGroupArray, + Handler eventHandler, + MediaSourceEventListener eventListener, + FakeChunkSource.Factory chunkSourceFactory) { super(timeline, manifest, trackGroupArray); this.eventDispatcher = new EventDispatcher(eventHandler, eventListener); this.chunkSourceFactory = chunkSourceFactory; From 10b24be6f089b77713e5f1b1fc1c431aa2b83599 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 12 Dec 2017 21:40:58 +0000 Subject: [PATCH 0919/2472] Fix build --- .../source/DeferredMediaPeriod.java | 4 +-- .../source/dash/DashMediaSource.java | 23 +++++++--------- .../exoplayer2/source/hls/HlsMediaSource.java | 26 +++++++++---------- .../source/smoothstreaming/SsMediaSource.java | 26 ++++++++----------- 4 files changed, 34 insertions(+), 45 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java index bc29b2fdf1..f93d30cb04 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DeferredMediaPeriod.java @@ -95,8 +95,8 @@ public final class DeferredMediaPeriod implements MediaPeriod, MediaPeriod.Callb } @Override - public void discardBuffer(long positionUs, boolean toKeyframe) { - mediaPeriod.discardBuffer(positionUs, toKeyframe); + public void discardBuffer(long positionUs) { + mediaPeriod.discardBuffer(positionUs); } @Override diff --git a/library/dash/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 index 498c7c6de3..548811cf92 100644 --- a/library/dash/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 @@ -26,9 +26,9 @@ import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.dash.manifest.DashManifest; import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser; @@ -69,7 +69,7 @@ public final class DashMediaSource implements MediaSource { private final DashChunkSource.Factory chunkSourceFactory; private ParsingLoadable.Parser manifestParser; - private AdaptiveMediaSourceEventListener eventListener; + private MediaSourceEventListener eventListener; private Handler eventHandler; private int minLoadableRetryCount; @@ -151,8 +151,7 @@ public final class DashMediaSource implements MediaSource { * @param eventListener A listener of events. * @return This builder. */ - public Builder setEventListener(Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { + public Builder setEventListener(Handler eventHandler, MediaSourceEventListener eventListener) { this.eventHandler = eventHandler; this.eventListener = eventListener; return this; @@ -261,7 +260,7 @@ public final class DashMediaSource implements MediaSource { */ @Deprecated public DashMediaSource(DashManifest manifest, DashChunkSource.Factory chunkSourceFactory, - Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { + Handler eventHandler, MediaSourceEventListener eventListener) { this(manifest, chunkSourceFactory, DEFAULT_MIN_LOADABLE_RETRY_COUNT, eventHandler, eventListener); } @@ -278,8 +277,7 @@ public final class DashMediaSource implements MediaSource { */ @Deprecated public DashMediaSource(DashManifest manifest, DashChunkSource.Factory chunkSourceFactory, - int minLoadableRetryCount, Handler eventHandler, AdaptiveMediaSourceEventListener - eventListener) { + int minLoadableRetryCount, Handler eventHandler, MediaSourceEventListener eventListener) { this(manifest, null, null, null, chunkSourceFactory, minLoadableRetryCount, DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS, eventHandler, eventListener); } @@ -299,7 +297,7 @@ public final class DashMediaSource implements MediaSource { @Deprecated public DashMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, DashChunkSource.Factory chunkSourceFactory, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { + MediaSourceEventListener eventListener) { this(manifestUri, manifestDataSourceFactory, chunkSourceFactory, DEFAULT_MIN_LOADABLE_RETRY_COUNT, DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS, eventHandler, eventListener); @@ -325,8 +323,7 @@ public final class DashMediaSource implements MediaSource { @Deprecated public DashMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, DashChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, - long livePresentationDelayMs, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { + long livePresentationDelayMs, Handler eventHandler, MediaSourceEventListener eventListener) { this(manifestUri, manifestDataSourceFactory, new DashManifestParser(), chunkSourceFactory, minLoadableRetryCount, livePresentationDelayMs, eventHandler, eventListener); } @@ -353,8 +350,7 @@ public final class DashMediaSource implements MediaSource { public DashMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, ParsingLoadable.Parser manifestParser, DashChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, - long livePresentationDelayMs, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { + long livePresentationDelayMs, Handler eventHandler, MediaSourceEventListener eventListener) { this(null, manifestUri, manifestDataSourceFactory, manifestParser, chunkSourceFactory, minLoadableRetryCount, livePresentationDelayMs, eventHandler, eventListener); } @@ -363,8 +359,7 @@ public final class DashMediaSource implements MediaSource { DataSource.Factory manifestDataSourceFactory, ParsingLoadable.Parser manifestParser, DashChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, - long livePresentationDelayMs, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { + long livePresentationDelayMs, Handler eventHandler, MediaSourceEventListener eventListener) { this.manifest = manifest; this.manifestUri = manifestUri; this.manifestDataSourceFactory = manifestDataSourceFactory; diff --git a/library/hls/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 index 563f4da059..4e904032fd 100644 --- a/library/hls/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 @@ -21,9 +21,9 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.extractor.Extractor; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SinglePeriodTimeline; import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; @@ -57,7 +57,7 @@ public final class HlsMediaSource implements MediaSource, private HlsExtractorFactory extractorFactory; private ParsingLoadable.Parser playlistParser; - private AdaptiveMediaSourceEventListener eventListener; + private MediaSourceEventListener eventListener; private Handler eventHandler; private int minLoadableRetryCount; private boolean isBuildCalled; @@ -132,7 +132,7 @@ public final class HlsMediaSource implements MediaSource, * @return This builder. */ public Builder setEventListener(Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { + MediaSourceEventListener eventListener) { this.eventHandler = eventHandler; this.eventListener = eventListener; return this; @@ -193,13 +193,13 @@ public final class HlsMediaSource implements MediaSource, * @param dataSourceFactory An {@link HlsDataSourceFactory} for {@link DataSource}s for manifests, * segments and keys. * @param eventHandler A handler for events. May be null if delivery of events is not required. - * @param eventListener An {@link AdaptiveMediaSourceEventListener}. May be null if delivery of - * events is not required. + * @param eventListener An {@link MediaSourceEventListener}. May be null if delivery of events is + * not required. * @deprecated Use {@link Builder} instead. */ @Deprecated public HlsMediaSource(Uri manifestUri, DataSource.Factory dataSourceFactory, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { + MediaSourceEventListener eventListener) { this(manifestUri, dataSourceFactory, DEFAULT_MIN_LOADABLE_RETRY_COUNT, eventHandler, eventListener); } @@ -211,14 +211,13 @@ public final class HlsMediaSource implements MediaSource, * @param minLoadableRetryCount The minimum number of times loads must be retried before * errors are propagated. * @param eventHandler A handler for events. May be null if delivery of events is not required. - * @param eventListener An {@link AdaptiveMediaSourceEventListener}. May be null if delivery of - * events is not required. + * @param eventListener An {@link MediaSourceEventListener}. May be null if delivery of events is + * not required. * @deprecated Use {@link Builder} instead. */ @Deprecated public HlsMediaSource(Uri manifestUri, DataSource.Factory dataSourceFactory, - int minLoadableRetryCount, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { + int minLoadableRetryCount, Handler eventHandler, MediaSourceEventListener eventListener) { this(manifestUri, new DefaultHlsDataSourceFactory(dataSourceFactory), HlsExtractorFactory.DEFAULT, minLoadableRetryCount, eventHandler, eventListener, new HlsPlaylistParser()); @@ -232,16 +231,15 @@ public final class HlsMediaSource implements MediaSource, * @param minLoadableRetryCount The minimum number of times loads must be retried before * errors are propagated. * @param eventHandler A handler for events. May be null if delivery of events is not required. - * @param eventListener An {@link AdaptiveMediaSourceEventListener}. May be null if delivery of - * events is not required. + * @param eventListener An {@link MediaSourceEventListener}. May be null if delivery of events is + * not required. * @param playlistParser A {@link ParsingLoadable.Parser} for HLS playlists. * @deprecated Use {@link Builder} instead. */ @Deprecated public HlsMediaSource(Uri manifestUri, HlsDataSourceFactory dataSourceFactory, HlsExtractorFactory extractorFactory, int minLoadableRetryCount, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener, - ParsingLoadable.Parser playlistParser) { + MediaSourceEventListener eventListener, ParsingLoadable.Parser playlistParser) { this.manifestUri = manifestUri; this.dataSourceFactory = dataSourceFactory; this.extractorFactory = extractorFactory; diff --git a/library/smoothstreaming/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 index 5a26585874..34343e08e3 100644 --- a/library/smoothstreaming/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 @@ -24,9 +24,9 @@ import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SinglePeriodTimeline; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest; @@ -63,7 +63,7 @@ public final class SsMediaSource implements MediaSource, private final SsChunkSource.Factory chunkSourceFactory; private ParsingLoadable.Parser manifestParser; - private AdaptiveMediaSourceEventListener eventListener; + private MediaSourceEventListener eventListener; private Handler eventHandler; private int minLoadableRetryCount; @@ -143,8 +143,7 @@ public final class SsMediaSource implements MediaSource, * @param eventListener A listener of events. * @return This builder. */ - public Builder setEventListener(Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { + public Builder setEventListener(Handler eventHandler, MediaSourceEventListener eventListener) { this.eventHandler = eventHandler; this.eventListener = eventListener; return this; @@ -233,9 +232,9 @@ public final class SsMediaSource implements MediaSource, */ @Deprecated public SsMediaSource(SsManifest manifest, SsChunkSource.Factory chunkSourceFactory, - Handler eventHandler, AdaptiveMediaSourceEventListener eventListener) { - this(manifest, chunkSourceFactory, DEFAULT_MIN_LOADABLE_RETRY_COUNT, - eventHandler, eventListener); + Handler eventHandler, MediaSourceEventListener eventListener) { + this(manifest, chunkSourceFactory, DEFAULT_MIN_LOADABLE_RETRY_COUNT, eventHandler, + eventListener); } /** @@ -250,8 +249,7 @@ public final class SsMediaSource implements MediaSource, */ @Deprecated public SsMediaSource(SsManifest manifest, SsChunkSource.Factory chunkSourceFactory, - int minLoadableRetryCount, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { + int minLoadableRetryCount, Handler eventHandler, MediaSourceEventListener eventListener) { this(manifest, null, null, null, chunkSourceFactory, minLoadableRetryCount, DEFAULT_LIVE_PRESENTATION_DELAY_MS, eventHandler, eventListener); } @@ -271,7 +269,7 @@ public final class SsMediaSource implements MediaSource, @Deprecated public SsMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, SsChunkSource.Factory chunkSourceFactory, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { + MediaSourceEventListener eventListener) { this(manifestUri, manifestDataSourceFactory, chunkSourceFactory, DEFAULT_MIN_LOADABLE_RETRY_COUNT, DEFAULT_LIVE_PRESENTATION_DELAY_MS, eventHandler, eventListener); @@ -295,8 +293,7 @@ public final class SsMediaSource implements MediaSource, @Deprecated public SsMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, SsChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, - long livePresentationDelayMs, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { + long livePresentationDelayMs, Handler eventHandler, MediaSourceEventListener eventListener) { this(manifestUri, manifestDataSourceFactory, new SsManifestParser(), chunkSourceFactory, minLoadableRetryCount, livePresentationDelayMs, eventHandler, eventListener); } @@ -321,8 +318,7 @@ public final class SsMediaSource implements MediaSource, public SsMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, ParsingLoadable.Parser manifestParser, SsChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, - long livePresentationDelayMs, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { + long livePresentationDelayMs, Handler eventHandler, MediaSourceEventListener eventListener) { this(null, manifestUri, manifestDataSourceFactory, manifestParser, chunkSourceFactory, minLoadableRetryCount, livePresentationDelayMs, eventHandler, eventListener); } @@ -332,7 +328,7 @@ public final class SsMediaSource implements MediaSource, ParsingLoadable.Parser manifestParser, SsChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, long livePresentationDelayMs, Handler eventHandler, - AdaptiveMediaSourceEventListener eventListener) { + MediaSourceEventListener eventListener) { Assertions.checkState(manifest == null || !manifest.isLive); this.manifest = manifest; this.manifestUri = manifestUri == null ? null From a8298b4c563b7df7d80125882dd358a73d7a688d Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 4 Dec 2017 06:08:33 -0800 Subject: [PATCH 0920/2472] Tentative fix for roll-up row count Issue: #3513 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177804505 --- RELEASENOTES.md | 2 + .../exoplayer2/text/cea/Cea608Decoder.java | 40 +++++++++++++------ 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 3a73a9e716..51ac077cef 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -15,6 +15,8 @@ seek. * Use the same listener `MediaSourceEventListener` for all MediaSource implementations. +* CEA-608: Fix handling of row count changes in roll-up mode + ([#3513](https://github.com/google/ExoPlayer/issues/3513)). ### 2.6.0 ### diff --git a/library/core/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 index e2c592be6b..0483f909b3 100644 --- a/library/core/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 @@ -33,7 +33,6 @@ import com.google.android.exoplayer2.text.SubtitleInputBuffer; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.ParsableByteArray; import java.util.ArrayList; -import java.util.LinkedList; import java.util.List; /** @@ -185,7 +184,7 @@ public final class Cea608Decoder extends CeaDecoder { private final ParsableByteArray ccData; private final int packetLength; private final int selectedField; - private final LinkedList cueBuilders; + private final ArrayList cueBuilders; private CueBuilder currentCueBuilder; private List cues; @@ -200,7 +199,7 @@ public final class Cea608Decoder extends CeaDecoder { public Cea608Decoder(String mimeType, int accessibilityChannel) { ccData = new ParsableByteArray(); - cueBuilders = new LinkedList<>(); + cueBuilders = new ArrayList<>(); currentCueBuilder = new CueBuilder(CC_MODE_UNKNOWN, DEFAULT_CAPTIONS_ROW_COUNT); packetLength = MimeTypes.APPLICATION_MP4CEA608.equals(mimeType) ? 2 : 3; switch (accessibilityChannel) { @@ -230,8 +229,8 @@ public final class Cea608Decoder extends CeaDecoder { cues = null; lastCues = null; setCaptionMode(CC_MODE_UNKNOWN); + setCaptionRowCount(DEFAULT_CAPTIONS_ROW_COUNT); resetCueBuilders(); - captionRowCount = DEFAULT_CAPTIONS_ROW_COUNT; repeatableControlSet = false; repeatableControlCc1 = 0; repeatableControlCc2 = 0; @@ -434,16 +433,16 @@ public final class Cea608Decoder extends CeaDecoder { private void handleMiscCode(byte cc2) { switch (cc2) { case CTRL_ROLL_UP_CAPTIONS_2_ROWS: - captionRowCount = 2; setCaptionMode(CC_MODE_ROLL_UP); + setCaptionRowCount(2); return; case CTRL_ROLL_UP_CAPTIONS_3_ROWS: - captionRowCount = 3; setCaptionMode(CC_MODE_ROLL_UP); + setCaptionRowCount(3); return; case CTRL_ROLL_UP_CAPTIONS_4_ROWS: - captionRowCount = 4; setCaptionMode(CC_MODE_ROLL_UP); + setCaptionRowCount(4); return; case CTRL_RESUME_CAPTION_LOADING: setCaptionMode(CC_MODE_POP_ON); @@ -451,6 +450,9 @@ public final class Cea608Decoder extends CeaDecoder { case CTRL_RESUME_DIRECT_CAPTIONING: setCaptionMode(CC_MODE_PAINT_ON); return; + default: + // Fall through. + break; } if (captionMode == CC_MODE_UNKNOWN) { @@ -484,6 +486,9 @@ public final class Cea608Decoder extends CeaDecoder { case CTRL_DELETE_TO_END_OF_ROW: // TODO: implement break; + default: + // Fall through. + break; } } @@ -515,8 +520,13 @@ public final class Cea608Decoder extends CeaDecoder { } } + private void setCaptionRowCount(int captionRowCount) { + this.captionRowCount = captionRowCount; + currentCueBuilder.setCaptionRowCount(captionRowCount); + } + private void resetCueBuilders() { - currentCueBuilder.reset(captionMode, captionRowCount); + currentCueBuilder.reset(captionMode); cueBuilders.clear(); cueBuilders.add(currentCueBuilder); } @@ -594,12 +604,14 @@ public final class Cea608Decoder extends CeaDecoder { public CueBuilder(int captionMode, int captionRowCount) { preambleStyles = new ArrayList<>(); midrowStyles = new ArrayList<>(); - rolledUpCaptions = new LinkedList<>(); + rolledUpCaptions = new ArrayList<>(); captionStringBuilder = new SpannableStringBuilder(); - reset(captionMode, captionRowCount); + reset(captionMode); + setCaptionRowCount(captionRowCount); } - public void reset(int captionMode, int captionRowCount) { + public void reset(int captionMode) { + this.captionMode = captionMode; preambleStyles.clear(); midrowStyles.clear(); rolledUpCaptions.clear(); @@ -607,11 +619,13 @@ public final class Cea608Decoder extends CeaDecoder { row = BASE_ROW; indent = 0; tabOffset = 0; - this.captionMode = captionMode; - this.captionRowCount = captionRowCount; underlineStartPosition = POSITION_UNSET; } + public void setCaptionRowCount(int captionRowCount) { + this.captionRowCount = captionRowCount; + } + public boolean isEmpty() { return preambleStyles.isEmpty() && midrowStyles.isEmpty() && rolledUpCaptions.isEmpty() && captionStringBuilder.length() == 0; From 29f6351b192b192618bff721f63fc8c14669ea9a Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 4 Dec 2017 06:56:04 -0800 Subject: [PATCH 0921/2472] Support timezone offsets in ISO8601 timestamps Issue: #3524 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177808106 --- RELEASENOTES.md | 3 + .../source/dash/DashMediaSource.java | 56 ++++++++++--------- 2 files changed, 33 insertions(+), 26 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 51ac077cef..a55044ad7e 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -4,6 +4,9 @@ * Add Builder to ExtractorMediaSource, HlsMediaSource, SsMediaSource, DashMediaSource, SingleSampleMediaSource. +* DASH: + * Support time zone designators in ISO8601 UTCTiming elements + ([#3524](https://github.com/google/ExoPlayer/issues/3524)). * Support 32-bit PCM float output from `DefaultAudioSink`, and add an option to use this with `FfmpegAudioRenderer`. * Support extraction and decoding of Dolby Atmos diff --git a/library/dash/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 index 548811cf92..22bc2b08ec 100644 --- a/library/dash/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 @@ -19,6 +19,7 @@ import android.net.Uri; import android.os.Handler; import android.os.SystemClock; import android.support.annotation.Nullable; +import android.text.TextUtils; import android.util.Log; import android.util.SparseArray; import com.google.android.exoplayer2.C; @@ -48,6 +49,8 @@ import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Locale; import java.util.TimeZone; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * A DASH {@link MediaSource}. @@ -926,41 +929,42 @@ public final class DashMediaSource implements MediaSource { } - private static final class Iso8601Parser implements ParsingLoadable.Parser { + /* package */ static final class Iso8601Parser implements ParsingLoadable.Parser { - private static final String ISO_8601_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'"; - private static final String ISO_8601_WITH_OFFSET_FORMAT = "yyyy-MM-dd'T'HH:mm:ssZ"; - private static final String ISO_8601_WITH_OFFSET_FORMAT_REGEX_PATTERN = ".*[+\\-]\\d{2}:\\d{2}$"; - private static final String ISO_8601_WITH_OFFSET_FORMAT_REGEX_PATTERN_2 = ".*[+\\-]\\d{4}$"; + private static final Pattern TIMESTAMP_WITH_TIMEZONE_PATTERN = + Pattern.compile("(.+?)(Z|((\\+|-|−)(\\d\\d)(:?(\\d\\d))?))"); @Override public Long parse(Uri uri, InputStream inputStream) throws IOException { String firstLine = new BufferedReader(new InputStreamReader(inputStream)).readLine(); - - if (firstLine != null) { - //determine format pattern - String formatPattern; - if (firstLine.matches(ISO_8601_WITH_OFFSET_FORMAT_REGEX_PATTERN)) { - formatPattern = ISO_8601_WITH_OFFSET_FORMAT; - } else if (firstLine.matches(ISO_8601_WITH_OFFSET_FORMAT_REGEX_PATTERN_2)) { - formatPattern = ISO_8601_WITH_OFFSET_FORMAT; + try { + Matcher matcher = TIMESTAMP_WITH_TIMEZONE_PATTERN.matcher(firstLine); + if (!matcher.matches()) { + throw new ParserException("Couldn't parse timestamp: " + firstLine); + } + // Parse the timestamp. + String timestampWithoutTimezone = matcher.group(1); + SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US); + format.setTimeZone(TimeZone.getTimeZone("UTC")); + long timestampMs = format.parse(timestampWithoutTimezone).getTime(); + // Parse the timezone. + String timezone = matcher.group(2); + if ("Z".equals(timezone)) { + // UTC (no offset). } else { - formatPattern = ISO_8601_FORMAT; + long sign = "+".equals(matcher.group(4)) ? 1 : -1; + long hours = Long.parseLong(matcher.group(5)); + String minutesString = matcher.group(7); + long minutes = TextUtils.isEmpty(minutesString) ? 0 : Long.parseLong(minutesString); + long timestampOffsetMs = sign * (((hours * 60) + minutes) * 60 * 1000); + timestampMs -= timestampOffsetMs; } - //parse - try { - SimpleDateFormat format = new SimpleDateFormat(formatPattern, Locale.US); - format.setTimeZone(TimeZone.getTimeZone("UTC")); - return format.parse(firstLine).getTime(); - } catch (ParseException e) { - throw new ParserException(e); - } - - } else { - throw new ParserException("Unable to parse ISO 8601. Input value is null"); + return timestampMs; + } catch (ParseException e) { + throw new ParserException(e); } } } - + } From bc7bfb4e7cccdbdb203e3e3af1a07c32e3b47d51 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 4 Dec 2017 07:29:56 -0800 Subject: [PATCH 0922/2472] Fix setting supported ad MIME types without preloading ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177810991 --- .../exoplayer2/ext/ima/ImaAdsLoader.java | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index cf8b8a3f6d..00bf0bd493 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -315,20 +315,13 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A this.adsManager = adsManager; adsManager.addAdErrorListener(this); adsManager.addAdEventListener(this); - if (ENABLE_PRELOADING) { - ImaSdkFactory imaSdkFactory = ImaSdkFactory.getInstance(); - AdsRenderingSettings adsRenderingSettings = imaSdkFactory.createAdsRenderingSettings(); - adsRenderingSettings.setEnablePreloading(true); - adsRenderingSettings.setMimeTypes(supportedMimeTypes); - adsManager.init(adsRenderingSettings); - if (DEBUG) { - Log.d(TAG, "Initialized with preloading"); - } - } else { - adsManager.init(); - if (DEBUG) { - Log.d(TAG, "Initialized without preloading"); - } + ImaSdkFactory imaSdkFactory = ImaSdkFactory.getInstance(); + AdsRenderingSettings adsRenderingSettings = imaSdkFactory.createAdsRenderingSettings(); + adsRenderingSettings.setEnablePreloading(ENABLE_PRELOADING); + adsRenderingSettings.setMimeTypes(supportedMimeTypes); + adsManager.init(adsRenderingSettings); + if (DEBUG) { + Log.d(TAG, "Initialized with ads rendering settings: " + adsRenderingSettings); } long[] adGroupTimesUs = getAdGroupTimesUs(adsManager.getAdCuePoints()); adPlaybackState = new AdPlaybackState(adGroupTimesUs); From 13a7037c82a91290757862e5704920e6f89d6392 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 4 Dec 2017 07:35:50 -0800 Subject: [PATCH 0923/2472] Fix playback of FLV live streams with no audio track Issue: #3188 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177811487 --- RELEASENOTES.md | 2 ++ .../exoplayer2/extractor/flv/FlvExtractor.java | 17 ++++++++++++----- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index a55044ad7e..ff0af256cf 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -16,6 +16,8 @@ ([#2980](https://github.com/google/ExoPlayer/issues/2980)). * Fix handling of playback parameters changes while paused when followed by a seek. +* Fix playback of live FLV streams that do not contain an audio track + ([#3188](https://github.com/google/ExoPlayer/issues/3188)). * Use the same listener `MediaSourceEventListener` for all MediaSource implementations. * CEA-608: Fix handling of row count changes in roll-up mode diff --git a/library/core/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 index 2da075ff53..d908f28945 100644 --- a/library/core/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 @@ -78,6 +78,7 @@ public final class FlvExtractor implements Extractor { private ExtractorOutput extractorOutput; private @States int state; + private long mediaTagTimestampOffsetUs; private int bytesToNextTagHeader; private int tagType; private int tagDataSize; @@ -93,6 +94,7 @@ public final class FlvExtractor implements Extractor { tagData = new ParsableByteArray(); metadataReader = new ScriptTagPayloadReader(); state = STATE_READING_FLV_HEADER; + mediaTagTimestampOffsetUs = C.TIME_UNSET; } @Override @@ -134,6 +136,7 @@ public final class FlvExtractor implements Extractor { @Override public void seek(long position, long timeUs) { state = STATE_READING_FLV_HEADER; + mediaTagTimestampOffsetUs = C.TIME_UNSET; bytesToNextTagHeader = 0; } @@ -255,11 +258,11 @@ public final class FlvExtractor implements Extractor { private boolean readTagData(ExtractorInput input) throws IOException, InterruptedException { boolean wasConsumed = true; if (tagType == TAG_TYPE_AUDIO && audioReader != null) { - ensureOutputSeekMap(); - audioReader.consume(prepareTagData(input), tagTimestampUs); + ensureReadyForMediaOutput(); + audioReader.consume(prepareTagData(input), mediaTagTimestampOffsetUs + tagTimestampUs); } else if (tagType == TAG_TYPE_VIDEO && videoReader != null) { - ensureOutputSeekMap(); - videoReader.consume(prepareTagData(input), tagTimestampUs); + ensureReadyForMediaOutput(); + videoReader.consume(prepareTagData(input), mediaTagTimestampOffsetUs + tagTimestampUs); } else if (tagType == TAG_TYPE_SCRIPT_DATA && !outputSeekMap) { metadataReader.consume(prepareTagData(input), tagTimestampUs); long durationUs = metadataReader.getDurationUs(); @@ -288,11 +291,15 @@ public final class FlvExtractor implements Extractor { return tagData; } - private void ensureOutputSeekMap() { + private void ensureReadyForMediaOutput() { if (!outputSeekMap) { extractorOutput.seekMap(new SeekMap.Unseekable(C.TIME_UNSET)); outputSeekMap = true; } + if (mediaTagTimestampOffsetUs == C.TIME_UNSET) { + mediaTagTimestampOffsetUs = + metadataReader.getDurationUs() == C.TIME_UNSET ? -tagTimestampUs : 0; + } } } From aef9063e946a0ade455b90aa783220a18cc8c47d Mon Sep 17 00:00:00 2001 From: amesbah Date: Mon, 4 Dec 2017 10:50:51 -0800 Subject: [PATCH 0924/2472] Add @SuppressWarnings("ComparableType") for instances of a class implementing 'Comparable' where T is not compatible with the type of the class. In order to facilitate enabling a compile-time error check, we are suppressing these existing instances. Once the compile-time error is enabled, we will file bugs to clean up any unfixed instances in []. Note that this CL should result in no effective changes to the code, but the code as currently-written might contain a real bug. If you'd prefer to fix the bug now, please either reply with edits, or accept this CL then follow up with a change that fixes the underlying issue. Tested: tap_presubmit: [] Some tests failed; test failures are believed to be unrelated to this CL ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177836122 --- .../exoplayer2/source/hls/playlist/HlsMediaPlaylist.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/library/hls/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 index b21ecb02d5..1f44607f98 100644 --- a/library/hls/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 @@ -29,9 +29,8 @@ import java.util.List; */ public final class HlsMediaPlaylist extends HlsPlaylist { - /** - * Media segment reference. - */ + /** Media segment reference. */ + @SuppressWarnings("ComparableType") public static final class Segment implements Comparable { /** From 82122b9f3bf994ab326be6dd026b6b54f323c527 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 5 Dec 2017 00:29:56 -0800 Subject: [PATCH 0925/2472] Make one ad request in ImaAdsLoader This fixes an issue where quickly detaching and reattaching the player might cause ads to be requested multiple times with both responses handled. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177922167 --- .../exoplayer2/ext/ima/ImaAdsLoader.java | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index 00bf0bd493..92ca24c889 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -50,6 +50,7 @@ import com.google.android.exoplayer2.source.ads.AdPlaybackState; import com.google.android.exoplayer2.source.ads.AdsLoader; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -120,6 +121,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A private final AdDisplayContainer adDisplayContainer; private final com.google.ads.interactivemedia.v3.api.AdsLoader adsLoader; + private Object pendingAdRequestContext; private List supportedMimeTypes; private EventListener eventListener; private Player player; @@ -183,10 +185,6 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A * Whether {@link #getContentProgress()} has sent {@link #pendingContentPositionMs} to IMA. */ private boolean sentPendingContentPositionMs; - /** - * Whether {@link #release()} has been called. - */ - private boolean released; /** * Creates a new IMA ads loader. @@ -296,7 +294,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A @Override public void release() { - released = true; + pendingAdRequestContext = null; if (adsManager != null) { adsManager.destroy(); adsManager = null; @@ -308,10 +306,11 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A @Override public void onAdsManagerLoaded(AdsManagerLoadedEvent adsManagerLoadedEvent) { AdsManager adsManager = adsManagerLoadedEvent.getAdsManager(); - if (released) { + if (!Util.areEqual(pendingAdRequestContext, adsManagerLoadedEvent.getUserRequestContext())) { adsManager.destroy(); return; } + pendingAdRequestContext = null; this.adsManager = adsManager; adsManager.addAdErrorListener(this); adsManager.addAdEventListener(this); @@ -403,6 +402,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A Log.d(TAG, "onAdError " + adErrorEvent); } if (adsManager == null) { + pendingAdRequestContext = null; adPlaybackState = new AdPlaybackState(new long[0]); updateAdPlaybackState(); } @@ -622,10 +622,16 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A // Internal methods. private void requestAds() { + if (pendingAdRequestContext != null) { + // Ad request already in flight. + return; + } + pendingAdRequestContext = new Object(); AdsRequest request = imaSdkFactory.createAdsRequest(); request.setAdTagUrl(adTagUri.toString()); request.setAdDisplayContainer(adDisplayContainer); request.setContentProgressProvider(this); + request.setUserRequestContext(pendingAdRequestContext); adsLoader.requestAds(request); } From 3c0bb7263b73c0472a5274f545b9be8b4f086cca Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 5 Dec 2017 03:53:01 -0800 Subject: [PATCH 0926/2472] Invoke onLoadCanceled/Completed for ExtractorMediaSource ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177936271 --- .../source/ExtractorMediaPeriod.java | 30 +++++++++++++++++-- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java index e3c9012dbc..f557d4ac97 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -405,14 +405,26 @@ import java.util.Arrays; @Override public void onLoadCompleted(ExtractingLoadable loadable, long elapsedRealtimeMs, long loadDurationMs) { - copyLengthFromLoader(loadable); - loadingFinished = true; if (durationUs == C.TIME_UNSET) { long largestQueuedTimestampUs = getLargestQueuedTimestampUs(); durationUs = largestQueuedTimestampUs == Long.MIN_VALUE ? 0 : largestQueuedTimestampUs + DEFAULT_LAST_SAMPLE_DURATION_US; listener.onSourceInfoRefreshed(durationUs, seekMap.isSeekable()); } + eventDispatcher.loadCompleted( + loadable.dataSpec, + C.DATA_TYPE_MEDIA, + C.TRACK_TYPE_UNKNOWN, + /* trackFormat= */ null, + C.SELECTION_REASON_UNKNOWN, + /* trackSelectionData= */ null, + /* mediaStartTimeUs= */ 0, + durationUs, + elapsedRealtimeMs, + loadDurationMs, + loadable.bytesLoaded); + copyLengthFromLoader(loadable); + loadingFinished = true; callback.onContinueLoadingRequested(this); } @@ -422,6 +434,18 @@ import java.util.Arrays; if (released) { return; } + eventDispatcher.loadCanceled( + loadable.dataSpec, + C.DATA_TYPE_MEDIA, + C.TRACK_TYPE_UNKNOWN, + /* trackFormat= */ null, + C.SELECTION_REASON_UNKNOWN, + /* trackSelectionData= */ null, + /* mediaStartTimeUs= */ 0, + durationUs, + elapsedRealtimeMs, + loadDurationMs, + loadable.bytesLoaded); copyLengthFromLoader(loadable); for (SampleQueue sampleQueue : sampleQueues) { sampleQueue.reset(); @@ -434,7 +458,6 @@ import java.util.Arrays; @Override public int onLoadError(ExtractingLoadable loadable, long elapsedRealtimeMs, long loadDurationMs, IOException error) { - copyLengthFromLoader(loadable); boolean isErrorFatal = isLoadableExceptionFatal(error); eventDispatcher.loadError( loadable.dataSpec, @@ -450,6 +473,7 @@ import java.util.Arrays; loadable.bytesLoaded, error, /* wasCanceled= */ isErrorFatal); + copyLengthFromLoader(loadable); if (isErrorFatal) { return Loader.DONT_RETRY_FATAL; } From 9cb0c2f70292c65e0f430263e210e5ea48c11878 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 5 Dec 2017 04:21:18 -0800 Subject: [PATCH 0927/2472] Remove self @link ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177938212 --- .../java/com/google/android/exoplayer2/SimpleExoPlayer.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 1374b73709..0d724d4fd2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -865,15 +865,15 @@ public class SimpleExoPlayer implements ExoPlayer { // Internal methods. /** - * Creates the ExoPlayer implementation used by this {@link SimpleExoPlayer}. + * Creates the {@link ExoPlayer} implementation used by this instance. * * @param renderers The {@link Renderer}s that will 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. * @return A new {@link ExoPlayer} instance. */ - protected ExoPlayer createExoPlayerImpl(Renderer[] renderers, TrackSelector trackSelector, - LoadControl loadControl) { + protected ExoPlayer createExoPlayerImpl( + Renderer[] renderers, TrackSelector trackSelector, LoadControl loadControl) { return new ExoPlayerImpl(renderers, trackSelector, loadControl); } From 02e32a183881490b581528407965d8de4f6469f6 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 5 Dec 2017 05:19:48 -0800 Subject: [PATCH 0928/2472] Hide subtitles when switching player in SimpleExoPlayerView ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=177941993 --- .../google/android/exoplayer2/ui/SimpleExoPlayerView.java | 3 +++ .../java/com/google/android/exoplayer2/ui/SubtitleView.java | 5 +++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index b09e80c591..dcc1c62569 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -425,6 +425,9 @@ public final class SimpleExoPlayerView extends FrameLayout { if (shutterView != null) { shutterView.setVisibility(VISIBLE); } + if (subtitleView != null) { + subtitleView.setCues(null); + } if (player != null) { if (surfaceView instanceof TextureView) { player.setVideoTextureView((TextureView) surfaceView); diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java index 618f2fa336..d89f82b7c4 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java @@ -19,6 +19,7 @@ import android.annotation.TargetApi; import android.content.Context; import android.content.res.Resources; import android.graphics.Canvas; +import android.support.annotation.Nullable; import android.util.AttributeSet; import android.util.TypedValue; import android.view.View; @@ -87,9 +88,9 @@ public final class SubtitleView extends View implements TextOutput { /** * Sets the cues to be displayed by the view. * - * @param cues The cues to display. + * @param cues The cues to display, or null to clear the cues. */ - public void setCues(List cues) { + public void setCues(@Nullable List cues) { if (this.cues == cues) { return; } From 39e8f07566a61c4d45d5c8f4003f03d5440ca549 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 6 Dec 2017 10:33:23 -0800 Subject: [PATCH 0929/2472] Add missing Nullable annotation ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178117289 --- .../java/com/google/android/exoplayer2/SimpleExoPlayer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 0d724d4fd2..544b10b7ef 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -726,7 +726,7 @@ public class SimpleExoPlayer implements ExoPlayer { } @Override - public void setPlaybackParameters(PlaybackParameters playbackParameters) { + public void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters) { player.setPlaybackParameters(playbackParameters); } From e7d4524c27820e8bb0b9954b5113364b34eca0a9 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 7 Dec 2017 02:45:16 -0800 Subject: [PATCH 0930/2472] Use mappedTrackInfo local ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178216750 --- .../com/google/android/exoplayer2/demo/PlayerActivity.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index 7d0975a750..0623f48a51 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -233,8 +233,8 @@ public class PlayerActivity extends Activity implements OnClickListener, } else if (view.getParent() == debugRootView) { MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo(); if (mappedTrackInfo != null) { - trackSelectionHelper.showSelectionDialog(this, ((Button) view).getText(), - trackSelector.getCurrentMappedTrackInfo(), (int) view.getTag()); + trackSelectionHelper.showSelectionDialog( + this, ((Button) view).getText(), mappedTrackInfo, (int) view.getTag()); } } } From a4a02f74498c86e65b95c5961b8c794ef4f53f2a Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 7 Dec 2017 03:05:22 -0800 Subject: [PATCH 0931/2472] Skip ads before the initial player position Issue: #3527 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178218391 --- RELEASENOTES.md | 3 + .../exoplayer2/ext/ima/ImaAdsLoader.java | 57 +++++++++++++++++-- 2 files changed, 54 insertions(+), 6 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index ff0af256cf..a7199301d7 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -22,6 +22,9 @@ implementations. * CEA-608: Fix handling of row count changes in roll-up mode ([#3513](https://github.com/google/ExoPlayer/issues/3513)). +* IMA extension: + * Skip ads before the ad preceding the player's initial seek position + ([#3527](https://github.com/google/ExoPlayer/issues/3527)). ### 2.6.0 ### diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index 92ca24c889..58268d5670 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -65,7 +65,6 @@ import java.util.Map; */ public final class ImaAdsLoader extends Player.DefaultEventListener implements AdsLoader, VideoAdPlayer, ContentProgressProvider, AdErrorListener, AdsLoadedListener, AdEventListener { - static { ExoPlayerLibraryInfo.registerModule("goog.exo.ima"); } @@ -132,6 +131,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A private AdsManager adsManager; private Timeline timeline; private long contentDurationMs; + private int podIndexOffset; private AdPlaybackState adPlaybackState; // Fields tracking IMA's state. @@ -274,6 +274,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A adsManager.resume(); } } else { + pendingContentPositionMs = player.getCurrentPosition(); requestAds(); } } @@ -311,19 +312,45 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A return; } pendingAdRequestContext = null; + + long[] adGroupTimesUs = getAdGroupTimesUs(adsManager.getAdCuePoints()); + adPlaybackState = new AdPlaybackState(adGroupTimesUs); + this.adsManager = adsManager; adsManager.addAdErrorListener(this); adsManager.addAdEventListener(this); + ImaSdkFactory imaSdkFactory = ImaSdkFactory.getInstance(); AdsRenderingSettings adsRenderingSettings = imaSdkFactory.createAdsRenderingSettings(); adsRenderingSettings.setEnablePreloading(ENABLE_PRELOADING); adsRenderingSettings.setMimeTypes(supportedMimeTypes); + int adGroupIndexForPosition = + getAdGroupIndexForPosition(adGroupTimesUs, C.msToUs(pendingContentPositionMs)); + if (adGroupIndexForPosition == C.INDEX_UNSET) { + pendingContentPositionMs = C.TIME_UNSET; + } else if (adGroupIndexForPosition > 0) { + // Skip ad groups before the one at or immediately before the playback position. + for (int i = 0; i < adGroupIndexForPosition; i++) { + adPlaybackState.playedAdGroup(i); + } + // Play ads after the midpoint between the ad to play and the one before it, to avoid issues + // with rounding one of the two ad times. + long adGroupForPositionTimeUs = adGroupTimesUs[adGroupIndexForPosition]; + long adGroupBeforeTimeUs = adGroupTimesUs[adGroupIndexForPosition - 1]; + double midpointTimeUs = (adGroupForPositionTimeUs + adGroupBeforeTimeUs) / 2d; + adsRenderingSettings.setPlayAdsAfterTime(midpointTimeUs / C.MICROS_PER_SECOND); + + // We're removing one or more ads, which means that the earliest ad (if any) will be a + // midroll/postroll. According to the AdPodInfo documentation, midroll pod indices always + // start at 1, so take this into account when offsetting the pod index for the skipped ads. + podIndexOffset = adGroupIndexForPosition - 1; + } + adsManager.init(adsRenderingSettings); if (DEBUG) { Log.d(TAG, "Initialized with ads rendering settings: " + adsRenderingSettings); } - long[] adGroupTimesUs = getAdGroupTimesUs(adsManager.getAdCuePoints()); - adPlaybackState = new AdPlaybackState(adGroupTimesUs); + updateAdPlaybackState(); } @@ -351,13 +378,15 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A // The ad position is not always accurate when using preloading. See [Internal: b/62613240]. AdPodInfo adPodInfo = ad.getAdPodInfo(); int podIndex = adPodInfo.getPodIndex(); - adGroupIndex = podIndex == -1 ? adPlaybackState.adGroupCount - 1 : podIndex; + adGroupIndex = + podIndex == -1 ? (adPlaybackState.adGroupCount - 1) : (podIndex + podIndexOffset); int adPosition = adPodInfo.getAdPosition(); int adCountInAdGroup = adPodInfo.getTotalAds(); adsManager.start(); if (DEBUG) { - Log.d(TAG, "Loaded ad " + adPosition + " of " + adCountInAdGroup + " in ad group " - + adGroupIndex); + Log.d( + TAG, + "Loaded ad " + adPosition + " of " + adCountInAdGroup + " in group " + adGroupIndex); } adPlaybackState.setAdCount(adGroupIndex, adCountInAdGroup); updateAdPlaybackState(); @@ -740,4 +769,20 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A return adGroupTimesUs; } + /** + * Returns the index of the ad group that should be played before playing the content at {@code + * playbackPositionUs} when starting playback for the first time. This is the latest ad group at + * or before the specified playback position. If the first ad is after the playback position, + * returns {@link C#INDEX_UNSET}. + */ + private int getAdGroupIndexForPosition(long[] adGroupTimesUs, long playbackPositionUs) { + for (int i = 0; i < adGroupTimesUs.length; i++) { + long adGroupTimeUs = adGroupTimesUs[i]; + // A postroll ad is after any position in the content. + if (adGroupTimeUs == C.TIME_END_OF_SOURCE || playbackPositionUs < adGroupTimeUs) { + return i == 0 ? C.INDEX_UNSET : (i - 1); + } + } + return adGroupTimesUs.length == 0 ? C.INDEX_UNSET : (adGroupTimesUs.length - 1); + } } From f677d1309d7971d7f7bbdd07e5ca412288ea4c3d Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 7 Dec 2017 03:06:54 -0800 Subject: [PATCH 0932/2472] Blacklist Moto Z from using secure DummySurface. Issue: #3215 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178218535 --- .../com/google/android/exoplayer2/video/DummySurface.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java index 2d7a9dfd33..cc50443296 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java @@ -150,14 +150,17 @@ public final class DummySurface extends Surface { */ @TargetApi(24) private static boolean enableSecureDummySurfaceV24(Context context) { - if (Util.SDK_INT < 26 && "samsung".equals(Util.MANUFACTURER)) { + if (Util.SDK_INT < 26 && ("samsung".equals(Util.MANUFACTURER) || "XT1650".equals(Util.MODEL))) { // Samsung devices running Nougat are known to be broken. See // https://github.com/google/ExoPlayer/issues/3373 and [Internal: b/37197802]. + // Moto Z XT1650 is also affected. See + // https://github.com/google/ExoPlayer/issues/3215. return false; } if (Util.SDK_INT < 26 && !context.getPackageManager().hasSystemFeature( PackageManager.FEATURE_VR_MODE_HIGH_PERFORMANCE)) { - // Pre API level 26 devices were not well tested unless they supported VR mode. + // Pre API level 26 devices were not well tested unless they supported VR mode. See + // https://github.com/google/ExoPlayer/issues/3215. return false; } EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY); From a99295b364d86d9113addc34ff5ff503343f9d40 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 7 Dec 2017 06:38:39 -0800 Subject: [PATCH 0933/2472] Fix ad loading when there is no preroll ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178234009 --- RELEASENOTES.md | 1 + .../android/exoplayer2/ext/ima/ImaAdsLoader.java | 11 +++++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index a7199301d7..e4a0b40b23 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -25,6 +25,7 @@ * IMA extension: * Skip ads before the ad preceding the player's initial seek position ([#3527](https://github.com/google/ExoPlayer/issues/3527)). + * Fix ad loading when there is no preroll. ### 2.6.0 ### diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index 58268d5670..0fa34e5144 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -326,9 +326,13 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A adsRenderingSettings.setMimeTypes(supportedMimeTypes); int adGroupIndexForPosition = getAdGroupIndexForPosition(adGroupTimesUs, C.msToUs(pendingContentPositionMs)); - if (adGroupIndexForPosition == C.INDEX_UNSET) { + if (adGroupIndexForPosition == 0) { + podIndexOffset = 0; + } else if (adGroupIndexForPosition == C.INDEX_UNSET) { pendingContentPositionMs = C.TIME_UNSET; - } else if (adGroupIndexForPosition > 0) { + // There is no preroll and midroll pod indices start at 1. + podIndexOffset = -1; + } else /* adGroupIndexForPosition > 0 */ { // Skip ad groups before the one at or immediately before the playback position. for (int i = 0; i < adGroupIndexForPosition; i++) { adPlaybackState.playedAdGroup(i); @@ -341,8 +345,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A adsRenderingSettings.setPlayAdsAfterTime(midpointTimeUs / C.MICROS_PER_SECOND); // We're removing one or more ads, which means that the earliest ad (if any) will be a - // midroll/postroll. According to the AdPodInfo documentation, midroll pod indices always - // start at 1, so take this into account when offsetting the pod index for the skipped ads. + // midroll/postroll. Midroll pod indices start at 1. podIndexOffset = adGroupIndexForPosition - 1; } From 88d012bad0fbf482629d8a07d8d221fe735c9628 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 8 Dec 2017 04:44:55 -0800 Subject: [PATCH 0934/2472] Treat captions that are wider than expected as middle aligned Issue: #3534 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178364353 --- .../google/android/exoplayer2/text/cea/Cea608Decoder.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/library/core/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 index 0483f909b3..f018e055fb 100644 --- a/library/core/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 @@ -740,8 +740,10 @@ public final class Cea608Decoder extends CeaDecoder { // The number of empty columns after the end of the text, in the same range. int endPadding = SCREEN_CHARWIDTH - startPadding - cueString.length(); int startEndPaddingDelta = startPadding - endPadding; - if (captionMode == CC_MODE_POP_ON && Math.abs(startEndPaddingDelta) < 3) { - // Treat approximately centered pop-on captions are middle aligned. + if (captionMode == CC_MODE_POP_ON && (Math.abs(startEndPaddingDelta) < 3 || endPadding < 0)) { + // Treat approximately centered pop-on captions as middle aligned. We also treat captions + // that are wider than they should be in this way. See + // https://github.com/google/ExoPlayer/issues/3534. position = 0.5f; positionAnchor = Cue.ANCHOR_TYPE_MIDDLE; } else if (captionMode == CC_MODE_POP_ON && startEndPaddingDelta > 0) { From 3e9c86fcb5826fc3fab0c94bfae317f50ecb0477 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 8 Dec 2017 06:51:51 -0800 Subject: [PATCH 0935/2472] Add an option to turn off hiding controls during ads Issue: #3532 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178372763 --- RELEASENOTES.md | 2 + .../exoplayer2/ui/SimpleExoPlayerView.java | 122 ++++++++++-------- library/ui/src/main/res/values/attrs.xml | 1 + 3 files changed, 70 insertions(+), 55 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index e4a0b40b23..c702a22ce6 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -26,6 +26,8 @@ * Skip ads before the ad preceding the player's initial seek position ([#3527](https://github.com/google/ExoPlayer/issues/3527)). * Fix ad loading when there is no preroll. + * Add an option to turn off hiding controls during ad playback + ([#3532](https://github.com/google/ExoPlayer/issues/3532)). ### 2.6.0 ### diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index dcc1c62569..1f67b83ba0 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -56,146 +56,144 @@ import java.util.List; /** * A high level view for {@link SimpleExoPlayer} media playbacks. It displays video, subtitles and * album art during playback, and displays playback controls using a {@link PlaybackControlView}. - *

          - * A SimpleExoPlayerView can be customized by setting attributes (or calling corresponding methods), - * overriding the view's layout file or by specifying a custom view layout file, as outlined below. + * + *

          A SimpleExoPlayerView can be customized by setting attributes (or calling corresponding + * methods), overriding the view's layout file or by specifying a custom view layout file, as + * outlined below. * *

          Attributes

          + * * The following attributes can be set on a SimpleExoPlayerView when used in a layout XML file: + * *

          + * *

            *
          • {@code use_artwork} - Whether artwork is used if available in audio streams. *
              - *
            • Corresponding method: {@link #setUseArtwork(boolean)}
            • - *
            • Default: {@code true}
            • + *
            • Corresponding method: {@link #setUseArtwork(boolean)} + *
            • Default: {@code true} *
            - *
          • *
          • {@code default_artwork} - Default artwork to use if no artwork available in audio * streams. *
              - *
            • Corresponding method: {@link #setDefaultArtwork(Bitmap)}
            • - *
            • Default: {@code null}
            • + *
            • Corresponding method: {@link #setDefaultArtwork(Bitmap)} + *
            • Default: {@code null} *
            - *
          • *
          • {@code use_controller} - Whether the playback controls can be shown. *
              - *
            • Corresponding method: {@link #setUseController(boolean)}
            • - *
            • Default: {@code true}
            • + *
            • 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}
            • + *
            • Corresponding method: {@link #setControllerHideOnTouch(boolean)} + *
            • Default: {@code true} *
            - *
          • *
          • {@code auto_show} - Whether the playback controls are automatically shown when * playback starts, pauses, ends, or fails. If set to false, the playback controls can be * manually operated with {@link #showController()} and {@link #hideController()}. *
              - *
            • Corresponding method: {@link #setControllerAutoShow(boolean)}
            • - *
            • Default: {@code true}
            • + *
            • Corresponding method: {@link #setControllerAutoShow(boolean)} + *
            • Default: {@code true} + *
            + *
          • {@code hide_during_ads} - Whether the playback controls are hidden during ads. + * Controls are always shown during ads if they are enabled and the player is paused. + *
              + *
            • Corresponding method: {@link #setControllerHideDuringAds(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}. *
              - *
            • Corresponding method: {@link #setResizeMode(int)}
            • - *
            • Default: {@code fit}
            • + *
            • Corresponding method: {@link #setResizeMode(int)} + *
            • Default: {@code fit} *
            - *
          • *
          • {@code surface_type} - The type of surface view used for video playbacks. Valid * values are {@code surface_view}, {@code texture_view} and {@code none}. Using {@code none} * is recommended for audio only applications, since creating the surface can be expensive. * Using {@code surface_view} is recommended for video applications. *
              - *
            • Corresponding method: None
            • - *
            • Default: {@code surface_view}
            • + *
            • Corresponding method: None + *
            • Default: {@code surface_view} *
            - *
          • *
          • {@code shutter_background_color} - The background color of the {@code exo_shutter} * view. *
              - *
            • Corresponding method: {@link #setShutterBackgroundColor(int)}
            • - *
            • Default: {@code unset}
            • + *
            • Corresponding method: {@link #setShutterBackgroundColor(int)} + *
            • Default: {@code unset} *
            - *
          • *
          • {@code player_layout_id} - Specifies the id of the layout to be inflated. See below * for more details. *
              - *
            • Corresponding method: None
            • - *
            • Default: {@code R.id.exo_simple_player_view}
            • + *
            • Corresponding method: None + *
            • Default: {@code R.id.exo_simple_player_view} *
            *
          • {@code controller_layout_id} - Specifies the id of the layout resource to be * inflated by the child {@link PlaybackControlView}. See below for more details. *
              - *
            • Corresponding method: None
            • - *
            • Default: {@code R.id.exo_playback_control_view}
            • + *
            • Corresponding method: None + *
            • Default: {@code R.id.exo_playback_control_view} *
            *
          • All attributes that can be set on a {@link PlaybackControlView} can also be set on a * SimpleExoPlayerView, and will be propagated to the inflated {@link PlaybackControlView} * unless the layout is overridden to specify a custom {@code exo_controller} (see below). - *
          • *
          * *

          Overriding the layout file

          + * * To customize the layout of SimpleExoPlayerView throughout your app, or just for certain * configurations, you can define {@code exo_simple_player_view.xml} layout files in your * application {@code res/layout*} directories. These layouts will override the one provided by the * ExoPlayer library, and will be inflated for use by SimpleExoPlayerView. The view identifies and * binds its children by looking for the following ids: + * *

          + * *

            *
          • {@code exo_content_frame} - A frame whose aspect ratio is resized based on the video * or album art of the media being played, and the configured {@code resize_mode}. The video * surface view is inflated into this frame as its first child. *
              - *
            • Type: {@link AspectRatioFrameLayout}
            • + *
            • Type: {@link AspectRatioFrameLayout} *
            - *
          • *
          • {@code exo_shutter} - A view that's made visible when video should be hidden. This * view is typically an opaque view that covers the video surface view, thereby obscuring it * when visible. *
              - *
            • Type: {@link View}
            • + *
            • Type: {@link View} *
            - *
          • *
          • {@code exo_subtitles} - Displays subtitles. *
              - *
            • Type: {@link SubtitleView}
            • + *
            • Type: {@link SubtitleView} *
            - *
          • *
          • {@code exo_artwork} - Displays album art. *
              - *
            • Type: {@link ImageView}
            • + *
            • Type: {@link ImageView} *
            - *
          • *
          • {@code exo_controller_placeholder} - A placeholder that's replaced with the inflated * {@link PlaybackControlView}. Ignored if an {@code exo_controller} view exists. *
              - *
            • Type: {@link View}
            • + *
            • Type: {@link View} *
            - *
          • *
          • {@code exo_controller} - An already inflated {@link PlaybackControlView}. Allows use - * of a custom extension of {@link PlaybackControlView}. Note that attributes such as - * {@code rewind_increment} will not be automatically propagated through to this instance. If - * a view exists with this id, any {@code exo_controller_placeholder} view will be ignored. + * of a custom extension of {@link PlaybackControlView}. Note that attributes such as {@code + * rewind_increment} will not be automatically propagated through to this instance. If a view + * exists with this id, any {@code exo_controller_placeholder} view will be ignored. *
              - *
            • Type: {@link PlaybackControlView}
            • + *
            • Type: {@link PlaybackControlView} *
            - *
          • *
          • {@code exo_overlay} - A {@link FrameLayout} positioned on top of the player which * the app can access via {@link #getOverlayFrameLayout()}, provided for convenience. *
              - *
            • Type: {@link FrameLayout}
            • + *
            • Type: {@link FrameLayout} *
            - *
          • *
          - *

          - * All child views are optional and so can be omitted if not required, however where defined they + * + *

          All child views are optional and so can be omitted if not required, however where defined they * must be of the expected type. * *

          Specifying a custom layout file

          + * * Defining your own {@code exo_simple_player_view.xml} is useful to customize the layout of * SimpleExoPlayerView throughout your application. It's also possible to customize the layout for a * single instance in a layout file. This is achieved by setting the {@code player_layout_id} @@ -224,6 +222,7 @@ public final class SimpleExoPlayerView extends FrameLayout { private Bitmap defaultArtwork; private int controllerShowTimeoutMs; private boolean controllerAutoShow; + private boolean controllerHideDuringAds; private boolean controllerHideOnTouch; public SimpleExoPlayerView(Context context) { @@ -267,6 +266,7 @@ public final class SimpleExoPlayerView extends FrameLayout { int controllerShowTimeoutMs = PlaybackControlView.DEFAULT_SHOW_TIMEOUT_MS; boolean controllerHideOnTouch = true; boolean controllerAutoShow = true; + boolean controllerHideDuringAds = true; if (attrs != null) { TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.SimpleExoPlayerView, 0, 0); @@ -288,6 +288,8 @@ public final class SimpleExoPlayerView extends FrameLayout { controllerHideOnTouch); controllerAutoShow = a.getBoolean(R.styleable.SimpleExoPlayerView_auto_show, controllerAutoShow); + controllerHideDuringAds = + a.getBoolean(R.styleable.SimpleExoPlayerView_hide_during_ads, controllerHideDuringAds); } finally { a.recycle(); } @@ -358,6 +360,7 @@ public final class SimpleExoPlayerView extends FrameLayout { this.controllerShowTimeoutMs = controller != null ? controllerShowTimeoutMs : 0; this.controllerHideOnTouch = controllerHideOnTouch; this.controllerAutoShow = controllerAutoShow; + this.controllerHideDuringAds = controllerHideDuringAds; this.useController = useController && controller != null; hideController(); } @@ -649,6 +652,16 @@ public final class SimpleExoPlayerView extends FrameLayout { this.controllerAutoShow = controllerAutoShow; } + /** + * Sets whether the playback controls are hidden when ads are playing. Controls are always shown + * during ads if they are enabled and the player is paused. + * + * @param controllerHideDuringAds Whether the playback controls are hidden when ads are playing. + */ + public void setControllerHideDuringAds(boolean controllerHideDuringAds) { + this.controllerHideDuringAds = controllerHideDuringAds; + } + /** * Set the {@link PlaybackControlView.VisibilityListener}. * @@ -784,8 +797,7 @@ public final class SimpleExoPlayerView extends FrameLayout { * Shows the playback controls, but only if forced or shown indefinitely. */ private void maybeShowController(boolean isForced) { - if (isPlayingAd()) { - // Never show the controller if an ad is currently playing. + if (isPlayingAd() && controllerHideDuringAds) { return; } if (useController) { @@ -956,7 +968,7 @@ public final class SimpleExoPlayerView extends FrameLayout { @Override public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { - if (isPlayingAd()) { + if (isPlayingAd() && controllerHideDuringAds) { hideController(); } else { maybeShowController(false); @@ -965,7 +977,7 @@ public final class SimpleExoPlayerView extends FrameLayout { @Override public void onPositionDiscontinuity(@DiscontinuityReason int reason) { - if (isPlayingAd()) { + if (isPlayingAd() && controllerHideDuringAds) { hideController(); } } diff --git a/library/ui/src/main/res/values/attrs.xml b/library/ui/src/main/res/values/attrs.xml index 525f95768c..1ab3854d21 100644 --- a/library/ui/src/main/res/values/attrs.xml +++ b/library/ui/src/main/res/values/attrs.xml @@ -47,6 +47,7 @@ + From a0d42b53b734ec8d533ef56681ffad9453903d59 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 8 Dec 2017 07:57:06 -0800 Subject: [PATCH 0936/2472] Make DashMediaSource.Builder a factory for DashMediaSources This is in preparation for supporting non-extractor MediaSources for ads in AdsMediaSource. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178377627 --- .../exoplayer2/demo/PlayerActivity.java | 9 +- .../exoplayer2/source/ads/AdsMediaSource.java | 65 +++--- .../source/dash/DashMediaSource.java | 192 ++++++++++-------- .../playbacktests/gts/DashTestRunner.java | 5 +- 4 files changed, 141 insertions(+), 130 deletions(-) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index 0623f48a51..1be6df8437 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -371,11 +371,10 @@ public class PlayerActivity extends Activity implements OnClickListener, .setEventListener(mainHandler, eventLogger) .build(); case C.TYPE_DASH: - return DashMediaSource.Builder - .forManifestUri(uri, buildDataSourceFactory(false), - new DefaultDashChunkSource.Factory(mediaDataSourceFactory)) - .setEventListener(mainHandler, eventLogger) - .build(); + return new DashMediaSource.Factory( + new DefaultDashChunkSource.Factory(mediaDataSourceFactory), + buildDataSourceFactory(false)) + .createMediaSource(uri, mainHandler, eventLogger); case C.TYPE_HLS: return HlsMediaSource.Builder .forDataSource(uri, mediaDataSourceFactory) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java index 54a8fd96ae..c701d6ca64 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java @@ -44,6 +44,31 @@ import java.util.Map; */ public final class AdsMediaSource implements MediaSource { + /** Factory for creating {@link MediaSource}s to play ad media. */ + public interface MediaSourceFactory { + + /** + * Creates a new {@link MediaSource} for loading the ad media with the specified {@code uri}. + * + * @param uri The URI of the media or manifest to play. + * @param handler A handler for listener events. May be null if delivery of events is not + * required. + * @param listener A listener for events. May be null if delivery of events is not required. + * @return The new media source. + */ + MediaSource createMediaSource( + Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener); + + /** + * Returns the content types supported by media sources created by this factory. Each element + * should be one of {@link C#TYPE_DASH}, {@link C#TYPE_SS}, {@link C#TYPE_HLS} or {@link + * C#TYPE_OTHER}. + * + * @return The content types supported by media sources created by this factory. + */ + int[] getSupportedTypes(); + } + /** Listener for ads media source events. */ public interface EventListener extends MediaSourceEventListener { @@ -77,7 +102,7 @@ public final class AdsMediaSource implements MediaSource { @Nullable private final EventListener eventListener; private final Handler mainHandler; private final ComponentListener componentListener; - private final AdMediaSourceFactory adMediaSourceFactory; + private final MediaSourceFactory adMediaSourceFactory; private final Map> deferredMediaPeriodByAdMediaSource; private final Timeline.Period period; @@ -138,7 +163,7 @@ public final class AdsMediaSource implements MediaSource { this.eventListener = eventListener; mainHandler = new Handler(Looper.getMainLooper()); componentListener = new ComponentListener(); - adMediaSourceFactory = new ExtractorAdMediaSourceFactory(dataSourceFactory); + adMediaSourceFactory = new ExtractorMediaSourceFactory(dataSourceFactory); deferredMediaPeriodByAdMediaSource = new HashMap<>(); period = new Timeline.Period(); adGroupMediaSources = new MediaSource[0][]; @@ -186,7 +211,7 @@ public final class AdsMediaSource implements MediaSource { if (adGroupMediaSources[adGroupIndex].length <= adIndexInAdGroup) { Uri adUri = adPlaybackState.adUris[id.adGroupIndex][id.adIndexInAdGroup]; final MediaSource adMediaSource = - adMediaSourceFactory.createAdMediaSource(adUri, eventHandler, eventListener); + adMediaSourceFactory.createMediaSource(adUri, eventHandler, eventListener); int oldAdCount = adGroupMediaSources[id.adGroupIndex].length; if (adIndexInAdGroup >= oldAdCount) { int adCount = adIndexInAdGroup + 1; @@ -371,44 +396,16 @@ public final class AdsMediaSource implements MediaSource { } - /** - * Factory for {@link MediaSource}s for loading ad media. - */ - private interface AdMediaSourceFactory { - - /** - * Creates a new {@link MediaSource} for loading the ad media with the specified {@code uri}. - * - * @param uri The URI of the ad. - * @param handler A handler for listener events. May be null if delivery of events is not - * required. - * @param listener A listener for events. May be null if delivery of events is not required. - * @return The new media source. - */ - MediaSource createAdMediaSource( - Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener); - - /** - * Returns the content types supported by media sources created by this factory. Each element - * should be one of {@link C#TYPE_DASH}, {@link C#TYPE_SS}, {@link C#TYPE_HLS} or - * {@link C#TYPE_OTHER}. - * - * @return The content types supported by the factory. - */ - int[] getSupportedTypes(); - - } - - private static final class ExtractorAdMediaSourceFactory implements AdMediaSourceFactory { + private static final class ExtractorMediaSourceFactory implements MediaSourceFactory { private final DataSource.Factory dataSourceFactory; - public ExtractorAdMediaSourceFactory(DataSource.Factory dataSourceFactory) { + public ExtractorMediaSourceFactory(DataSource.Factory dataSourceFactory) { this.dataSourceFactory = dataSourceFactory; } @Override - public MediaSource createAdMediaSource( + public MediaSource createMediaSource( Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener) { return new ExtractorMediaSource.Builder(uri, dataSourceFactory) .setEventListener(handler, listener) diff --git a/library/dash/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 index 22bc2b08ec..e2a6097411 100644 --- a/library/dash/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 @@ -31,6 +31,7 @@ import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; +import com.google.android.exoplayer2.source.ads.AdsMediaSource; 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.UtcTimingElement; @@ -61,59 +62,31 @@ public final class DashMediaSource implements MediaSource { ExoPlayerLibraryInfo.registerModule("goog.exo.dash"); } - /** - * Builder for {@link DashMediaSource}. Each builder instance can only be used once. - */ - public static final class Builder { + /** Factory for {@link DashMediaSource}s. */ + public static final class Factory implements AdsMediaSource.MediaSourceFactory { - private final DashManifest manifest; - private final Uri manifestUri; - private final DataSource.Factory manifestDataSourceFactory; private final DashChunkSource.Factory chunkSourceFactory; + private final @Nullable DataSource.Factory manifestDataSourceFactory; - private ParsingLoadable.Parser manifestParser; - private MediaSourceEventListener eventListener; - private Handler eventHandler; - + private @Nullable ParsingLoadable.Parser manifestParser; private int minLoadableRetryCount; private long livePresentationDelayMs; - private boolean isBuildCalled; + private boolean isCreateCalled; /** - * Creates a {@link Builder} for a {@link DashMediaSource} with a side-loaded manifest. + * Creates a new factory for {@link DashMediaSource}s. * - * @param manifest The manifest. {@link DashManifest#dynamic} must be false. * @param chunkSourceFactory A factory for {@link DashChunkSource} instances. - * @return A new builder. - */ - public static Builder forSideloadedManifest(DashManifest manifest, - DashChunkSource.Factory chunkSourceFactory) { - Assertions.checkArgument(!manifest.dynamic); - return new Builder(manifest, null, null, chunkSourceFactory); - } - - /** - * Creates a {@link Builder} for a {@link DashMediaSource} with a loadable manifest Uri. - * - * @param manifestUri The manifest {@link Uri}. * @param manifestDataSourceFactory A factory for {@link DataSource} instances that will be used - * to load (and refresh) the manifest. - * @param chunkSourceFactory A factory for {@link DashChunkSource} instances. - * @return A new builder. + * to load (and refresh) the manifest. May be {@code null} if the factory will only ever be + * used to create create media sources with sideloaded manifests via {@link + * #createMediaSource(DashManifest, Handler, MediaSourceEventListener)}. */ - public static Builder forManifestUri(Uri manifestUri, - DataSource.Factory manifestDataSourceFactory, DashChunkSource.Factory chunkSourceFactory) { - return new Builder(null, manifestUri, manifestDataSourceFactory, chunkSourceFactory); - } - - private Builder(@Nullable DashManifest manifest, @Nullable Uri manifestUri, - @Nullable DataSource.Factory manifestDataSourceFactory, - DashChunkSource.Factory chunkSourceFactory) { - this.manifest = manifest; - this.manifestUri = manifestUri; + public Factory( + DashChunkSource.Factory chunkSourceFactory, + @Nullable DataSource.Factory manifestDataSourceFactory) { + this.chunkSourceFactory = Assertions.checkNotNull(chunkSourceFactory); this.manifestDataSourceFactory = manifestDataSourceFactory; - this.chunkSourceFactory = chunkSourceFactory; - minLoadableRetryCount = DEFAULT_MIN_LOADABLE_RETRY_COUNT; livePresentationDelayMs = DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS; } @@ -123,76 +96,119 @@ public final class DashMediaSource implements MediaSource { * {@link #DEFAULT_MIN_LOADABLE_RETRY_COUNT}. * * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. - * @return This builder. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Builder setMinLoadableRetryCount(int minLoadableRetryCount) { + public Factory setMinLoadableRetryCount(int minLoadableRetryCount) { + Assertions.checkState(!isCreateCalled); this.minLoadableRetryCount = minLoadableRetryCount; return this; } /** * Sets the duration in milliseconds by which the default start position should precede the end - * of the live window for live playbacks. The default value is - * {@link #DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS}. + * of the live window for live playbacks. The default value is {@link + * #DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS}. * * @param livePresentationDelayMs For live playbacks, the duration in milliseconds by which the - * default start position should precede the end of the live window. Use - * {@link #DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS} to use the value specified by - * the manifest, if present. - * @return This builder. + * default start position should precede the end of the live window. Use {@link + * #DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS} to use the value specified by the + * manifest, if present. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Builder setLivePresentationDelayMs(long livePresentationDelayMs) { + public Factory setLivePresentationDelayMs(long livePresentationDelayMs) { + Assertions.checkState(!isCreateCalled); this.livePresentationDelayMs = livePresentationDelayMs; return this; } /** - * Sets the listener to respond to adaptive {@link MediaSource} events and the handler to - * deliver these events. - * - * @param eventHandler A handler for events. - * @param eventListener A listener of events. - * @return This builder. - */ - public Builder setEventListener(Handler eventHandler, MediaSourceEventListener eventListener) { - this.eventHandler = eventHandler; - this.eventListener = eventListener; - return this; - } - - /** - * Sets the manifest parser to parse loaded manifest data. The default is - * {@link DashManifestParser}, or {@code null} if the manifest is sideloaded. + * Sets the manifest parser to parse loaded manifest data when loading a manifest URI. * * @param manifestParser A parser for loaded manifest data. - * @return This builder. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Builder setManifestParser( + public Factory setManifestParser( ParsingLoadable.Parser manifestParser) { - this.manifestParser = manifestParser; + Assertions.checkState(!isCreateCalled); + this.manifestParser = Assertions.checkNotNull(manifestParser); return this; } /** - * Builds a new {@link DashMediaSource} using the current parameters. - *

          - * After this call, the builder should not be re-used. + * Returns a new {@link DashMediaSource} using the current parameters and the specified + * sideloaded manifest. * - * @return The newly built {@link DashMediaSource}. + * @param manifest The manifest. {@link DashManifest#dynamic} must be false. + * @param eventHandler A handler for events. + * @param eventListener A listener of events. + * @return The new {@link DashMediaSource}. + * @throws IllegalArgumentException If {@link DashManifest#dynamic} is true. */ - public DashMediaSource build() { - Assertions.checkArgument((eventListener == null) == (eventHandler == null)); - Assertions.checkState(!isBuildCalled); - isBuildCalled = true; - boolean loadableManifestUri = manifestUri != null; - if (loadableManifestUri && manifestParser == null) { - manifestParser = new DashManifestParser(); - } - return new DashMediaSource(manifest, manifestUri, manifestDataSourceFactory, manifestParser, - chunkSourceFactory, minLoadableRetryCount, livePresentationDelayMs, eventHandler, + public DashMediaSource createMediaSource( + DashManifest manifest, + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { + Assertions.checkArgument(!manifest.dynamic); + isCreateCalled = true; + return new DashMediaSource( + manifest, + null, + null, + null, + chunkSourceFactory, + minLoadableRetryCount, + livePresentationDelayMs, + eventHandler, eventListener); } + /** + * Returns a new {@link DashMediaSource} using the current parameters. Media source events will + * not be delivered. + * + * @param manifestUri The manifest {@link Uri}. + * @return The new {@link DashMediaSource}. + */ + public DashMediaSource createMediaSource(Uri manifestUri) { + return createMediaSource(manifestUri, null, null); + } + + /** + * Returns a new {@link DashMediaSource} using the current parameters. + * + * @param manifestUri The manifest {@link Uri}. + * @param eventHandler A handler for events. + * @param eventListener A listener of events. + * @return The new {@link DashMediaSource}. + */ + @Override + public DashMediaSource createMediaSource( + Uri manifestUri, + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { + isCreateCalled = true; + if (manifestParser == null) { + manifestParser = new DashManifestParser(); + } + return new DashMediaSource( + null, + Assertions.checkNotNull(manifestUri), + manifestDataSourceFactory, + manifestParser, + chunkSourceFactory, + minLoadableRetryCount, + livePresentationDelayMs, + eventHandler, + eventListener); + } + + @Override + public int[] getSupportedTypes() { + return new int[] {C.TYPE_DASH}; + } } /** @@ -259,7 +275,7 @@ public final class DashMediaSource implements MediaSource { * @param chunkSourceFactory A factory for {@link DashChunkSource} instances. * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated public DashMediaSource(DashManifest manifest, DashChunkSource.Factory chunkSourceFactory, @@ -276,7 +292,7 @@ public final class DashMediaSource implements MediaSource { * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated public DashMediaSource(DashManifest manifest, DashChunkSource.Factory chunkSourceFactory, @@ -295,7 +311,7 @@ public final class DashMediaSource implements MediaSource { * @param chunkSourceFactory A factory for {@link DashChunkSource} instances. * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated public DashMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, @@ -321,7 +337,7 @@ public final class DashMediaSource implements MediaSource { * the manifest, if present. * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated public DashMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, @@ -347,7 +363,7 @@ public final class DashMediaSource implements MediaSource { * the manifest, if present. * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated public DashMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java index 215d8a0518..8973853245 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java @@ -316,11 +316,10 @@ public final class DashTestRunner { Uri manifestUri = Uri.parse(manifestUrl); DefaultDashChunkSource.Factory chunkSourceFactory = new DefaultDashChunkSource.Factory( mediaDataSourceFactory); - return DashMediaSource.Builder - .forManifestUri(manifestUri, manifestDataSourceFactory, chunkSourceFactory) + return new DashMediaSource.Factory(chunkSourceFactory, manifestDataSourceFactory) .setMinLoadableRetryCount(MIN_LOADABLE_RETRY_COUNT) .setLivePresentationDelayMs(0) - .build(); + .createMediaSource(manifestUri); } @Override From 4c71d6361ddb322f53aca12be0f333a8e287abfe Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 8 Dec 2017 08:32:46 -0800 Subject: [PATCH 0937/2472] Make SsMediaSource.Builder a factory for SsMediaSources ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178380856 --- .../exoplayer2/demo/PlayerActivity.java | 9 +- .../source/smoothstreaming/SsMediaSource.java | 183 ++++++++++-------- 2 files changed, 104 insertions(+), 88 deletions(-) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index 1be6df8437..38938bd367 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -365,11 +365,10 @@ public class PlayerActivity extends Activity implements OnClickListener, : Util.inferContentType("." + overrideExtension); switch (type) { case C.TYPE_SS: - return SsMediaSource.Builder - .forManifestUri(uri, buildDataSourceFactory(false), - new DefaultSsChunkSource.Factory(mediaDataSourceFactory)) - .setEventListener(mainHandler, eventLogger) - .build(); + return new SsMediaSource.Factory( + new DefaultSsChunkSource.Factory(mediaDataSourceFactory), + buildDataSourceFactory(false)) + .createMediaSource(uri, mainHandler, eventLogger); case C.TYPE_DASH: return new DashMediaSource.Factory( new DefaultDashChunkSource.Factory(mediaDataSourceFactory), diff --git a/library/smoothstreaming/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 index 34343e08e3..eb6ceb3dcc 100644 --- a/library/smoothstreaming/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 @@ -29,6 +29,7 @@ import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SinglePeriodTimeline; +import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest.StreamElement; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifestParser; @@ -52,59 +53,31 @@ public final class SsMediaSource implements MediaSource, ExoPlayerLibraryInfo.registerModule("goog.exo.smoothstreaming"); } - /** - * Builder for {@link SsMediaSource}. Each builder instance can only be used once. - */ - public static final class Builder { + /** Factory for {@link SsMediaSource}. */ + public static final class Factory implements AdsMediaSource.MediaSourceFactory { - private final SsManifest manifest; - private final Uri manifestUri; - private final DataSource.Factory manifestDataSourceFactory; private final SsChunkSource.Factory chunkSourceFactory; + private final @Nullable DataSource.Factory manifestDataSourceFactory; - private ParsingLoadable.Parser manifestParser; - private MediaSourceEventListener eventListener; - private Handler eventHandler; - + private @Nullable ParsingLoadable.Parser manifestParser; private int minLoadableRetryCount; private long livePresentationDelayMs; - private boolean isBuildCalled; + private boolean isCreateCalled; /** - * Creates a {@link Builder} for a {@link SsMediaSource} with a side-loaded manifest. + * Creates a new factory for {@link SsMediaSource}s. * - * @param manifest The manifest. {@link SsManifest#isLive} must be false. * @param chunkSourceFactory A factory for {@link SsChunkSource} instances. - * @return A new builder. - */ - public static Builder forSideLoadedManifest(SsManifest manifest, - SsChunkSource.Factory chunkSourceFactory) { - Assertions.checkArgument(!manifest.isLive); - return new Builder(manifest, null, null, chunkSourceFactory); - } - - /** - * Creates a {@link Builder} for a {@link SsMediaSource} with a loadable manifest Uri. - * - * @param manifestUri The manifest {@link Uri}. * @param manifestDataSourceFactory A factory for {@link DataSource} instances that will be used - * to load (and refresh) the manifest. - * @param chunkSourceFactory A factory for {@link SsChunkSource} instances. - * @return A new builder. + * to load (and refresh) the manifest. May be {@code null} if the factory will only ever be + * used to create create media sources with sideloaded manifests via {@link + * #createMediaSource(SsManifest, Handler, MediaSourceEventListener)}. */ - public static Builder forManifestUri(Uri manifestUri, - DataSource.Factory manifestDataSourceFactory, SsChunkSource.Factory chunkSourceFactory) { - return new Builder(null, manifestUri, manifestDataSourceFactory, chunkSourceFactory); - } - - private Builder(@Nullable SsManifest manifest, @Nullable Uri manifestUri, - @Nullable DataSource.Factory manifestDataSourceFactory, - SsChunkSource.Factory chunkSourceFactory) { - this.manifest = manifest; - this.manifestUri = manifestUri; + public Factory( + SsChunkSource.Factory chunkSourceFactory, + @Nullable DataSource.Factory manifestDataSourceFactory) { + this.chunkSourceFactory = Assertions.checkNotNull(chunkSourceFactory); this.manifestDataSourceFactory = manifestDataSourceFactory; - this.chunkSourceFactory = chunkSourceFactory; - minLoadableRetryCount = DEFAULT_MIN_LOADABLE_RETRY_COUNT; livePresentationDelayMs = DEFAULT_LIVE_PRESENTATION_DELAY_MS; } @@ -114,73 +87,117 @@ public final class SsMediaSource implements MediaSource, * {@link #DEFAULT_MIN_LOADABLE_RETRY_COUNT}. * * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. - * @return This builder. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Builder setMinLoadableRetryCount(int minLoadableRetryCount) { + public Factory setMinLoadableRetryCount(int minLoadableRetryCount) { + Assertions.checkState(!isCreateCalled); this.minLoadableRetryCount = minLoadableRetryCount; return this; } /** * Sets the duration in milliseconds by which the default start position should precede the end - * of the live window for live playbacks. The default value is - * {@link #DEFAULT_LIVE_PRESENTATION_DELAY_MS}. + * of the live window for live playbacks. The default value is {@link + * #DEFAULT_LIVE_PRESENTATION_DELAY_MS}. * * @param livePresentationDelayMs For live playbacks, the duration in milliseconds by which the * default start position should precede the end of the live window. - * @return This builder. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Builder setLivePresentationDelayMs(long livePresentationDelayMs) { + public Factory setLivePresentationDelayMs(long livePresentationDelayMs) { + Assertions.checkState(!isCreateCalled); this.livePresentationDelayMs = livePresentationDelayMs; return this; } /** - * Sets the listener to respond to adaptive {@link MediaSource} events and the handler to - * deliver these events. - * - * @param eventHandler A handler for events. - * @param eventListener A listener of events. - * @return This builder. - */ - public Builder setEventListener(Handler eventHandler, MediaSourceEventListener eventListener) { - this.eventHandler = eventHandler; - this.eventListener = eventListener; - return this; - } - - /** - * Sets the manifest parser to parse loaded manifest data. The default is an instance of - * {@link SsManifestParser}, or {@code null} if the manifest is sideloaded. + * Sets the manifest parser to parse loaded manifest data when loading a manifest URI. * * @param manifestParser A parser for loaded manifest data. - * @return This builder. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Builder setManifestParser(ParsingLoadable.Parser manifestParser) { - this.manifestParser = manifestParser; + public Factory setManifestParser(ParsingLoadable.Parser manifestParser) { + Assertions.checkState(!isCreateCalled); + this.manifestParser = Assertions.checkNotNull(manifestParser); return this; } /** - * Builds a new {@link SsMediaSource} using the current parameters. - *

          - * After this call, the builder should not be re-used. + * Returns a new {@link SsMediaSource} using the current parameters and the specified sideloaded + * manifest. * - * @return The newly built {@link SsMediaSource}. + * @param manifest The manifest. {@link SsManifest#isLive} must be false. + * @param eventHandler A handler for events. + * @param eventListener A listener of events. + * @return The new {@link SsMediaSource}. + * @throws IllegalArgumentException If {@link SsManifest#isLive} is true. */ - public SsMediaSource build() { - Assertions.checkArgument((eventListener == null) == (eventHandler == null)); - Assertions.checkState(!isBuildCalled); - isBuildCalled = true; - boolean loadableManifestUri = manifestUri != null; - if (loadableManifestUri && manifestParser == null) { + public SsMediaSource createMediaSource( + SsManifest manifest, + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { + Assertions.checkArgument(!manifest.isLive); + isCreateCalled = true; + return new SsMediaSource( + manifest, + null, + null, + null, + chunkSourceFactory, + minLoadableRetryCount, + livePresentationDelayMs, + eventHandler, + eventListener); + } + + /** + * Returns a new {@link SsMediaSource} using the current parameters. Media source events will + * not be delivered. + * + * @param manifestUri The manifest {@link Uri}. + * @return The new {@link SsMediaSource}. + */ + public SsMediaSource createMediaSource(Uri manifestUri) { + return createMediaSource(manifestUri, null, null); + } + + /** + * Returns a new {@link SsMediaSource} using the current parameters. + * + * @param manifestUri The manifest {@link Uri}. + * @param eventHandler A handler for events. + * @param eventListener A listener of events. + * @return The new {@link SsMediaSource}. + */ + @Override + public SsMediaSource createMediaSource( + Uri manifestUri, + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { + isCreateCalled = true; + if (manifestParser == null) { manifestParser = new SsManifestParser(); } - return new SsMediaSource(manifest, manifestUri, manifestDataSourceFactory, manifestParser, - chunkSourceFactory, minLoadableRetryCount, livePresentationDelayMs, eventHandler, + return new SsMediaSource( + null, + Assertions.checkNotNull(manifestUri), + manifestDataSourceFactory, + manifestParser, + chunkSourceFactory, + minLoadableRetryCount, + livePresentationDelayMs, + eventHandler, eventListener); } + @Override + public int[] getSupportedTypes() { + return new int[] {C.TYPE_SS}; + } + } /** @@ -228,7 +245,7 @@ public final class SsMediaSource implements MediaSource, * @param chunkSourceFactory A factory for {@link SsChunkSource} instances. * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated public SsMediaSource(SsManifest manifest, SsChunkSource.Factory chunkSourceFactory, @@ -245,7 +262,7 @@ public final class SsMediaSource implements MediaSource, * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated public SsMediaSource(SsManifest manifest, SsChunkSource.Factory chunkSourceFactory, @@ -264,7 +281,7 @@ public final class SsMediaSource implements MediaSource, * @param chunkSourceFactory A factory for {@link SsChunkSource} instances. * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated public SsMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, @@ -288,7 +305,7 @@ public final class SsMediaSource implements MediaSource, * default start position should precede the end of the live window. * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated public SsMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, @@ -312,7 +329,7 @@ public final class SsMediaSource implements MediaSource, * default start position should precede the end of the live window. * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated public SsMediaSource(Uri manifestUri, DataSource.Factory manifestDataSourceFactory, From 42b612a49f801f95bfa72b81cafef90329ac2737 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 8 Dec 2017 09:07:55 -0800 Subject: [PATCH 0938/2472] Make HlsMediaSource.Builder a factory for HlsMediaSources ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178384204 --- .../exoplayer2/demo/PlayerActivity.java | 6 +- .../exoplayer2/source/hls/HlsMediaSource.java | 151 +++++++++--------- 2 files changed, 75 insertions(+), 82 deletions(-) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index 38938bd367..215c4708e8 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -375,10 +375,8 @@ public class PlayerActivity extends Activity implements OnClickListener, buildDataSourceFactory(false)) .createMediaSource(uri, mainHandler, eventLogger); case C.TYPE_HLS: - return HlsMediaSource.Builder - .forDataSource(uri, mediaDataSourceFactory) - .setEventListener(mainHandler, eventLogger) - .build(); + return new HlsMediaSource.Factory(mediaDataSourceFactory) + .createMediaSource(uri, mainHandler, eventLogger); case C.TYPE_OTHER: return new ExtractorMediaSource.Builder(uri, mediaDataSourceFactory) .setEventListener(mainHandler, eventLogger) diff --git a/library/hls/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 index 4e904032fd..3a55cb8a17 100644 --- a/library/hls/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 @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source.hls; import android.net.Uri; import android.os.Handler; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; @@ -26,6 +27,7 @@ import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SinglePeriodTimeline; +import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylist; import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser; @@ -47,66 +49,51 @@ public final class HlsMediaSource implements MediaSource, ExoPlayerLibraryInfo.registerModule("goog.exo.hls"); } - /** - * Builder for {@link HlsMediaSource}. Each builder instance can only be used once. - */ - public static final class Builder { + /** Factory for {@link HlsMediaSource}s. */ + public static final class Factory implements AdsMediaSource.MediaSourceFactory { - private final Uri manifestUri; private final HlsDataSourceFactory hlsDataSourceFactory; private HlsExtractorFactory extractorFactory; - private ParsingLoadable.Parser playlistParser; - private MediaSourceEventListener eventListener; - private Handler eventHandler; + private @Nullable ParsingLoadable.Parser playlistParser; private int minLoadableRetryCount; - private boolean isBuildCalled; + private boolean isCreateCalled; /** - * Creates a {@link Builder} for a {@link HlsMediaSource} with a loadable manifest Uri and - * a {@link DataSource.Factory}. + * Creates a new factory for {@link HlsMediaSource}s. * - * @param manifestUri The {@link Uri} of the HLS manifest. - * @param dataSourceFactory A data source factory that will be wrapped by a - * {@link DefaultHlsDataSourceFactory} to build {@link DataSource}s for manifests, - * segments and keys. - * @return A new builder. + * @param dataSourceFactory A data source factory that will be wrapped by a {@link + * DefaultHlsDataSourceFactory} to create {@link DataSource}s for manifests, segments and + * keys. */ - public static Builder forDataSource(Uri manifestUri, DataSource.Factory dataSourceFactory) { - return new Builder(manifestUri, new DefaultHlsDataSourceFactory(dataSourceFactory)); + public Factory(DataSource.Factory dataSourceFactory) { + this(new DefaultHlsDataSourceFactory(dataSourceFactory)); } /** - * Creates a {@link Builder} for a {@link HlsMediaSource} with a loadable manifest Uri and - * a {@link HlsDataSourceFactory}. + * Creates a new factory for {@link HlsMediaSource}s. * - * @param manifestUri The {@link Uri} of the HLS manifest. - * @param dataSourceFactory An {@link HlsDataSourceFactory} for {@link DataSource}s for + * @param hlsDataSourceFactory An {@link HlsDataSourceFactory} for {@link DataSource}s for * manifests, segments and keys. - * @return A new builder. */ - public static Builder forHlsDataSource(Uri manifestUri, - HlsDataSourceFactory dataSourceFactory) { - return new Builder(manifestUri, dataSourceFactory); - } - - private Builder(Uri manifestUri, HlsDataSourceFactory hlsDataSourceFactory) { - this.manifestUri = manifestUri; - this.hlsDataSourceFactory = hlsDataSourceFactory; - + public Factory(HlsDataSourceFactory hlsDataSourceFactory) { + this.hlsDataSourceFactory = Assertions.checkNotNull(hlsDataSourceFactory); + extractorFactory = HlsExtractorFactory.DEFAULT; minLoadableRetryCount = DEFAULT_MIN_LOADABLE_RETRY_COUNT; } /** - * Sets the factory for {@link Extractor}s for the segments. Default value is - * {@link HlsExtractorFactory#DEFAULT}. + * Sets the factory for {@link Extractor}s for the segments. The default value is {@link + * HlsExtractorFactory#DEFAULT}. * * @param extractorFactory An {@link HlsExtractorFactory} for {@link Extractor}s for the - * segments. - * @return This builder. + * segments. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Builder setExtractorFactory(HlsExtractorFactory extractorFactory) { - this.extractorFactory = extractorFactory; + public Factory setExtractorFactory(HlsExtractorFactory extractorFactory) { + Assertions.checkState(!isCreateCalled); + this.extractorFactory = Assertions.checkNotNull(extractorFactory); return this; } @@ -114,63 +101,71 @@ public final class HlsMediaSource implements MediaSource, * Sets the minimum number of times to retry if a loading error occurs. The default value is * {@link #DEFAULT_MIN_LOADABLE_RETRY_COUNT}. * - * @param minLoadableRetryCount The minimum number of times loads must be retried before - * errors are propagated. - * @return This builder. + * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Builder setMinLoadableRetryCount(int minLoadableRetryCount) { + public Factory setMinLoadableRetryCount(int minLoadableRetryCount) { + Assertions.checkState(!isCreateCalled); this.minLoadableRetryCount = minLoadableRetryCount; return this; } /** - * Sets the listener to respond to adaptive {@link MediaSource} events and the handler to - * deliver these events. - * - * @param eventHandler A handler for events. - * @param eventListener A listener of events. - * @return This builder. - */ - public Builder setEventListener(Handler eventHandler, - MediaSourceEventListener eventListener) { - this.eventHandler = eventHandler; - this.eventListener = eventListener; - return this; - } - - /** - * Sets the parser to parse HLS playlists. The default is an instance of - * {@link HlsPlaylistParser}. + * Sets the parser to parse HLS playlists. The default is an instance of {@link + * HlsPlaylistParser}. * * @param playlistParser A {@link ParsingLoadable.Parser} for HLS playlists. - * @return This builder. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Builder setPlaylistParser(ParsingLoadable.Parser playlistParser) { - this.playlistParser = playlistParser; + public Factory setPlaylistParser(ParsingLoadable.Parser playlistParser) { + Assertions.checkState(!isCreateCalled); + this.playlistParser = Assertions.checkNotNull(playlistParser); return this; } /** - * Builds a new {@link HlsMediaSource} using the current parameters. - *

          - * After this call, the builder should not be re-used. + * Returns a new {@link HlsMediaSource} using the current parameters. Media source events will + * not be delivered. * - * @return The newly built {@link HlsMediaSource}. + * @return The new {@link HlsMediaSource}. */ - public HlsMediaSource build() { - Assertions.checkArgument((eventListener == null) == (eventHandler == null)); - Assertions.checkState(!isBuildCalled); - isBuildCalled = true; - if (extractorFactory == null) { - extractorFactory = HlsExtractorFactory.DEFAULT; - } + public MediaSource createMediaSource(Uri playlistUri) { + return createMediaSource(playlistUri, null, null); + } + + /** + * Returns a new {@link HlsMediaSource} using the current parameters. + * + * @param playlistUri The playlist {@link Uri}. + * @param eventHandler A handler for events. + * @param eventListener A listener of events. + * @return The new {@link HlsMediaSource}. + */ + @Override + public MediaSource createMediaSource( + Uri playlistUri, + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { + isCreateCalled = true; if (playlistParser == null) { playlistParser = new HlsPlaylistParser(); } - return new HlsMediaSource(manifestUri, hlsDataSourceFactory, extractorFactory, - minLoadableRetryCount, eventHandler, eventListener, playlistParser); + return new HlsMediaSource( + playlistUri, + hlsDataSourceFactory, + extractorFactory, + minLoadableRetryCount, + eventHandler, + eventListener, + playlistParser); } + @Override + public int[] getSupportedTypes() { + return new int[] {C.TYPE_HLS}; + } } /** @@ -195,7 +190,7 @@ public final class HlsMediaSource implements MediaSource, * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener An {@link MediaSourceEventListener}. May be null if delivery of events is * not required. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated public HlsMediaSource(Uri manifestUri, DataSource.Factory dataSourceFactory, Handler eventHandler, @@ -213,7 +208,7 @@ public final class HlsMediaSource implements MediaSource, * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener An {@link MediaSourceEventListener}. May be null if delivery of events is * not required. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated public HlsMediaSource(Uri manifestUri, DataSource.Factory dataSourceFactory, @@ -234,7 +229,7 @@ public final class HlsMediaSource implements MediaSource, * @param eventListener An {@link MediaSourceEventListener}. May be null if delivery of events is * not required. * @param playlistParser A {@link ParsingLoadable.Parser} for HLS playlists. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated public HlsMediaSource(Uri manifestUri, HlsDataSourceFactory dataSourceFactory, From babd5750e3411cf78ea597dbbe92f08ecdd54b59 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 11 Dec 2017 05:08:01 -0800 Subject: [PATCH 0939/2472] Use surfaceless context in DummySurface, if available Issue: #3558 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178604607 --- RELEASENOTES.md | 2 + .../exoplayer2/video/DummySurface.java | 108 ++++++++++-------- 2 files changed, 65 insertions(+), 45 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index c702a22ce6..01c91995a7 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -22,6 +22,8 @@ implementations. * CEA-608: Fix handling of row count changes in roll-up mode ([#3513](https://github.com/google/ExoPlayer/issues/3513)). +* Use surfaceless context for secure DummySurface, if available + ([#3558](https://github.com/google/ExoPlayer/issues/3558)). * IMA extension: * Skip ads before the ad preceding the player's initial seek position ([#3527](https://github.com/google/ExoPlayer/issues/3527)). diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java index cc50443296..2c172c086b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java @@ -24,6 +24,7 @@ import static android.opengl.EGL14.EGL_DEPTH_SIZE; import static android.opengl.EGL14.EGL_GREEN_SIZE; import static android.opengl.EGL14.EGL_HEIGHT; import static android.opengl.EGL14.EGL_NONE; +import static android.opengl.EGL14.EGL_NO_SURFACE; import static android.opengl.EGL14.EGL_OPENGL_ES2_BIT; import static android.opengl.EGL14.EGL_RED_SIZE; import static android.opengl.EGL14.EGL_RENDERABLE_TYPE; @@ -56,10 +57,13 @@ import android.os.Handler; import android.os.Handler.Callback; import android.os.HandlerThread; import android.os.Message; +import android.support.annotation.IntDef; import android.util.Log; import android.view.Surface; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import javax.microedition.khronos.egl.EGL10; /** @@ -70,16 +74,27 @@ public final class DummySurface extends Surface { private static final String TAG = "DummySurface"; + private static final String EXTENSION_PROTECTED_CONTENT = "EGL_EXT_protected_content"; + private static final String EXTENSION_SURFACELESS_CONTEXT = "EGL_KHR_surfaceless_context"; + private static final int EGL_PROTECTED_CONTENT_EXT = 0x32C0; - private static boolean secureSupported; - private static boolean secureSupportedInitialized; + @Retention(RetentionPolicy.SOURCE) + @IntDef({SECURE_MODE_NONE, SECURE_MODE_SURFACELESS_CONTEXT, SECURE_MODE_PROTECTED_PBUFFER}) + private @interface SecureMode {} + + private static final int SECURE_MODE_NONE = 0; + private static final int SECURE_MODE_SURFACELESS_CONTEXT = 1; + private static final int SECURE_MODE_PROTECTED_PBUFFER = 2; /** * Whether the surface is secure. */ public final boolean secure; + private static @SecureMode int secureMode; + private static boolean secureModeInitialized; + private final DummySurfaceThread thread; private boolean threadReleased; @@ -90,11 +105,11 @@ public final class DummySurface extends Surface { * @return Whether the device supports secure dummy surfaces. */ public static synchronized boolean isSecureSupported(Context context) { - if (!secureSupportedInitialized) { - secureSupported = Util.SDK_INT >= 24 && enableSecureDummySurfaceV24(context); - secureSupportedInitialized = true; + if (!secureModeInitialized) { + secureMode = Util.SDK_INT < 24 ? SECURE_MODE_NONE : getSecureModeV24(context); + secureModeInitialized = true; } - return secureSupported; + return secureMode != SECURE_MODE_NONE; } /** @@ -113,7 +128,7 @@ public final class DummySurface extends Surface { assertApiLevel17OrHigher(); Assertions.checkState(!secure || isSecureSupported(context)); DummySurfaceThread thread = new DummySurfaceThread(); - return thread.init(secure); + return thread.init(secure ? secureMode : SECURE_MODE_NONE); } private DummySurface(DummySurfaceThread thread, SurfaceTexture surfaceTexture, boolean secure) { @@ -143,33 +158,34 @@ public final class DummySurface extends Surface { } } - /** - * Returns whether use of secure dummy surfaces should be enabled. - * - * @param context Any {@link Context}. - */ @TargetApi(24) - private static boolean enableSecureDummySurfaceV24(Context context) { + private static @SecureMode int getSecureModeV24(Context context) { if (Util.SDK_INT < 26 && ("samsung".equals(Util.MANUFACTURER) || "XT1650".equals(Util.MODEL))) { // Samsung devices running Nougat are known to be broken. See // https://github.com/google/ExoPlayer/issues/3373 and [Internal: b/37197802]. // Moto Z XT1650 is also affected. See // https://github.com/google/ExoPlayer/issues/3215. - return false; + return SECURE_MODE_NONE; } if (Util.SDK_INT < 26 && !context.getPackageManager().hasSystemFeature( PackageManager.FEATURE_VR_MODE_HIGH_PERFORMANCE)) { - // Pre API level 26 devices were not well tested unless they supported VR mode. See - // https://github.com/google/ExoPlayer/issues/3215. - return false; + // Pre API level 26 devices were not well tested unless they supported VR mode. + return SECURE_MODE_NONE; } EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY); String eglExtensions = EGL14.eglQueryString(display, EGL10.EGL_EXTENSIONS); - if (eglExtensions == null || !eglExtensions.contains("EGL_EXT_protected_content")) { - // EGL_EXT_protected_content is required to enable secure dummy surfaces. - return false; + if (eglExtensions == null) { + return SECURE_MODE_NONE; } - return true; + if (!eglExtensions.contains(EXTENSION_PROTECTED_CONTENT)) { + return SECURE_MODE_NONE; + } + // If we can't use surfaceless contexts, we use a protected 1 * 1 pixel buffer surface. This may + // require support for EXT_protected_surface, but in practice it works on some devices that + // don't have that extension. See also https://github.com/google/ExoPlayer/issues/3558. + return eglExtensions.contains(EXTENSION_SURFACELESS_CONTEXT) + ? SECURE_MODE_SURFACELESS_CONTEXT + : SECURE_MODE_PROTECTED_PBUFFER; } private static class DummySurfaceThread extends HandlerThread implements OnFrameAvailableListener, @@ -195,12 +211,12 @@ public final class DummySurface extends Surface { textureIdHolder = new int[1]; } - public DummySurface init(boolean secure) { + public DummySurface init(@SecureMode int secureMode) { start(); handler = new Handler(getLooper(), this); boolean wasInterrupted = false; synchronized (this) { - handler.obtainMessage(MSG_INIT, secure ? 1 : 0, 0).sendToTarget(); + handler.obtainMessage(MSG_INIT, secureMode, 0).sendToTarget(); while (surface == null && initException == null && initError == null) { try { wait(); @@ -236,7 +252,7 @@ public final class DummySurface extends Surface { switch (msg.what) { case MSG_INIT: try { - initInternal(msg.arg1 != 0); + initInternal(/* secureMode= */ msg.arg1); } catch (RuntimeException e) { Log.e(TAG, "Failed to initialize dummy surface", e); initException = e; @@ -266,7 +282,7 @@ public final class DummySurface extends Surface { } } - private void initInternal(boolean secure) { + private void initInternal(@SecureMode int secureMode) { display = eglGetDisplay(EGL_DEFAULT_DISPLAY); Assertions.checkState(display != null, "eglGetDisplay failed"); @@ -294,43 +310,45 @@ public final class DummySurface extends Surface { EGLConfig config = configs[0]; int[] glAttributes; - if (secure) { + if (secureMode == SECURE_MODE_NONE) { glAttributes = new int[] { EGL_CONTEXT_CLIENT_VERSION, 2, - EGL_PROTECTED_CONTENT_EXT, EGL_TRUE, EGL_NONE}; } else { - glAttributes = new int[] { - EGL_CONTEXT_CLIENT_VERSION, 2, - EGL_NONE}; + glAttributes = + new int[] { + EGL_CONTEXT_CLIENT_VERSION, 2, EGL_PROTECTED_CONTENT_EXT, EGL_TRUE, EGL_NONE + }; } context = eglCreateContext(display, config, android.opengl.EGL14.EGL_NO_CONTEXT, glAttributes, 0); Assertions.checkState(context != null, "eglCreateContext failed"); - int[] pbufferAttributes; - if (secure) { - pbufferAttributes = new int[] { - EGL_WIDTH, 1, - EGL_HEIGHT, 1, - EGL_PROTECTED_CONTENT_EXT, EGL_TRUE, - EGL_NONE}; + EGLSurface surface; + if (secureMode == SECURE_MODE_SURFACELESS_CONTEXT) { + surface = EGL_NO_SURFACE; } else { - pbufferAttributes = new int[] { - EGL_WIDTH, 1, - EGL_HEIGHT, 1, - EGL_NONE}; + int[] pbufferAttributes; + if (secureMode == SECURE_MODE_PROTECTED_PBUFFER) { + pbufferAttributes = + new int[] { + EGL_WIDTH, 1, EGL_HEIGHT, 1, EGL_PROTECTED_CONTENT_EXT, EGL_TRUE, EGL_NONE + }; + } else { + pbufferAttributes = new int[] {EGL_WIDTH, 1, EGL_HEIGHT, 1, EGL_NONE}; + } + pbuffer = eglCreatePbufferSurface(display, config, pbufferAttributes, 0); + Assertions.checkState(pbuffer != null, "eglCreatePbufferSurface failed"); + surface = pbuffer; } - pbuffer = eglCreatePbufferSurface(display, config, pbufferAttributes, 0); - Assertions.checkState(pbuffer != null, "eglCreatePbufferSurface failed"); - boolean eglMadeCurrent = eglMakeCurrent(display, pbuffer, pbuffer, context); + boolean eglMadeCurrent = eglMakeCurrent(display, surface, surface, context); Assertions.checkState(eglMadeCurrent, "eglMakeCurrent failed"); glGenTextures(1, textureIdHolder, 0); surfaceTexture = new SurfaceTexture(textureIdHolder[0]); surfaceTexture.setOnFrameAvailableListener(this); - surface = new DummySurface(this, surfaceTexture, secure); + this.surface = new DummySurface(this, surfaceTexture, secureMode != SECURE_MODE_NONE); } private void releaseInternal() { From f31696b79a608328454f1ae20ef8801aba91dfd0 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 11 Dec 2017 05:23:00 -0800 Subject: [PATCH 0940/2472] Make ExtractorMediaSource.Builder a factory for ExtractorMediaSources ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178605481 --- .../exoplayer2/imademo/PlayerManager.java | 3 +- .../exoplayer2/demo/PlayerActivity.java | 5 +- .../exoplayer2/ext/flac/FlacPlaybackTest.java | 10 +- .../exoplayer2/ext/opus/OpusPlaybackTest.java | 10 +- .../exoplayer2/ext/vp9/VpxPlaybackTest.java | 10 +- .../source/ExtractorMediaSource.java | 185 +++++++++--------- .../exoplayer2/source/ads/AdsMediaSource.java | 26 +-- 7 files changed, 117 insertions(+), 132 deletions(-) diff --git a/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java b/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java index 6b840830c5..ec21f6d265 100644 --- a/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java +++ b/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java @@ -70,7 +70,8 @@ import com.google.android.exoplayer2.util.Util; // This is the MediaSource representing the content media (i.e. not the ad). String contentUrl = context.getString(R.string.content_url); MediaSource contentMediaSource = - new ExtractorMediaSource.Builder(Uri.parse(contentUrl), dataSourceFactory).build(); + new ExtractorMediaSource.Factory(dataSourceFactory) + .createMediaSource(Uri.parse(contentUrl)); // Compose the content media source into a new AdsMediaSource with both ads and content. MediaSource mediaSourceWithAds = new AdsMediaSource(contentMediaSource, dataSourceFactory, diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index 215c4708e8..a60ae0c876 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -378,9 +378,8 @@ public class PlayerActivity extends Activity implements OnClickListener, return new HlsMediaSource.Factory(mediaDataSourceFactory) .createMediaSource(uri, mainHandler, eventLogger); case C.TYPE_OTHER: - return new ExtractorMediaSource.Builder(uri, mediaDataSourceFactory) - .setEventListener(mainHandler, eventLogger) - .build(); + return new ExtractorMediaSource.Factory(mediaDataSourceFactory) + .createMediaSource(uri, mainHandler, eventLogger); default: { throw new IllegalStateException("Unsupported type: " + type); } 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 bd6e698dc6..fd18a3b1ae 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 @@ -26,6 +26,7 @@ import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; import com.google.android.exoplayer2.source.ExtractorMediaSource; +import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; @@ -76,10 +77,11 @@ public class FlacPlaybackTest extends InstrumentationTestCase { DefaultTrackSelector trackSelector = new DefaultTrackSelector(); player = ExoPlayerFactory.newInstance(new Renderer[] {audioRenderer}, trackSelector); player.addListener(this); - ExtractorMediaSource mediaSource = new ExtractorMediaSource.Builder( - uri, new DefaultDataSourceFactory(context, "ExoPlayerExtFlacTest")) - .setExtractorsFactory(MatroskaExtractor.FACTORY) - .build(); + MediaSource mediaSource = + new ExtractorMediaSource.Factory( + new DefaultDataSourceFactory(context, "ExoPlayerExtFlacTest")) + .setExtractorsFactory(MatroskaExtractor.FACTORY) + .createMediaSource(uri); player.prepare(mediaSource); player.setPlayWhenReady(true); Looper.loop(); 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 aa61df74d9..d3ab421655 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 @@ -26,6 +26,7 @@ import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; import com.google.android.exoplayer2.source.ExtractorMediaSource; +import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; @@ -76,10 +77,11 @@ public class OpusPlaybackTest extends InstrumentationTestCase { DefaultTrackSelector trackSelector = new DefaultTrackSelector(); player = ExoPlayerFactory.newInstance(new Renderer[] {audioRenderer}, trackSelector); player.addListener(this); - ExtractorMediaSource mediaSource = new ExtractorMediaSource.Builder( - uri, new DefaultDataSourceFactory(context, "ExoPlayerExtOpusTest")) - .setExtractorsFactory(MatroskaExtractor.FACTORY) - .build(); + MediaSource mediaSource = + new ExtractorMediaSource.Factory( + new DefaultDataSourceFactory(context, "ExoPlayerExtOpusTest")) + .setExtractorsFactory(MatroskaExtractor.FACTORY) + .createMediaSource(uri); player.prepare(mediaSource); player.setPlayWhenReady(true); Looper.loop(); 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 746f3d273f..3cc1a1d340 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 @@ -27,6 +27,7 @@ import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; import com.google.android.exoplayer2.source.ExtractorMediaSource; +import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; @@ -105,10 +106,11 @@ public class VpxPlaybackTest extends InstrumentationTestCase { DefaultTrackSelector trackSelector = new DefaultTrackSelector(); player = ExoPlayerFactory.newInstance(new Renderer[] {videoRenderer}, trackSelector); player.addListener(this); - ExtractorMediaSource mediaSource = new ExtractorMediaSource.Builder( - uri, new DefaultDataSourceFactory(context, "ExoPlayerExtVp9Test")) - .setExtractorsFactory(MatroskaExtractor.FACTORY) - .build(); + MediaSource mediaSource = + new ExtractorMediaSource.Factory( + new DefaultDataSourceFactory(context, "ExoPlayerExtVp9Test")) + .setExtractorsFactory(MatroskaExtractor.FACTORY) + .createMediaSource(uri); player.sendMessages(new ExoPlayer.ExoPlayerMessage(videoRenderer, LibvpxVideoRenderer.MSG_SET_OUTPUT_BUFFER_RENDERER, new VpxVideoSurfaceView(context))); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java index 247eacd519..e787c34a9c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java @@ -26,6 +26,7 @@ import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorsFactory; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; +import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; @@ -103,129 +104,113 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe private long timelineDurationUs; private boolean timelineIsSeekable; - /** - * Builder for {@link ExtractorMediaSource}. Each builder instance can only be used once. - */ - public static final class Builder { + /** Factory for {@link ExtractorMediaSource}s. */ + public static final class Factory implements AdsMediaSource.MediaSourceFactory { - private final Uri uri; private final DataSource.Factory dataSourceFactory; - private ExtractorsFactory extractorsFactory; + private @Nullable ExtractorsFactory extractorsFactory; + private @Nullable String customCacheKey; private int minLoadableRetryCount; - @Nullable private Handler eventHandler; - @Nullable private MediaSourceEventListener eventListener; - @Nullable private String customCacheKey; private int continueLoadingCheckIntervalBytes; - private boolean isBuildCalled; + private boolean isCreateCalled; /** - * @param uri The {@link Uri} of the media stream. + * Creates a new factory for {@link ExtractorMediaSource}s. + * * @param dataSourceFactory A factory for {@link DataSource}s to read the media. */ - public Builder(Uri uri, DataSource.Factory dataSourceFactory) { - this.uri = uri; + public Factory(DataSource.Factory dataSourceFactory) { this.dataSourceFactory = dataSourceFactory; - minLoadableRetryCount = MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA; continueLoadingCheckIntervalBytes = DEFAULT_LOADING_CHECK_INTERVAL_BYTES; } + /** + * Sets the factory for {@link Extractor}s to process the media stream. The default value is an + * instance of {@link DefaultExtractorsFactory}. + * + * @param extractorsFactory A factory for {@link Extractor}s to process the media stream. If the + * possible formats are known, pass a factory that instantiates extractors for those + * formats. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. + */ + public Factory setExtractorsFactory(ExtractorsFactory extractorsFactory) { + Assertions.checkState(!isCreateCalled); + this.extractorsFactory = extractorsFactory; + return this; + } + + /** + * Sets the custom key that uniquely identifies the original stream. Used for cache indexing. + * The default value is {@code null}. + * + * @param customCacheKey A custom key that uniquely identifies the original stream. Used for + * cache indexing. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. + */ + public Factory setCustomCacheKey(String customCacheKey) { + Assertions.checkState(!isCreateCalled); + this.customCacheKey = customCacheKey; + return this; + } + /** * Sets the minimum number of times to retry if a loading error occurs. The default value is * {@link #MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA}. * * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. - * @return This builder. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Builder setMinLoadableRetryCount(int minLoadableRetryCount) { + public Factory setMinLoadableRetryCount(int minLoadableRetryCount) { + Assertions.checkState(!isCreateCalled); this.minLoadableRetryCount = minLoadableRetryCount; return this; } /** - * Sets the factory for {@link Extractor}s to process the media stream. Default value is an - * instance of {@link DefaultExtractorsFactory}. - * - * @param extractorsFactory A factory for {@link Extractor}s to process the media stream. If the - * possible formats are known, pass a factory that instantiates extractors for those - * formats. - * @return This builder. - */ - public Builder setExtractorsFactory(ExtractorsFactory extractorsFactory) { - this.extractorsFactory = extractorsFactory; - return this; - } - - /** - * Sets the custom key that uniquely identifies the original stream. Used for cache indexing. - * Default value is null. - * - * @param customCacheKey A custom key that uniquely identifies the original stream. Used for - * cache indexing. - * @return This builder. - */ - public Builder setCustomCacheKey(String customCacheKey) { - this.customCacheKey = customCacheKey; - return this; - } - - /** - * Sets the number of bytes that should be loaded between each invocation of - * {@link MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}. Default value - * is {@link #DEFAULT_LOADING_CHECK_INTERVAL_BYTES}. + * Sets the number of bytes that should be loaded between each invocation of {@link + * MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}. The default value is + * {@link #DEFAULT_LOADING_CHECK_INTERVAL_BYTES}. * * @param continueLoadingCheckIntervalBytes The number of bytes that should be loaded between - * each invocation of - * {@link MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}. - * @return This builder. + * each invocation of {@link + * MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Builder setContinueLoadingCheckIntervalBytes(int continueLoadingCheckIntervalBytes) { + public Factory setContinueLoadingCheckIntervalBytes(int continueLoadingCheckIntervalBytes) { + Assertions.checkState(!isCreateCalled); this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes; return this; } /** - * Sets the listener to respond to {@link ExtractorMediaSource} events and the handler to - * deliver these events. + * Returns a new {@link ExtractorMediaSource} using the current parameters. Media source events + * will not be delivered. * - * @param eventHandler A handler for events. - * @param eventListener A listener of events. - * @return This builder. - * @deprecated Use {@link #setEventListener(Handler, MediaSourceEventListener)}. + * @param uri The {@link Uri}. + * @return The new {@link ExtractorMediaSource}. */ - @Deprecated - public Builder setEventListener(Handler eventHandler, EventListener eventListener) { - this.eventHandler = eventHandler; - this.eventListener = eventListener == null ? null : new EventListenerWrapper(eventListener); - return this; + public MediaSource createMediaSource(Uri uri) { + return createMediaSource(uri, null, null); } /** - * Sets the listener to respond to {@link ExtractorMediaSource} events and the handler to - * deliver these events. + * Returns a new {@link ExtractorMediaSource} using the current parameters. * + * @param uri The {@link Uri}. * @param eventHandler A handler for events. * @param eventListener A listener of events. - * @return This builder. + * @return The new {@link ExtractorMediaSource}. */ - public Builder setEventListener(Handler eventHandler, MediaSourceEventListener eventListener) { - this.eventHandler = eventHandler; - this.eventListener = eventListener; - return this; - } - - /** - * Builds a new {@link ExtractorMediaSource} using the current parameters. - *

          - * After this call, the builder should not be re-used. - * - * @return The newly built {@link ExtractorMediaSource}. - */ - public ExtractorMediaSource build() { - Assertions.checkArgument((eventListener == null) == (eventHandler == null)); - Assertions.checkState(!isBuildCalled); - isBuildCalled = true; + @Override + public MediaSource createMediaSource( + Uri uri, @Nullable Handler eventHandler, @Nullable MediaSourceEventListener eventListener) { + isCreateCalled = true; if (extractorsFactory == null) { extractorsFactory = new DefaultExtractorsFactory(); } @@ -234,6 +219,10 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe continueLoadingCheckIntervalBytes); } + @Override + public int[] getSupportedTypes() { + return new int[] {C.TYPE_OTHER}; + } } /** @@ -244,11 +233,15 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe * Otherwise, pass a {@link DefaultExtractorsFactory} to use default extractors. * @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated - public ExtractorMediaSource(Uri uri, DataSource.Factory dataSourceFactory, - ExtractorsFactory extractorsFactory, Handler eventHandler, EventListener eventListener) { + public ExtractorMediaSource( + Uri uri, + DataSource.Factory dataSourceFactory, + ExtractorsFactory extractorsFactory, + Handler eventHandler, + EventListener eventListener) { this(uri, dataSourceFactory, extractorsFactory, eventHandler, eventListener, null); } @@ -262,11 +255,15 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe * @param eventListener A listener of events. May be null if delivery of events is not required. * @param customCacheKey A custom key that uniquely identifies the original stream. Used for cache * indexing. May be null. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated - public ExtractorMediaSource(Uri uri, DataSource.Factory dataSourceFactory, - ExtractorsFactory extractorsFactory, Handler eventHandler, EventListener eventListener, + public ExtractorMediaSource( + Uri uri, + DataSource.Factory dataSourceFactory, + ExtractorsFactory extractorsFactory, + Handler eventHandler, + EventListener eventListener, String customCacheKey) { this(uri, dataSourceFactory, extractorsFactory, MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA, eventHandler, eventListener, customCacheKey, DEFAULT_LOADING_CHECK_INTERVAL_BYTES); @@ -285,12 +282,18 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe * indexing. May be null. * @param continueLoadingCheckIntervalBytes The number of bytes that should be loaded between each * invocation of {@link MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated - public ExtractorMediaSource(Uri uri, DataSource.Factory dataSourceFactory, - ExtractorsFactory extractorsFactory, int minLoadableRetryCount, Handler eventHandler, - EventListener eventListener, String customCacheKey, int continueLoadingCheckIntervalBytes) { + public ExtractorMediaSource( + Uri uri, + DataSource.Factory dataSourceFactory, + ExtractorsFactory extractorsFactory, + int minLoadableRetryCount, + Handler eventHandler, + EventListener eventListener, + String customCacheKey, + int continueLoadingCheckIntervalBytes) { this( uri, dataSourceFactory, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java index c701d6ca64..5611bedcca 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java @@ -163,7 +163,7 @@ public final class AdsMediaSource implements MediaSource { this.eventListener = eventListener; mainHandler = new Handler(Looper.getMainLooper()); componentListener = new ComponentListener(); - adMediaSourceFactory = new ExtractorMediaSourceFactory(dataSourceFactory); + adMediaSourceFactory = new ExtractorMediaSource.Factory(dataSourceFactory); deferredMediaPeriodByAdMediaSource = new HashMap<>(); period = new Timeline.Period(); adGroupMediaSources = new MediaSource[0][]; @@ -396,28 +396,4 @@ public final class AdsMediaSource implements MediaSource { } - private static final class ExtractorMediaSourceFactory implements MediaSourceFactory { - - private final DataSource.Factory dataSourceFactory; - - public ExtractorMediaSourceFactory(DataSource.Factory dataSourceFactory) { - this.dataSourceFactory = dataSourceFactory; - } - - @Override - public MediaSource createMediaSource( - Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener) { - return new ExtractorMediaSource.Builder(uri, dataSourceFactory) - .setEventListener(handler, listener) - .build(); - } - - @Override - public int[] getSupportedTypes() { - // Only ExtractorMediaSource is supported. - return new int[] {C.TYPE_OTHER}; - } - - } - } From 7a089c3a293e32642e8c81f4ba6dbb1749507619 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 11 Dec 2017 07:19:53 -0800 Subject: [PATCH 0941/2472] Support non-extractor ads in AdsMediaSource and demo apps Issue: #3302 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178615074 --- RELEASENOTES.md | 2 + demos/ima/build.gradle | 2 + .../exoplayer2/imademo/PlayerManager.java | 62 +++++++++++++++++-- .../exoplayer2/demo/PlayerActivity.java | 44 +++++++++---- .../exoplayer2/source/ads/AdsMediaSource.java | 58 ++++++++++++----- 5 files changed, 136 insertions(+), 32 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 01c91995a7..dfc6e3d087 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -25,6 +25,8 @@ * Use surfaceless context for secure DummySurface, if available ([#3558](https://github.com/google/ExoPlayer/issues/3558)). * IMA extension: + * Support non-ExtractorMediaSource ads + ([#3302](https://github.com/google/ExoPlayer/issues/3302)). * Skip ads before the ad preceding the player's initial seek position ([#3527](https://github.com/google/ExoPlayer/issues/3527)). * Fix ad loading when there is no preroll. diff --git a/demos/ima/build.gradle b/demos/ima/build.gradle index c32228de28..536d8d4662 100644 --- a/demos/ima/build.gradle +++ b/demos/ima/build.gradle @@ -43,5 +43,7 @@ android { dependencies { compile project(modulePrefix + 'library-core') compile project(modulePrefix + 'library-ui') + compile project(modulePrefix + 'library-dash') + compile project(modulePrefix + 'library-hls') compile project(modulePrefix + 'extension-ima') } diff --git a/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java b/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java index ec21f6d265..51959451d1 100644 --- a/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java +++ b/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java @@ -17,13 +17,21 @@ package com.google.android.exoplayer2.imademo; import android.content.Context; import android.net.Uri; +import android.os.Handler; +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.C.ContentType; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.ext.ima.ImaAdsLoader; import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.source.ads.AdsMediaSource; +import com.google.android.exoplayer2.source.dash.DashMediaSource; +import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource; +import com.google.android.exoplayer2.source.hls.HlsMediaSource; import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelection; @@ -35,12 +43,12 @@ import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; import com.google.android.exoplayer2.util.Util; -/** - * Manages the {@link ExoPlayer}, the IMA plugin and all video playback. - */ -/* package */ final class PlayerManager { +/** Manages the {@link ExoPlayer}, the IMA plugin and all video playback. */ +/* package */ final class PlayerManager implements AdsMediaSource.MediaSourceFactory { private final ImaAdsLoader adsLoader; + private final DataSource.Factory manifestDataSourceFactory; + private final DataSource.Factory mediaDataSourceFactory; private SimpleExoPlayer player; private long contentPosition; @@ -48,6 +56,14 @@ import com.google.android.exoplayer2.util.Util; public PlayerManager(Context context) { String adTag = context.getString(R.string.ad_tag_url); adsLoader = new ImaAdsLoader(context, Uri.parse(adTag)); + manifestDataSourceFactory = + new DefaultDataSourceFactory( + context, Util.getUserAgent(context, context.getString(R.string.application_name))); + mediaDataSourceFactory = + new DefaultDataSourceFactory( + context, + Util.getUserAgent(context, context.getString(R.string.application_name)), + new DefaultBandwidthMeter()); } public void init(Context context, SimpleExoPlayerView simpleExoPlayerView) { @@ -74,8 +90,14 @@ import com.google.android.exoplayer2.util.Util; .createMediaSource(Uri.parse(contentUrl)); // Compose the content media source into a new AdsMediaSource with both ads and content. - MediaSource mediaSourceWithAds = new AdsMediaSource(contentMediaSource, dataSourceFactory, - adsLoader, simpleExoPlayerView.getOverlayFrameLayout()); + MediaSource mediaSourceWithAds = + new AdsMediaSource( + contentMediaSource, + /* adMediaSourceFactory= */ this, + adsLoader, + simpleExoPlayerView.getOverlayFrameLayout(), + /* eventHandler= */ null, + /* eventListener= */ null); // Prepare the player with the source. player.seekTo(contentPosition); @@ -99,4 +121,32 @@ import com.google.android.exoplayer2.util.Util; adsLoader.release(); } + // AdsMediaSource.MediaSourceFactory implementation. + + @Override + public MediaSource createMediaSource( + Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener) { + @ContentType int type = Util.inferContentType(uri); + switch (type) { + case C.TYPE_DASH: + return new DashMediaSource.Factory( + new DefaultDashChunkSource.Factory(mediaDataSourceFactory), + manifestDataSourceFactory) + .createMediaSource(uri, handler, listener); + case C.TYPE_HLS: + return new HlsMediaSource.Factory(mediaDataSourceFactory) + .createMediaSource(uri, handler, listener); + case C.TYPE_OTHER: + return new ExtractorMediaSource.Factory(mediaDataSourceFactory) + .createMediaSource(uri, handler, listener); + case C.TYPE_SS: + default: + throw new IllegalStateException("Unsupported type: " + type); + } + } + + @Override + public int[] getSupportedTypes() { + return new int[] {C.TYPE_DASH, C.TYPE_HLS, C.TYPE_OTHER}; + } } diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index a60ae0c876..fa3c7d401a 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -23,6 +23,7 @@ import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.text.TextUtils; import android.view.KeyEvent; import android.view.View; @@ -52,6 +53,7 @@ import com.google.android.exoplayer2.source.BehindLiveWindowException; import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.ads.AdsLoader; import com.google.android.exoplayer2.source.ads.AdsMediaSource; @@ -332,7 +334,7 @@ public class PlayerActivity extends Activity implements OnClickListener, } MediaSource[] mediaSources = new MediaSource[uris.length]; for (int i = 0; i < uris.length; i++) { - mediaSources[i] = buildMediaSource(uris[i], extensions[i]); + mediaSources[i] = buildMediaSource(uris[i], extensions[i], mainHandler, eventLogger); } MediaSource mediaSource = mediaSources.length == 1 ? mediaSources[0] : new ConcatenatingMediaSource(mediaSources); @@ -360,26 +362,30 @@ public class PlayerActivity extends Activity implements OnClickListener, updateButtonVisibilities(); } - private MediaSource buildMediaSource(Uri uri, String overrideExtension) { + private MediaSource buildMediaSource( + Uri uri, + String overrideExtension, + @Nullable Handler handler, + @Nullable MediaSourceEventListener listener) { @ContentType int type = TextUtils.isEmpty(overrideExtension) ? Util.inferContentType(uri) : Util.inferContentType("." + overrideExtension); switch (type) { - case C.TYPE_SS: - return new SsMediaSource.Factory( - new DefaultSsChunkSource.Factory(mediaDataSourceFactory), - buildDataSourceFactory(false)) - .createMediaSource(uri, mainHandler, eventLogger); case C.TYPE_DASH: return new DashMediaSource.Factory( new DefaultDashChunkSource.Factory(mediaDataSourceFactory), buildDataSourceFactory(false)) - .createMediaSource(uri, mainHandler, eventLogger); + .createMediaSource(uri, handler, listener); + case C.TYPE_SS: + return new SsMediaSource.Factory( + new DefaultSsChunkSource.Factory(mediaDataSourceFactory), + buildDataSourceFactory(false)) + .createMediaSource(uri, handler, listener); case C.TYPE_HLS: return new HlsMediaSource.Factory(mediaDataSourceFactory) - .createMediaSource(uri, mainHandler, eventLogger); + .createMediaSource(uri, handler, listener); case C.TYPE_OTHER: return new ExtractorMediaSource.Factory(mediaDataSourceFactory) - .createMediaSource(uri, mainHandler, eventLogger); + .createMediaSource(uri, handler, listener); default: { throw new IllegalStateException("Unsupported type: " + type); } @@ -466,8 +472,22 @@ public class PlayerActivity extends Activity implements OnClickListener, // The demo app has a non-null overlay frame layout. simpleExoPlayerView.getOverlayFrameLayout().addView(adUiViewGroup); } - return new AdsMediaSource(mediaSource, mediaDataSourceFactory, adsLoader, adUiViewGroup, - mainHandler, eventLogger); + AdsMediaSource.MediaSourceFactory adMediaSourceFactory = + new AdsMediaSource.MediaSourceFactory() { + @Override + public MediaSource createMediaSource( + Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener) { + return PlayerActivity.this.buildMediaSource( + uri, /* overrideExtension= */ null, handler, listener); + } + + @Override + public int[] getSupportedTypes() { + return new int[] {C.TYPE_DASH, C.TYPE_SS, C.TYPE_HLS, C.TYPE_OTHER}; + } + }; + return new AdsMediaSource( + mediaSource, adMediaSourceFactory, adsLoader, adUiViewGroup, mainHandler, eventLogger); } private void releaseAdsLoader() { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java index 5611bedcca..0980e9d011 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java @@ -96,13 +96,13 @@ public final class AdsMediaSource implements MediaSource { private static final String TAG = "AdsMediaSource"; private final MediaSource contentMediaSource; + private final MediaSourceFactory adMediaSourceFactory; private final AdsLoader adsLoader; private final ViewGroup adUiViewGroup; @Nullable private final Handler eventHandler; @Nullable private final EventListener eventListener; private final Handler mainHandler; private final ComponentListener componentListener; - private final MediaSourceFactory adMediaSourceFactory; private final Map> deferredMediaPeriodByAdMediaSource; private final Timeline.Period period; @@ -119,28 +119,31 @@ public final class AdsMediaSource implements MediaSource { private MediaSource.Listener listener; /** - * Constructs a new source that inserts ads linearly with the content specified by - * {@code contentMediaSource}. - *

          - * Ad media is loaded using {@link ExtractorMediaSource}. If {@code eventListener} is - * non-{@code null} it will be notified of both ad tag and ad media load errors. + * Constructs a new source that inserts ads linearly with the content specified by {@code + * contentMediaSource}. Ad media is loaded using {@link ExtractorMediaSource}. * * @param contentMediaSource The {@link MediaSource} providing the content to play. * @param dataSourceFactory Factory for data sources used to load ad media. * @param adsLoader The loader for ads. * @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI. */ - public AdsMediaSource(MediaSource contentMediaSource, DataSource.Factory dataSourceFactory, - AdsLoader adsLoader, ViewGroup adUiViewGroup) { - this(contentMediaSource, dataSourceFactory, adsLoader, adUiViewGroup, null, null); + public AdsMediaSource( + MediaSource contentMediaSource, + DataSource.Factory dataSourceFactory, + AdsLoader adsLoader, + ViewGroup adUiViewGroup) { + this( + contentMediaSource, + dataSourceFactory, + adsLoader, + adUiViewGroup, + /* eventHandler= */ null, + /* eventListener= */ null); } /** * Constructs a new source that inserts ads linearly with the content specified by {@code - * contentMediaSource}. - * - *

          Ad media is loaded using {@link ExtractorMediaSource}. If {@code eventListener} is - * non-{@code null} it will be notified of both ad tag and ad media load errors. + * contentMediaSource}. Ad media is loaded using {@link ExtractorMediaSource}. * * @param contentMediaSource The {@link MediaSource} providing the content to play. * @param dataSourceFactory Factory for data sources used to load ad media. @@ -156,14 +159,41 @@ public final class AdsMediaSource implements MediaSource { ViewGroup adUiViewGroup, @Nullable Handler eventHandler, @Nullable EventListener eventListener) { + this( + contentMediaSource, + new ExtractorMediaSource.Factory(dataSourceFactory), + adsLoader, + adUiViewGroup, + eventHandler, + eventListener); + } + + /** + * Constructs a new source that inserts ads linearly with the content specified by {@code + * contentMediaSource}. + * + * @param contentMediaSource The {@link MediaSource} providing the content to play. + * @param adMediaSourceFactory Factory for media sources used to load ad media. + * @param adsLoader The loader for ads. + * @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI. + * @param eventHandler A handler for events. May be null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + */ + public AdsMediaSource( + MediaSource contentMediaSource, + MediaSourceFactory adMediaSourceFactory, + AdsLoader adsLoader, + ViewGroup adUiViewGroup, + @Nullable Handler eventHandler, + @Nullable EventListener eventListener) { this.contentMediaSource = contentMediaSource; + this.adMediaSourceFactory = adMediaSourceFactory; this.adsLoader = adsLoader; this.adUiViewGroup = adUiViewGroup; this.eventHandler = eventHandler; this.eventListener = eventListener; mainHandler = new Handler(Looper.getMainLooper()); componentListener = new ComponentListener(); - adMediaSourceFactory = new ExtractorMediaSource.Factory(dataSourceFactory); deferredMediaPeriodByAdMediaSource = new HashMap<>(); period = new Timeline.Period(); adGroupMediaSources = new MediaSource[0][]; From b223988e30c9f176de8a156c9ed49e5cef124b80 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 12 Dec 2017 09:03:15 -0800 Subject: [PATCH 0942/2472] Add Builder for ImaAdsLoader and allow early requestAds Also fix propagation of ad errors that occur when no player is attached. Issue: #3548 Issue: #3556 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178767997 --- RELEASENOTES.md | 4 + .../exoplayer2/ext/ima/ImaAdsLoader.java | 259 +++++++++++++----- 2 files changed, 195 insertions(+), 68 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index dfc6e3d087..1dab497cf2 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -32,6 +32,10 @@ * Fix ad loading when there is no preroll. * Add an option to turn off hiding controls during ad playback ([#3532](https://github.com/google/ExoPlayer/issues/3532)). + * Support specifying an ads response instead of an ad tag + ([#3548](https://github.com/google/ExoPlayer/issues/3548)). + * Support overriding the ad load timeout + ([#3556](https://github.com/google/ExoPlayer/issues/3556)). ### 2.6.0 ### diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index 0fa34e5144..b4bb886175 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -19,6 +19,7 @@ import android.content.Context; import android.net.Uri; import android.os.SystemClock; import android.support.annotation.IntDef; +import android.support.annotation.Nullable; import android.util.Log; import android.view.ViewGroup; import android.webkit.WebView; @@ -65,10 +66,80 @@ import java.util.Map; */ public final class ImaAdsLoader extends Player.DefaultEventListener implements AdsLoader, VideoAdPlayer, ContentProgressProvider, AdErrorListener, AdsLoadedListener, AdEventListener { + static { ExoPlayerLibraryInfo.registerModule("goog.exo.ima"); } + /** Builder for {@link ImaAdsLoader}. */ + public static final class Builder { + + private final Context context; + + private @Nullable ImaSdkSettings imaSdkSettings; + private long vastLoadTimeoutMs; + + /** + * Creates a new builder for {@link ImaAdsLoader}. + * + * @param context The context; + */ + public Builder(Context context) { + this.context = Assertions.checkNotNull(context); + vastLoadTimeoutMs = C.TIME_UNSET; + } + + /** + * Sets the IMA SDK settings. The provided settings instance's player type and version fields + * may be overwritten. + * + *

          If this method is not called the default settings will be used. + * + * @param imaSdkSettings The {@link ImaSdkSettings}. + * @return This builder, for convenience. + */ + public Builder setImaSdkSettings(ImaSdkSettings imaSdkSettings) { + this.imaSdkSettings = Assertions.checkNotNull(imaSdkSettings); + return this; + } + + /** + * Sets the VAST load timeout, in milliseconds. + * + * @param vastLoadTimeoutMs The VAST load timeout, in milliseconds. + * @return This builder, for convenience. + * @see AdsRequest#setVastLoadTimeout(float) + */ + public Builder setVastLoadTimeoutMs(long vastLoadTimeoutMs) { + Assertions.checkArgument(vastLoadTimeoutMs >= 0); + this.vastLoadTimeoutMs = vastLoadTimeoutMs; + return this; + } + + /** + * Returns a new {@link ImaAdsLoader} for the specified ad tag. + * + * @param adTagUri The URI of a compatible ad tag to load. See + * https://developers.google.com/interactive-media-ads/docs/sdks/android/compatibility for + * information on compatible ad tags. + * @return The new {@link ImaAdsLoader}. + */ + public ImaAdsLoader buildForAdTag(Uri adTagUri) { + return new ImaAdsLoader(context, adTagUri, imaSdkSettings, null, vastLoadTimeoutMs); + } + + /** + * Returns a new {@link ImaAdsLoader} with the specified sideloaded ads response. + * + * @param adsResponse The sideloaded VAST, VMAP, or ad rules response to be used instead of + * making a request via an ad tag URL. + * @return The new {@link ImaAdsLoader}. + */ + public ImaAdsLoader buildForAdsResponse(String adsResponse) { + return new ImaAdsLoader(context, null, imaSdkSettings, adsResponse, vastLoadTimeoutMs); + } + } + private static final boolean DEBUG = false; private static final String TAG = "ImaAdsLoader"; @@ -94,9 +165,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A private static final String FOCUS_SKIP_BUTTON_WORKAROUND_JS = "javascript:" + "try{ document.getElementsByClassName(\"videoAdUiSkipButton\")[0].focus(); } catch (e) {}"; - /** - * The state of ad playback based on IMA's calls to {@link #playAd()} and {@link #pauseAd()}. - */ + /** The state of ad playback. */ @Retention(RetentionPolicy.SOURCE) @IntDef({IMA_AD_STATE_NONE, IMA_AD_STATE_PLAYING, IMA_AD_STATE_PAUSED}) private @interface ImaAdState {} @@ -113,7 +182,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A */ private static final int IMA_AD_STATE_PAUSED = 2; - private final Uri adTagUri; + private final @Nullable Uri adTagUri; + private final @Nullable String adsResponse; + private final long vastLoadTimeoutMs; private final Timeline.Period period; private final List adCallbacks; private final ImaSdkFactory imaSdkFactory; @@ -129,6 +200,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A private VideoProgressUpdate lastAdProgress; private AdsManager adsManager; + private AdErrorEvent pendingAdErrorEvent; private Timeline timeline; private long contentDurationMs; private int podIndexOffset; @@ -144,9 +216,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A * Whether IMA has sent an ad event to pause content since the last resume content event. */ private boolean imaPausedContent; - /** - * The current ad playback state based on IMA's calls to {@link #playAd()} and {@link #stopAd()}. - */ + /** The current ad playback state. */ private @ImaAdState int imaAdState; /** * Whether {@link com.google.ads.interactivemedia.v3.api.AdsLoader#contentComplete()} has been @@ -189,13 +259,15 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A /** * Creates a new IMA ads loader. * + *

          If you need to customize the ad request, use {@link ImaAdsLoader.Builder} instead. + * * @param context The context. * @param adTagUri The {@link Uri} of an ad tag compatible with the Android IMA SDK. See * https://developers.google.com/interactive-media-ads/docs/sdks/android/compatibility for * more information. */ public ImaAdsLoader(Context context, Uri adTagUri) { - this(context, adTagUri, null); + this(context, adTagUri, null, null, C.TIME_UNSET); } /** @@ -207,9 +279,23 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A * more information. * @param imaSdkSettings {@link ImaSdkSettings} used to configure the IMA SDK, or {@code null} to * use the default settings. If set, the player type and version fields may be overwritten. + * @deprecated Use {@link ImaAdsLoader.Builder}. */ + @Deprecated public ImaAdsLoader(Context context, Uri adTagUri, ImaSdkSettings imaSdkSettings) { + this(context, adTagUri, imaSdkSettings, null, C.TIME_UNSET); + } + + private ImaAdsLoader( + Context context, + @Nullable Uri adTagUri, + @Nullable ImaSdkSettings imaSdkSettings, + @Nullable String adsResponse, + long vastLoadTimeoutMs) { + Assertions.checkArgument(adTagUri != null || adsResponse != null); this.adTagUri = adTagUri; + this.adsResponse = adsResponse; + this.vastLoadTimeoutMs = vastLoadTimeoutMs; period = new Timeline.Period(); adCallbacks = new ArrayList<>(1); imaSdkFactory = ImaSdkFactory.getInstance(); @@ -238,6 +324,37 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A return adsLoader; } + /** + * Requests ads, if they have not already been requested. Must be called on the main thread. + * + *

          Ads will be requested automatically when the player is prepared if this method has not been + * called, so it is only necessary to call this method if you want to request ads before preparing + * the player + * + * @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI. + */ + public void requestAds(ViewGroup adUiViewGroup) { + if (adPlaybackState != null || adsManager != null || pendingAdRequestContext != null) { + // Ads have already been requested. + return; + } + adDisplayContainer.setAdContainer(adUiViewGroup); + pendingAdRequestContext = new Object(); + AdsRequest request = imaSdkFactory.createAdsRequest(); + if (adTagUri != null) { + request.setAdTagUrl(adTagUri.toString()); + } else /* adsResponse != null */ { + request.setAdsResponse(adsResponse); + } + if (vastLoadTimeoutMs != C.TIME_UNSET) { + request.setVastLoadTimeout(vastLoadTimeoutMs); + } + request.setAdDisplayContainer(adDisplayContainer); + request.setContentProgressProvider(this); + request.setUserRequestContext(pendingAdRequestContext); + adsLoader.requestAds(request); + } + // AdsLoader implementation. @Override @@ -268,14 +385,19 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A lastContentProgress = null; adDisplayContainer.setAdContainer(adUiViewGroup); player.addListener(this); + maybeNotifyAdError(); if (adPlaybackState != null) { + // Pass the ad playback state to the player, and resume ads if necessary. eventListener.onAdPlaybackState(adPlaybackState.copy()); if (imaPausedContent && player.getPlayWhenReady()) { adsManager.resume(); } + } else if (adsManager != null) { + // Ads have loaded but the ads manager is not initialized. + startAdPlayback(); } else { - pendingContentPositionMs = player.getCurrentPosition(); - requestAds(); + // Ads haven't loaded yet, so request them. + requestAds(adUiViewGroup); } } @@ -312,49 +434,13 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A return; } pendingAdRequestContext = null; - - long[] adGroupTimesUs = getAdGroupTimesUs(adsManager.getAdCuePoints()); - adPlaybackState = new AdPlaybackState(adGroupTimesUs); - this.adsManager = adsManager; adsManager.addAdErrorListener(this); adsManager.addAdEventListener(this); - - ImaSdkFactory imaSdkFactory = ImaSdkFactory.getInstance(); - AdsRenderingSettings adsRenderingSettings = imaSdkFactory.createAdsRenderingSettings(); - adsRenderingSettings.setEnablePreloading(ENABLE_PRELOADING); - adsRenderingSettings.setMimeTypes(supportedMimeTypes); - int adGroupIndexForPosition = - getAdGroupIndexForPosition(adGroupTimesUs, C.msToUs(pendingContentPositionMs)); - if (adGroupIndexForPosition == 0) { - podIndexOffset = 0; - } else if (adGroupIndexForPosition == C.INDEX_UNSET) { - pendingContentPositionMs = C.TIME_UNSET; - // There is no preroll and midroll pod indices start at 1. - podIndexOffset = -1; - } else /* adGroupIndexForPosition > 0 */ { - // Skip ad groups before the one at or immediately before the playback position. - for (int i = 0; i < adGroupIndexForPosition; i++) { - adPlaybackState.playedAdGroup(i); - } - // Play ads after the midpoint between the ad to play and the one before it, to avoid issues - // with rounding one of the two ad times. - long adGroupForPositionTimeUs = adGroupTimesUs[adGroupIndexForPosition]; - long adGroupBeforeTimeUs = adGroupTimesUs[adGroupIndexForPosition - 1]; - double midpointTimeUs = (adGroupForPositionTimeUs + adGroupBeforeTimeUs) / 2d; - adsRenderingSettings.setPlayAdsAfterTime(midpointTimeUs / C.MICROS_PER_SECOND); - - // We're removing one or more ads, which means that the earliest ad (if any) will be a - // midroll/postroll. Midroll pod indices start at 1. - podIndexOffset = adGroupIndexForPosition - 1; + if (player != null) { + // If a player is attached already, start playback immediately. + startAdPlayback(); } - - adsManager.init(adsRenderingSettings); - if (DEBUG) { - Log.d(TAG, "Initialized with ads rendering settings: " + adsRenderingSettings); - } - - updateAdPlaybackState(); } // AdEvent.AdEventListener implementation. @@ -384,14 +470,12 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A adGroupIndex = podIndex == -1 ? (adPlaybackState.adGroupCount - 1) : (podIndex + podIndexOffset); int adPosition = adPodInfo.getAdPosition(); - int adCountInAdGroup = adPodInfo.getTotalAds(); + int adCount = adPodInfo.getTotalAds(); adsManager.start(); if (DEBUG) { - Log.d( - TAG, - "Loaded ad " + adPosition + " of " + adCountInAdGroup + " in group " + adGroupIndex); + Log.d(TAG, "Loaded ad " + adPosition + " of " + adCount + " in group " + adGroupIndex); } - adPlaybackState.setAdCount(adGroupIndex, adCountInAdGroup); + adPlaybackState.setAdCount(adGroupIndex, adCount); updateAdPlaybackState(); break; case CONTENT_PAUSE_REQUESTED: @@ -434,14 +518,15 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A Log.d(TAG, "onAdError " + adErrorEvent); } if (adsManager == null) { + // No ads were loaded, so allow playback to start without any ads. pendingAdRequestContext = null; adPlaybackState = new AdPlaybackState(new long[0]); updateAdPlaybackState(); } - if (eventListener != null) { - IOException exception = new IOException("Ad error: " + adErrorEvent, adErrorEvent.getError()); - eventListener.onLoadError(exception); + if (pendingAdErrorEvent == null) { + pendingAdErrorEvent = adErrorEvent; } + maybeNotifyAdError(); } // ContentProgressProvider implementation. @@ -653,18 +738,56 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A // Internal methods. - private void requestAds() { - if (pendingAdRequestContext != null) { - // Ad request already in flight. - return; + private void startAdPlayback() { + ImaSdkFactory imaSdkFactory = ImaSdkFactory.getInstance(); + AdsRenderingSettings adsRenderingSettings = imaSdkFactory.createAdsRenderingSettings(); + adsRenderingSettings.setEnablePreloading(ENABLE_PRELOADING); + adsRenderingSettings.setMimeTypes(supportedMimeTypes); + + // Set up the ad playback state, skipping ads based on the start position as required. + pendingContentPositionMs = player.getCurrentPosition(); + long[] adGroupTimesUs = getAdGroupTimesUs(adsManager.getAdCuePoints()); + adPlaybackState = new AdPlaybackState(adGroupTimesUs); + int adGroupIndexForPosition = + getAdGroupIndexForPosition(adGroupTimesUs, C.msToUs(pendingContentPositionMs)); + if (adGroupIndexForPosition == 0) { + podIndexOffset = 0; + } else if (adGroupIndexForPosition == C.INDEX_UNSET) { + pendingContentPositionMs = C.TIME_UNSET; + // There is no preroll and midroll pod indices start at 1. + podIndexOffset = -1; + } else /* adGroupIndexForPosition > 0 */ { + // Skip ad groups before the one at or immediately before the playback position. + for (int i = 0; i < adGroupIndexForPosition; i++) { + adPlaybackState.playedAdGroup(i); + } + // Play ads after the midpoint between the ad to play and the one before it, to avoid issues + // with rounding one of the two ad times. + long adGroupForPositionTimeUs = adGroupTimesUs[adGroupIndexForPosition]; + long adGroupBeforeTimeUs = adGroupTimesUs[adGroupIndexForPosition - 1]; + double midpointTimeUs = (adGroupForPositionTimeUs + adGroupBeforeTimeUs) / 2d; + adsRenderingSettings.setPlayAdsAfterTime(midpointTimeUs / C.MICROS_PER_SECOND); + + // We're removing one or more ads, which means that the earliest ad (if any) will be a + // midroll/postroll. Midroll pod indices start at 1. + podIndexOffset = adGroupIndexForPosition - 1; + } + + // Start ad playback. + adsManager.init(adsRenderingSettings); + updateAdPlaybackState(); + if (DEBUG) { + Log.d(TAG, "Initialized with ads rendering settings: " + adsRenderingSettings); + } + } + + private void maybeNotifyAdError() { + if (eventListener != null && pendingAdErrorEvent != null) { + IOException exception = + new IOException("Ad error: " + pendingAdErrorEvent, pendingAdErrorEvent.getError()); + eventListener.onLoadError(exception); + pendingAdErrorEvent = null; } - pendingAdRequestContext = new Object(); - AdsRequest request = imaSdkFactory.createAdsRequest(); - request.setAdTagUrl(adTagUri.toString()); - request.setAdDisplayContainer(adDisplayContainer); - request.setContentProgressProvider(this); - request.setUserRequestContext(pendingAdRequestContext); - adsLoader.requestAds(request); } private void updateImaStateForPlayerState() { From f3dc075cabe720002580581463cebf165f1a9ff8 Mon Sep 17 00:00:00 2001 From: bachinger Date: Tue, 12 Dec 2017 11:05:57 -0800 Subject: [PATCH 0943/2472] Propagate extras from queue item to metadata item. norelnotes=true ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178785377 --- .../mediasession/MediaSessionConnector.java | 45 +++++++++++++++---- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java index d80487f2bd..2b4409e0fb 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.ext.mediasession; +import android.graphics.Bitmap; import android.net.Uri; import android.os.Bundle; import android.os.Handler; @@ -330,6 +331,7 @@ public final class MediaSessionConnector { private final ExoPlayerEventListener exoPlayerEventListener; private final MediaSessionCallback mediaSessionCallback; private final PlaybackController playbackController; + private final String metadataExtrasPrefix; private final Map commandMap; private Player player; @@ -356,15 +358,15 @@ public final class MediaSessionConnector { /** * Creates an instance. Must be called on the same thread that is used to construct the player * instances passed to {@link #setPlayer(Player, PlaybackPreparer, CustomActionProvider...)}. - *

          - * Equivalent to {@code MediaSessionConnector(mediaSession, playbackController, true)}. + * + *

          Equivalent to {@code MediaSessionConnector(mediaSession, playbackController, true, null)}. * * @param mediaSession The {@link MediaSessionCompat} to connect to. * @param playbackController A {@link PlaybackController} for handling playback actions. */ - public MediaSessionConnector(MediaSessionCompat mediaSession, - PlaybackController playbackController) { - this(mediaSession, playbackController, true); + public MediaSessionConnector( + MediaSessionCompat mediaSession, PlaybackController playbackController) { + this(mediaSession, playbackController, true, null); } /** @@ -372,17 +374,23 @@ public final class MediaSessionConnector { * instances passed to {@link #setPlayer(Player, PlaybackPreparer, CustomActionProvider...)}. * * @param mediaSession The {@link MediaSessionCompat} to connect to. - * @param playbackController A {@link PlaybackController} for handling playback actions, or - * {@code null} if the connector should handle playback actions directly. + * @param playbackController A {@link PlaybackController} for handling playback actions, or {@code + * null} if the connector should handle playback actions directly. * @param doMaintainMetadata Whether the connector should maintain the metadata of the session. If * {@code false}, you need to maintain the metadata of the media session yourself (provide at * least the duration to allow clients to show a progress bar). + * @param metadataExtrasPrefix A string to prefix extra keys which are propagated from the active + * queue item to the session metadata. */ - public MediaSessionConnector(MediaSessionCompat mediaSession, - PlaybackController playbackController, boolean doMaintainMetadata) { + public MediaSessionConnector( + MediaSessionCompat mediaSession, + PlaybackController playbackController, + boolean doMaintainMetadata, + @Nullable String metadataExtrasPrefix) { this.mediaSession = mediaSession; this.playbackController = playbackController != null ? playbackController : new DefaultPlaybackController(); + this.metadataExtrasPrefix = metadataExtrasPrefix != null ? metadataExtrasPrefix : ""; this.handler = new Handler(Looper.myLooper() != null ? Looper.myLooper() : Looper.getMainLooper()); this.doMaintainMetadata = doMaintainMetadata; @@ -553,6 +561,25 @@ public final class MediaSessionConnector { MediaSessionCompat.QueueItem queueItem = queue.get(i); if (queueItem.getQueueId() == activeQueueItemId) { MediaDescriptionCompat description = queueItem.getDescription(); + Bundle extras = description.getExtras(); + if (extras != null) { + for (String key : extras.keySet()) { + Object value = extras.get(key); + if (value instanceof String) { + builder.putString(metadataExtrasPrefix + key, (String) value); + } else if (value instanceof CharSequence) { + builder.putText(metadataExtrasPrefix + key, (CharSequence) value); + } else if (value instanceof Long) { + builder.putLong(metadataExtrasPrefix + key, (Long) value); + } else if (value instanceof Integer) { + builder.putLong(metadataExtrasPrefix + key, (Integer) value); + } else if (value instanceof Bitmap) { + builder.putBitmap(metadataExtrasPrefix + key, (Bitmap) value); + } else if (value instanceof RatingCompat) { + builder.putRating(metadataExtrasPrefix + key, (RatingCompat) value); + } + } + } if (description.getTitle() != null) { builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, String.valueOf(description.getTitle())); From 6c4bb2cdec71ecfae08af6bf2104e4d9e6d0c780 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 12 Dec 2017 23:43:16 -0800 Subject: [PATCH 0944/2472] Update release notes to reflect builder -> factory change ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178866131 --- RELEASENOTES.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index b71faff349..fce791015f 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -11,9 +11,9 @@ * Replace `DefaultTrackSelector.Parameters` copy methods with a builder. * Allow more flexible loading strategy when playing media containing multiple sub-streams, by allowing injection of custom `CompositeSequenceableLoader` - factories through `DashMediaSource.Builder`, `HlsMediaSource.Builder`, - `SsMediaSource.Builder`, and `MergingMediaSource`. -* Add Builder to `ExtractorMediaSource`, `HlsMediaSource`, `SsMediaSource`, + factories through `DashMediaSource.Factory`, `HlsMediaSource.Factory`, + `SsMediaSource.Factory`, and `MergingMediaSource`. +* Add Factory to `ExtractorMediaSource`, `HlsMediaSource`, `SsMediaSource`, `DashMediaSource`, `SingleSampleMediaSource`. * DASH: * Support in-MPD EventStream. From 318618d7a2412facf1274bd166608b6c2f8f0b7d Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 13 Dec 2017 01:59:28 -0800 Subject: [PATCH 0945/2472] Fix seek/prepare/stop acks when exception is thrown. 1. The player doesn't acknowledge phantom stops when an exception is thrown anymore. 2. It also makes sure it doesn't reset the pendingPrepareCount unless it's actually immediately acknowledging these prepares. 3. It ensures a seek is acknowledged even though an exception is thrown during seeking. Added tests (which previously failed) for all three cases. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178876362 --- .../android/exoplayer2/ExoPlayerTest.java | 100 +++++++++++++++ .../exoplayer2/ExoPlayerImplInternal.java | 121 ++++++++---------- .../testutil/ExoPlayerTestRunner.java | 6 +- 3 files changed, 158 insertions(+), 69 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index 714dfff676..dad891718e 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2; +import com.google.android.exoplayer2.Player.DefaultEventListener; +import com.google.android.exoplayer2.Player.EventListener; import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.TrackGroup; @@ -344,6 +346,39 @@ public final class ExoPlayerTest extends TestCase { assertEquals(Player.STATE_BUFFERING, (int) playbackStatesWhenSeekProcessed.get(3)); } + public void testSeekProcessedCalledWithIllegalSeekPosition() throws Exception { + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSeekProcessedCalledWithIllegalSeekPosition") + .waitForPlaybackState(Player.STATE_BUFFERING) + // Cause an illegal seek exception by seeking to an invalid position while the media + // source is still being prepared and the player doesn't immediately know it will fail. + // Because the media source prepares immediately, the exception will be thrown when the + // player processed the seek. + .seek(/* windowIndex= */ 100, /* positionMs= */ 0) + .waitForPlaybackState(Player.STATE_IDLE) + .build(); + final boolean[] onSeekProcessedCalled = new boolean[1]; + EventListener listener = + new DefaultEventListener() { + @Override + public void onSeekProcessed() { + onSeekProcessedCalled[0] = true; + } + }; + ExoPlayerTestRunner testRunner = + new ExoPlayerTestRunner.Builder() + .setActionSchedule(actionSchedule) + .setEventListener(listener) + .build(); + try { + testRunner.start().blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS); + fail(); + } catch (ExoPlaybackException e) { + // Expected exception. + } + assertTrue(onSeekProcessedCalled[0]); + } + public void testSeekDiscontinuity() throws Exception { FakeTimeline timeline = new FakeTimeline(1); ActionSchedule actionSchedule = new ActionSchedule.Builder("testSeekDiscontinuity") @@ -808,4 +843,69 @@ public final class ExoPlayerTest extends TestCase { testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_SEEK); } + + public void testReprepareAfterPlaybackError() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testReprepareAfterPlaybackError") + .waitForPlaybackState(Player.STATE_BUFFERING) + // Cause an internal exception by seeking to an invalid position while the media source + // is still being prepared and the player doesn't immediately know it will fail. + .seek(/* windowIndex= */ 100, /* positionMs= */ 0) + .waitForPlaybackState(Player.STATE_IDLE) + .prepareSource( + new FakeMediaSource(timeline, /* manifest= */ null), + /* resetPosition= */ false, + /* resetState= */ false) + .build(); + ExoPlayerTestRunner testRunner = + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build(); + try { + testRunner.start().blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS); + fail(); + } catch (ExoPlaybackException e) { + // Expected exception. + } + testRunner.assertTimelinesEqual(timeline, timeline); + testRunner.assertTimelineChangeReasonsEqual( + Player.TIMELINE_CHANGE_REASON_PREPARED, Player.TIMELINE_CHANGE_REASON_PREPARED); + } + + public void testPlaybackErrorDuringSourceInfoRefreshStillUpdatesTimeline() throws Exception { + final Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + final FakeMediaSource mediaSource = + new FakeMediaSource(/* timeline= */ null, /* manifest= */ null); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testPlaybackErrorDuringSourceInfoRefreshStillUpdatesTimeline") + .waitForPlaybackState(Player.STATE_BUFFERING) + // Cause an internal exception by seeking to an invalid position while the media source + // is still being prepared. The error will be thrown while the player handles the new + // source info. + .seek(/* windowIndex= */ 100, /* positionMs= */ 0) + .executeRunnable( + new Runnable() { + @Override + public void run() { + mediaSource.setNewSourceInfo(timeline, /* manifest= */ null); + } + }) + .waitForPlaybackState(Player.STATE_IDLE) + .build(); + ExoPlayerTestRunner testRunner = + new ExoPlayerTestRunner.Builder() + .setMediaSource(mediaSource) + .setActionSchedule(actionSchedule) + .build(); + try { + testRunner.start().blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS); + fail(); + } catch (ExoPlaybackException e) { + // Expected exception. + } + testRunner.assertTimelinesEqual(timeline); + testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 20d75ec1bd..a1fe8c09c5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -328,7 +328,7 @@ import java.io.IOException; setSeekParametersInternal((SeekParameters) msg.obj); return true; case MSG_STOP: - stopInternal(/* reset= */ msg.arg1 != 0); + stopInternal(/* reset= */ msg.arg1 != 0, /* acknowledgeStop= */ true); return true; case MSG_RELEASE: releaseInternal(); @@ -353,19 +353,19 @@ import java.io.IOException; } } catch (ExoPlaybackException e) { Log.e(TAG, "Renderer error.", e); + stopInternal(/* reset= */ false, /* acknowledgeStop= */ false); eventHandler.obtainMessage(MSG_ERROR, e).sendToTarget(); - stopInternal(/* reset= */ false); return true; } catch (IOException e) { Log.e(TAG, "Source error.", e); + stopInternal(/* reset= */ false, /* acknowledgeStop= */ false); eventHandler.obtainMessage(MSG_ERROR, ExoPlaybackException.createForSource(e)).sendToTarget(); - stopInternal(/* reset= */ false); return true; } catch (RuntimeException e) { Log.e(TAG, "Internal runtime error.", e); + stopInternal(/* reset= */ false, /* acknowledgeStop= */ false); eventHandler.obtainMessage(MSG_ERROR, ExoPlaybackException.createForUnexpected(e)) .sendToTarget(); - stopInternal(/* reset= */ false); return true; } } @@ -635,49 +635,50 @@ import java.io.IOException; return; } - Pair periodPosition = resolveSeekPosition(seekPosition); - if (periodPosition == null) { - // The seek position was valid for the timeline that it was performed into, but the - // timeline has changed and a suitable seek position could not be resolved in the new one. - setState(Player.STATE_ENDED); - // Reset, but retain the source so that it can still be used should a seek occur. - resetInternal( - /* releaseMediaSource= */ false, /* resetPosition= */ true, /* resetState= */ false); - eventHandler - .obtainMessage(MSG_SEEK_ACK, /* seekAdjusted */ 1, 0, playbackInfo) - .sendToTarget(); - return; - } - boolean seekPositionAdjusted = seekPosition.windowPositionUs == C.TIME_UNSET; - int periodIndex = periodPosition.first; - long periodPositionUs = periodPosition.second; - long contentPositionUs = periodPositionUs; - MediaPeriodId periodId = - mediaPeriodInfoSequence.resolvePeriodPositionForAds(periodIndex, periodPositionUs); - if (periodId.isAd()) { - seekPositionAdjusted = true; - periodPositionUs = 0; - } try { - if (periodId.equals(playbackInfo.periodId)) { - long adjustedPeriodPositionUs = periodPositionUs; - if (playingPeriodHolder != null) { - adjustedPeriodPositionUs = - playingPeriodHolder.mediaPeriod.getAdjustedSeekPositionUs( - adjustedPeriodPositionUs, SeekParameters.DEFAULT); - } - if ((adjustedPeriodPositionUs / 1000) == (playbackInfo.positionUs / 1000)) { - // Seek will be performed to the current position. Do nothing. - periodPositionUs = playbackInfo.positionUs; - return; - } + Pair periodPosition = resolveSeekPosition(seekPosition); + if (periodPosition == null) { + // The seek position was valid for the timeline that it was performed into, but the + // timeline has changed and a suitable seek position could not be resolved in the new one. + setState(Player.STATE_ENDED); + // Reset, but retain the source so that it can still be used should a seek occur. + resetInternal( + /* releaseMediaSource= */ false, /* resetPosition= */ true, /* resetState= */ false); + seekPositionAdjusted = true; + return; + } + + int periodIndex = periodPosition.first; + long periodPositionUs = periodPosition.second; + long contentPositionUs = periodPositionUs; + MediaPeriodId periodId = + mediaPeriodInfoSequence.resolvePeriodPositionForAds(periodIndex, periodPositionUs); + if (periodId.isAd()) { + seekPositionAdjusted = true; + periodPositionUs = 0; + } + try { + if (periodId.equals(playbackInfo.periodId)) { + long adjustedPeriodPositionUs = periodPositionUs; + if (playingPeriodHolder != null) { + adjustedPeriodPositionUs = + playingPeriodHolder.mediaPeriod.getAdjustedSeekPositionUs( + adjustedPeriodPositionUs, SeekParameters.DEFAULT); + } + if ((adjustedPeriodPositionUs / 1000) == (playbackInfo.positionUs / 1000)) { + // Seek will be performed to the current position. Do nothing. + periodPositionUs = playbackInfo.positionUs; + return; + } + } + long newPeriodPositionUs = seekToPeriodPosition(periodId, periodPositionUs); + seekPositionAdjusted |= periodPositionUs != newPeriodPositionUs; + periodPositionUs = newPeriodPositionUs; + } finally { + playbackInfo = playbackInfo.fromNewPosition(periodId, periodPositionUs, contentPositionUs); } - long newPeriodPositionUs = seekToPeriodPosition(periodId, periodPositionUs); - seekPositionAdjusted |= periodPositionUs != newPeriodPositionUs; - periodPositionUs = newPeriodPositionUs; } finally { - playbackInfo = playbackInfo.fromNewPosition(periodId, periodPositionUs, contentPositionUs); eventHandler.obtainMessage(MSG_SEEK_ACK, seekPositionAdjusted ? 1 : 0, 0, playbackInfo) .sendToTarget(); } @@ -775,12 +776,10 @@ import java.io.IOException; this.seekParameters = seekParameters; } - private void stopInternal(boolean reset) { + private void stopInternal(boolean reset, boolean acknowledgeStop) { resetInternal( /* releaseMediaSource= */ true, /* resetPosition= */ reset, /* resetState= */ reset); - int prepareOrStopAcks = pendingPrepareCount + 1; - pendingPrepareCount = 0; - notifySourceInfoRefresh(prepareOrStopAcks, playbackInfo); + notifySourceInfoRefresh(acknowledgeStop); loadControl.onStopped(); setState(Player.STATE_IDLE); } @@ -1011,15 +1010,13 @@ import java.io.IOException; playbackInfo = playbackInfo.copyWithTimeline(timeline, manifest); if (oldTimeline == null) { - int processedPrepareAcks = pendingPrepareCount; - pendingPrepareCount = 0; if (pendingInitialSeekPosition != null) { Pair periodPosition = resolveSeekPosition(pendingInitialSeekPosition); pendingInitialSeekPosition = null; if (periodPosition == null) { // The seek position was valid for the timeline that it was performed into, but the // timeline has changed and a suitable seek position could not be resolved in the new one. - handleSourceInfoRefreshEndedPlayback(processedPrepareAcks); + handleSourceInfoRefreshEndedPlayback(); } else { int periodIndex = periodPosition.first; long positionUs = periodPosition.second; @@ -1027,11 +1024,11 @@ import java.io.IOException; mediaPeriodInfoSequence.resolvePeriodPositionForAds(periodIndex, positionUs); playbackInfo = playbackInfo.fromNewPosition(periodId, periodId.isAd() ? 0 : positionUs, positionUs); - notifySourceInfoRefresh(processedPrepareAcks); + notifySourceInfoRefresh(); } } else if (playbackInfo.startPositionUs == C.TIME_UNSET) { if (timeline.isEmpty()) { - handleSourceInfoRefreshEndedPlayback(processedPrepareAcks); + handleSourceInfoRefreshEndedPlayback(); } else { Pair defaultPosition = getPeriodPosition(timeline, timeline.getFirstWindowIndex(shuffleModeEnabled), C.TIME_UNSET); @@ -1041,10 +1038,10 @@ import java.io.IOException; startPositionUs); playbackInfo = playbackInfo.fromNewPosition(periodId, periodId.isAd() ? 0 : startPositionUs, startPositionUs); - notifySourceInfoRefresh(processedPrepareAcks); + notifySourceInfoRefresh(); } } else { - notifySourceInfoRefresh(processedPrepareAcks); + notifySourceInfoRefresh(); } return; } @@ -1171,26 +1168,20 @@ import java.io.IOException; } private void handleSourceInfoRefreshEndedPlayback() { - handleSourceInfoRefreshEndedPlayback(0); - } - - private void handleSourceInfoRefreshEndedPlayback(int prepareAcks) { setState(Player.STATE_ENDED); // Reset, but retain the source so that it can still be used should a seek occur. resetInternal( /* releaseMediaSource= */ false, /* resetPosition= */ true, /* resetState= */ false); - notifySourceInfoRefresh(prepareAcks, playbackInfo); + notifySourceInfoRefresh(); } private void notifySourceInfoRefresh() { - notifySourceInfoRefresh(0); + notifySourceInfoRefresh(/* acknowledgeStop= */ false); } - private void notifySourceInfoRefresh(int prepareOrStopAcks) { - notifySourceInfoRefresh(prepareOrStopAcks, playbackInfo); - } - - private void notifySourceInfoRefresh(int prepareOrStopAcks, PlaybackInfo playbackInfo) { + private void notifySourceInfoRefresh(boolean acknowledgeStop) { + int prepareOrStopAcks = pendingPrepareCount + (acknowledgeStop ? 1 : 0); + pendingPrepareCount = 0; eventHandler.obtainMessage(MSG_SOURCE_INFO_REFRESHED, prepareOrStopAcks, 0, playbackInfo) .sendToTarget(); } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java index fddeb60bf0..4905fc2233 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java @@ -478,9 +478,8 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener } /** - * Blocks the current thread until the action schedule finished. Also returns when an - * {@link ExoPlaybackException} is thrown. This does not release the test runner and the test must - * still call {@link #blockUntilEnded(long)}. + * Blocks the current thread until the action schedule finished. This does not release the test + * runner and the test must still call {@link #blockUntilEnded(long)}. * * @param timeoutMs The maximum time to wait for the action schedule to finish. * @return This test runner. @@ -611,7 +610,6 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener while (endedCountDownLatch.getCount() > 0) { endedCountDownLatch.countDown(); } - actionScheduleFinishedCountDownLatch.countDown(); } // Player.EventListener From a092262d0b3f3852affb83253574a2abd8b4998c Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 13 Dec 2017 02:16:15 -0800 Subject: [PATCH 0946/2472] Update release notes for current 2.6.1 feature set ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178877884 --- RELEASENOTES.md | 83 +++++++++++++++++++++++++++---------------------- 1 file changed, 45 insertions(+), 38 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index fce791015f..95eda228ea 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,47 +2,35 @@ ### dev-v2 (not yet released) ### -* Add initial support for chunkless preparation in HLS. This allows an HLS media - source to finish preparation without donwloading any chunks, which might - considerably reduce the initial buffering time - ([#3149](https://github.com/google/ExoPlayer/issues/2980)). -* Add ability for `SequenceableLoader` to reevaluate its buffer and discard - buffered media so that it can be re-buffered in a different quality. -* Replace `DefaultTrackSelector.Parameters` copy methods with a builder. -* Allow more flexible loading strategy when playing media containing multiple - sub-streams, by allowing injection of custom `CompositeSequenceableLoader` - factories through `DashMediaSource.Factory`, `HlsMediaSource.Factory`, - `SsMediaSource.Factory`, and `MergingMediaSource`. -* Add Factory to `ExtractorMediaSource`, `HlsMediaSource`, `SsMediaSource`, - `DashMediaSource`, `SingleSampleMediaSource`. -* DASH: - * Support in-MPD EventStream. - * Support time zone designators in ISO8601 UTCTiming elements - ([#3524](https://github.com/google/ExoPlayer/issues/3524)). -* Allow a back-buffer of media to be retained behind the current playback - position, for fast backward seeking. The back-buffer can be configured by - custom `LoadControl` implementations. +* Player interface: + * Add optional parameter to `stop` to reset the player when stopping. + * Add a reason to `EventListener.onTimelineChanged` to distinguish between + initial preparation, reset and dynamic updates. +* Buffering: + * Allow a back-buffer of media to be retained behind the current playback + position, for fast backward seeking. The back-buffer can be configured by + custom `LoadControl` implementations. + * Add ability for `SequenceableLoader` to reevaluate its buffer and discard + buffered media so that it can be re-buffered in a different quality. + * Allow more flexible loading strategy when playing media containing multiple + sub-streams, by allowing injection of custom `CompositeSequenceableLoader` + factories through `DashMediaSource.Factory`, `HlsMediaSource.Factory`, + `SsMediaSource.Factory`, and `MergingMediaSource`. +* DASH: Support DASH manifest EventStream elements. +* HLS: Add opt-in support for chunkless preparation in HLS. This allows an + HLS source to finish preparation without downloading any chunks, which can + significantly reduce initial buffering time + ([#3149](https://github.com/google/ExoPlayer/issues/3149)). +* DefaultTrackSelector: Replace `DefaultTrackSelector.Parameters` copy methods + with a builder. * New Cast extension: Simplifies toggling between local and Cast playbacks. -* Support 32-bit PCM float output from `DefaultAudioSink`, and add an option to - use this with `FfmpegAudioRenderer`. -* Support extraction and decoding of Dolby Atmos - ([#2465](https://github.com/google/ExoPlayer/issues/2465)). -* Add a reason to `EventListener.onTimelineChanged` to distinguish between - initial preparation, reset and dynamic updates. -* DefaultTrackSelector: Support undefined language text track selection when the - preferred language is not available - ([#2980](https://github.com/google/ExoPlayer/issues/2980)). -* Add optional parameter to `Player.stop` to reset the player when stopping. -* Fix handling of playback parameters changes while paused when followed by a - seek. -* Fix playback of live FLV streams that do not contain an audio track - ([#3188](https://github.com/google/ExoPlayer/issues/3188)). + +### 2.6.1 ### + +* Add factories to `ExtractorMediaSource`, `HlsMediaSource`, `SsMediaSource`, + `DashMediaSource` and `SingleSampleMediaSource`. * Use the same listener `MediaSourceEventListener` for all MediaSource implementations. -* CEA-608: Fix handling of row count changes in roll-up mode - ([#3513](https://github.com/google/ExoPlayer/issues/3513)). -* Use surfaceless context for secure DummySurface, if available - ([#3558](https://github.com/google/ExoPlayer/issues/3558)). * IMA extension: * Support non-ExtractorMediaSource ads ([#3302](https://github.com/google/ExoPlayer/issues/3302)). @@ -55,6 +43,25 @@ ([#3548](https://github.com/google/ExoPlayer/issues/3548)). * Support overriding the ad load timeout ([#3556](https://github.com/google/ExoPlayer/issues/3556)). +* DASH: Support time zone designators in ISO8601 UTCTiming elements + ([#3524](https://github.com/google/ExoPlayer/issues/3524)). +* Audio: + * Support 32-bit PCM float output from `DefaultAudioSink`, and add an option + to use this with `FfmpegAudioRenderer`. + * Support extraction and decoding of Dolby Atmos + ([#2465](https://github.com/google/ExoPlayer/issues/2465)). + * Fix handling of playback parameter changes while paused when followed by a + seek. +* SimpleExoPlayer: Allow multiple audio and video debug listeners. +* DefaultTrackSelector: Support undefined language text track selection when the + preferred language is not available + ([#2980](https://github.com/google/ExoPlayer/issues/2980)). +* Use surfaceless context for secure `DummySurface`, if available + ([#3558](https://github.com/google/ExoPlayer/issues/3558)). +* FLV: Fix playback of live streams that do not contain an audio track + ([#3188](https://github.com/google/ExoPlayer/issues/3188)). +* CEA-608: Fix handling of row count changes in roll-up mode + ([#3513](https://github.com/google/ExoPlayer/issues/3513)). ### 2.6.0 ### From 106e69b3fca7a1e53ad367897d64e9d3b016c7c0 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 13 Dec 2017 08:09:21 -0800 Subject: [PATCH 0947/2472] Check if native libraries are available in tests. If the library is not available, no tracks can be selected and the tests silently run through by immediately switching to ended state without error. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178904347 --- .../android/exoplayer2/ext/flac/FlacExtractorTest.java | 8 ++++++++ .../android/exoplayer2/ext/flac/FlacPlaybackTest.java | 9 ++++++++- .../android/exoplayer2/ext/opus/OpusPlaybackTest.java | 8 ++++++++ .../android/exoplayer2/ext/vp9/VpxPlaybackTest.java | 9 ++++++++- 4 files changed, 32 insertions(+), 2 deletions(-) diff --git a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java index 7b193997c3..57ce487ac7 100644 --- a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java +++ b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java @@ -25,6 +25,14 @@ import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; */ public class FlacExtractorTest extends InstrumentationTestCase { + @Override + protected void setUp() throws Exception { + super.setUp(); + if (!FlacLibrary.isAvailable()) { + fail("Flac library not available."); + } + } + public void testSample() throws Exception { ExtractorAsserts.assertBehavior(new ExtractorFactory() { @Override 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 fd18a3b1ae..b236b706b8 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 @@ -37,6 +37,14 @@ public class FlacPlaybackTest extends InstrumentationTestCase { private static final String BEAR_FLAC_URI = "asset:///bear-flac.mka"; + @Override + protected void setUp() throws Exception { + super.setUp(); + if (!FlacLibrary.isAvailable()) { + fail("Flac library not available."); + } + } + public void testBasicPlayback() throws ExoPlaybackException { playUri(BEAR_FLAC_URI); } @@ -100,7 +108,6 @@ public class FlacPlaybackTest extends InstrumentationTestCase { Looper.myLooper().quit(); } } - } } 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 d3ab421655..c547cff434 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 @@ -37,6 +37,14 @@ public class OpusPlaybackTest extends InstrumentationTestCase { private static final String BEAR_OPUS_URI = "asset:///bear-opus.webm"; + @Override + protected void setUp() throws Exception { + super.setUp(); + if (!OpusLibrary.isAvailable()) { + fail("Opus library not available."); + } + } + public void testBasicPlayback() throws ExoPlaybackException { playUri(BEAR_OPUS_URI); } 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 3cc1a1d340..0a902e2efe 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 @@ -43,6 +43,14 @@ public class VpxPlaybackTest extends InstrumentationTestCase { private static final String TAG = "VpxPlaybackTest"; + @Override + protected void setUp() throws Exception { + super.setUp(); + if (!VpxLibrary.isAvailable()) { + fail("Vpx library not available."); + } + } + public void testBasicPlayback() throws ExoPlaybackException { playUri(BEAR_URI); } @@ -132,7 +140,6 @@ public class VpxPlaybackTest extends InstrumentationTestCase { Looper.myLooper().quit(); } } - } } From a5cd0b87bc504593771b14926f02e84c71d9b490 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 13 Dec 2017 08:32:28 -0800 Subject: [PATCH 0948/2472] Update SingleSampleMediaSource with factory/listener changes - Convert the Builder into a Factory - Have it use MediaSourceEventListener - Also made some misc related fixes to other sources ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178906521 --- .../source/ExtractorMediaPeriod.java | 17 +- .../source/ExtractorMediaSource.java | 4 +- .../source/SingleSampleMediaPeriod.java | 99 +++--- .../source/SingleSampleMediaSource.java | 282 +++++++++++++----- .../exoplayer2/source/hls/HlsMediaSource.java | 4 +- 5 files changed, 280 insertions(+), 126 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java index f8021c24df..4773ac53a1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -443,9 +443,6 @@ import java.util.Arrays; @Override public void onLoadCanceled(ExtractingLoadable loadable, long elapsedRealtimeMs, long loadDurationMs, boolean released) { - if (released) { - return; - } eventDispatcher.loadCanceled( loadable.dataSpec, C.DATA_TYPE_MEDIA, @@ -458,12 +455,14 @@ import java.util.Arrays; elapsedRealtimeMs, loadDurationMs, loadable.bytesLoaded); - copyLengthFromLoader(loadable); - for (SampleQueue sampleQueue : sampleQueues) { - sampleQueue.reset(); - } - if (enabledTrackCount > 0) { - callback.onContinueLoadingRequested(this); + if (!released) { + copyLengthFromLoader(loadable); + for (SampleQueue sampleQueue : sampleQueues) { + sampleQueue.reset(); + } + if (enabledTrackCount > 0) { + callback.onContinueLoadingRequested(this); + } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java index a2d7941c3f..14453653af 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java @@ -195,7 +195,7 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe * @param uri The {@link Uri}. * @return The new {@link ExtractorMediaSource}. */ - public MediaSource createMediaSource(Uri uri) { + public ExtractorMediaSource createMediaSource(Uri uri) { return createMediaSource(uri, null, null); } @@ -208,7 +208,7 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe * @return The new {@link ExtractorMediaSource}. */ @Override - public MediaSource createMediaSource( + public ExtractorMediaSource createMediaSource( Uri uri, @Nullable Handler eventHandler, @Nullable MediaSourceEventListener eventListener) { isCreateCalled = true; if (extractorsFactory == null) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java index 9fff3b4d85..e76de60b86 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java @@ -15,14 +15,12 @@ */ package com.google.android.exoplayer2.source; -import android.net.Uri; -import android.os.Handler; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; -import com.google.android.exoplayer2.source.SingleSampleMediaSource.EventListener; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; @@ -44,14 +42,14 @@ import java.util.Arrays; */ private static final int INITIAL_SAMPLE_SIZE = 1024; - private final Uri uri; + private final DataSpec dataSpec; private final DataSource.Factory dataSourceFactory; private final int minLoadableRetryCount; - private final Handler eventHandler; - private final EventListener eventListener; - private final int eventSourceId; + private final EventDispatcher eventDispatcher; private final TrackGroupArray tracks; private final ArrayList sampleStreams; + private final long durationUs; + // Package private to avoid thunk methods. /* package */ final Loader loader; /* package */ final Format format; @@ -63,16 +61,20 @@ import java.util.Arrays; /* package */ int sampleSize; private int errorCount; - public SingleSampleMediaPeriod(Uri uri, DataSource.Factory dataSourceFactory, Format format, - int minLoadableRetryCount, Handler eventHandler, EventListener eventListener, - int eventSourceId, boolean treatLoadErrorsAsEndOfStream) { - this.uri = uri; + public SingleSampleMediaPeriod( + DataSpec dataSpec, + DataSource.Factory dataSourceFactory, + Format format, + long durationUs, + int minLoadableRetryCount, + EventDispatcher eventDispatcher, + boolean treatLoadErrorsAsEndOfStream) { + this.dataSpec = dataSpec; this.dataSourceFactory = dataSourceFactory; this.format = format; + this.durationUs = durationUs; this.minLoadableRetryCount = minLoadableRetryCount; - this.eventHandler = eventHandler; - this.eventListener = eventListener; - this.eventSourceId = eventSourceId; + this.eventDispatcher = eventDispatcher; this.treatLoadErrorsAsEndOfStream = treatLoadErrorsAsEndOfStream; tracks = new TrackGroupArray(new TrackGroup(format)); sampleStreams = new ArrayList<>(); @@ -131,7 +133,9 @@ import java.util.Arrays; if (loadingFinished || loader.isLoading()) { return false; } - loader.startLoading(new SourceLoadable(uri, dataSourceFactory.createDataSource()), this, + loader.startLoading( + new SourceLoadable(dataSpec, dataSourceFactory.createDataSource()), + this, minLoadableRetryCount); return true; } @@ -169,6 +173,18 @@ import java.util.Arrays; @Override public void onLoadCompleted(SourceLoadable loadable, long elapsedRealtimeMs, long loadDurationMs) { + eventDispatcher.loadCompleted( + loadable.dataSpec, + C.DATA_TYPE_MEDIA, + C.TRACK_TYPE_UNKNOWN, + format, + C.SELECTION_REASON_UNKNOWN, + /* trackSelectionData= */ null, + /* mediaStartTimeUs= */ 0, + durationUs, + elapsedRealtimeMs, + loadDurationMs, + loadable.sampleSize); sampleSize = loadable.sampleSize; sampleData = loadable.sampleData; loadingFinished = true; @@ -178,34 +194,46 @@ import java.util.Arrays; @Override public void onLoadCanceled(SourceLoadable loadable, long elapsedRealtimeMs, long loadDurationMs, boolean released) { - // Do nothing. + eventDispatcher.loadCanceled( + loadable.dataSpec, + C.DATA_TYPE_MEDIA, + C.TRACK_TYPE_UNKNOWN, + /* trackFormat= */ null, + C.SELECTION_REASON_UNKNOWN, + /* trackSelectionData= */ null, + /* mediaStartTimeUs= */ 0, + durationUs, + elapsedRealtimeMs, + loadDurationMs, + loadable.sampleSize); } @Override public int onLoadError(SourceLoadable loadable, long elapsedRealtimeMs, long loadDurationMs, IOException error) { - notifyLoadError(error); errorCount++; - if (treatLoadErrorsAsEndOfStream && errorCount >= minLoadableRetryCount) { + boolean cancel = treatLoadErrorsAsEndOfStream && errorCount >= minLoadableRetryCount; + eventDispatcher.loadError( + loadable.dataSpec, + C.DATA_TYPE_MEDIA, + C.TRACK_TYPE_UNKNOWN, + format, + C.SELECTION_REASON_UNKNOWN, + /* trackSelectionData= */ null, + /* mediaStartTimeUs= */ 0, + durationUs, + elapsedRealtimeMs, + loadDurationMs, + loadable.sampleSize, + error, + /* wasCanceled= */ cancel); + if (cancel) { loadingFinished = true; return Loader.DONT_RETRY; } return Loader.RETRY; } - // Internal methods. - - private void notifyLoadError(final IOException e) { - if (eventHandler != null && eventListener != null) { - eventHandler.post(new Runnable() { - @Override - public void run() { - eventListener.onLoadError(eventSourceId, e); - } - }); - } - } - private final class SampleStreamImpl implements SampleStream { private static final int STREAM_STATE_SEND_FORMAT = 0; @@ -270,14 +298,15 @@ import java.util.Arrays; /* package */ static final class SourceLoadable implements Loadable { - private final Uri uri; + public final DataSpec dataSpec; + private final DataSource dataSource; private int sampleSize; private byte[] sampleData; - public SourceLoadable(Uri uri, DataSource dataSource) { - this.uri = uri; + public SourceLoadable(DataSpec dataSpec, DataSource dataSource) { + this.dataSpec = dataSpec; this.dataSource = dataSource; } @@ -297,7 +326,7 @@ import java.util.Arrays; sampleSize = 0; try { // Create and open the input. - dataSource.open(new DataSpec(uri)); + dataSource.open(dataSpec); // Load the sample data. int result = 0; while (result != C.RESULT_END_OF_INPUT) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java index 51afb8eee9..b92085d15e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java @@ -17,11 +17,14 @@ package com.google.android.exoplayer2.source; import android.net.Uri; import android.os.Handler; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; @@ -32,7 +35,10 @@ public final class SingleSampleMediaSource implements MediaSource { /** * Listener of {@link SingleSampleMediaSource} events. + * + * @deprecated Use {@link MediaSourceEventListener}. */ + @Deprecated public interface EventListener { /** @@ -45,35 +51,23 @@ public final class SingleSampleMediaSource implements MediaSource { } - /** - * Builder for {@link SingleSampleMediaSource}. Each builder instance can only be used once. - */ - public static final class Builder { + /** Factory for {@link SingleSampleMediaSource}. */ + public static final class Factory { - private final Uri uri; private final DataSource.Factory dataSourceFactory; - private final Format format; - private final long durationUs; private int minLoadableRetryCount; - private Handler eventHandler; - private EventListener eventListener; - private int eventSourceId; private boolean treatLoadErrorsAsEndOfStream; - private boolean isBuildCalled; + private boolean isCreateCalled; /** - * @param uri The {@link Uri} of the media stream. + * Creates a factory for {@link SingleSampleMediaSource}s. + * * @param dataSourceFactory The factory from which the {@link DataSource} to read the media will * be obtained. - * @param format The {@link Format} associated with the output track. - * @param durationUs The duration of the media stream in microseconds. */ - public Builder(Uri uri, DataSource.Factory dataSourceFactory, Format format, long durationUs) { - this.uri = uri; - this.dataSourceFactory = dataSourceFactory; - this.format = format; - this.durationUs = durationUs; + public Factory(DataSource.Factory dataSourceFactory) { + this.dataSourceFactory = Assertions.checkNotNull(dataSourceFactory); this.minLoadableRetryCount = DEFAULT_MIN_LOADABLE_RETRY_COUNT; } @@ -82,37 +76,15 @@ public final class SingleSampleMediaSource implements MediaSource { * {@link #DEFAULT_MIN_LOADABLE_RETRY_COUNT}. * * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. - * @return This builder. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Builder setMinLoadableRetryCount(int minLoadableRetryCount) { + public Factory setMinLoadableRetryCount(int minLoadableRetryCount) { + Assertions.checkState(!isCreateCalled); this.minLoadableRetryCount = minLoadableRetryCount; return this; } - /** - * Sets the listener to respond to events and the handler to deliver these events. - * - * @param eventHandler A handler for events. - * @param eventListener A listener of events. - * @return This builder. - */ - public Builder setEventListener(Handler eventHandler, EventListener eventListener) { - this.eventHandler = eventHandler; - this.eventListener = eventListener; - return this; - } - - /** - * Sets an identifier that gets passed to {@code eventListener} methods. The default value is 0. - * - * @param eventSourceId An identifier that gets passed to {@code eventListener} methods. - * @return This builder. - */ - public Builder setEventSourceId(int eventSourceId) { - this.eventSourceId = eventSourceId; - return this; - } - /** * Sets whether load errors will be treated as end-of-stream signal (load errors will not be * propagated). The default value is false. @@ -120,27 +92,53 @@ public final class SingleSampleMediaSource implements MediaSource { * @param treatLoadErrorsAsEndOfStream If true, load errors will not be propagated by sample * streams, treating them as ended instead. If false, load errors will be propagated * normally by {@link SampleStream#maybeThrowError()}. - * @return This builder. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Builder setTreatLoadErrorsAsEndOfStream(boolean treatLoadErrorsAsEndOfStream) { + public Factory setTreatLoadErrorsAsEndOfStream(boolean treatLoadErrorsAsEndOfStream) { + Assertions.checkState(!isCreateCalled); this.treatLoadErrorsAsEndOfStream = treatLoadErrorsAsEndOfStream; return this; } /** - * Builds a new {@link SingleSampleMediaSource} using the current parameters. - *

          - * After this call, the builder should not be re-used. + * Returns a new {@link ExtractorMediaSource} using the current parameters. Media source events + * will not be delivered. * + * @param uri The {@link Uri}. + * @param format The {@link Format} of the media stream. + * @param durationUs The duration of the media stream in microseconds. + * @return The new {@link ExtractorMediaSource}. + */ + public SingleSampleMediaSource createMediaSource(Uri uri, Format format, long durationUs) { + return createMediaSource(uri, format, durationUs, null, null); + } + + /** + * Returns a new {@link SingleSampleMediaSource} using the current parameters. + * + * @param uri The {@link Uri}. + * @param format The {@link Format} of the media stream. + * @param durationUs The duration of the media stream in microseconds. + * @param eventHandler A handler for events. + * @param eventListener A listener of events., Format format, long durationUs * @return The newly built {@link SingleSampleMediaSource}. */ - public SingleSampleMediaSource build() { - Assertions.checkArgument((eventListener == null) == (eventHandler == null)); - Assertions.checkState(!isBuildCalled); - isBuildCalled = true; - - return new SingleSampleMediaSource(uri, dataSourceFactory, format, durationUs, - minLoadableRetryCount, eventHandler, eventListener, eventSourceId, + public SingleSampleMediaSource createMediaSource( + Uri uri, + Format format, + long durationUs, + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { + isCreateCalled = true; + return new SingleSampleMediaSource( + uri, + dataSourceFactory, + format, + durationUs, + minLoadableRetryCount, + eventHandler, + eventListener, treatLoadErrorsAsEndOfStream); } @@ -151,13 +149,12 @@ public final class SingleSampleMediaSource implements MediaSource { */ public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 3; - private final Uri uri; + private final DataSpec dataSpec; private final DataSource.Factory dataSourceFactory; private final Format format; + private final long durationUs; + private final MediaSourceEventListener.EventDispatcher eventDispatcher; private final int minLoadableRetryCount; - private final Handler eventHandler; - private final EventListener eventListener; - private final int eventSourceId; private final boolean treatLoadErrorsAsEndOfStream; private final Timeline timeline; @@ -167,11 +164,11 @@ public final class SingleSampleMediaSource implements MediaSource { * be obtained. * @param format The {@link Format} associated with the output track. * @param durationUs The duration of the media stream in microseconds. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated - public SingleSampleMediaSource(Uri uri, DataSource.Factory dataSourceFactory, Format format, - long durationUs) { + public SingleSampleMediaSource( + Uri uri, DataSource.Factory dataSourceFactory, Format format, long durationUs) { this(uri, dataSourceFactory, format, durationUs, DEFAULT_MIN_LOADABLE_RETRY_COUNT); } @@ -182,12 +179,16 @@ public final class SingleSampleMediaSource implements MediaSource { * @param format The {@link Format} associated with the output track. * @param durationUs The duration of the media stream in microseconds. * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated - public SingleSampleMediaSource(Uri uri, DataSource.Factory dataSourceFactory, Format format, - long durationUs, int minLoadableRetryCount) { - this(uri, dataSourceFactory, format, durationUs, minLoadableRetryCount, null, null, 0, false); + public SingleSampleMediaSource( + Uri uri, + DataSource.Factory dataSourceFactory, + Format format, + long durationUs, + int minLoadableRetryCount) { + this(uri, dataSourceFactory, format, durationUs, minLoadableRetryCount, null, null, false); } /** @@ -203,20 +204,46 @@ public final class SingleSampleMediaSource implements MediaSource { * @param treatLoadErrorsAsEndOfStream If true, load errors will not be propagated by sample * streams, treating them as ended instead. If false, load errors will be propagated normally * by {@link SampleStream#maybeThrowError()}. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated - public SingleSampleMediaSource(Uri uri, DataSource.Factory dataSourceFactory, Format format, - long durationUs, int minLoadableRetryCount, Handler eventHandler, EventListener eventListener, - int eventSourceId, boolean treatLoadErrorsAsEndOfStream) { - this.uri = uri; + public SingleSampleMediaSource( + Uri uri, + DataSource.Factory dataSourceFactory, + Format format, + long durationUs, + int minLoadableRetryCount, + Handler eventHandler, + EventListener eventListener, + int eventSourceId, + boolean treatLoadErrorsAsEndOfStream) { + this( + uri, + dataSourceFactory, + format, + durationUs, + minLoadableRetryCount, + eventHandler, + eventListener == null ? null : new EventListenerWrapper(eventListener, eventSourceId), + treatLoadErrorsAsEndOfStream); + } + + private SingleSampleMediaSource( + Uri uri, + DataSource.Factory dataSourceFactory, + Format format, + long durationUs, + int minLoadableRetryCount, + Handler eventHandler, + MediaSourceEventListener eventListener, + boolean treatLoadErrorsAsEndOfStream) { this.dataSourceFactory = dataSourceFactory; this.format = format; + this.durationUs = durationUs; this.minLoadableRetryCount = minLoadableRetryCount; - this.eventHandler = eventHandler; - this.eventListener = eventListener; - this.eventSourceId = eventSourceId; this.treatLoadErrorsAsEndOfStream = treatLoadErrorsAsEndOfStream; + this.eventDispatcher = new EventDispatcher(eventHandler, eventListener); + dataSpec = new DataSpec(uri); timeline = new SinglePeriodTimeline(durationUs, true, false); } @@ -235,8 +262,14 @@ public final class SingleSampleMediaSource implements MediaSource { @Override public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { Assertions.checkArgument(id.periodIndex == 0); - return new SingleSampleMediaPeriod(uri, dataSourceFactory, format, minLoadableRetryCount, - eventHandler, eventListener, eventSourceId, treatLoadErrorsAsEndOfStream); + return new SingleSampleMediaPeriod( + dataSpec, + dataSourceFactory, + format, + durationUs, + minLoadableRetryCount, + eventDispatcher, + treatLoadErrorsAsEndOfStream); } @Override @@ -249,4 +282,97 @@ public final class SingleSampleMediaSource implements MediaSource { // Do nothing. } + /** + * Wraps a deprecated {@link EventListener}, invoking its callback from the equivalent callback in + * {@link MediaSourceEventListener}. + */ + private static final class EventListenerWrapper implements MediaSourceEventListener { + + private final EventListener eventListener; + private final int eventSourceId; + + public EventListenerWrapper(EventListener eventListener, int eventSourceId) { + this.eventListener = Assertions.checkNotNull(eventListener); + this.eventSourceId = eventSourceId; + } + + @Override + public void onLoadStarted( + DataSpec dataSpec, + int dataType, + int trackType, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + long mediaStartTimeMs, + long mediaEndTimeMs, + long elapsedRealtimeMs) { + // Do nothing. + } + + @Override + public void onLoadCompleted( + DataSpec dataSpec, + int dataType, + int trackType, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + long mediaStartTimeMs, + long mediaEndTimeMs, + long elapsedRealtimeMs, + long loadDurationMs, + long bytesLoaded) { + // Do nothing. + } + + @Override + public void onLoadCanceled( + DataSpec dataSpec, + int dataType, + int trackType, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + long mediaStartTimeMs, + long mediaEndTimeMs, + long elapsedRealtimeMs, + long loadDurationMs, + long bytesLoaded) { + // Do nothing. + } + + @Override + public void onLoadError( + DataSpec dataSpec, + int dataType, + int trackType, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + long mediaStartTimeMs, + long mediaEndTimeMs, + long elapsedRealtimeMs, + long loadDurationMs, + long bytesLoaded, + IOException error, + boolean wasCanceled) { + eventListener.onLoadError(eventSourceId, error); + } + + @Override + public void onUpstreamDiscarded(int trackType, long mediaStartTimeMs, long mediaEndTimeMs) { + // Do nothing. + } + + @Override + public void onDownstreamFormatChanged( + int trackType, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + long mediaTimeMs) { + // Do nothing. + } + } } diff --git a/library/hls/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 index 4c14d2029e..b628807109 100644 --- a/library/hls/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 @@ -170,7 +170,7 @@ public final class HlsMediaSource implements MediaSource, * * @return The new {@link HlsMediaSource}. */ - public MediaSource createMediaSource(Uri playlistUri) { + public HlsMediaSource createMediaSource(Uri playlistUri) { return createMediaSource(playlistUri, null, null); } @@ -183,7 +183,7 @@ public final class HlsMediaSource implements MediaSource, * @return The new {@link HlsMediaSource}. */ @Override - public MediaSource createMediaSource( + public HlsMediaSource createMediaSource( Uri playlistUri, @Nullable Handler eventHandler, @Nullable MediaSourceEventListener eventListener) { From 2cbf0ef0ab978979988fbe35251e644df2faf6a6 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 13 Dec 2017 08:37:27 -0800 Subject: [PATCH 0949/2472] Move playback state, isLoading, and track selector result to PlaybackInfo. This is a no-op change replacing the local variables in ExoPlayerImplInternal with the new ones in PlaybackInfo. *** Use playbackState, isLoading and trackSelectorResult from playbackInfo in ExoPlayerImpl. *** Move duplicated listener notification in ExoPlayerImpl to new method. Also split reset method in one parts which creates the new playback info and one part which notifies the listeners. The increment of the pending operation counter needs to happen in between. *** Use only one pending operation counter in ExoPlayerImpl. This also allows to move onSeekProcessed into the notification chain. *** Replace playback info changing messages to ExoPlayerImpl by single message type. As they are all handled in the same way, they can be summarized to one message. *** Only send playback info change notifications once per playback thread message. This ensures that all concurrent changes actually reach ExoPlayerImpl concurrently. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178907165 --- .../android/exoplayer2/ExoPlayerTest.java | 88 +++--- .../android/exoplayer2/ExoPlayerImpl.java | 290 ++++++++++-------- .../exoplayer2/ExoPlayerImplInternal.java | 247 ++++++++------- .../android/exoplayer2/PlaybackInfo.java | 119 ++++++- .../trackselection/TrackSelectorResult.java | 2 +- 5 files changed, 460 insertions(+), 286 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index dad891718e..a227aa3575 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -56,18 +56,15 @@ public final class ExoPlayerTest extends TestCase { * error. */ public void testPlayEmptyTimeline() throws Exception { - Timeline timeline = new FakeTimeline(/* windowCount= */ 0); + Timeline timeline = Timeline.EMPTY; FakeRenderer renderer = new FakeRenderer(); - // TODO(b/69665207): Without waiting for the timeline update, this test is flaky as the timeline - // update happens after the transition to STATE_ENDED and the test runner may already have been - // stopped. Remove action schedule as soon as state changes are part of the masking and the - // correct order of events is restored. - ActionSchedule actionSchedule = new ActionSchedule.Builder("testPlayEmptyTimeline") - .waitForTimelineChanged(timeline) - .build(); - ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() - .setTimeline(timeline).setRenderers(renderer).setActionSchedule(actionSchedule) - .build().start().blockUntilActionScheduleFinished(TIMEOUT_MS).blockUntilEnded(TIMEOUT_MS); + ExoPlayerTestRunner testRunner = + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setRenderers(renderer) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); testRunner.assertNoPositionDiscontinuities(); testRunner.assertTimelinesEqual(timeline); assertEquals(0, renderer.formatReadCount); @@ -307,21 +304,28 @@ public final class ExoPlayerTest extends TestCase { public void testSeekProcessedCallback() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 2); - ActionSchedule actionSchedule = new ActionSchedule.Builder("testSeekProcessedCallback") - // Initial seek before timeline preparation started. Expect immediate seek processed while - // the player is still in STATE_IDLE. - .pause().seek(5) - // Wait until the media source starts preparing and issue more initial seeks. Expect only - // one seek processed after the source has been prepared. - .waitForPlaybackState(Player.STATE_BUFFERING).seek(2).seek(10) - // Wait until media source prepared and re-seek to same position. Expect a seek processed - // while still being in STATE_READY. - .waitForPlaybackState(Player.STATE_READY).seek(10) - // Start playback and wait until playback reaches second window. - .play().waitForPositionDiscontinuity() - // Seek twice in concession, expecting the first seek to be replaced (and thus except only - // on seek processed callback). - .seek(5).seek(60).build(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSeekProcessedCallback") + // Initial seek. Expect immediate seek processed. + .pause() + .seek(5) + .waitForSeekProcessed() + // Multiple overlapping seeks while the player is still preparing. Expect only one seek + // processed. + .seek(2) + .seek(10) + // Wait until media source prepared and re-seek to same position. Expect a seek + // processed while still being in STATE_READY. + .waitForPlaybackState(Player.STATE_READY) + .seek(10) + // Start playback and wait until playback reaches second window. + .play() + .waitForPositionDiscontinuity() + // Seek twice in concession, expecting the first seek to be replaced (and thus except + // only on seek processed callback). + .seek(5) + .seek(60) + .build(); final List playbackStatesWhenSeekProcessed = new ArrayList<>(); Player.EventListener eventListener = new Player.DefaultEventListener() { private int currentPlaybackState = Player.STATE_IDLE; @@ -340,7 +344,7 @@ public final class ExoPlayerTest extends TestCase { .setTimeline(timeline).setEventListener(eventListener).setActionSchedule(actionSchedule) .build().start().blockUntilEnded(TIMEOUT_MS); assertEquals(4, playbackStatesWhenSeekProcessed.size()); - assertEquals(Player.STATE_IDLE, (int) playbackStatesWhenSeekProcessed.get(0)); + assertEquals(Player.STATE_BUFFERING, (int) playbackStatesWhenSeekProcessed.get(0)); assertEquals(Player.STATE_BUFFERING, (int) playbackStatesWhenSeekProcessed.get(1)); assertEquals(Player.STATE_READY, (int) playbackStatesWhenSeekProcessed.get(2)); assertEquals(Player.STATE_BUFFERING, (int) playbackStatesWhenSeekProcessed.get(3)); @@ -804,19 +808,24 @@ public final class ExoPlayerTest extends TestCase { public void testStopDuringPreparationOverwritesPreparation() throws Exception { Timeline timeline = new FakeTimeline(/* windowCount= */ 1); - ActionSchedule actionSchedule = new ActionSchedule.Builder("testStopOverwritesPrepare") - .waitForPlaybackState(Player.STATE_BUFFERING) - .stop(true) - .build(); - ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() - .setTimeline(timeline) - .setActionSchedule(actionSchedule) - .build() - .start() - .blockUntilEnded(TIMEOUT_MS); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testStopOverwritesPrepare") + .waitForPlaybackState(Player.STATE_BUFFERING) + .seek(0) + .stop(true) + .waitForSeekProcessed() + .build(); + ExoPlayerTestRunner testRunner = + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); testRunner.assertTimelinesEqual(Timeline.EMPTY); testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); - testRunner.assertNoPositionDiscontinuities(); + testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_SEEK); } public void testStopAndSeekAfterStopDoesNotResetTimeline() throws Exception { @@ -855,8 +864,9 @@ public final class ExoPlayerTest extends TestCase { .waitForPlaybackState(Player.STATE_IDLE) .prepareSource( new FakeMediaSource(timeline, /* manifest= */ null), - /* resetPosition= */ false, + /* resetPosition= */ true, /* resetState= */ false) + .waitForPlaybackState(Player.STATE_READY) .build(); ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder() diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index 3fe6cc6eed..2869a7668e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -42,24 +42,19 @@ import java.util.concurrent.CopyOnWriteArraySet; private final Renderer[] renderers; private final TrackSelector trackSelector; - private final TrackSelectionArray emptyTrackSelections; + private final TrackSelectorResult emptyTrackSelectorResult; private final Handler eventHandler; private final ExoPlayerImplInternal internalPlayer; private final CopyOnWriteArraySet listeners; private final Timeline.Window window; private final Timeline.Period period; - private boolean tracksSelected; private boolean playWhenReady; private @RepeatMode int repeatMode; private boolean shuffleModeEnabled; - private int playbackState; - private int pendingSeekAcks; - private int pendingPrepareOrStopAcks; - private boolean waitingForInitialTimeline; - private boolean isLoading; - private TrackGroupArray trackGroups; - private TrackSelectionArray trackSelections; + private int pendingOperationAcks; + private boolean hasPendingPrepare; + private boolean hasPendingSeek; private PlaybackParameters playbackParameters; // Playback information when there is no pending seek/set source operation. @@ -87,13 +82,16 @@ import java.util.concurrent.CopyOnWriteArraySet; this.playWhenReady = false; this.repeatMode = Player.REPEAT_MODE_OFF; this.shuffleModeEnabled = false; - this.playbackState = Player.STATE_IDLE; this.listeners = new CopyOnWriteArraySet<>(); - emptyTrackSelections = new TrackSelectionArray(new TrackSelection[renderers.length]); + emptyTrackSelectorResult = + new TrackSelectorResult( + TrackGroupArray.EMPTY, + new boolean[renderers.length], + new TrackSelectionArray(new TrackSelection[renderers.length]), + null, + new RendererConfiguration[renderers.length]); window = new Timeline.Window(); period = new Timeline.Period(); - trackGroups = TrackGroupArray.EMPTY; - trackSelections = emptyTrackSelections; playbackParameters = PlaybackParameters.DEFAULT; Looper eventLooper = Looper.myLooper() != null ? Looper.myLooper() : Looper.getMainLooper(); eventHandler = new Handler(eventLooper) { @@ -102,9 +100,19 @@ import java.util.concurrent.CopyOnWriteArraySet; ExoPlayerImpl.this.handleEvent(msg); } }; - playbackInfo = new PlaybackInfo(Timeline.EMPTY, null, 0, 0); - internalPlayer = new ExoPlayerImplInternal(renderers, trackSelector, loadControl, playWhenReady, - repeatMode, shuffleModeEnabled, eventHandler, this); + playbackInfo = + new PlaybackInfo(Timeline.EMPTY, /* startPositionUs= */ 0, emptyTrackSelectorResult); + internalPlayer = + new ExoPlayerImplInternal( + renderers, + trackSelector, + emptyTrackSelectorResult, + loadControl, + playWhenReady, + repeatMode, + shuffleModeEnabled, + eventHandler, + this); } @Override @@ -124,7 +132,7 @@ import java.util.concurrent.CopyOnWriteArraySet; @Override public int getPlaybackState() { - return playbackState; + return playbackInfo.playbackState; } @Override @@ -134,10 +142,22 @@ import java.util.concurrent.CopyOnWriteArraySet; @Override public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) { - waitingForInitialTimeline = true; - pendingPrepareOrStopAcks++; - reset(resetPosition, resetState); + PlaybackInfo playbackInfo = + getResetPlaybackInfo( + resetPosition, resetState, /* playbackState= */ Player.STATE_BUFFERING); + // Trigger internal prepare first before updating the playback info and notifying external + // listeners to ensure that new operations issued in the listener notifications reach the + // player after this prepare. The internal player can't change the playback info immediately + // because it uses a callback. + hasPendingPrepare = true; + pendingOperationAcks++; internalPlayer.prepare(mediaSource, resetPosition); + updatePlaybackInfo( + playbackInfo, + /* positionDiscontinuity= */ false, + /* ignored */ DISCONTINUITY_REASON_INTERNAL, + TIMELINE_CHANGE_REASON_RESET, + /* seekProcessed= */ false); } @Override @@ -146,7 +166,7 @@ import java.util.concurrent.CopyOnWriteArraySet; this.playWhenReady = playWhenReady; internalPlayer.setPlayWhenReady(playWhenReady); for (Player.EventListener listener : listeners) { - listener.onPlayerStateChanged(playWhenReady, playbackState); + listener.onPlayerStateChanged(playWhenReady, playbackInfo.playbackState); } } } @@ -190,7 +210,7 @@ import java.util.concurrent.CopyOnWriteArraySet; @Override public boolean isLoading() { - return isLoading; + return playbackInfo.isLoading; } @Override @@ -214,19 +234,22 @@ import java.util.concurrent.CopyOnWriteArraySet; if (windowIndex < 0 || (!timeline.isEmpty() && windowIndex >= timeline.getWindowCount())) { throw new IllegalSeekPositionException(timeline, windowIndex, positionMs); } + hasPendingSeek = true; + pendingOperationAcks++; if (isPlayingAd()) { // TODO: Investigate adding support for seeking during ads. This is complicated to do in // general because the midroll ad preceding the seek destination must be played before the // content position can be played, if a different ad is playing at the moment. Log.w(TAG, "seekTo ignored because an ad is playing"); - if (pendingSeekAcks == 0) { - for (Player.EventListener listener : listeners) { - listener.onSeekProcessed(); - } - } + eventHandler + .obtainMessage( + ExoPlayerImplInternal.MSG_PLAYBACK_INFO_CHANGED, + /* operationAcks */ 1, + /* positionDiscontinuityReason */ C.INDEX_UNSET, + playbackInfo) + .sendToTarget(); return; } - pendingSeekAcks++; maskingWindowIndex = windowIndex; if (timeline.isEmpty()) { maskingWindowPositionMs = positionMs == C.TIME_UNSET ? 0 : positionMs; @@ -273,9 +296,23 @@ import java.util.concurrent.CopyOnWriteArraySet; @Override public void stop(boolean reset) { - pendingPrepareOrStopAcks++; - reset(/* resetPosition= */ reset, /* resetState= */ reset); + PlaybackInfo playbackInfo = + getResetPlaybackInfo( + /* resetPosition= */ reset, + /* resetState= */ reset, + /* playbackState= */ Player.STATE_IDLE); + // Trigger internal stop first before updating the playback info and notifying external + // listeners to ensure that new operations issued in the listener notifications reach the + // player after this stop. The internal player can't change the playback info immediately + // because it uses a callback. + pendingOperationAcks++; internalPlayer.stop(reset); + updatePlaybackInfo( + playbackInfo, + /* positionDiscontinuity= */ false, + /* ignored */ DISCONTINUITY_REASON_INTERNAL, + TIMELINE_CHANGE_REASON_RESET, + /* seekProcessed= */ false); } @Override @@ -421,12 +458,12 @@ import java.util.concurrent.CopyOnWriteArraySet; @Override public TrackGroupArray getCurrentTrackGroups() { - return trackGroups; + return playbackInfo.trackSelectorResult.groups; } @Override public TrackSelectionArray getCurrentTrackSelections() { - return trackSelections; + return playbackInfo.trackSelectorResult.selections; } @Override @@ -442,51 +479,14 @@ 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_STATE_CHANGED: { - playbackState = msg.arg1; - for (Player.EventListener listener : listeners) { - listener.onPlayerStateChanged(playWhenReady, playbackState); - } + case ExoPlayerImplInternal.MSG_PLAYBACK_INFO_CHANGED: + handlePlaybackInfo( + (PlaybackInfo) msg.obj, + /* operationAcks= */ msg.arg1, + /* positionDiscontinuity= */ msg.arg2 != C.INDEX_UNSET, + /* positionDiscontinuityReason= */ msg.arg2); break; - } - case ExoPlayerImplInternal.MSG_LOADING_CHANGED: { - isLoading = msg.arg1 != 0; - for (Player.EventListener listener : listeners) { - listener.onLoadingChanged(isLoading); - } - break; - } - case ExoPlayerImplInternal.MSG_SOURCE_INFO_REFRESHED: { - int prepareOrStopAcks = msg.arg1; - handlePlaybackInfo((PlaybackInfo) msg.obj, prepareOrStopAcks, 0, false, - /* ignored */ DISCONTINUITY_REASON_INTERNAL); - break; - } - case ExoPlayerImplInternal.MSG_TRACKS_CHANGED: { - if (pendingPrepareOrStopAcks == 0) { - TrackSelectorResult trackSelectorResult = (TrackSelectorResult) msg.obj; - tracksSelected = true; - trackGroups = trackSelectorResult.groups; - trackSelections = trackSelectorResult.selections; - trackSelector.onSelectionActivated(trackSelectorResult.info); - for (Player.EventListener listener : listeners) { - listener.onTracksChanged(trackGroups, trackSelections); - } - } - break; - } - case ExoPlayerImplInternal.MSG_SEEK_ACK: { - boolean seekPositionAdjusted = msg.arg1 != 0; - handlePlaybackInfo((PlaybackInfo) msg.obj, 0, 1, seekPositionAdjusted, - DISCONTINUITY_REASON_SEEK_ADJUSTMENT); - break; - } - case ExoPlayerImplInternal.MSG_POSITION_DISCONTINUITY: { - @DiscontinuityReason int discontinuityReason = msg.arg1; - handlePlaybackInfo((PlaybackInfo) msg.obj, 0, 0, true, discontinuityReason); - break; - } - case ExoPlayerImplInternal.MSG_PLAYBACK_PARAMETERS_CHANGED: { + case ExoPlayerImplInternal.MSG_PLAYBACK_PARAMETERS_CHANGED: PlaybackParameters playbackParameters = (PlaybackParameters) msg.obj; if (!this.playbackParameters.equals(playbackParameters)) { this.playbackParameters = playbackParameters; @@ -495,24 +495,24 @@ import java.util.concurrent.CopyOnWriteArraySet; } } break; - } - case ExoPlayerImplInternal.MSG_ERROR: { + case ExoPlayerImplInternal.MSG_ERROR: ExoPlaybackException exception = (ExoPlaybackException) msg.obj; for (Player.EventListener listener : listeners) { listener.onPlayerError(exception); } break; - } default: throw new IllegalStateException(); } } - private void handlePlaybackInfo(PlaybackInfo playbackInfo, int prepareOrStopAcks, int seekAcks, - boolean positionDiscontinuity, @DiscontinuityReason int positionDiscontinuityReason) { - pendingPrepareOrStopAcks -= prepareOrStopAcks; - pendingSeekAcks -= seekAcks; - if (pendingPrepareOrStopAcks == 0 && pendingSeekAcks == 0) { + private void handlePlaybackInfo( + PlaybackInfo playbackInfo, + int operationAcks, + boolean positionDiscontinuity, + @DiscontinuityReason int positionDiscontinuityReason) { + pendingOperationAcks -= operationAcks; + if (pendingOperationAcks == 0) { if (playbackInfo.timeline == null) { // Replace internal null timeline with externally visible empty timeline. playbackInfo = playbackInfo.copyWithTimeline(Timeline.EMPTY, playbackInfo.manifest); @@ -523,37 +523,32 @@ import java.util.concurrent.CopyOnWriteArraySet; playbackInfo.fromNewPosition( playbackInfo.periodId, /* startPositionUs= */ 0, playbackInfo.contentPositionUs); } - boolean timelineOrManifestChanged = this.playbackInfo.timeline != playbackInfo.timeline - || this.playbackInfo.manifest != playbackInfo.manifest; - this.playbackInfo = playbackInfo; - if (timelineOrManifestChanged || waitingForInitialTimeline) { - if (playbackInfo.timeline.isEmpty()) { - // Update the masking variables, which are used when the timeline becomes empty. - maskingPeriodIndex = 0; - maskingWindowIndex = 0; - maskingWindowPositionMs = 0; - } - @Player.TimelineChangeReason int reason = waitingForInitialTimeline - ? Player.TIMELINE_CHANGE_REASON_PREPARED : Player.TIMELINE_CHANGE_REASON_DYNAMIC; - waitingForInitialTimeline = false; - for (Player.EventListener listener : listeners) { - listener.onTimelineChanged(playbackInfo.timeline, playbackInfo.manifest, reason); - } - } - if (positionDiscontinuity) { - for (Player.EventListener listener : listeners) { - listener.onPositionDiscontinuity(positionDiscontinuityReason); - } - } - } - if (pendingSeekAcks == 0 && seekAcks > 0) { - for (Player.EventListener listener : listeners) { - listener.onSeekProcessed(); + if ((!this.playbackInfo.timeline.isEmpty() || hasPendingPrepare) + && playbackInfo.timeline.isEmpty()) { + // Update the masking variables, which are used when the timeline becomes empty. + maskingPeriodIndex = 0; + maskingWindowIndex = 0; + maskingWindowPositionMs = 0; } + @Player.TimelineChangeReason + int timelineChangeReason = + hasPendingPrepare + ? Player.TIMELINE_CHANGE_REASON_PREPARED + : Player.TIMELINE_CHANGE_REASON_DYNAMIC; + boolean seekProcessed = hasPendingSeek; + hasPendingPrepare = false; + hasPendingSeek = false; + updatePlaybackInfo( + playbackInfo, + positionDiscontinuity, + positionDiscontinuityReason, + timelineChangeReason, + seekProcessed); } } - private void reset(boolean resetPosition, boolean resetState) { + private PlaybackInfo getResetPlaybackInfo( + boolean resetPosition, boolean resetState, int playbackState) { if (resetPosition) { maskingWindowIndex = 0; maskingPeriodIndex = 0; @@ -563,22 +558,62 @@ import java.util.concurrent.CopyOnWriteArraySet; maskingPeriodIndex = getCurrentPeriodIndex(); maskingWindowPositionMs = getCurrentPosition(); } - if (resetState) { - if (!playbackInfo.timeline.isEmpty() || playbackInfo.manifest != null) { - playbackInfo = playbackInfo.copyWithTimeline(Timeline.EMPTY, null); - for (Player.EventListener listener : listeners) { - listener.onTimelineChanged(playbackInfo.timeline, playbackInfo.manifest, - Player.TIMELINE_CHANGE_REASON_RESET); - } + return new PlaybackInfo( + resetState ? Timeline.EMPTY : playbackInfo.timeline, + resetState ? null : playbackInfo.manifest, + playbackInfo.periodId, + playbackInfo.startPositionUs, + playbackInfo.contentPositionUs, + playbackState, + /* isLoading= */ false, + resetState ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult); + } + + private void updatePlaybackInfo( + PlaybackInfo newPlaybackInfo, + boolean positionDiscontinuity, + @Player.DiscontinuityReason int positionDiscontinuityReason, + @Player.TimelineChangeReason int timelineChangeReason, + boolean seekProcessed) { + boolean timelineOrManifestChanged = + playbackInfo.timeline != newPlaybackInfo.timeline + || playbackInfo.manifest != newPlaybackInfo.manifest; + boolean playbackStateChanged = playbackInfo.playbackState != newPlaybackInfo.playbackState; + boolean isLoadingChanged = playbackInfo.isLoading != newPlaybackInfo.isLoading; + boolean trackSelectorResultChanged = + this.playbackInfo.trackSelectorResult != newPlaybackInfo.trackSelectorResult; + playbackInfo = newPlaybackInfo; + if (timelineOrManifestChanged || timelineChangeReason == TIMELINE_CHANGE_REASON_PREPARED) { + for (Player.EventListener listener : listeners) { + listener.onTimelineChanged( + playbackInfo.timeline, playbackInfo.manifest, timelineChangeReason); } - if (tracksSelected) { - tracksSelected = false; - trackGroups = TrackGroupArray.EMPTY; - trackSelections = emptyTrackSelections; - trackSelector.onSelectionActivated(null); - for (Player.EventListener listener : listeners) { - listener.onTracksChanged(trackGroups, trackSelections); - } + } + if (positionDiscontinuity) { + for (Player.EventListener listener : listeners) { + listener.onPositionDiscontinuity(positionDiscontinuityReason); + } + } + if (trackSelectorResultChanged) { + trackSelector.onSelectionActivated(playbackInfo.trackSelectorResult.info); + for (Player.EventListener listener : listeners) { + listener.onTracksChanged( + playbackInfo.trackSelectorResult.groups, playbackInfo.trackSelectorResult.selections); + } + } + if (isLoadingChanged) { + for (Player.EventListener listener : listeners) { + listener.onLoadingChanged(playbackInfo.isLoading); + } + } + if (playbackStateChanged) { + for (Player.EventListener listener : listeners) { + listener.onPlayerStateChanged(playWhenReady, playbackInfo.playbackState); + } + } + if (seekProcessed) { + for (Player.EventListener listener : listeners) { + listener.onSeekProcessed(); } } } @@ -593,7 +628,6 @@ import java.util.concurrent.CopyOnWriteArraySet; } private boolean shouldMaskPosition() { - return playbackInfo.timeline.isEmpty() || pendingSeekAcks > 0 || pendingPrepareOrStopAcks > 0; + return playbackInfo.timeline.isEmpty() || pendingOperationAcks > 0; } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index a1fe8c09c5..b52696533d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -27,6 +27,7 @@ import android.util.Pair; import com.google.android.exoplayer2.DefaultMediaClock.PlaybackParameterListener; import com.google.android.exoplayer2.ExoPlayer.ExoPlayerMessage; import com.google.android.exoplayer2.MediaPeriodInfoSequence.MediaPeriodInfo; +import com.google.android.exoplayer2.Player.DiscontinuityReason; import com.google.android.exoplayer2.source.ClippingMediaPeriod; import com.google.android.exoplayer2.source.EmptySampleStream; import com.google.android.exoplayer2.source.MediaPeriod; @@ -51,14 +52,9 @@ import java.io.IOException; private static final String TAG = "ExoPlayerImplInternal"; // External messages - public static final int MSG_STATE_CHANGED = 0; - public static final int MSG_LOADING_CHANGED = 1; - public static final int MSG_TRACKS_CHANGED = 2; - public static final int MSG_SEEK_ACK = 3; - public static final int MSG_POSITION_DISCONTINUITY = 4; - public static final int MSG_SOURCE_INFO_REFRESHED = 5; - public static final int MSG_PLAYBACK_PARAMETERS_CHANGED = 6; - public static final int MSG_ERROR = 7; + public static final int MSG_PLAYBACK_INFO_CHANGED = 0; + public static final int MSG_PLAYBACK_PARAMETERS_CHANGED = 1; + public static final int MSG_ERROR = 2; // Internal messages private static final int MSG_PREPARE = 0; @@ -99,6 +95,7 @@ import java.io.IOException; private final Renderer[] renderers; private final RendererCapabilities[] rendererCapabilities; private final TrackSelector trackSelector; + private final TrackSelectorResult emptyTrackSelectorResult; private final LoadControl loadControl; private final Handler handler; private final HandlerThread internalPlaybackThread; @@ -110,6 +107,7 @@ import java.io.IOException; private final long backBufferDurationUs; private final boolean retainBackBufferFromKeyframe; private final DefaultMediaClock mediaClock; + private final PlaybackInfoUpdate playbackInfoUpdate; @SuppressWarnings("unused") private SeekParameters seekParameters; @@ -120,8 +118,6 @@ import java.io.IOException; private boolean released; private boolean playWhenReady; private boolean rebuffering; - private boolean isLoading; - private int state; private @Player.RepeatMode int repeatMode; private boolean shuffleModeEnabled; private int customMessagesSent; @@ -136,24 +132,34 @@ import java.io.IOException; private MediaPeriodHolder readingPeriodHolder; private MediaPeriodHolder playingPeriodHolder; - public ExoPlayerImplInternal(Renderer[] renderers, TrackSelector trackSelector, - LoadControl loadControl, boolean playWhenReady, @Player.RepeatMode int repeatMode, - boolean shuffleModeEnabled, Handler eventHandler, ExoPlayer player) { + public ExoPlayerImplInternal( + Renderer[] renderers, + TrackSelector trackSelector, + TrackSelectorResult emptyTrackSelectorResult, + LoadControl loadControl, + boolean playWhenReady, + @Player.RepeatMode int repeatMode, + boolean shuffleModeEnabled, + Handler eventHandler, + ExoPlayer player) { this.renderers = renderers; this.trackSelector = trackSelector; + this.emptyTrackSelectorResult = emptyTrackSelectorResult; this.loadControl = loadControl; this.playWhenReady = playWhenReady; this.repeatMode = repeatMode; this.shuffleModeEnabled = shuffleModeEnabled; this.eventHandler = eventHandler; - this.state = Player.STATE_IDLE; this.player = player; backBufferDurationUs = loadControl.getBackBufferDurationUs(); retainBackBufferFromKeyframe = loadControl.retainBackBufferFromKeyframe(); seekParameters = SeekParameters.DEFAULT; - playbackInfo = new PlaybackInfo(null, null, 0, C.TIME_UNSET); + playbackInfo = + new PlaybackInfo( + /* timeline= */ null, /* startPositionUs= */ C.TIME_UNSET, emptyTrackSelectorResult); + playbackInfoUpdate = new PlaybackInfoUpdate(); rendererCapabilities = new RendererCapabilities[renderers.length]; for (int i = 0; i < renderers.length; i++) { renderers[i].setIndex(i); @@ -305,84 +311,99 @@ import java.io.IOException; switch (msg.what) { case MSG_PREPARE: prepareInternal((MediaSource) msg.obj, msg.arg1 != 0); - return true; + break; case MSG_SET_PLAY_WHEN_READY: setPlayWhenReadyInternal(msg.arg1 != 0); - return true; + break; case MSG_SET_REPEAT_MODE: setRepeatModeInternal(msg.arg1); - return true; + break; case MSG_SET_SHUFFLE_ENABLED: setShuffleModeEnabledInternal(msg.arg1 != 0); - return true; + break; case MSG_DO_SOME_WORK: doSomeWork(); - return true; + break; case MSG_SEEK_TO: seekToInternal((SeekPosition) msg.obj); - return true; + break; case MSG_SET_PLAYBACK_PARAMETERS: setPlaybackParametersInternal((PlaybackParameters) msg.obj); - return true; + break; case MSG_SET_SEEK_PARAMETERS: setSeekParametersInternal((SeekParameters) msg.obj); - return true; + break; case MSG_STOP: stopInternal(/* reset= */ msg.arg1 != 0, /* acknowledgeStop= */ true); - return true; + break; case MSG_RELEASE: releaseInternal(); - return true; + break; case MSG_PERIOD_PREPARED: handlePeriodPrepared((MediaPeriod) msg.obj); - return true; + break; case MSG_REFRESH_SOURCE_INFO: handleSourceInfoRefreshed((MediaSourceRefreshInfo) msg.obj); - return true; + break; case MSG_SOURCE_CONTINUE_LOADING_REQUESTED: handleContinueLoadingRequested((MediaPeriod) msg.obj); - return true; + break; case MSG_TRACK_SELECTION_INVALIDATED: reselectTracksInternal(); - return true; + break; case MSG_CUSTOM: sendMessagesInternal((ExoPlayerMessage[]) msg.obj); - return true; + break; default: return false; } + maybeNotifyPlaybackInfoChanged(); } catch (ExoPlaybackException e) { Log.e(TAG, "Renderer error.", e); stopInternal(/* reset= */ false, /* acknowledgeStop= */ false); eventHandler.obtainMessage(MSG_ERROR, e).sendToTarget(); - return true; + maybeNotifyPlaybackInfoChanged(); } catch (IOException e) { Log.e(TAG, "Source error.", e); stopInternal(/* reset= */ false, /* acknowledgeStop= */ false); eventHandler.obtainMessage(MSG_ERROR, ExoPlaybackException.createForSource(e)).sendToTarget(); - return true; + maybeNotifyPlaybackInfoChanged(); } catch (RuntimeException e) { Log.e(TAG, "Internal runtime error.", e); stopInternal(/* reset= */ false, /* acknowledgeStop= */ false); eventHandler.obtainMessage(MSG_ERROR, ExoPlaybackException.createForUnexpected(e)) .sendToTarget(); - return true; + maybeNotifyPlaybackInfoChanged(); } + return true; } // Private methods. private void setState(int state) { - if (this.state != state) { - this.state = state; - eventHandler.obtainMessage(MSG_STATE_CHANGED, state, 0).sendToTarget(); + if (playbackInfo.playbackState != state) { + playbackInfo = playbackInfo.copyWithPlaybackState(state); } } private void setIsLoading(boolean isLoading) { - if (this.isLoading != isLoading) { - this.isLoading = isLoading; - eventHandler.obtainMessage(MSG_LOADING_CHANGED, isLoading ? 1 : 0, 0).sendToTarget(); + if (playbackInfo.isLoading != isLoading) { + playbackInfo = playbackInfo.copyWithIsLoading(isLoading); + } + } + + private void maybeNotifyPlaybackInfoChanged() { + if (playbackInfoUpdate.hasPendingUpdate(playbackInfo)) { + eventHandler + .obtainMessage( + MSG_PLAYBACK_INFO_CHANGED, + playbackInfoUpdate.operationAcks, + playbackInfoUpdate.positionDiscontinuity + ? playbackInfoUpdate.discontinuityReason + : C.INDEX_UNSET, + playbackInfo) + .sendToTarget(); + playbackInfoUpdate.reset(playbackInfo); } } @@ -403,10 +424,10 @@ import java.io.IOException; stopRenderers(); updatePlaybackPositions(); } else { - if (state == Player.STATE_READY) { + if (playbackInfo.playbackState == Player.STATE_READY) { startRenderers(); handler.sendEmptyMessage(MSG_DO_SOME_WORK); - } else if (state == Player.STATE_BUFFERING) { + } else if (playbackInfo.playbackState == Player.STATE_BUFFERING) { handler.sendEmptyMessage(MSG_DO_SOME_WORK); } } @@ -474,10 +495,9 @@ import java.io.IOException; MediaPeriodId periodId = playingPeriodHolder.info.id; long newPositionUs = seekToPeriodPosition(periodId, playbackInfo.positionUs); if (newPositionUs != playbackInfo.positionUs) { - playbackInfo = playbackInfo.fromNewPosition(periodId, newPositionUs, - playbackInfo.contentPositionUs); - eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, Player.DISCONTINUITY_REASON_INTERNAL, - 0, playbackInfo).sendToTarget(); + playbackInfo = + playbackInfo.fromNewPosition(periodId, newPositionUs, playbackInfo.contentPositionUs); + playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_INTERNAL); } } } @@ -511,8 +531,7 @@ import java.io.IOException; if (periodPositionUs != playbackInfo.positionUs) { playbackInfo = playbackInfo.fromNewPosition(playbackInfo.periodId, periodPositionUs, playbackInfo.contentPositionUs); - eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, Player.DISCONTINUITY_REASON_INTERNAL, - 0, playbackInfo).sendToTarget(); + playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_INTERNAL); } } else { rendererPositionUs = mediaClock.syncAndGetPositionUs(); @@ -575,7 +594,7 @@ import java.io.IOException; && playingPeriodHolder.info.isFinal) { setState(Player.STATE_ENDED); stopRenderers(); - } else if (state == Player.STATE_BUFFERING) { + } else if (playbackInfo.playbackState == Player.STATE_BUFFERING) { float playbackSpeed = mediaClock.getPlaybackParameters().speed; boolean isNewlyReady = enabledRenderers.length > 0 ? (allRenderersReadyOrEnded && loadingPeriodHolder.haveSufficientBuffer( @@ -587,7 +606,7 @@ import java.io.IOException; startRenderers(); } } - } else if (state == Player.STATE_READY) { + } else if (playbackInfo.playbackState == Player.STATE_READY) { boolean isStillReady = enabledRenderers.length > 0 ? allRenderersReadyOrEnded : isTimelineReady(playingPeriodDurationUs); if (!isStillReady) { @@ -597,15 +616,16 @@ import java.io.IOException; } } - if (state == Player.STATE_BUFFERING) { + if (playbackInfo.playbackState == Player.STATE_BUFFERING) { for (Renderer renderer : enabledRenderers) { renderer.maybeThrowStreamError(); } } - if ((playWhenReady && state == Player.STATE_READY) || state == Player.STATE_BUFFERING) { + if ((playWhenReady && playbackInfo.playbackState == Player.STATE_READY) + || playbackInfo.playbackState == Player.STATE_BUFFERING) { scheduleNextWork(operationStartTimeMs, RENDERING_INTERVAL_MS); - } else if (enabledRenderers.length != 0 && state != Player.STATE_ENDED) { + } else if (enabledRenderers.length != 0 && playbackInfo.playbackState != Player.STATE_ENDED) { scheduleNextWork(operationStartTimeMs, IDLE_INTERVAL_MS); } else { handler.removeMessages(MSG_DO_SOME_WORK); @@ -626,12 +646,10 @@ import java.io.IOException; } private void seekToInternal(SeekPosition seekPosition) throws ExoPlaybackException { + playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1); Timeline timeline = playbackInfo.timeline; if (mediaSource == null || timeline == null) { pendingInitialSeekPosition = seekPosition; - eventHandler - .obtainMessage(MSG_SEEK_ACK, /* seekAdjusted */ 0, 0, playbackInfo) - .sendToTarget(); return; } @@ -679,8 +697,9 @@ import java.io.IOException; playbackInfo = playbackInfo.fromNewPosition(periodId, periodPositionUs, contentPositionUs); } } finally { - eventHandler.obtainMessage(MSG_SEEK_ACK, seekPositionAdjusted ? 1 : 0, 0, playbackInfo) - .sendToTarget(); + if (seekPositionAdjusted) { + playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT); + } } } @@ -779,7 +798,9 @@ import java.io.IOException; private void stopInternal(boolean reset, boolean acknowledgeStop) { resetInternal( /* releaseMediaSource= */ true, /* resetPosition= */ reset, /* resetState= */ reset); - notifySourceInfoRefresh(acknowledgeStop); + playbackInfoUpdate.incrementPendingOperationAcks( + pendingPrepareCount + (acknowledgeStop ? 1 : 0)); + pendingPrepareCount = 0; loadControl.onStopped(); setState(Player.STATE_IDLE); } @@ -817,25 +838,29 @@ import java.io.IOException; readingPeriodHolder = null; playingPeriodHolder = null; setIsLoading(false); + Timeline timeline = playbackInfo.timeline; + int firstPeriodIndex = + timeline == null || timeline.isEmpty() + ? 0 + : timeline.getWindow(timeline.getFirstWindowIndex(shuffleModeEnabled), window) + .firstPeriodIndex; if (resetPosition) { - // Set the internal position to (firstPeriodIndex,TIME_UNSET) so that a subsequent seek to - // (firstPeriodIndex,0) isn't ignored. - Timeline timeline = playbackInfo.timeline; - int firstPeriodIndex = timeline == null || timeline.isEmpty() - ? 0 - : timeline.getWindow(timeline.getFirstWindowIndex(shuffleModeEnabled), window) - .firstPeriodIndex; pendingInitialSeekPosition = null; - playbackInfo = playbackInfo.fromNewPosition(firstPeriodIndex, C.TIME_UNSET, C.TIME_UNSET); - } else { - // The new start position is the current playback position. - playbackInfo = playbackInfo.fromNewPosition(playbackInfo.periodId, playbackInfo.positionUs, - playbackInfo.contentPositionUs); } if (resetState) { mediaPeriodInfoSequence.setTimeline(null); - playbackInfo = playbackInfo.copyWithTimeline(null, null); } + playbackInfo = + new PlaybackInfo( + resetState ? null : playbackInfo.timeline, + resetState ? null : playbackInfo.manifest, + resetPosition ? new MediaPeriodId(firstPeriodIndex) : playbackInfo.periodId, + // Set the start position to TIME_UNSET so that a subsequent seek to 0 isn't ignored. + resetPosition ? C.TIME_UNSET : playbackInfo.startPositionUs, + resetPosition ? C.TIME_UNSET : playbackInfo.contentPositionUs, + playbackInfo.playbackState, + /* isLoading= */ false, + resetState ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult); if (releaseMediaSource) { if (mediaSource != null) { mediaSource.releaseSource(); @@ -849,7 +874,8 @@ import java.io.IOException; for (ExoPlayerMessage message : messages) { message.target.handleMessage(message.messageType, message.message); } - if (state == Player.STATE_READY || state == Player.STATE_BUFFERING) { + if (playbackInfo.playbackState == Player.STATE_READY + || playbackInfo.playbackState == Player.STATE_BUFFERING) { // The message may have caused something to change that now requires us to do work. handler.sendEmptyMessage(MSG_DO_SOME_WORK); } @@ -909,11 +935,11 @@ import java.io.IOException; boolean[] streamResetFlags = new boolean[renderers.length]; long periodPositionUs = playingPeriodHolder.updatePeriodTrackSelection( playbackInfo.positionUs, recreateStreams, streamResetFlags); - if (state != Player.STATE_ENDED && periodPositionUs != playbackInfo.positionUs) { + if (playbackInfo.playbackState != Player.STATE_ENDED + && periodPositionUs != playbackInfo.positionUs) { playbackInfo = playbackInfo.fromNewPosition(playbackInfo.periodId, periodPositionUs, playbackInfo.contentPositionUs); - eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, Player.DISCONTINUITY_REASON_INTERNAL, - 0, playbackInfo).sendToTarget(); + playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_INTERNAL); resetRendererPosition(periodPositionUs); } @@ -936,8 +962,7 @@ import java.io.IOException; } } } - eventHandler.obtainMessage(MSG_TRACKS_CHANGED, periodHolder.trackSelectorResult) - .sendToTarget(); + playbackInfo = playbackInfo.copyWithTrackSelectorResult(periodHolder.trackSelectorResult); enableRenderers(rendererWasEnabledFlags, enabledRendererCount); } else { // Release and re-prepare/buffer periods after the one whose selection changed. @@ -954,7 +979,7 @@ import java.io.IOException; loadingPeriodHolder.updatePeriodTrackSelection(loadingPeriodPositionUs, false); } } - if (state != Player.STATE_ENDED) { + if (playbackInfo.playbackState != Player.STATE_ENDED) { maybeContinueLoading(); updatePlaybackPositions(); handler.sendEmptyMessage(MSG_DO_SOME_WORK); @@ -1010,6 +1035,8 @@ import java.io.IOException; playbackInfo = playbackInfo.copyWithTimeline(timeline, manifest); if (oldTimeline == null) { + playbackInfoUpdate.incrementPendingOperationAcks(pendingPrepareCount); + pendingPrepareCount = 0; if (pendingInitialSeekPosition != null) { Pair periodPosition = resolveSeekPosition(pendingInitialSeekPosition); pendingInitialSeekPosition = null; @@ -1024,7 +1051,6 @@ import java.io.IOException; mediaPeriodInfoSequence.resolvePeriodPositionForAds(periodIndex, positionUs); playbackInfo = playbackInfo.fromNewPosition(periodId, periodId.isAd() ? 0 : positionUs, positionUs); - notifySourceInfoRefresh(); } } else if (playbackInfo.startPositionUs == C.TIME_UNSET) { if (timeline.isEmpty()) { @@ -1038,10 +1064,7 @@ import java.io.IOException; startPositionUs); playbackInfo = playbackInfo.fromNewPosition(periodId, periodId.isAd() ? 0 : startPositionUs, startPositionUs); - notifySourceInfoRefresh(); } - } else { - notifySourceInfoRefresh(); } return; } @@ -1050,7 +1073,6 @@ import java.io.IOException; MediaPeriodHolder periodHolder = playingPeriodHolder != null ? playingPeriodHolder : loadingPeriodHolder; if (periodHolder == null && playingPeriodIndex >= oldTimeline.getPeriodCount()) { - notifySourceInfoRefresh(); return; } Object playingPeriodUid = periodHolder == null @@ -1090,7 +1112,6 @@ import java.io.IOException; MediaPeriodId periodId = new MediaPeriodId(newPeriodIndex); newPositionUs = seekToPeriodPosition(periodId, newPositionUs); playbackInfo = playbackInfo.fromNewPosition(periodId, newPositionUs, C.TIME_UNSET); - notifySourceInfoRefresh(); return; } @@ -1107,14 +1128,12 @@ import java.io.IOException; long newPositionUs = seekToPeriodPosition(periodId, playbackInfo.contentPositionUs); long contentPositionUs = periodId.isAd() ? playbackInfo.contentPositionUs : C.TIME_UNSET; playbackInfo = playbackInfo.fromNewPosition(periodId, newPositionUs, contentPositionUs); - notifySourceInfoRefresh(); return; } } if (periodHolder == null) { // We don't have any period holders, so we're done. - notifySourceInfoRefresh(); return; } @@ -1152,8 +1171,6 @@ import java.io.IOException; break; } } - - notifySourceInfoRefresh(); } private MediaPeriodHolder updatePeriodInfo(MediaPeriodHolder periodHolder, int periodIndex) { @@ -1172,18 +1189,6 @@ import java.io.IOException; // Reset, but retain the source so that it can still be used should a seek occur. resetInternal( /* releaseMediaSource= */ false, /* resetPosition= */ true, /* resetState= */ false); - notifySourceInfoRefresh(); - } - - private void notifySourceInfoRefresh() { - notifySourceInfoRefresh(/* acknowledgeStop= */ false); - } - - private void notifySourceInfoRefresh(boolean acknowledgeStop) { - int prepareOrStopAcks = pendingPrepareCount + (acknowledgeStop ? 1 : 0); - pendingPrepareCount = 0; - eventHandler.obtainMessage(MSG_SOURCE_INFO_REFRESHED, prepareOrStopAcks, 0, playbackInfo) - .sendToTarget(); } /** @@ -1287,7 +1292,7 @@ import java.io.IOException; if (loadingPeriodHolder == null || loadingPeriodHolder.isFullyBuffered()) { setIsLoading(false); - } else if (loadingPeriodHolder != null && !isLoading) { + } else if (loadingPeriodHolder != null && !playbackInfo.isLoading) { maybeContinueLoading(); } @@ -1305,9 +1310,8 @@ import java.io.IOException; setPlayingPeriodHolder(playingPeriodHolder.next); playbackInfo = playbackInfo.fromNewPosition(playingPeriodHolder.info.id, playingPeriodHolder.info.startPositionUs, playingPeriodHolder.info.contentPositionUs); + playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_PERIOD_TRANSITION); updatePlaybackPositions(); - eventHandler.obtainMessage(MSG_POSITION_DISCONTINUITY, - Player.DISCONTINUITY_REASON_PERIOD_TRANSITION, 0, playbackInfo).sendToTarget(); } if (readingPeriodHolder.info.isFinal) { @@ -1488,7 +1492,7 @@ import java.io.IOException; } playingPeriodHolder = periodHolder; - eventHandler.obtainMessage(MSG_TRACKS_CHANGED, periodHolder.trackSelectorResult).sendToTarget(); + playbackInfo = playbackInfo.copyWithTrackSelectorResult(periodHolder.trackSelectorResult); enableRenderers(rendererWasEnabledFlags, enabledRendererCount); } @@ -1514,7 +1518,7 @@ import java.io.IOException; rendererIndex); Format[] formats = getFormats(newSelection); // The renderer needs enabling with its new track selection. - boolean playing = playWhenReady && state == Player.STATE_READY; + boolean playing = playWhenReady && playbackInfo.playbackState == Player.STATE_READY; // Consider as joining only if the renderer was previously disabled. boolean joining = !wasRendererEnabled && playing; // Enable the renderer. @@ -1805,7 +1809,40 @@ import java.io.IOException; this.timeline = timeline; this.manifest = manifest; } + } + private static final class PlaybackInfoUpdate { + + private PlaybackInfo lastPlaybackInfo; + private int operationAcks; + private boolean positionDiscontinuity; + private @DiscontinuityReason int discontinuityReason; + + public boolean hasPendingUpdate(PlaybackInfo playbackInfo) { + return playbackInfo != lastPlaybackInfo || operationAcks > 0 || positionDiscontinuity; + } + + public void reset(PlaybackInfo playbackInfo) { + lastPlaybackInfo = playbackInfo; + operationAcks = 0; + positionDiscontinuity = false; + } + + public void incrementPendingOperationAcks(int operationAcks) { + this.operationAcks += operationAcks; + } + + public void setPositionDiscontinuity(@DiscontinuityReason int discontinuityReason) { + if (positionDiscontinuity + && this.discontinuityReason != Player.DISCONTINUITY_REASON_INTERNAL) { + // We always prefer non-internal discontinuity reasons. We also assume that we won't report + // more than one non-internal discontinuity per message iteration. + Assertions.checkArgument(discontinuityReason == Player.DISCONTINUITY_REASON_INTERNAL); + return; + } + positionDiscontinuity = true; + this.discontinuityReason = discontinuityReason; + } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java index a2ffa43c4b..65392ba269 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/PlaybackInfo.java @@ -15,35 +15,59 @@ */ package com.google.android.exoplayer2; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import com.google.android.exoplayer2.trackselection.TrackSelectorResult; /** * Information about an ongoing playback. */ /* package */ final class PlaybackInfo { - public final Timeline timeline; - public final Object manifest; + public final @Nullable Timeline timeline; + public final @Nullable Object manifest; public final MediaPeriodId periodId; public final long startPositionUs; public final long contentPositionUs; + public final int playbackState; + public final boolean isLoading; + public final TrackSelectorResult trackSelectorResult; public volatile long positionUs; public volatile long bufferedPositionUs; - public PlaybackInfo(Timeline timeline, Object manifest, int periodIndex, long startPositionUs) { - this(timeline, manifest, new MediaPeriodId(periodIndex), startPositionUs, C.TIME_UNSET); + public PlaybackInfo( + @Nullable Timeline timeline, long startPositionUs, TrackSelectorResult trackSelectorResult) { + this( + timeline, + /* manifest= */ null, + new MediaPeriodId(0), + startPositionUs, + /* contentPositionUs =*/ C.TIME_UNSET, + Player.STATE_IDLE, + /* isLoading= */ false, + trackSelectorResult); } - public PlaybackInfo(Timeline timeline, Object manifest, MediaPeriodId periodId, - long startPositionUs, long contentPositionUs) { + public PlaybackInfo( + @Nullable Timeline timeline, + @Nullable Object manifest, + MediaPeriodId periodId, + long startPositionUs, + long contentPositionUs, + int playbackState, + boolean isLoading, + TrackSelectorResult trackSelectorResult) { this.timeline = timeline; this.manifest = manifest; this.periodId = periodId; this.startPositionUs = startPositionUs; this.contentPositionUs = contentPositionUs; - positionUs = startPositionUs; - bufferedPositionUs = startPositionUs; + this.positionUs = startPositionUs; + this.bufferedPositionUs = startPositionUs; + this.playbackState = playbackState; + this.isLoading = isLoading; + this.trackSelectorResult = trackSelectorResult; } public PlaybackInfo fromNewPosition(int periodIndex, long startPositionUs, @@ -53,19 +77,88 @@ import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; public PlaybackInfo fromNewPosition(MediaPeriodId periodId, long startPositionUs, long contentPositionUs) { - return new PlaybackInfo(timeline, manifest, periodId, startPositionUs, contentPositionUs); + return new PlaybackInfo( + timeline, + manifest, + periodId, + startPositionUs, + contentPositionUs, + playbackState, + isLoading, + trackSelectorResult); } public PlaybackInfo copyWithPeriodIndex(int periodIndex) { - PlaybackInfo playbackInfo = new PlaybackInfo(timeline, manifest, - periodId.copyWithPeriodIndex(periodIndex), startPositionUs, contentPositionUs); + PlaybackInfo playbackInfo = + new PlaybackInfo( + timeline, + manifest, + periodId.copyWithPeriodIndex(periodIndex), + startPositionUs, + contentPositionUs, + playbackState, + isLoading, + trackSelectorResult); copyMutablePositions(this, playbackInfo); return playbackInfo; } public PlaybackInfo copyWithTimeline(Timeline timeline, Object manifest) { - PlaybackInfo playbackInfo = new PlaybackInfo(timeline, manifest, periodId, startPositionUs, - contentPositionUs); + PlaybackInfo playbackInfo = + new PlaybackInfo( + timeline, + manifest, + periodId, + startPositionUs, + contentPositionUs, + playbackState, + isLoading, + trackSelectorResult); + copyMutablePositions(this, playbackInfo); + return playbackInfo; + } + + public PlaybackInfo copyWithPlaybackState(int playbackState) { + PlaybackInfo playbackInfo = + new PlaybackInfo( + timeline, + manifest, + periodId, + startPositionUs, + contentPositionUs, + playbackState, + isLoading, + trackSelectorResult); + copyMutablePositions(this, playbackInfo); + return playbackInfo; + } + + public PlaybackInfo copyWithIsLoading(boolean isLoading) { + PlaybackInfo playbackInfo = + new PlaybackInfo( + timeline, + manifest, + periodId, + startPositionUs, + contentPositionUs, + playbackState, + isLoading, + trackSelectorResult); + copyMutablePositions(this, playbackInfo); + return playbackInfo; + } + + public PlaybackInfo copyWithTrackSelectorResult(TrackSelectorResult trackSelectorResult) { + PlaybackInfo playbackInfo = + new PlaybackInfo( + timeline, + manifest, + periodId, + startPositionUs, + contentPositionUs, + playbackState, + isLoading, + trackSelectorResult); copyMutablePositions(this, playbackInfo); return playbackInfo; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java index 801f5b9584..68adc32395 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java @@ -74,7 +74,7 @@ public final class TrackSelectorResult { * @return Whether this result is equivalent to {@code other} for all renderers. */ public boolean isEquivalent(TrackSelectorResult other) { - if (other == null) { + if (other == null || other.selections.length != selections.length) { return false; } for (int i = 0; i < selections.length; i++) { From 073c92ed3618030cb7a5cb3901b6026b6f1a1e0c Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 13 Dec 2017 09:54:25 -0800 Subject: [PATCH 0950/2472] Don't send playback info updates when released. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178916145 --- .../google/android/exoplayer2/ExoPlayerImplInternal.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index b52696533d..c34e947046 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -336,9 +336,6 @@ import java.io.IOException; case MSG_STOP: stopInternal(/* reset= */ msg.arg1 != 0, /* acknowledgeStop= */ true); break; - case MSG_RELEASE: - releaseInternal(); - break; case MSG_PERIOD_PREPARED: handlePeriodPrepared((MediaPeriod) msg.obj); break; @@ -354,6 +351,10 @@ import java.io.IOException; case MSG_CUSTOM: sendMessagesInternal((ExoPlayerMessage[]) msg.obj); break; + case MSG_RELEASE: + releaseInternal(); + // Return immediately to not send playback info updates after release. + return true; default: return false; } From f5d7b67eeab2a5fe3d5edcbebcaa6ba9fdd9b9bc Mon Sep 17 00:00:00 2001 From: jschlag Date: Wed, 13 Dec 2017 15:46:30 -0800 Subject: [PATCH 0951/2472] try turning off vp9 loop filter on android ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178970007 --- .../android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java | 9 ++++++--- .../google/android/exoplayer2/ext/vp9/VpxDecoder.java | 7 ++++--- extensions/vp9/src/main/jni/vpx_jni.cc | 5 ++++- 3 files changed, 14 insertions(+), 7 deletions(-) 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 dd303af0d8..ac944a7b01 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 @@ -92,6 +92,7 @@ public final class LibvpxVideoRenderer extends BaseRenderer { private static final int INITIAL_INPUT_BUFFER_SIZE = 768 * 1024; // Value based on cs/SoftVpx.cpp. private final boolean scaleToFit; + private final boolean disableLoopFilter; private final long allowedJoiningTimeMs; private final int maxDroppedFramesToNotify; private final boolean playClearSamplesWithoutKeys; @@ -154,7 +155,7 @@ public final class LibvpxVideoRenderer extends BaseRenderer { Handler eventHandler, VideoRendererEventListener eventListener, int maxDroppedFramesToNotify) { this(scaleToFit, allowedJoiningTimeMs, eventHandler, eventListener, maxDroppedFramesToNotify, - null, false); + null, false, false); } /** @@ -173,13 +174,15 @@ public final class LibvpxVideoRenderer extends BaseRenderer { * begin in parallel with key acquisition. This parameter specifies whether the renderer is * permitted to play clear regions of encrypted media files before {@code drmSessionManager} * has obtained the keys necessary to decrypt encrypted regions of the media. + * @param disableLoopFilter Disable the libvpx in-loop smoothing filter. */ public LibvpxVideoRenderer(boolean scaleToFit, long allowedJoiningTimeMs, Handler eventHandler, VideoRendererEventListener eventListener, int maxDroppedFramesToNotify, DrmSessionManager drmSessionManager, - boolean playClearSamplesWithoutKeys) { + boolean playClearSamplesWithoutKeys, boolean disableLoopFilter) { super(C.TRACK_TYPE_VIDEO); this.scaleToFit = scaleToFit; + this.disableLoopFilter = disableLoopFilter; this.allowedJoiningTimeMs = allowedJoiningTimeMs; this.maxDroppedFramesToNotify = maxDroppedFramesToNotify; this.drmSessionManager = drmSessionManager; @@ -625,7 +628,7 @@ public final class LibvpxVideoRenderer extends BaseRenderer { long codecInitializingTimestamp = SystemClock.elapsedRealtime(); TraceUtil.beginSection("createVpxDecoder"); decoder = new VpxDecoder(NUM_INPUT_BUFFERS, NUM_OUTPUT_BUFFERS, INITIAL_INPUT_BUFFER_SIZE, - mediaCrypto); + mediaCrypto, disableLoopFilter); decoder.setOutputMode(outputMode); TraceUtil.endSection(); long codecInitializedTimestamp = SystemClock.elapsedRealtime(); diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java index ef999d5d2b..6a15023c0b 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java @@ -49,10 +49,11 @@ import java.nio.ByteBuffer; * @param initialInputBufferSize The initial size of each input buffer. * @param exoMediaCrypto The {@link ExoMediaCrypto} object required for decoding encrypted * content. Maybe null and can be ignored if decoder does not handle encrypted content. + * @param disableLoopFilter Disable the libvpx in-loop smoothing filter. * @throws VpxDecoderException Thrown if an exception occurs when initializing the decoder. */ public VpxDecoder(int numInputBuffers, int numOutputBuffers, int initialInputBufferSize, - ExoMediaCrypto exoMediaCrypto) throws VpxDecoderException { + ExoMediaCrypto exoMediaCrypto, boolean disableLoopFilter) throws VpxDecoderException { super(new VpxInputBuffer[numInputBuffers], new VpxOutputBuffer[numOutputBuffers]); if (!VpxLibrary.isAvailable()) { throw new VpxDecoderException("Failed to load decoder native libraries."); @@ -61,7 +62,7 @@ import java.nio.ByteBuffer; if (exoMediaCrypto != null && !VpxLibrary.vpxIsSecureDecodeSupported()) { throw new VpxDecoderException("Vpx decoder does not support secure decode."); } - vpxDecContext = vpxInit(); + vpxDecContext = vpxInit(disableLoopFilter); if (vpxDecContext == 0) { throw new VpxDecoderException("Failed to initialize decoder"); } @@ -139,7 +140,7 @@ import java.nio.ByteBuffer; vpxClose(vpxDecContext); } - private native long vpxInit(); + private native long vpxInit(boolean disableLoopFilter); private native long vpxClose(long context); private native long vpxDecode(long context, ByteBuffer encoded, int length); private native long vpxSecureDecode(long context, ByteBuffer encoded, int length, diff --git a/extensions/vp9/src/main/jni/vpx_jni.cc b/extensions/vp9/src/main/jni/vpx_jni.cc index 5c480d1525..9666875b04 100644 --- a/extensions/vp9/src/main/jni/vpx_jni.cc +++ b/extensions/vp9/src/main/jni/vpx_jni.cc @@ -283,7 +283,7 @@ static void convert_16_to_8_standard(const vpx_image_t* const img, } } -DECODER_FUNC(jlong, vpxInit) { +DECODER_FUNC(jlong, vpxInit, jboolean disableLoopFilter) { vpx_codec_ctx_t* context = new vpx_codec_ctx_t(); vpx_codec_dec_cfg_t cfg = {0, 0, 0}; cfg.threads = android_getCpuCount(); @@ -295,6 +295,9 @@ DECODER_FUNC(jlong, vpxInit) { errorCode = err; return 0; } + if (disableLoopFilter) { + vpx_codec_control_(context, VP9_SET_SKIP_LOOP_FILTER, true); + } // Populate JNI References. const jclass outputBufferClass = env->FindClass( From a17375b7d3a5b95751ad15d4cae4a80081149c8c Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 14 Dec 2017 04:25:51 -0800 Subject: [PATCH 0952/2472] Resend playback info update when skipping very short periods. Skipping short periods in a while loop is conceptually a new operation and thus we need to send out the updated playback info in between for the listeners to receive multiple period transition discontinuities. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179027334 --- .../android/exoplayer2/ExoPlayerTest.java | 24 +++++++++++++++++++ .../exoplayer2/ExoPlayerImplInternal.java | 6 +++++ 2 files changed, 30 insertions(+) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index a227aa3575..40b4b2d383 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -35,6 +35,7 @@ import com.google.android.exoplayer2.testutil.FakeTrackSelection; import com.google.android.exoplayer2.testutil.FakeTrackSelector; import com.google.android.exoplayer2.upstream.Allocator; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.concurrent.CountDownLatch; import junit.framework.TestCase; @@ -111,6 +112,29 @@ public final class ExoPlayerTest extends TestCase { assertTrue(renderer.isEnded); } + /** Tests playback of periods with very short duration. */ + public void testPlayShortDurationPeriods() throws Exception { + // TimelineWindowDefinition.DEFAULT_WINDOW_DURATION_US / 100 = 1000 us per period. + Timeline timeline = + new FakeTimeline(new TimelineWindowDefinition(/* periodCount= */ 100, /* id= */ 0)); + FakeRenderer renderer = new FakeRenderer(Builder.VIDEO_FORMAT); + ExoPlayerTestRunner testRunner = + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setRenderers(renderer) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + int[] expectedReasons = new int[99]; + Arrays.fill(expectedReasons, Player.DISCONTINUITY_REASON_PERIOD_TRANSITION); + testRunner.assertPositionDiscontinuityReasonsEqual(expectedReasons); + testRunner.assertTimelinesEqual(timeline); + testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); + assertEquals(100, renderer.formatReadCount); + assertEquals(1, renderer.bufferReadCount); + assertTrue(renderer.isEnded); + } + /** * Tests that the player does not unnecessarily reset renderers when playing a multi-period * source. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index c34e947046..7b52f79be5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -1303,16 +1303,22 @@ import java.io.IOException; } // Advance the playing period if necessary. + boolean advancedPlayingPeriod = false; while (playWhenReady && playingPeriodHolder != readingPeriodHolder && rendererPositionUs >= playingPeriodHolder.next.rendererPositionOffsetUs) { // All enabled renderers' streams have been read to the end, and the playback position reached // the end of the playing period, so advance playback to the next period. + if (advancedPlayingPeriod) { + // If we advance more than one period at a time, notify listeners after each update. + maybeNotifyPlaybackInfoChanged(); + } playingPeriodHolder.release(); setPlayingPeriodHolder(playingPeriodHolder.next); playbackInfo = playbackInfo.fromNewPosition(playingPeriodHolder.info.id, playingPeriodHolder.info.startPositionUs, playingPeriodHolder.info.contentPositionUs); playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_PERIOD_TRANSITION); updatePlaybackPositions(); + advancedPlayingPeriod = true; } if (readingPeriodHolder.info.isFinal) { From 37a275f67e85f6077548bb5d8894fb958e81923c Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 14 Dec 2017 05:18:07 -0800 Subject: [PATCH 0953/2472] Enable SeekParameters functionality for ExtractorMediaSource Also fix ClippingMediaSource to consider the start position an artificial key-frame, and to properly offset the value returned by getAdjustedSeekPositionUs. Issue: #2882 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179032243 --- RELEASENOTES.md | 6 ++++ .../exoplayer2/ExoPlayerImplInternal.java | 12 +++---- .../source/ClippingMediaPeriod.java | 23 +++++++----- .../source/ExtractorMediaPeriod.java | 32 +++++++++++++++-- .../google/android/exoplayer2/util/Util.java | 34 ++++++++++++++++++ .../android/exoplayer2/util/UtilTest.java | 36 +++++++++++++++++++ 6 files changed, 126 insertions(+), 17 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 95eda228ea..9875333dad 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -16,6 +16,12 @@ sub-streams, by allowing injection of custom `CompositeSequenceableLoader` factories through `DashMediaSource.Factory`, `HlsMediaSource.Factory`, `SsMediaSource.Factory`, and `MergingMediaSource`. +* Add `ExoPlayer.setSeekParameters` for controlling how seek operations are + performed. The `SeekParameters` class contains defaults for exact seeking and + seeking to the closest sync points before, either side or after specified seek + positions. + * Note: `SeekParameters` are only currently effective when playing + `ExtractorMediaSource`s (i.e. progressive streams). * DASH: Support DASH manifest EventStream elements. * HLS: Add opt-in support for chunkless preparation in HLS. This allows an HLS source to finish preparation without downloading any chunks, which can diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 7b52f79be5..09b3231467 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -678,20 +678,20 @@ import java.io.IOException; periodPositionUs = 0; } try { + long newPeriodPositionUs = periodPositionUs; if (periodId.equals(playbackInfo.periodId)) { - long adjustedPeriodPositionUs = periodPositionUs; - if (playingPeriodHolder != null) { - adjustedPeriodPositionUs = + if (playingPeriodHolder != null && newPeriodPositionUs != 0) { + newPeriodPositionUs = playingPeriodHolder.mediaPeriod.getAdjustedSeekPositionUs( - adjustedPeriodPositionUs, SeekParameters.DEFAULT); + newPeriodPositionUs, seekParameters); } - if ((adjustedPeriodPositionUs / 1000) == (playbackInfo.positionUs / 1000)) { + if ((newPeriodPositionUs / 1000) == (playbackInfo.positionUs / 1000)) { // Seek will be performed to the current position. Do nothing. periodPositionUs = playbackInfo.positionUs; return; } } - long newPeriodPositionUs = seekToPeriodPosition(periodId, periodPositionUs); + newPeriodPositionUs = seekToPeriodPosition(periodId, newPeriodPositionUs); seekPositionAdjusted |= periodPositionUs != newPeriodPositionUs; periodPositionUs = newPeriodPositionUs; } finally { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java index b1c12d6192..5685b8b70b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java @@ -165,16 +165,23 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb sampleStream.clearSentEos(); } } - long seekUs = mediaPeriod.seekToUs(positionUs + startUs); - Assertions.checkState(seekUs == positionUs + startUs - || (seekUs >= startUs && (endUs == C.TIME_END_OF_SOURCE || seekUs <= endUs))); + long offsetPositionUs = positionUs + startUs; + long seekUs = mediaPeriod.seekToUs(offsetPositionUs); + Assertions.checkState( + seekUs == offsetPositionUs + || (seekUs >= startUs && (endUs == C.TIME_END_OF_SOURCE || seekUs <= endUs))); return seekUs - startUs; } @Override public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { - return mediaPeriod.getAdjustedSeekPositionUs( - positionUs + startUs, adjustSeekParameters(positionUs + startUs, seekParameters)); + if (positionUs == startUs) { + // Never adjust seeks to the start of the clipped view. + return 0; + } + long offsetPositionUs = positionUs + startUs; + SeekParameters clippedSeekParameters = clipSeekParameters(offsetPositionUs, seekParameters); + return mediaPeriod.getAdjustedSeekPositionUs(offsetPositionUs, clippedSeekParameters) - startUs; } @Override @@ -209,12 +216,12 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb return pendingInitialDiscontinuityPositionUs != C.TIME_UNSET; } - private SeekParameters adjustSeekParameters(long positionUs, SeekParameters seekParameters) { - long toleranceBeforeMs = Math.min(positionUs - startUs, seekParameters.toleranceBeforeUs); + private SeekParameters clipSeekParameters(long offsetPositionUs, SeekParameters seekParameters) { + long toleranceBeforeMs = Math.min(offsetPositionUs - startUs, seekParameters.toleranceBeforeUs); long toleranceAfterMs = endUs == C.TIME_END_OF_SOURCE ? seekParameters.toleranceAfterUs - : Math.min(endUs - positionUs, seekParameters.toleranceAfterUs); + : Math.min(endUs - offsetPositionUs, seekParameters.toleranceAfterUs); if (toleranceBeforeMs == seekParameters.toleranceBeforeUs && toleranceAfterMs == seekParameters.toleranceAfterUs) { return seekParameters; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java index 4773ac53a1..e5d1fae7bd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -29,6 +29,7 @@ import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.SeekMap; +import com.google.android.exoplayer2.extractor.SeekMap.SeekPoints; import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SampleQueue.UpstreamFormatChangedListener; @@ -372,8 +373,33 @@ import java.util.Arrays; @Override public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { - // Treat all seeks into non-seekable media as being to t=0. - return seekMap.isSeekable() ? positionUs : 0; + if (!seekMap.isSeekable()) { + // Treat all seeks into non-seekable media as being to t=0. + return 0; + } + SeekPoints seekPoints = seekMap.getSeekPoints(positionUs); + long minPositionUs = + Util.subtractWithOverflowDefault( + positionUs, seekParameters.toleranceBeforeUs, Long.MIN_VALUE); + long maxPositionUs = + Util.addWithOverflowDefault(positionUs, seekParameters.toleranceAfterUs, Long.MAX_VALUE); + long firstPointUs = seekPoints.first.timeUs; + boolean firstPointValid = minPositionUs <= firstPointUs && firstPointUs <= maxPositionUs; + long secondPointUs = seekPoints.second.timeUs; + boolean secondPointValid = minPositionUs <= secondPointUs && secondPointUs <= maxPositionUs; + if (firstPointValid && secondPointValid) { + if (Math.abs(firstPointUs - positionUs) <= Math.abs(secondPointUs - positionUs)) { + return firstPointUs; + } else { + return secondPointUs; + } + } else if (firstPointValid) { + return firstPointUs; + } else if (secondPointValid) { + return secondPointUs; + } else { + return minPositionUs; + } } // SampleStream methods. @@ -657,7 +683,7 @@ import java.util.Arrays; return pendingResetPositionUs != C.TIME_UNSET; } - private boolean isLoadableExceptionFatal(IOException e) { + private static boolean isLoadableExceptionFatal(IOException e) { return e instanceof UnrecognizedInputFormatException; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java index 0594f52288..d796e6936f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -362,6 +362,40 @@ public final class Util { return Math.max(min, Math.min(value, max)); } + /** + * Returns the sum of two arguments, or a third argument if the result overflows. + * + * @param x The first value. + * @param y The second value. + * @param overflowResult The return value if {@code x + y} overflows. + * @return {@code x + y}, or {@code overflowResult} if the result overflows. + */ + public static long addWithOverflowDefault(long x, long y, long overflowResult) { + long result = x + y; + // See Hacker's Delight 2-13 (H. Warren Jr). + if (((x ^ result) & (y ^ result)) < 0) { + return overflowResult; + } + return result; + } + + /** + * Returns the difference between two arguments, or a third argument if the result overflows. + * + * @param x The first value. + * @param y The second value. + * @param overflowResult The return value if {@code x - y} overflows. + * @return {@code x - y}, or {@code overflowResult} if the result overflows. + */ + public static long subtractWithOverflowDefault(long x, long y, long overflowResult) { + long result = x - y; + // See Hacker's Delight 2-13 (H. Warren Jr). + if (((x ^ y) & (x ^ result)) < 0) { + return overflowResult; + } + return result; + } + /** * Returns the index of the largest element in {@code array} that is less than (or optionally * equal to) a specified {@code value}. diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/UtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/UtilTest.java index 68ed686c62..ca7a3b199d 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/util/UtilTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/UtilTest.java @@ -41,6 +41,42 @@ import org.robolectric.annotation.Config; @Config(sdk = Config.TARGET_SDK, manifest = Config.NONE) public class UtilTest { + @Test + public void testAddWithOverflowDefault() { + long res = Util.addWithOverflowDefault(5, 10, /* overflowResult= */ 0); + assertThat(res).isEqualTo(15); + + res = Util.addWithOverflowDefault(Long.MAX_VALUE - 1, 1, /* overflowResult= */ 12345); + assertThat(res).isEqualTo(Long.MAX_VALUE); + + res = Util.addWithOverflowDefault(Long.MIN_VALUE + 1, -1, /* overflowResult= */ 12345); + assertThat(res).isEqualTo(Long.MIN_VALUE); + + res = Util.addWithOverflowDefault(Long.MAX_VALUE, 1, /* overflowResult= */ 12345); + assertThat(res).isEqualTo(12345); + + res = Util.addWithOverflowDefault(Long.MIN_VALUE, -1, /* overflowResult= */ 12345); + assertThat(res).isEqualTo(12345); + } + + @Test + public void testSubtrackWithOverflowDefault() { + long res = Util.subtractWithOverflowDefault(5, 10, /* overflowResult= */ 0); + assertThat(res).isEqualTo(-5); + + res = Util.subtractWithOverflowDefault(Long.MIN_VALUE + 1, 1, /* overflowResult= */ 12345); + assertThat(res).isEqualTo(Long.MIN_VALUE); + + res = Util.subtractWithOverflowDefault(Long.MAX_VALUE - 1, -1, /* overflowResult= */ 12345); + assertThat(res).isEqualTo(Long.MAX_VALUE); + + res = Util.subtractWithOverflowDefault(Long.MIN_VALUE, 1, /* overflowResult= */ 12345); + assertThat(res).isEqualTo(12345); + + res = Util.subtractWithOverflowDefault(Long.MAX_VALUE, -1, /* overflowResult= */ 12345); + assertThat(res).isEqualTo(12345); + } + @Test public void testInferContentType() { assertThat(Util.inferContentType("http://a.b/c.ism")).isEqualTo(C.TYPE_SS); From 0ccf816a5caabb99484cfedc23df9a703edfc68e Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 14 Dec 2017 06:54:12 -0800 Subject: [PATCH 0954/2472] Add a bitmask for text tracks' selection flags in DefaultTrackSelector ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179039563 --- RELEASENOTES.md | 2 + .../trackselection/DefaultTrackSelector.java | 128 +++++++++++++----- .../DefaultTrackSelectorTest.java | 128 ++++++++++++++++++ 3 files changed, 226 insertions(+), 32 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 9875333dad..686a6d10ba 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -29,6 +29,8 @@ ([#3149](https://github.com/google/ExoPlayer/issues/3149)). * DefaultTrackSelector: Replace `DefaultTrackSelector.Parameters` copy methods with a builder. +* DefaultTrackSelector: Support disabling of individual text track selection + flags. * New Cast extension: Simplifies toggling between local and Cast playbacks. ### 2.6.1 ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index 2f0dc8f04e..09bd81416c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -82,6 +82,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { private String preferredAudioLanguage; private String preferredTextLanguage; private boolean selectUndeterminedTextLanguage; + private int disabledTextTrackSelectionFlags; private boolean forceLowestBitrate; private boolean allowMixedMimeAdaptiveness; private boolean allowNonSeamlessAdaptiveness; @@ -109,6 +110,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { preferredAudioLanguage = initialValues.preferredAudioLanguage; preferredTextLanguage = initialValues.preferredTextLanguage; selectUndeterminedTextLanguage = initialValues.selectUndeterminedTextLanguage; + disabledTextTrackSelectionFlags = initialValues.disabledTextTrackSelectionFlags; forceLowestBitrate = initialValues.forceLowestBitrate; allowMixedMimeAdaptiveness = initialValues.allowMixedMimeAdaptiveness; allowNonSeamlessAdaptiveness = initialValues.allowNonSeamlessAdaptiveness; @@ -153,6 +155,17 @@ public class DefaultTrackSelector extends MappingTrackSelector { return this; } + /** + * See {@link Parameters#disabledTextTrackSelectionFlags}. + * + * @return This builder. + */ + public ParametersBuilder setDisabledTextTrackSelectionFlags( + int disabledTextTrackSelectionFlags) { + this.disabledTextTrackSelectionFlags = disabledTextTrackSelectionFlags; + return this; + } + /** * See {@link Parameters#forceLowestBitrate}. * @@ -287,11 +300,22 @@ public class DefaultTrackSelector extends MappingTrackSelector { * Builds a {@link Parameters} instance with the selected values. */ public Parameters build() { - return new Parameters(preferredAudioLanguage, preferredTextLanguage, - selectUndeterminedTextLanguage, forceLowestBitrate, allowMixedMimeAdaptiveness, - allowNonSeamlessAdaptiveness, maxVideoWidth, maxVideoHeight, maxVideoBitrate, - exceedVideoConstraintsIfNecessary, exceedRendererCapabilitiesIfNecessary, viewportWidth, - viewportHeight, viewportOrientationMayChange); + return new Parameters( + preferredAudioLanguage, + preferredTextLanguage, + selectUndeterminedTextLanguage, + disabledTextTrackSelectionFlags, + forceLowestBitrate, + allowMixedMimeAdaptiveness, + allowNonSeamlessAdaptiveness, + maxVideoWidth, + maxVideoHeight, + maxVideoBitrate, + exceedVideoConstraintsIfNecessary, + exceedRendererCapabilitiesIfNecessary, + viewportWidth, + viewportHeight, + viewportOrientationMayChange); } } @@ -303,19 +327,21 @@ public class DefaultTrackSelector extends MappingTrackSelector { /** * An instance with default values: + * *

            - *
          • No preferred audio language.
          • - *
          • No preferred text language.
          • - *
          • Text tracks with undetermined language are not selected if no track with - * {@link #preferredTextLanguage} is available.
          • - *
          • Lowest bitrate track selections are not forced.
          • - *
          • Adaptation between different mime types is not allowed.
          • - *
          • Non seamless adaptation is allowed.
          • - *
          • No max limit for video width/height.
          • - *
          • No max video bitrate.
          • - *
          • Video constraints are exceeded if no supported selection can be made otherwise.
          • - *
          • Renderer capabilities are exceeded if no supported selection can be made.
          • - *
          • No viewport constraints.
          • + *
          • No preferred audio language. + *
          • No preferred text language. + *
          • Text tracks with undetermined language are not selected if no track with {@link + * #preferredTextLanguage} is available. + *
          • All selection flags are considered for text track selections. + *
          • Lowest bitrate track selections are not forced. + *
          • Adaptation between different mime types is not allowed. + *
          • Non seamless adaptation is allowed. + *
          • No max limit for video width/height. + *
          • No max video bitrate. + *
          • Video constraints are exceeded if no supported selection can be made otherwise. + *
          • Renderer capabilities are exceeded if no supported selection can be made. + *
          • No viewport constraints. *
          */ public static final Parameters DEFAULT = new Parameters(); @@ -338,6 +364,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { * {@link #preferredTextLanguage} is available, or if {@link #preferredTextLanguage} is unset. */ public final boolean selectUndeterminedTextLanguage; + /** + * Bitmask of selection flags that are disabled for text track selections. See {@link + * C.SelectionFlags}. + */ + public final int disabledTextTrackSelectionFlags; // Video /** @@ -392,19 +423,44 @@ public class DefaultTrackSelector extends MappingTrackSelector { public final boolean exceedRendererCapabilitiesIfNecessary; private Parameters() { - this(null, null, false, false, false, true, Integer.MAX_VALUE, Integer.MAX_VALUE, - Integer.MAX_VALUE, true, true, Integer.MAX_VALUE, Integer.MAX_VALUE, true); + this( + null, + null, + false, + 0, + false, + false, + true, + Integer.MAX_VALUE, + Integer.MAX_VALUE, + Integer.MAX_VALUE, + true, + true, + Integer.MAX_VALUE, + Integer.MAX_VALUE, + true); } - private Parameters(String preferredAudioLanguage, String preferredTextLanguage, - boolean selectUndeterminedTextLanguage, boolean forceLowestBitrate, - boolean allowMixedMimeAdaptiveness, boolean allowNonSeamlessAdaptiveness, int maxVideoWidth, - int maxVideoHeight, int maxVideoBitrate, boolean exceedVideoConstraintsIfNecessary, - boolean exceedRendererCapabilitiesIfNecessary, int viewportWidth, int viewportHeight, + private Parameters( + String preferredAudioLanguage, + String preferredTextLanguage, + boolean selectUndeterminedTextLanguage, + int disabledTextTrackSelectionFlags, + boolean forceLowestBitrate, + boolean allowMixedMimeAdaptiveness, + boolean allowNonSeamlessAdaptiveness, + int maxVideoWidth, + int maxVideoHeight, + int maxVideoBitrate, + boolean exceedVideoConstraintsIfNecessary, + boolean exceedRendererCapabilitiesIfNecessary, + int viewportWidth, + int viewportHeight, boolean viewportOrientationMayChange) { this.preferredAudioLanguage = Util.normalizeLanguageCode(preferredAudioLanguage); this.preferredTextLanguage = Util.normalizeLanguageCode(preferredTextLanguage); this.selectUndeterminedTextLanguage = selectUndeterminedTextLanguage; + this.disabledTextTrackSelectionFlags = disabledTextTrackSelectionFlags; this.forceLowestBitrate = forceLowestBitrate; this.allowMixedMimeAdaptiveness = allowMixedMimeAdaptiveness; this.allowNonSeamlessAdaptiveness = allowNonSeamlessAdaptiveness; @@ -434,14 +490,18 @@ public class DefaultTrackSelector extends MappingTrackSelector { return false; } Parameters other = (Parameters) obj; - return forceLowestBitrate == other.forceLowestBitrate + return selectUndeterminedTextLanguage == other.selectUndeterminedTextLanguage + && disabledTextTrackSelectionFlags == other.disabledTextTrackSelectionFlags + && forceLowestBitrate == other.forceLowestBitrate && allowMixedMimeAdaptiveness == other.allowMixedMimeAdaptiveness && allowNonSeamlessAdaptiveness == other.allowNonSeamlessAdaptiveness - && maxVideoWidth == other.maxVideoWidth && maxVideoHeight == other.maxVideoHeight + && maxVideoWidth == other.maxVideoWidth + && maxVideoHeight == other.maxVideoHeight && exceedVideoConstraintsIfNecessary == other.exceedVideoConstraintsIfNecessary && exceedRendererCapabilitiesIfNecessary == other.exceedRendererCapabilitiesIfNecessary && viewportOrientationMayChange == other.viewportOrientationMayChange - && viewportWidth == other.viewportWidth && viewportHeight == other.viewportHeight + && viewportWidth == other.viewportWidth + && viewportHeight == other.viewportHeight && maxVideoBitrate == other.maxVideoBitrate && TextUtils.equals(preferredAudioLanguage, other.preferredAudioLanguage) && TextUtils.equals(preferredTextLanguage, other.preferredTextLanguage); @@ -449,19 +509,21 @@ public class DefaultTrackSelector extends MappingTrackSelector { @Override public int hashCode() { - int result = preferredAudioLanguage.hashCode(); - result = 31 * result + preferredTextLanguage.hashCode(); + int result = selectUndeterminedTextLanguage ? 1 : 0; + result = 31 * result + disabledTextTrackSelectionFlags; result = 31 * result + (forceLowestBitrate ? 1 : 0); result = 31 * result + (allowMixedMimeAdaptiveness ? 1 : 0); result = 31 * result + (allowNonSeamlessAdaptiveness ? 1 : 0); result = 31 * result + maxVideoWidth; result = 31 * result + maxVideoHeight; - result = 31 * result + maxVideoBitrate; result = 31 * result + (exceedVideoConstraintsIfNecessary ? 1 : 0); result = 31 * result + (exceedRendererCapabilitiesIfNecessary ? 1 : 0); result = 31 * result + (viewportOrientationMayChange ? 1 : 0); result = 31 * result + viewportWidth; result = 31 * result + viewportHeight; + result = 31 * result + maxVideoBitrate; + result = 31 * result + preferredAudioLanguage.hashCode(); + result = 31 * result + preferredTextLanguage.hashCode(); return result; } @@ -923,8 +985,10 @@ public class DefaultTrackSelector extends MappingTrackSelector { if (isSupported(trackFormatSupport[trackIndex], params.exceedRendererCapabilitiesIfNecessary)) { Format format = trackGroup.getFormat(trackIndex); - boolean isDefault = (format.selectionFlags & C.SELECTION_FLAG_DEFAULT) != 0; - boolean isForced = (format.selectionFlags & C.SELECTION_FLAG_FORCED) != 0; + int maskedSelectionFlags = + format.selectionFlags & ~params.disabledTextTrackSelectionFlags; + boolean isDefault = (maskedSelectionFlags & C.SELECTION_FLAG_DEFAULT) != 0; + boolean isForced = (maskedSelectionFlags & C.SELECTION_FLAG_FORCED) != 0; int trackScore; boolean preferredLanguageFound = formatHasLanguage(format, params.preferredTextLanguage); if (preferredLanguageFound diff --git a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java index 1eff48b730..24362d1570 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java @@ -529,6 +529,134 @@ public final class DefaultTrackSelectorTest { .isEqualTo(lowerSampleRateHigherBitrateFormat); } + /** Tests text track selection flags. */ + @Test + public void testsTextTrackSelectionFlags() throws ExoPlaybackException { + Format forcedOnly = + Format.createTextContainerFormat( + "forcedOnly", + null, + MimeTypes.TEXT_VTT, + null, + Format.NO_VALUE, + C.SELECTION_FLAG_FORCED, + "eng"); + Format forcedDefault = + Format.createTextContainerFormat( + "forcedDefault", + null, + MimeTypes.TEXT_VTT, + null, + Format.NO_VALUE, + C.SELECTION_FLAG_FORCED | C.SELECTION_FLAG_DEFAULT, + "eng"); + Format defaultOnly = + Format.createTextContainerFormat( + "defaultOnly", + null, + MimeTypes.TEXT_VTT, + null, + Format.NO_VALUE, + C.SELECTION_FLAG_DEFAULT, + "eng"); + Format forcedOnlySpanish = + Format.createTextContainerFormat( + "forcedOnlySpanish", + null, + MimeTypes.TEXT_VTT, + null, + Format.NO_VALUE, + C.SELECTION_FLAG_FORCED, + "spa"); + Format noFlag = + Format.createTextContainerFormat( + "noFlag", null, MimeTypes.TEXT_VTT, null, Format.NO_VALUE, 0, "eng"); + + RendererCapabilities[] textRendererCapabilities = + new RendererCapabilities[] {ALL_TEXT_FORMAT_SUPPORTED_RENDERER_CAPABILITIES}; + + TrackSelectorResult result; + + // There is no text language preference, the first track flagged as default should be selected. + result = + trackSelector.selectTracks( + textRendererCapabilities, wrapFormats(forcedOnly, forcedDefault, defaultOnly, noFlag)); + assertThat(result.selections.get(0).getFormat(0)).isSameAs(forcedDefault); + + // Ditto. + result = + trackSelector.selectTracks( + textRendererCapabilities, wrapFormats(forcedOnly, noFlag, defaultOnly)); + assertThat(result.selections.get(0).getFormat(0)).isSameAs(defaultOnly); + + // With no language preference and no text track flagged as default, the first forced should be + // selected. + result = trackSelector.selectTracks(textRendererCapabilities, wrapFormats(forcedOnly, noFlag)); + assertThat(result.selections.get(0).getFormat(0)).isSameAs(forcedOnly); + + trackSelector.setParameters( + Parameters.DEFAULT + .buildUpon() + .setDisabledTextTrackSelectionFlags(C.SELECTION_FLAG_DEFAULT) + .build()); + + // Default flags are disabled, so the first track flagged as forced should be selected. + result = + trackSelector.selectTracks( + textRendererCapabilities, wrapFormats(defaultOnly, noFlag, forcedOnly, forcedDefault)); + assertThat(result.selections.get(0).getFormat(0)).isSameAs(forcedOnly); + + trackSelector.setParameters( + trackSelector.getParameters().buildUpon().setPreferredAudioLanguage("spa").build()); + + // Default flags are disabled, but there is a text track flagged as forced whose language + // matches the preferred audio language. + result = + trackSelector.selectTracks( + textRendererCapabilities, + wrapFormats(forcedDefault, forcedOnly, defaultOnly, noFlag, forcedOnlySpanish)); + assertThat(result.selections.get(0).getFormat(0)).isSameAs(forcedOnlySpanish); + + trackSelector.setParameters( + trackSelector + .getParameters() + .buildUpon() + .setDisabledTextTrackSelectionFlags(C.SELECTION_FLAG_DEFAULT | C.SELECTION_FLAG_FORCED) + .build()); + + // All selection flags are disabled and there is no language preference, so nothing should be + // selected. + result = + trackSelector.selectTracks( + textRendererCapabilities, wrapFormats(forcedOnly, forcedDefault, defaultOnly, noFlag)); + assertThat(result.selections.get(0)).isNull(); + + trackSelector.setParameters( + Parameters.DEFAULT.buildUpon().setPreferredTextLanguage("eng").build()); + + // There is a preferred language, so the first language-matching track flagged as default should + // be selected. + result = + trackSelector.selectTracks( + textRendererCapabilities, wrapFormats(forcedOnly, forcedDefault, defaultOnly, noFlag)); + assertThat(result.selections.get(0).getFormat(0)).isSameAs(forcedDefault); + + trackSelector.setParameters( + trackSelector + .getParameters() + .buildUpon() + .setDisabledTextTrackSelectionFlags(C.SELECTION_FLAG_DEFAULT) + .build()); + + // Same as above, but the default flag is disabled. If multiple tracks match the preferred + // language, those not flagged as forced are preferred, as they likely include the contents of + // forced subtitles. + result = + trackSelector.selectTracks( + textRendererCapabilities, wrapFormats(noFlag, forcedOnly, forcedDefault, defaultOnly)); + assertThat(result.selections.get(0).getFormat(0)).isSameAs(noFlag); + } + /** * Tests that the default track selector will select a text track with undetermined language if no * text track with the preferred language is available but From 435686f96923c56212cab540c0940f39ce180a18 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 14 Dec 2017 08:27:32 -0800 Subject: [PATCH 0955/2472] Add missing attrs to SimpleExoplayerView They worked without being present in the declare-styleable, but they need to be present for Android Studio auto-complete to suggest them. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179047478 --- library/ui/src/main/res/values/attrs.xml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/library/ui/src/main/res/values/attrs.xml b/library/ui/src/main/res/values/attrs.xml index 1ab3854d21..b6ed4b17af 100644 --- a/library/ui/src/main/res/values/attrs.xml +++ b/library/ui/src/main/res/values/attrs.xml @@ -51,10 +51,13 @@ + + - + + From edbb9795517363e486b203e0dd5e0dd3a5e41e53 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 14 Dec 2017 08:38:02 -0800 Subject: [PATCH 0956/2472] Allow to configure maximum buffer size in DefaultLoadControl. This adds a parameter to configure a maximum buffer size in bytes. If left at its default of C.LENGTH_UNSET, the target buffer is determined using a overridable method based on the track selection. Also adding a parameter to decide whether to prioritize time or size constraints. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179048554 --- RELEASENOTES.md | 2 + .../exoplayer2/DefaultLoadControl.java | 115 +++++++++++++++--- 2 files changed, 97 insertions(+), 20 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 686a6d10ba..c7f7ed7bbd 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -64,6 +64,8 @@ * DefaultTrackSelector: Support undefined language text track selection when the preferred language is not available ([#2980](https://github.com/google/ExoPlayer/issues/2980)). +* Add options to `DefaultLoadControl` to set maximum buffer size in bytes and + to choose whether size or time constraints are prioritized. * Use surfaceless context for secure `DummySurface`, if available ([#3558](https://github.com/google/ExoPlayer/issues/3558)). * FLV: Fix playback of live streams that do not contain an audio track diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java index d329f6584b..3708500d9f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java @@ -51,12 +51,23 @@ public class DefaultLoadControl implements LoadControl { */ public static final int DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = 5000; + /** + * The default target buffer size in bytes. When set to {@link C#LENGTH_UNSET}, the load control + * automatically determines its target buffer size. + */ + public static final int DEFAULT_TARGET_BUFFER_BYTES = C.LENGTH_UNSET; + + /** The default prioritization of buffer time constraints over size constraints. */ + public static final boolean DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS = true; + private final DefaultAllocator allocator; private final long minBufferUs; private final long maxBufferUs; private final long bufferForPlaybackUs; private final long bufferForPlaybackAfterRebufferUs; + private final int targetBufferBytesOverwrite; + private final boolean prioritizeTimeOverSizeThresholds; private final PriorityTaskManager priorityTaskManager; private int targetBufferSize; @@ -75,8 +86,14 @@ public class DefaultLoadControl implements LoadControl { * @param allocator The {@link DefaultAllocator} used by the loader. */ public DefaultLoadControl(DefaultAllocator allocator) { - this(allocator, DEFAULT_MIN_BUFFER_MS, DEFAULT_MAX_BUFFER_MS, DEFAULT_BUFFER_FOR_PLAYBACK_MS, - DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS); + this( + allocator, + DEFAULT_MIN_BUFFER_MS, + DEFAULT_MAX_BUFFER_MS, + DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS, + DEFAULT_BUFFER_FOR_PLAYBACK_MS, + DEFAULT_TARGET_BUFFER_BYTES, + DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS); } /** @@ -92,10 +109,27 @@ public class DefaultLoadControl implements LoadControl { * @param bufferForPlaybackAfterRebufferMs The default duration of media that must be buffered for * playback to resume after a rebuffer, in milliseconds. A rebuffer is defined to be caused by * buffer depletion rather than a user action. + * @param targetBufferBytes The target buffer size in bytes. If set to {@link C#LENGTH_UNSET}, the + * target buffer size will be calculated using {@link #calculateTargetBufferSize(Renderer[], + * TrackSelectionArray)}. + * @param prioritizeTimeOverSizeThresholds Whether the load control prioritizes buffer time */ - public DefaultLoadControl(DefaultAllocator allocator, int minBufferMs, int maxBufferMs, - long bufferForPlaybackMs, long bufferForPlaybackAfterRebufferMs) { - this(allocator, minBufferMs, maxBufferMs, bufferForPlaybackMs, bufferForPlaybackAfterRebufferMs, + public DefaultLoadControl( + DefaultAllocator allocator, + int minBufferMs, + int maxBufferMs, + int bufferForPlaybackMs, + int bufferForPlaybackAfterRebufferMs, + int targetBufferBytes, + boolean prioritizeTimeOverSizeThresholds) { + this( + allocator, + minBufferMs, + maxBufferMs, + bufferForPlaybackMs, + bufferForPlaybackAfterRebufferMs, + targetBufferBytes, + prioritizeTimeOverSizeThresholds, null); } @@ -112,18 +146,30 @@ public class DefaultLoadControl implements LoadControl { * @param bufferForPlaybackAfterRebufferMs The default duration of media that must be buffered for * playback to resume after a rebuffer, in milliseconds. A rebuffer is defined to be caused by * buffer depletion rather than a user action. - * @param priorityTaskManager If not null, registers itself as a task with priority - * {@link C#PRIORITY_PLAYBACK} during loading periods, and unregisters itself during draining - * periods. + * @param targetBufferBytes The target buffer size in bytes. If set to {@link C#LENGTH_UNSET}, the + * target buffer size will be calculated using {@link #calculateTargetBufferSize(Renderer[], + * TrackSelectionArray)}. + * @param prioritizeTimeOverSizeThresholds Whether the load control prioritizes buffer time + * constraints over buffer size constraints. + * @param priorityTaskManager If not null, registers itself as a task with priority {@link + * C#PRIORITY_PLAYBACK} during loading periods, and unregisters itself during draining */ - public DefaultLoadControl(DefaultAllocator allocator, int minBufferMs, int maxBufferMs, - long bufferForPlaybackMs, long bufferForPlaybackAfterRebufferMs, + public DefaultLoadControl( + DefaultAllocator allocator, + int minBufferMs, + int maxBufferMs, + int bufferForPlaybackMs, + int bufferForPlaybackAfterRebufferMs, + int targetBufferBytes, + boolean prioritizeTimeOverSizeThresholds, PriorityTaskManager priorityTaskManager) { this.allocator = allocator; minBufferUs = minBufferMs * 1000L; maxBufferUs = maxBufferMs * 1000L; + targetBufferBytesOverwrite = targetBufferBytes; bufferForPlaybackUs = bufferForPlaybackMs * 1000L; bufferForPlaybackAfterRebufferUs = bufferForPlaybackAfterRebufferMs * 1000L; + this.prioritizeTimeOverSizeThresholds = prioritizeTimeOverSizeThresholds; this.priorityTaskManager = priorityTaskManager; } @@ -135,12 +181,10 @@ public class DefaultLoadControl implements LoadControl { @Override public void onTracksSelected(Renderer[] renderers, TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { - targetBufferSize = 0; - for (int i = 0; i < renderers.length; i++) { - if (trackSelections.get(i) != null) { - targetBufferSize += Util.getDefaultBufferSize(renderers[i].getTrackType()); - } - } + targetBufferSize = + targetBufferBytesOverwrite == C.LENGTH_UNSET + ? calculateTargetBufferSize(renderers, trackSelections) + : targetBufferBytesOverwrite; allocator.setTargetBufferSize(targetBufferSize); } @@ -178,16 +222,28 @@ public class DefaultLoadControl implements LoadControl { } bufferedDurationUs = Util.getPlayoutDurationForMediaDuration(bufferedDurationUs, playbackSpeed); long minBufferDurationUs = rebuffering ? bufferForPlaybackAfterRebufferUs : bufferForPlaybackUs; - return minBufferDurationUs <= 0 || bufferedDurationUs >= minBufferDurationUs; + return minBufferDurationUs <= 0 + || bufferedDurationUs >= minBufferDurationUs + || (!prioritizeTimeOverSizeThresholds + && allocator.getTotalBytesAllocated() >= targetBufferSize); } @Override public boolean shouldContinueLoading(long bufferedDurationUs, float playbackSpeed) { boolean targetBufferSizeReached = allocator.getTotalBytesAllocated() >= targetBufferSize; boolean wasBuffering = isBuffering; - isBuffering = bufferedDurationUs < minBufferUs // below low watermark - || (bufferedDurationUs <= maxBufferUs // between watermarks - && isBuffering && !targetBufferSizeReached); + if (prioritizeTimeOverSizeThresholds) { + isBuffering = + bufferedDurationUs < minBufferUs // below low watermark + || (bufferedDurationUs <= maxBufferUs // between watermarks + && isBuffering + && !targetBufferSizeReached); + } else { + isBuffering = + !targetBufferSizeReached + && (bufferedDurationUs < minBufferUs // below low watermark + || (bufferedDurationUs <= maxBufferUs && isBuffering)); // between watermarks + } if (priorityTaskManager != null && isBuffering != wasBuffering) { if (isBuffering) { priorityTaskManager.add(C.PRIORITY_PLAYBACK); @@ -198,6 +254,25 @@ public class DefaultLoadControl implements LoadControl { return isBuffering; } + /** + * Calculate target buffer size in bytes based on the selected tracks. The player will try not to + * exceed this target buffer. Only used when {@code targetBufferBytes} is {@link C#LENGTH_UNSET}. + * + * @param renderers The renderers for which the track were selected. + * @param trackSelectionArray The selected tracks. + * @return The target buffer size in bytes. + */ + protected int calculateTargetBufferSize( + Renderer[] renderers, TrackSelectionArray trackSelectionArray) { + int targetBufferSize = 0; + for (int i = 0; i < renderers.length; i++) { + if (trackSelectionArray.get(i) != null) { + targetBufferSize += Util.getDefaultBufferSize(renderers[i].getTrackType()); + } + } + return targetBufferSize; + } + private void reset(boolean resetAllocator) { targetBufferSize = 0; if (priorityTaskManager != null && isBuffering) { From 65360760c2fba971de5e8433f94b0448eb22f41f Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 15 Dec 2017 02:17:30 -0800 Subject: [PATCH 0957/2472] Pass -1 not C.TIME_UNSET when duration is unknown ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179165479 --- .../google/android/exoplayer2/ext/ima/ImaAdsLoader.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index acfe143952..79074d3956 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -151,6 +151,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A private static final String IMA_SDK_SETTINGS_PLAYER_TYPE = "google/exo.ext.ima"; private static final String IMA_SDK_SETTINGS_PLAYER_VERSION = ExoPlayerLibraryInfo.VERSION; + /** The value used in {@link VideoProgressUpdate}s to indicate an unset duration. */ + private static final long IMA_DURATION_UNSET = -1L; + /** * Threshold before the end of content at which IMA is notified that content is complete if the * player buffers, in milliseconds. @@ -533,6 +536,8 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A @Override public VideoProgressUpdate getContentProgress() { + boolean hasContentDuration = contentDurationMs != C.TIME_UNSET; + long contentDurationMs = hasContentDuration ? this.contentDurationMs : IMA_DURATION_UNSET; if (player == null) { return lastContentProgress; } else if (pendingContentPositionMs != C.TIME_UNSET) { @@ -542,7 +547,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A long elapsedSinceEndMs = SystemClock.elapsedRealtime() - fakeContentProgressElapsedRealtimeMs; long fakePositionMs = fakeContentProgressOffsetMs + elapsedSinceEndMs; return new VideoProgressUpdate(fakePositionMs, contentDurationMs); - } else if (playingAd || contentDurationMs == C.TIME_UNSET) { + } else if (playingAd || !hasContentDuration) { return VideoProgressUpdate.VIDEO_TIME_NOT_READY; } else { return new VideoProgressUpdate(player.getCurrentPosition(), contentDurationMs); From e913ede77e65cfbea63426ef35005c740048e783 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 15 Dec 2017 02:47:56 -0800 Subject: [PATCH 0958/2472] Fix condition for detecting that an ad has ended onEnded was being called also for content finishing, as in this case the playing ad index changed (from INDEX_UNSET to 0). Fix this test so we only detect ads finishing. Also add logging for onEnded callbacks. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179167737 --- .../android/exoplayer2/ext/ima/ImaAdsLoader.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index 79074d3956..0f0f64c068 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -703,6 +703,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A for (int i = 0; i < adCallbacks.size(); i++) { adCallbacks.get(i).onEnded(); } + if (DEBUG) { + Log.d(TAG, "VideoAdPlayerCallback.onEnded in onPlayerStateChanged"); + } } } @@ -798,16 +801,20 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A private void updateImaStateForPlayerState() { boolean wasPlayingAd = playingAd; + int oldPlayingAdIndexInAdGroup = playingAdIndexInAdGroup; playingAd = player.isPlayingAd(); + playingAdIndexInAdGroup = playingAd ? player.getCurrentAdIndexInAdGroup() : C.INDEX_UNSET; if (!sentContentComplete) { - boolean adFinished = (wasPlayingAd && !playingAd) - || playingAdIndexInAdGroup != player.getCurrentAdIndexInAdGroup(); + boolean adFinished = wasPlayingAd && playingAdIndexInAdGroup != oldPlayingAdIndexInAdGroup; if (adFinished) { // IMA is waiting for the ad playback to finish so invoke the callback now. // Either CONTENT_RESUME_REQUESTED will be passed next, or playAd will be called again. for (int i = 0; i < adCallbacks.size(); i++) { adCallbacks.get(i).onEnded(); } + if (DEBUG) { + Log.d(TAG, "VideoAdPlayerCallback.onEnded in onTimelineChanged/onPositionDiscontinuity"); + } } if (!wasPlayingAd && playingAd) { int adGroupIndex = player.getCurrentAdGroupIndex(); @@ -819,7 +826,6 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A } } } - playingAdIndexInAdGroup = playingAd ? player.getCurrentAdIndexInAdGroup() : C.INDEX_UNSET; } private void resumeContentInternal() { From 3cc08d0ea3d28c88b2aef7c650bbe5f85a67c712 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 15 Dec 2017 02:47:56 -0800 Subject: [PATCH 0959/2472] Fix typo Issue #3594 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179167738 --- .../com/google/android/exoplayer2/text/cea/Cea708Decoder.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/core/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 index 030f0cdbb0..6bdbebc73b 100644 --- a/library/core/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 @@ -104,7 +104,7 @@ public final class Cea708Decoder extends CeaDecoder { private static final int COMMAND_DF1 = 0x99; // DefineWindow 1 (+6 bytes) private static final int COMMAND_DF2 = 0x9A; // DefineWindow 2 (+6 bytes) private static final int COMMAND_DF3 = 0x9B; // DefineWindow 3 (+6 bytes) - private static final int COMMAND_DS4 = 0x9C; // DefineWindow 4 (+6 bytes) + private static final int COMMAND_DF4 = 0x9C; // DefineWindow 4 (+6 bytes) private static final int COMMAND_DF5 = 0x9D; // DefineWindow 5 (+6 bytes) private static final int COMMAND_DF6 = 0x9E; // DefineWindow 6 (+6 bytes) private static final int COMMAND_DF7 = 0x9F; // DefineWindow 7 (+6 bytes) @@ -464,7 +464,7 @@ public final class Cea708Decoder extends CeaDecoder { case COMMAND_DF1: case COMMAND_DF2: case COMMAND_DF3: - case COMMAND_DS4: + case COMMAND_DF4: case COMMAND_DF5: case COMMAND_DF6: case COMMAND_DF7: From 8e35bffcc3f6a1bbadf94b748c7ea7e215874e8b Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 15 Dec 2017 03:06:10 -0800 Subject: [PATCH 0960/2472] Use playAd/stopAd to control position updates switching Previously the ad/content progress updates were toggled based on whether the player was playing ads or content. After this change, we switch based on whether playAd/stopAd has been called instead. This seems to resolve an issue where occasionally the player would get stuck at the start of an ad, but as I don't have a root cause for that issue and it's only sporadically reproducible I'm not certain this is a reliable fix. Issue: #3525 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179169296 --- .../com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index 0f0f64c068..284d716582 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -547,7 +547,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A long elapsedSinceEndMs = SystemClock.elapsedRealtime() - fakeContentProgressElapsedRealtimeMs; long fakePositionMs = fakeContentProgressOffsetMs + elapsedSinceEndMs; return new VideoProgressUpdate(fakePositionMs, contentDurationMs); - } else if (playingAd || !hasContentDuration) { + } else if (imaAdState != IMA_AD_STATE_NONE || !hasContentDuration) { return VideoProgressUpdate.VIDEO_TIME_NOT_READY; } else { return new VideoProgressUpdate(player.getCurrentPosition(), contentDurationMs); @@ -560,7 +560,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A public VideoProgressUpdate getAdProgress() { if (player == null) { return lastAdProgress; - } else if (!playingAd) { + } else if (imaAdState == IMA_AD_STATE_NONE) { return VideoProgressUpdate.VIDEO_TIME_NOT_READY; } else { long adDuration = player.getDuration(); From bb8c60879517280a1fc10601ed871f377c1f5138 Mon Sep 17 00:00:00 2001 From: hoangtc Date: Fri, 15 Dec 2017 03:42:17 -0800 Subject: [PATCH 0961/2472] Make updating showTimeoutMs takes effect immediately. Update PlaybackControlView and SimpleExoPlayerView so when showTimeoutMs is set while the controller is shown, the new timeout takes effect immediately. GitHub: #3554 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179171727 --- .../google/android/exoplayer2/ui/PlaybackControlView.java | 4 ++++ .../google/android/exoplayer2/ui/SimpleExoPlayerView.java | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java index 751a6c81a9..7659dff9c6 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java @@ -519,6 +519,10 @@ public class PlaybackControlView extends FrameLayout { */ public void setShowTimeoutMs(int showTimeoutMs) { this.showTimeoutMs = showTimeoutMs; + // showTimeoutMs is changed, so call hideAfterTimeout to reset the timeout. + if (isVisible()) { + hideAfterTimeout(); + } } /** diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index 1f67b83ba0..c5a4bc8086 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -613,6 +613,11 @@ public final class SimpleExoPlayerView extends FrameLayout { public void setControllerShowTimeoutMs(int controllerShowTimeoutMs) { Assertions.checkState(controller != null); this.controllerShowTimeoutMs = controllerShowTimeoutMs; + // If controller is already visible, call showController to update the controller's timeout + // if necessary. + if (controller.isVisible()) { + showController(); + } } /** From bf9a919005a0968909f565e5a6840d8eb4b12e08 Mon Sep 17 00:00:00 2001 From: bachinger Date: Tue, 12 Dec 2017 11:05:57 -0800 Subject: [PATCH 0962/2472] Propagate extras from queue item to metadata item. norelnotes=true ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178785377 --- .../mediasession/MediaSessionConnector.java | 45 +++++++++++++++---- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java index aa007ea1d6..1b1224273f 100644 --- a/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java +++ b/extensions/mediasession/src/main/java/com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.ext.mediasession; +import android.graphics.Bitmap; import android.net.Uri; import android.os.Bundle; import android.os.Handler; @@ -330,6 +331,7 @@ public final class MediaSessionConnector { private final ExoPlayerEventListener exoPlayerEventListener; private final MediaSessionCallback mediaSessionCallback; private final PlaybackController playbackController; + private final String metadataExtrasPrefix; private final Map commandMap; private Player player; @@ -356,15 +358,15 @@ public final class MediaSessionConnector { /** * Creates an instance. Must be called on the same thread that is used to construct the player * instances passed to {@link #setPlayer(Player, PlaybackPreparer, CustomActionProvider...)}. - *

          - * Equivalent to {@code MediaSessionConnector(mediaSession, playbackController, true)}. + * + *

          Equivalent to {@code MediaSessionConnector(mediaSession, playbackController, true, null)}. * * @param mediaSession The {@link MediaSessionCompat} to connect to. * @param playbackController A {@link PlaybackController} for handling playback actions. */ - public MediaSessionConnector(MediaSessionCompat mediaSession, - PlaybackController playbackController) { - this(mediaSession, playbackController, true); + public MediaSessionConnector( + MediaSessionCompat mediaSession, PlaybackController playbackController) { + this(mediaSession, playbackController, true, null); } /** @@ -372,17 +374,23 @@ public final class MediaSessionConnector { * instances passed to {@link #setPlayer(Player, PlaybackPreparer, CustomActionProvider...)}. * * @param mediaSession The {@link MediaSessionCompat} to connect to. - * @param playbackController A {@link PlaybackController} for handling playback actions, or - * {@code null} if the connector should handle playback actions directly. + * @param playbackController A {@link PlaybackController} for handling playback actions, or {@code + * null} if the connector should handle playback actions directly. * @param doMaintainMetadata Whether the connector should maintain the metadata of the session. If * {@code false}, you need to maintain the metadata of the media session yourself (provide at * least the duration to allow clients to show a progress bar). + * @param metadataExtrasPrefix A string to prefix extra keys which are propagated from the active + * queue item to the session metadata. */ - public MediaSessionConnector(MediaSessionCompat mediaSession, - PlaybackController playbackController, boolean doMaintainMetadata) { + public MediaSessionConnector( + MediaSessionCompat mediaSession, + PlaybackController playbackController, + boolean doMaintainMetadata, + @Nullable String metadataExtrasPrefix) { this.mediaSession = mediaSession; this.playbackController = playbackController != null ? playbackController : new DefaultPlaybackController(); + this.metadataExtrasPrefix = metadataExtrasPrefix != null ? metadataExtrasPrefix : ""; this.handler = new Handler(Looper.myLooper() != null ? Looper.myLooper() : Looper.getMainLooper()); this.doMaintainMetadata = doMaintainMetadata; @@ -553,6 +561,25 @@ public final class MediaSessionConnector { MediaSessionCompat.QueueItem queueItem = queue.get(i); if (queueItem.getQueueId() == activeQueueItemId) { MediaDescriptionCompat description = queueItem.getDescription(); + Bundle extras = description.getExtras(); + if (extras != null) { + for (String key : extras.keySet()) { + Object value = extras.get(key); + if (value instanceof String) { + builder.putString(metadataExtrasPrefix + key, (String) value); + } else if (value instanceof CharSequence) { + builder.putText(metadataExtrasPrefix + key, (CharSequence) value); + } else if (value instanceof Long) { + builder.putLong(metadataExtrasPrefix + key, (Long) value); + } else if (value instanceof Integer) { + builder.putLong(metadataExtrasPrefix + key, (Integer) value); + } else if (value instanceof Bitmap) { + builder.putBitmap(metadataExtrasPrefix + key, (Bitmap) value); + } else if (value instanceof RatingCompat) { + builder.putRating(metadataExtrasPrefix + key, (RatingCompat) value); + } + } + } if (description.getTitle() != null) { builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, String.valueOf(description.getTitle())); From 65ccff24483a60bb93ac31255b062803d9c60f86 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 12 Dec 2017 23:43:16 -0800 Subject: [PATCH 0963/2472] Update release notes to reflect builder -> factory change ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178866131 --- RELEASENOTES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 1dab497cf2..61e6d2759d 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,7 +2,7 @@ ### dev-v2 (not yet released) ### -* Add Builder to ExtractorMediaSource, HlsMediaSource, SsMediaSource, +* Add Factory to ExtractorMediaSource, HlsMediaSource, SsMediaSource, DashMediaSource, SingleSampleMediaSource. * DASH: * Support time zone designators in ISO8601 UTCTiming elements From ae514b68ff3c93e09972d6410ab2b64cffe62684 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 13 Dec 2017 02:16:15 -0800 Subject: [PATCH 0964/2472] Update release notes for current 2.6.1 feature set ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178877884 --- RELEASENOTES.md | 43 ++++++++++++++++++++++--------------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 61e6d2759d..2f9008045e 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,29 +1,11 @@ # Release notes # -### dev-v2 (not yet released) ### +### 2.6.1 ### -* Add Factory to ExtractorMediaSource, HlsMediaSource, SsMediaSource, - DashMediaSource, SingleSampleMediaSource. -* DASH: - * Support time zone designators in ISO8601 UTCTiming elements - ([#3524](https://github.com/google/ExoPlayer/issues/3524)). -* Support 32-bit PCM float output from `DefaultAudioSink`, and add an option to - use this with `FfmpegAudioRenderer`. -* Support extraction and decoding of Dolby Atmos - ([#2465](https://github.com/google/ExoPlayer/issues/2465)). -* DefaultTrackSelector: Support undefined language text track selection when the - preferred language is not available - ([#2980](https://github.com/google/ExoPlayer/issues/2980)). -* Fix handling of playback parameters changes while paused when followed by a - seek. -* Fix playback of live FLV streams that do not contain an audio track - ([#3188](https://github.com/google/ExoPlayer/issues/3188)). +* Add factories to `ExtractorMediaSource`, `HlsMediaSource`, `SsMediaSource`, + `DashMediaSource` and `SingleSampleMediaSource`. * Use the same listener `MediaSourceEventListener` for all MediaSource implementations. -* CEA-608: Fix handling of row count changes in roll-up mode - ([#3513](https://github.com/google/ExoPlayer/issues/3513)). -* Use surfaceless context for secure DummySurface, if available - ([#3558](https://github.com/google/ExoPlayer/issues/3558)). * IMA extension: * Support non-ExtractorMediaSource ads ([#3302](https://github.com/google/ExoPlayer/issues/3302)). @@ -36,6 +18,25 @@ ([#3548](https://github.com/google/ExoPlayer/issues/3548)). * Support overriding the ad load timeout ([#3556](https://github.com/google/ExoPlayer/issues/3556)). +* DASH: Support time zone designators in ISO8601 UTCTiming elements + ([#3524](https://github.com/google/ExoPlayer/issues/3524)). +* Audio: + * Support 32-bit PCM float output from `DefaultAudioSink`, and add an option + to use this with `FfmpegAudioRenderer`. + * Support extraction and decoding of Dolby Atmos + ([#2465](https://github.com/google/ExoPlayer/issues/2465)). + * Fix handling of playback parameter changes while paused when followed by a + seek. +* SimpleExoPlayer: Allow multiple audio and video debug listeners. +* DefaultTrackSelector: Support undefined language text track selection when the + preferred language is not available + ([#2980](https://github.com/google/ExoPlayer/issues/2980)). +* Use surfaceless context for secure `DummySurface`, if available + ([#3558](https://github.com/google/ExoPlayer/issues/3558)). +* FLV: Fix playback of live streams that do not contain an audio track + ([#3188](https://github.com/google/ExoPlayer/issues/3188)). +* CEA-608: Fix handling of row count changes in roll-up mode + ([#3513](https://github.com/google/ExoPlayer/issues/3513)). ### 2.6.0 ### From 8a6c375c53005fec5de6790bb1109f3937363632 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 13 Dec 2017 08:09:21 -0800 Subject: [PATCH 0965/2472] Check if native libraries are available in tests. If the library is not available, no tracks can be selected and the tests silently run through by immediately switching to ended state without error. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178904347 --- .../android/exoplayer2/ext/flac/FlacExtractorTest.java | 8 ++++++++ .../android/exoplayer2/ext/flac/FlacPlaybackTest.java | 9 ++++++++- .../android/exoplayer2/ext/opus/OpusPlaybackTest.java | 8 ++++++++ .../android/exoplayer2/ext/vp9/VpxPlaybackTest.java | 9 ++++++++- 4 files changed, 32 insertions(+), 2 deletions(-) diff --git a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java index 7b193997c3..57ce487ac7 100644 --- a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java +++ b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorTest.java @@ -25,6 +25,14 @@ import com.google.android.exoplayer2.testutil.ExtractorAsserts.ExtractorFactory; */ public class FlacExtractorTest extends InstrumentationTestCase { + @Override + protected void setUp() throws Exception { + super.setUp(); + if (!FlacLibrary.isAvailable()) { + fail("Flac library not available."); + } + } + public void testSample() throws Exception { ExtractorAsserts.assertBehavior(new ExtractorFactory() { @Override 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 fd18a3b1ae..b236b706b8 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 @@ -37,6 +37,14 @@ public class FlacPlaybackTest extends InstrumentationTestCase { private static final String BEAR_FLAC_URI = "asset:///bear-flac.mka"; + @Override + protected void setUp() throws Exception { + super.setUp(); + if (!FlacLibrary.isAvailable()) { + fail("Flac library not available."); + } + } + public void testBasicPlayback() throws ExoPlaybackException { playUri(BEAR_FLAC_URI); } @@ -100,7 +108,6 @@ public class FlacPlaybackTest extends InstrumentationTestCase { Looper.myLooper().quit(); } } - } } 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 d3ab421655..c547cff434 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 @@ -37,6 +37,14 @@ public class OpusPlaybackTest extends InstrumentationTestCase { private static final String BEAR_OPUS_URI = "asset:///bear-opus.webm"; + @Override + protected void setUp() throws Exception { + super.setUp(); + if (!OpusLibrary.isAvailable()) { + fail("Opus library not available."); + } + } + public void testBasicPlayback() throws ExoPlaybackException { playUri(BEAR_OPUS_URI); } 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 3cc1a1d340..0a902e2efe 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 @@ -43,6 +43,14 @@ public class VpxPlaybackTest extends InstrumentationTestCase { private static final String TAG = "VpxPlaybackTest"; + @Override + protected void setUp() throws Exception { + super.setUp(); + if (!VpxLibrary.isAvailable()) { + fail("Vpx library not available."); + } + } + public void testBasicPlayback() throws ExoPlaybackException { playUri(BEAR_URI); } @@ -132,7 +140,6 @@ public class VpxPlaybackTest extends InstrumentationTestCase { Looper.myLooper().quit(); } } - } } From 67b94a72a55ba1e1e50665bbede907a0e3fc749e Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 13 Dec 2017 08:32:28 -0800 Subject: [PATCH 0966/2472] Update SingleSampleMediaSource with factory/listener changes - Convert the Builder into a Factory - Have it use MediaSourceEventListener - Also made some misc related fixes to other sources ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=178906521 --- .../source/ExtractorMediaPeriod.java | 17 +- .../source/ExtractorMediaSource.java | 4 +- .../source/SingleSampleMediaPeriod.java | 99 +++--- .../source/SingleSampleMediaSource.java | 282 +++++++++++++----- .../exoplayer2/source/hls/HlsMediaSource.java | 4 +- 5 files changed, 280 insertions(+), 126 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java index f557d4ac97..f907dc6229 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -431,9 +431,6 @@ import java.util.Arrays; @Override public void onLoadCanceled(ExtractingLoadable loadable, long elapsedRealtimeMs, long loadDurationMs, boolean released) { - if (released) { - return; - } eventDispatcher.loadCanceled( loadable.dataSpec, C.DATA_TYPE_MEDIA, @@ -446,12 +443,14 @@ import java.util.Arrays; elapsedRealtimeMs, loadDurationMs, loadable.bytesLoaded); - copyLengthFromLoader(loadable); - for (SampleQueue sampleQueue : sampleQueues) { - sampleQueue.reset(); - } - if (enabledTrackCount > 0) { - callback.onContinueLoadingRequested(this); + if (!released) { + copyLengthFromLoader(loadable); + for (SampleQueue sampleQueue : sampleQueues) { + sampleQueue.reset(); + } + if (enabledTrackCount > 0) { + callback.onContinueLoadingRequested(this); + } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java index e787c34a9c..3b650482f5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java @@ -195,7 +195,7 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe * @param uri The {@link Uri}. * @return The new {@link ExtractorMediaSource}. */ - public MediaSource createMediaSource(Uri uri) { + public ExtractorMediaSource createMediaSource(Uri uri) { return createMediaSource(uri, null, null); } @@ -208,7 +208,7 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe * @return The new {@link ExtractorMediaSource}. */ @Override - public MediaSource createMediaSource( + public ExtractorMediaSource createMediaSource( Uri uri, @Nullable Handler eventHandler, @Nullable MediaSourceEventListener eventListener) { isCreateCalled = true; if (extractorsFactory == null) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java index 6101c79b7f..5069a2d633 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java @@ -15,13 +15,11 @@ */ package com.google.android.exoplayer2.source; -import android.net.Uri; -import android.os.Handler; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; -import com.google.android.exoplayer2.source.SingleSampleMediaSource.EventListener; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; @@ -43,14 +41,14 @@ import java.util.Arrays; */ private static final int INITIAL_SAMPLE_SIZE = 1024; - private final Uri uri; + private final DataSpec dataSpec; private final DataSource.Factory dataSourceFactory; private final int minLoadableRetryCount; - private final Handler eventHandler; - private final EventListener eventListener; - private final int eventSourceId; + private final EventDispatcher eventDispatcher; private final TrackGroupArray tracks; private final ArrayList sampleStreams; + private final long durationUs; + // Package private to avoid thunk methods. /* package */ final Loader loader; /* package */ final Format format; @@ -62,16 +60,20 @@ import java.util.Arrays; /* package */ int sampleSize; private int errorCount; - public SingleSampleMediaPeriod(Uri uri, DataSource.Factory dataSourceFactory, Format format, - int minLoadableRetryCount, Handler eventHandler, EventListener eventListener, - int eventSourceId, boolean treatLoadErrorsAsEndOfStream) { - this.uri = uri; + public SingleSampleMediaPeriod( + DataSpec dataSpec, + DataSource.Factory dataSourceFactory, + Format format, + long durationUs, + int minLoadableRetryCount, + EventDispatcher eventDispatcher, + boolean treatLoadErrorsAsEndOfStream) { + this.dataSpec = dataSpec; this.dataSourceFactory = dataSourceFactory; this.format = format; + this.durationUs = durationUs; this.minLoadableRetryCount = minLoadableRetryCount; - this.eventHandler = eventHandler; - this.eventListener = eventListener; - this.eventSourceId = eventSourceId; + this.eventDispatcher = eventDispatcher; this.treatLoadErrorsAsEndOfStream = treatLoadErrorsAsEndOfStream; tracks = new TrackGroupArray(new TrackGroup(format)); sampleStreams = new ArrayList<>(); @@ -125,7 +127,9 @@ import java.util.Arrays; if (loadingFinished || loader.isLoading()) { return false; } - loader.startLoading(new SourceLoadable(uri, dataSourceFactory.createDataSource()), this, + loader.startLoading( + new SourceLoadable(dataSpec, dataSourceFactory.createDataSource()), + this, minLoadableRetryCount); return true; } @@ -158,6 +162,18 @@ import java.util.Arrays; @Override public void onLoadCompleted(SourceLoadable loadable, long elapsedRealtimeMs, long loadDurationMs) { + eventDispatcher.loadCompleted( + loadable.dataSpec, + C.DATA_TYPE_MEDIA, + C.TRACK_TYPE_UNKNOWN, + format, + C.SELECTION_REASON_UNKNOWN, + /* trackSelectionData= */ null, + /* mediaStartTimeUs= */ 0, + durationUs, + elapsedRealtimeMs, + loadDurationMs, + loadable.sampleSize); sampleSize = loadable.sampleSize; sampleData = loadable.sampleData; loadingFinished = true; @@ -167,34 +183,46 @@ import java.util.Arrays; @Override public void onLoadCanceled(SourceLoadable loadable, long elapsedRealtimeMs, long loadDurationMs, boolean released) { - // Do nothing. + eventDispatcher.loadCanceled( + loadable.dataSpec, + C.DATA_TYPE_MEDIA, + C.TRACK_TYPE_UNKNOWN, + /* trackFormat= */ null, + C.SELECTION_REASON_UNKNOWN, + /* trackSelectionData= */ null, + /* mediaStartTimeUs= */ 0, + durationUs, + elapsedRealtimeMs, + loadDurationMs, + loadable.sampleSize); } @Override public int onLoadError(SourceLoadable loadable, long elapsedRealtimeMs, long loadDurationMs, IOException error) { - notifyLoadError(error); errorCount++; - if (treatLoadErrorsAsEndOfStream && errorCount >= minLoadableRetryCount) { + boolean cancel = treatLoadErrorsAsEndOfStream && errorCount >= minLoadableRetryCount; + eventDispatcher.loadError( + loadable.dataSpec, + C.DATA_TYPE_MEDIA, + C.TRACK_TYPE_UNKNOWN, + format, + C.SELECTION_REASON_UNKNOWN, + /* trackSelectionData= */ null, + /* mediaStartTimeUs= */ 0, + durationUs, + elapsedRealtimeMs, + loadDurationMs, + loadable.sampleSize, + error, + /* wasCanceled= */ cancel); + if (cancel) { loadingFinished = true; return Loader.DONT_RETRY; } return Loader.RETRY; } - // Internal methods. - - private void notifyLoadError(final IOException e) { - if (eventHandler != null && eventListener != null) { - eventHandler.post(new Runnable() { - @Override - public void run() { - eventListener.onLoadError(eventSourceId, e); - } - }); - } - } - private final class SampleStreamImpl implements SampleStream { private static final int STREAM_STATE_SEND_FORMAT = 0; @@ -259,14 +287,15 @@ import java.util.Arrays; /* package */ static final class SourceLoadable implements Loadable { - private final Uri uri; + public final DataSpec dataSpec; + private final DataSource dataSource; private int sampleSize; private byte[] sampleData; - public SourceLoadable(Uri uri, DataSource dataSource) { - this.uri = uri; + public SourceLoadable(DataSpec dataSpec, DataSource dataSource) { + this.dataSpec = dataSpec; this.dataSource = dataSource; } @@ -286,7 +315,7 @@ import java.util.Arrays; sampleSize = 0; try { // Create and open the input. - dataSource.open(new DataSpec(uri)); + dataSource.open(dataSpec); // Load the sample data. int result = 0; while (result != C.RESULT_END_OF_INPUT) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java index 2aa8ccc712..3b0a5a16c8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java @@ -17,11 +17,14 @@ package com.google.android.exoplayer2.source; import android.net.Uri; import android.os.Handler; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; @@ -32,7 +35,10 @@ public final class SingleSampleMediaSource implements MediaSource { /** * Listener of {@link SingleSampleMediaSource} events. + * + * @deprecated Use {@link MediaSourceEventListener}. */ + @Deprecated public interface EventListener { /** @@ -45,35 +51,23 @@ public final class SingleSampleMediaSource implements MediaSource { } - /** - * Builder for {@link SingleSampleMediaSource}. Each builder instance can only be used once. - */ - public static final class Builder { + /** Factory for {@link SingleSampleMediaSource}. */ + public static final class Factory { - private final Uri uri; private final DataSource.Factory dataSourceFactory; - private final Format format; - private final long durationUs; private int minLoadableRetryCount; - private Handler eventHandler; - private EventListener eventListener; - private int eventSourceId; private boolean treatLoadErrorsAsEndOfStream; - private boolean isBuildCalled; + private boolean isCreateCalled; /** - * @param uri The {@link Uri} of the media stream. + * Creates a factory for {@link SingleSampleMediaSource}s. + * * @param dataSourceFactory The factory from which the {@link DataSource} to read the media will * be obtained. - * @param format The {@link Format} associated with the output track. - * @param durationUs The duration of the media stream in microseconds. */ - public Builder(Uri uri, DataSource.Factory dataSourceFactory, Format format, long durationUs) { - this.uri = uri; - this.dataSourceFactory = dataSourceFactory; - this.format = format; - this.durationUs = durationUs; + public Factory(DataSource.Factory dataSourceFactory) { + this.dataSourceFactory = Assertions.checkNotNull(dataSourceFactory); this.minLoadableRetryCount = DEFAULT_MIN_LOADABLE_RETRY_COUNT; } @@ -82,37 +76,15 @@ public final class SingleSampleMediaSource implements MediaSource { * {@link #DEFAULT_MIN_LOADABLE_RETRY_COUNT}. * * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. - * @return This builder. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Builder setMinLoadableRetryCount(int minLoadableRetryCount) { + public Factory setMinLoadableRetryCount(int minLoadableRetryCount) { + Assertions.checkState(!isCreateCalled); this.minLoadableRetryCount = minLoadableRetryCount; return this; } - /** - * Sets the listener to respond to events and the handler to deliver these events. - * - * @param eventHandler A handler for events. - * @param eventListener A listener of events. - * @return This builder. - */ - public Builder setEventListener(Handler eventHandler, EventListener eventListener) { - this.eventHandler = eventHandler; - this.eventListener = eventListener; - return this; - } - - /** - * Sets an identifier that gets passed to {@code eventListener} methods. The default value is 0. - * - * @param eventSourceId An identifier that gets passed to {@code eventListener} methods. - * @return This builder. - */ - public Builder setEventSourceId(int eventSourceId) { - this.eventSourceId = eventSourceId; - return this; - } - /** * Sets whether load errors will be treated as end-of-stream signal (load errors will not be * propagated). The default value is false. @@ -120,27 +92,53 @@ public final class SingleSampleMediaSource implements MediaSource { * @param treatLoadErrorsAsEndOfStream If true, load errors will not be propagated by sample * streams, treating them as ended instead. If false, load errors will be propagated * normally by {@link SampleStream#maybeThrowError()}. - * @return This builder. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. */ - public Builder setTreatLoadErrorsAsEndOfStream(boolean treatLoadErrorsAsEndOfStream) { + public Factory setTreatLoadErrorsAsEndOfStream(boolean treatLoadErrorsAsEndOfStream) { + Assertions.checkState(!isCreateCalled); this.treatLoadErrorsAsEndOfStream = treatLoadErrorsAsEndOfStream; return this; } /** - * Builds a new {@link SingleSampleMediaSource} using the current parameters. - *

          - * After this call, the builder should not be re-used. + * Returns a new {@link ExtractorMediaSource} using the current parameters. Media source events + * will not be delivered. * + * @param uri The {@link Uri}. + * @param format The {@link Format} of the media stream. + * @param durationUs The duration of the media stream in microseconds. + * @return The new {@link ExtractorMediaSource}. + */ + public SingleSampleMediaSource createMediaSource(Uri uri, Format format, long durationUs) { + return createMediaSource(uri, format, durationUs, null, null); + } + + /** + * Returns a new {@link SingleSampleMediaSource} using the current parameters. + * + * @param uri The {@link Uri}. + * @param format The {@link Format} of the media stream. + * @param durationUs The duration of the media stream in microseconds. + * @param eventHandler A handler for events. + * @param eventListener A listener of events., Format format, long durationUs * @return The newly built {@link SingleSampleMediaSource}. */ - public SingleSampleMediaSource build() { - Assertions.checkArgument((eventListener == null) == (eventHandler == null)); - Assertions.checkState(!isBuildCalled); - isBuildCalled = true; - - return new SingleSampleMediaSource(uri, dataSourceFactory, format, durationUs, - minLoadableRetryCount, eventHandler, eventListener, eventSourceId, + public SingleSampleMediaSource createMediaSource( + Uri uri, + Format format, + long durationUs, + @Nullable Handler eventHandler, + @Nullable MediaSourceEventListener eventListener) { + isCreateCalled = true; + return new SingleSampleMediaSource( + uri, + dataSourceFactory, + format, + durationUs, + minLoadableRetryCount, + eventHandler, + eventListener, treatLoadErrorsAsEndOfStream); } @@ -151,13 +149,12 @@ public final class SingleSampleMediaSource implements MediaSource { */ public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 3; - private final Uri uri; + private final DataSpec dataSpec; private final DataSource.Factory dataSourceFactory; private final Format format; + private final long durationUs; + private final MediaSourceEventListener.EventDispatcher eventDispatcher; private final int minLoadableRetryCount; - private final Handler eventHandler; - private final EventListener eventListener; - private final int eventSourceId; private final boolean treatLoadErrorsAsEndOfStream; private final Timeline timeline; @@ -167,11 +164,11 @@ public final class SingleSampleMediaSource implements MediaSource { * be obtained. * @param format The {@link Format} associated with the output track. * @param durationUs The duration of the media stream in microseconds. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated - public SingleSampleMediaSource(Uri uri, DataSource.Factory dataSourceFactory, Format format, - long durationUs) { + public SingleSampleMediaSource( + Uri uri, DataSource.Factory dataSourceFactory, Format format, long durationUs) { this(uri, dataSourceFactory, format, durationUs, DEFAULT_MIN_LOADABLE_RETRY_COUNT); } @@ -182,12 +179,16 @@ public final class SingleSampleMediaSource implements MediaSource { * @param format The {@link Format} associated with the output track. * @param durationUs The duration of the media stream in microseconds. * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated - public SingleSampleMediaSource(Uri uri, DataSource.Factory dataSourceFactory, Format format, - long durationUs, int minLoadableRetryCount) { - this(uri, dataSourceFactory, format, durationUs, minLoadableRetryCount, null, null, 0, false); + public SingleSampleMediaSource( + Uri uri, + DataSource.Factory dataSourceFactory, + Format format, + long durationUs, + int minLoadableRetryCount) { + this(uri, dataSourceFactory, format, durationUs, minLoadableRetryCount, null, null, false); } /** @@ -203,20 +204,46 @@ public final class SingleSampleMediaSource implements MediaSource { * @param treatLoadErrorsAsEndOfStream If true, load errors will not be propagated by sample * streams, treating them as ended instead. If false, load errors will be propagated normally * by {@link SampleStream#maybeThrowError()}. - * @deprecated Use {@link Builder} instead. + * @deprecated Use {@link Factory} instead. */ @Deprecated - public SingleSampleMediaSource(Uri uri, DataSource.Factory dataSourceFactory, Format format, - long durationUs, int minLoadableRetryCount, Handler eventHandler, EventListener eventListener, - int eventSourceId, boolean treatLoadErrorsAsEndOfStream) { - this.uri = uri; + public SingleSampleMediaSource( + Uri uri, + DataSource.Factory dataSourceFactory, + Format format, + long durationUs, + int minLoadableRetryCount, + Handler eventHandler, + EventListener eventListener, + int eventSourceId, + boolean treatLoadErrorsAsEndOfStream) { + this( + uri, + dataSourceFactory, + format, + durationUs, + minLoadableRetryCount, + eventHandler, + eventListener == null ? null : new EventListenerWrapper(eventListener, eventSourceId), + treatLoadErrorsAsEndOfStream); + } + + private SingleSampleMediaSource( + Uri uri, + DataSource.Factory dataSourceFactory, + Format format, + long durationUs, + int minLoadableRetryCount, + Handler eventHandler, + MediaSourceEventListener eventListener, + boolean treatLoadErrorsAsEndOfStream) { this.dataSourceFactory = dataSourceFactory; this.format = format; + this.durationUs = durationUs; this.minLoadableRetryCount = minLoadableRetryCount; - this.eventHandler = eventHandler; - this.eventListener = eventListener; - this.eventSourceId = eventSourceId; this.treatLoadErrorsAsEndOfStream = treatLoadErrorsAsEndOfStream; + this.eventDispatcher = new EventDispatcher(eventHandler, eventListener); + dataSpec = new DataSpec(uri); timeline = new SinglePeriodTimeline(durationUs, true); } @@ -235,8 +262,14 @@ public final class SingleSampleMediaSource implements MediaSource { @Override public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { Assertions.checkArgument(id.periodIndex == 0); - return new SingleSampleMediaPeriod(uri, dataSourceFactory, format, minLoadableRetryCount, - eventHandler, eventListener, eventSourceId, treatLoadErrorsAsEndOfStream); + return new SingleSampleMediaPeriod( + dataSpec, + dataSourceFactory, + format, + durationUs, + minLoadableRetryCount, + eventDispatcher, + treatLoadErrorsAsEndOfStream); } @Override @@ -249,4 +282,97 @@ public final class SingleSampleMediaSource implements MediaSource { // Do nothing. } + /** + * Wraps a deprecated {@link EventListener}, invoking its callback from the equivalent callback in + * {@link MediaSourceEventListener}. + */ + private static final class EventListenerWrapper implements MediaSourceEventListener { + + private final EventListener eventListener; + private final int eventSourceId; + + public EventListenerWrapper(EventListener eventListener, int eventSourceId) { + this.eventListener = Assertions.checkNotNull(eventListener); + this.eventSourceId = eventSourceId; + } + + @Override + public void onLoadStarted( + DataSpec dataSpec, + int dataType, + int trackType, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + long mediaStartTimeMs, + long mediaEndTimeMs, + long elapsedRealtimeMs) { + // Do nothing. + } + + @Override + public void onLoadCompleted( + DataSpec dataSpec, + int dataType, + int trackType, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + long mediaStartTimeMs, + long mediaEndTimeMs, + long elapsedRealtimeMs, + long loadDurationMs, + long bytesLoaded) { + // Do nothing. + } + + @Override + public void onLoadCanceled( + DataSpec dataSpec, + int dataType, + int trackType, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + long mediaStartTimeMs, + long mediaEndTimeMs, + long elapsedRealtimeMs, + long loadDurationMs, + long bytesLoaded) { + // Do nothing. + } + + @Override + public void onLoadError( + DataSpec dataSpec, + int dataType, + int trackType, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + long mediaStartTimeMs, + long mediaEndTimeMs, + long elapsedRealtimeMs, + long loadDurationMs, + long bytesLoaded, + IOException error, + boolean wasCanceled) { + eventListener.onLoadError(eventSourceId, error); + } + + @Override + public void onUpstreamDiscarded(int trackType, long mediaStartTimeMs, long mediaEndTimeMs) { + // Do nothing. + } + + @Override + public void onDownstreamFormatChanged( + int trackType, + Format trackFormat, + int trackSelectionReason, + Object trackSelectionData, + long mediaTimeMs) { + // Do nothing. + } + } } diff --git a/library/hls/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 index 3a55cb8a17..8ed202d961 100644 --- a/library/hls/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 @@ -131,7 +131,7 @@ public final class HlsMediaSource implements MediaSource, * * @return The new {@link HlsMediaSource}. */ - public MediaSource createMediaSource(Uri playlistUri) { + public HlsMediaSource createMediaSource(Uri playlistUri) { return createMediaSource(playlistUri, null, null); } @@ -144,7 +144,7 @@ public final class HlsMediaSource implements MediaSource, * @return The new {@link HlsMediaSource}. */ @Override - public MediaSource createMediaSource( + public HlsMediaSource createMediaSource( Uri playlistUri, @Nullable Handler eventHandler, @Nullable MediaSourceEventListener eventListener) { From 403f773f8703f013b129ef02336b881678f1311f Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 14 Dec 2017 08:27:32 -0800 Subject: [PATCH 0967/2472] Add missing attrs to SimpleExoplayerView They worked without being present in the declare-styleable, but they need to be present for Android Studio auto-complete to suggest them. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179047478 --- library/ui/src/main/res/values/attrs.xml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/library/ui/src/main/res/values/attrs.xml b/library/ui/src/main/res/values/attrs.xml index 1ab3854d21..b6ed4b17af 100644 --- a/library/ui/src/main/res/values/attrs.xml +++ b/library/ui/src/main/res/values/attrs.xml @@ -51,10 +51,13 @@ + + - + + From b97ce44182ea61fc42584938c79a554c9891654c Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 15 Dec 2017 02:17:30 -0800 Subject: [PATCH 0968/2472] Pass -1 not C.TIME_UNSET when duration is unknown ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179165479 --- .../google/android/exoplayer2/ext/ima/ImaAdsLoader.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index b4bb886175..e0bca20d38 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -151,6 +151,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A private static final String IMA_SDK_SETTINGS_PLAYER_TYPE = "google/exo.ext.ima"; private static final String IMA_SDK_SETTINGS_PLAYER_VERSION = ExoPlayerLibraryInfo.VERSION; + /** The value used in {@link VideoProgressUpdate}s to indicate an unset duration. */ + private static final long IMA_DURATION_UNSET = -1L; + /** * Threshold before the end of content at which IMA is notified that content is complete if the * player buffers, in milliseconds. @@ -533,6 +536,8 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A @Override public VideoProgressUpdate getContentProgress() { + boolean hasContentDuration = contentDurationMs != C.TIME_UNSET; + long contentDurationMs = hasContentDuration ? this.contentDurationMs : IMA_DURATION_UNSET; if (player == null) { return lastContentProgress; } else if (pendingContentPositionMs != C.TIME_UNSET) { @@ -542,7 +547,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A long elapsedSinceEndMs = SystemClock.elapsedRealtime() - fakeContentProgressElapsedRealtimeMs; long fakePositionMs = fakeContentProgressOffsetMs + elapsedSinceEndMs; return new VideoProgressUpdate(fakePositionMs, contentDurationMs); - } else if (playingAd || contentDurationMs == C.TIME_UNSET) { + } else if (playingAd || !hasContentDuration) { return VideoProgressUpdate.VIDEO_TIME_NOT_READY; } else { return new VideoProgressUpdate(player.getCurrentPosition(), contentDurationMs); From 04108eec48f85cc0bf1b5eecadba8b7a5127fdb2 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 15 Dec 2017 02:47:56 -0800 Subject: [PATCH 0969/2472] Fix condition for detecting that an ad has ended onEnded was being called also for content finishing, as in this case the playing ad index changed (from INDEX_UNSET to 0). Fix this test so we only detect ads finishing. Also add logging for onEnded callbacks. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179167737 --- .../android/exoplayer2/ext/ima/ImaAdsLoader.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index e0bca20d38..d65bf87605 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -702,6 +702,9 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A for (int i = 0; i < adCallbacks.size(); i++) { adCallbacks.get(i).onEnded(); } + if (DEBUG) { + Log.d(TAG, "VideoAdPlayerCallback.onEnded in onPlayerStateChanged"); + } } } @@ -797,16 +800,20 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A private void updateImaStateForPlayerState() { boolean wasPlayingAd = playingAd; + int oldPlayingAdIndexInAdGroup = playingAdIndexInAdGroup; playingAd = player.isPlayingAd(); + playingAdIndexInAdGroup = playingAd ? player.getCurrentAdIndexInAdGroup() : C.INDEX_UNSET; if (!sentContentComplete) { - boolean adFinished = (wasPlayingAd && !playingAd) - || playingAdIndexInAdGroup != player.getCurrentAdIndexInAdGroup(); + boolean adFinished = wasPlayingAd && playingAdIndexInAdGroup != oldPlayingAdIndexInAdGroup; if (adFinished) { // IMA is waiting for the ad playback to finish so invoke the callback now. // Either CONTENT_RESUME_REQUESTED will be passed next, or playAd will be called again. for (int i = 0; i < adCallbacks.size(); i++) { adCallbacks.get(i).onEnded(); } + if (DEBUG) { + Log.d(TAG, "VideoAdPlayerCallback.onEnded in onTimelineChanged/onPositionDiscontinuity"); + } } if (!wasPlayingAd && playingAd) { int adGroupIndex = player.getCurrentAdGroupIndex(); @@ -818,7 +825,6 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A } } } - playingAdIndexInAdGroup = playingAd ? player.getCurrentAdIndexInAdGroup() : C.INDEX_UNSET; } private void resumeContentInternal() { From 52794e749d1ae214cbb9be1a2f7124ff1dd1e2d0 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 15 Dec 2017 02:47:56 -0800 Subject: [PATCH 0970/2472] Fix typo Issue #3594 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179167738 --- .../com/google/android/exoplayer2/text/cea/Cea708Decoder.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/core/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 index 030f0cdbb0..6bdbebc73b 100644 --- a/library/core/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 @@ -104,7 +104,7 @@ public final class Cea708Decoder extends CeaDecoder { private static final int COMMAND_DF1 = 0x99; // DefineWindow 1 (+6 bytes) private static final int COMMAND_DF2 = 0x9A; // DefineWindow 2 (+6 bytes) private static final int COMMAND_DF3 = 0x9B; // DefineWindow 3 (+6 bytes) - private static final int COMMAND_DS4 = 0x9C; // DefineWindow 4 (+6 bytes) + private static final int COMMAND_DF4 = 0x9C; // DefineWindow 4 (+6 bytes) private static final int COMMAND_DF5 = 0x9D; // DefineWindow 5 (+6 bytes) private static final int COMMAND_DF6 = 0x9E; // DefineWindow 6 (+6 bytes) private static final int COMMAND_DF7 = 0x9F; // DefineWindow 7 (+6 bytes) @@ -464,7 +464,7 @@ public final class Cea708Decoder extends CeaDecoder { case COMMAND_DF1: case COMMAND_DF2: case COMMAND_DF3: - case COMMAND_DS4: + case COMMAND_DF4: case COMMAND_DF5: case COMMAND_DF6: case COMMAND_DF7: From ab7503843c3c336b5df397a0a97ece2278da168d Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 15 Dec 2017 03:06:10 -0800 Subject: [PATCH 0971/2472] Use playAd/stopAd to control position updates switching Previously the ad/content progress updates were toggled based on whether the player was playing ads or content. After this change, we switch based on whether playAd/stopAd has been called instead. This seems to resolve an issue where occasionally the player would get stuck at the start of an ad, but as I don't have a root cause for that issue and it's only sporadically reproducible I'm not certain this is a reliable fix. Issue: #3525 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179169296 --- .../com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index d65bf87605..70a8322bba 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -547,7 +547,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A long elapsedSinceEndMs = SystemClock.elapsedRealtime() - fakeContentProgressElapsedRealtimeMs; long fakePositionMs = fakeContentProgressOffsetMs + elapsedSinceEndMs; return new VideoProgressUpdate(fakePositionMs, contentDurationMs); - } else if (playingAd || !hasContentDuration) { + } else if (imaAdState != IMA_AD_STATE_NONE || !hasContentDuration) { return VideoProgressUpdate.VIDEO_TIME_NOT_READY; } else { return new VideoProgressUpdate(player.getCurrentPosition(), contentDurationMs); @@ -560,7 +560,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A public VideoProgressUpdate getAdProgress() { if (player == null) { return lastAdProgress; - } else if (!playingAd) { + } else if (imaAdState == IMA_AD_STATE_NONE) { return VideoProgressUpdate.VIDEO_TIME_NOT_READY; } else { long adDuration = player.getDuration(); From 236df9a571890d0eed256782e5ed484652b12776 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 21 Nov 2017 08:58:02 -0800 Subject: [PATCH 0972/2472] Remove DefaultLoadControl buffer time state ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=176515168 --- .../android/exoplayer2/DefaultLoadControl.java | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java index d8bc042ad7..a89655c8e9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java @@ -51,10 +51,6 @@ public final class DefaultLoadControl implements LoadControl { */ public static final int DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = 5000; - private static final int ABOVE_HIGH_WATERMARK = 0; - private static final int BETWEEN_WATERMARKS = 1; - private static final int BELOW_LOW_WATERMARK = 2; - private final DefaultAllocator allocator; private final long minBufferUs; @@ -171,11 +167,11 @@ public final class DefaultLoadControl implements LoadControl { @Override public boolean shouldContinueLoading(long bufferedDurationUs) { - int bufferTimeState = getBufferTimeState(bufferedDurationUs); boolean targetBufferSizeReached = allocator.getTotalBytesAllocated() >= targetBufferSize; boolean wasBuffering = isBuffering; - isBuffering = bufferTimeState == BELOW_LOW_WATERMARK - || (bufferTimeState == BETWEEN_WATERMARKS && isBuffering && !targetBufferSizeReached); + isBuffering = bufferedDurationUs < minBufferUs // below low watermark + || (bufferedDurationUs <= maxBufferUs // between watermarks + && isBuffering && !targetBufferSizeReached); if (priorityTaskManager != null && isBuffering != wasBuffering) { if (isBuffering) { priorityTaskManager.add(C.PRIORITY_PLAYBACK); @@ -186,11 +182,6 @@ public final class DefaultLoadControl implements LoadControl { return isBuffering; } - private int getBufferTimeState(long bufferedDurationUs) { - return bufferedDurationUs > maxBufferUs ? ABOVE_HIGH_WATERMARK - : (bufferedDurationUs < minBufferUs ? BELOW_LOW_WATERMARK : BETWEEN_WATERMARKS); - } - private void reset(boolean resetAllocator) { targetBufferSize = 0; if (priorityTaskManager != null && isBuffering) { From 1e36c76654004a00b9805d9bf6a365334f724c03 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 14 Dec 2017 08:38:02 -0800 Subject: [PATCH 0973/2472] Allow to configure maximum buffer size in DefaultLoadControl. This adds a parameter to configure a maximum buffer size in bytes. If left at its default of C.LENGTH_UNSET, the target buffer is determined using a overridable method based on the track selection. Also adding a parameter to decide whether to prioritize time or size constraints. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179048554 --- RELEASENOTES.md | 2 + .../exoplayer2/DefaultLoadControl.java | 115 +++++++++++++++--- 2 files changed, 97 insertions(+), 20 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 2f9008045e..2a2347686d 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -31,6 +31,8 @@ * DefaultTrackSelector: Support undefined language text track selection when the preferred language is not available ([#2980](https://github.com/google/ExoPlayer/issues/2980)). +* Add options to `DefaultLoadControl` to set maximum buffer size in bytes and + to choose whether size or time constraints are prioritized. * Use surfaceless context for secure `DummySurface`, if available ([#3558](https://github.com/google/ExoPlayer/issues/3558)). * FLV: Fix playback of live streams that do not contain an audio track diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java index a89655c8e9..b7b68de7d2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java @@ -51,12 +51,23 @@ public final class DefaultLoadControl implements LoadControl { */ public static final int DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = 5000; + /** + * The default target buffer size in bytes. When set to {@link C#LENGTH_UNSET}, the load control + * automatically determines its target buffer size. + */ + public static final int DEFAULT_TARGET_BUFFER_BYTES = C.LENGTH_UNSET; + + /** The default prioritization of buffer time constraints over size constraints. */ + public static final boolean DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS = true; + private final DefaultAllocator allocator; private final long minBufferUs; private final long maxBufferUs; private final long bufferForPlaybackUs; private final long bufferForPlaybackAfterRebufferUs; + private final int targetBufferBytesOverwrite; + private final boolean prioritizeTimeOverSizeThresholds; private final PriorityTaskManager priorityTaskManager; private int targetBufferSize; @@ -75,8 +86,14 @@ public final class DefaultLoadControl implements LoadControl { * @param allocator The {@link DefaultAllocator} used by the loader. */ public DefaultLoadControl(DefaultAllocator allocator) { - this(allocator, DEFAULT_MIN_BUFFER_MS, DEFAULT_MAX_BUFFER_MS, DEFAULT_BUFFER_FOR_PLAYBACK_MS, - DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS); + this( + allocator, + DEFAULT_MIN_BUFFER_MS, + DEFAULT_MAX_BUFFER_MS, + DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS, + DEFAULT_BUFFER_FOR_PLAYBACK_MS, + DEFAULT_TARGET_BUFFER_BYTES, + DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS); } /** @@ -92,10 +109,27 @@ public final class DefaultLoadControl implements LoadControl { * @param bufferForPlaybackAfterRebufferMs The default duration of media that must be buffered for * playback to resume after a rebuffer, in milliseconds. A rebuffer is defined to be caused by * buffer depletion rather than a user action. + * @param targetBufferBytes The target buffer size in bytes. If set to {@link C#LENGTH_UNSET}, the + * target buffer size will be calculated using {@link #calculateTargetBufferSize(Renderer[], + * TrackSelectionArray)}. + * @param prioritizeTimeOverSizeThresholds Whether the load control prioritizes buffer time */ - public DefaultLoadControl(DefaultAllocator allocator, int minBufferMs, int maxBufferMs, - long bufferForPlaybackMs, long bufferForPlaybackAfterRebufferMs) { - this(allocator, minBufferMs, maxBufferMs, bufferForPlaybackMs, bufferForPlaybackAfterRebufferMs, + public DefaultLoadControl( + DefaultAllocator allocator, + int minBufferMs, + int maxBufferMs, + int bufferForPlaybackMs, + int bufferForPlaybackAfterRebufferMs, + int targetBufferBytes, + boolean prioritizeTimeOverSizeThresholds) { + this( + allocator, + minBufferMs, + maxBufferMs, + bufferForPlaybackMs, + bufferForPlaybackAfterRebufferMs, + targetBufferBytes, + prioritizeTimeOverSizeThresholds, null); } @@ -112,18 +146,30 @@ public final class DefaultLoadControl implements LoadControl { * @param bufferForPlaybackAfterRebufferMs The default duration of media that must be buffered for * playback to resume after a rebuffer, in milliseconds. A rebuffer is defined to be caused by * buffer depletion rather than a user action. - * @param priorityTaskManager If not null, registers itself as a task with priority - * {@link C#PRIORITY_PLAYBACK} during loading periods, and unregisters itself during draining - * periods. + * @param targetBufferBytes The target buffer size in bytes. If set to {@link C#LENGTH_UNSET}, the + * target buffer size will be calculated using {@link #calculateTargetBufferSize(Renderer[], + * TrackSelectionArray)}. + * @param prioritizeTimeOverSizeThresholds Whether the load control prioritizes buffer time + * constraints over buffer size constraints. + * @param priorityTaskManager If not null, registers itself as a task with priority {@link + * C#PRIORITY_PLAYBACK} during loading periods, and unregisters itself during draining */ - public DefaultLoadControl(DefaultAllocator allocator, int minBufferMs, int maxBufferMs, - long bufferForPlaybackMs, long bufferForPlaybackAfterRebufferMs, + public DefaultLoadControl( + DefaultAllocator allocator, + int minBufferMs, + int maxBufferMs, + int bufferForPlaybackMs, + int bufferForPlaybackAfterRebufferMs, + int targetBufferBytes, + boolean prioritizeTimeOverSizeThresholds, PriorityTaskManager priorityTaskManager) { this.allocator = allocator; minBufferUs = minBufferMs * 1000L; maxBufferUs = maxBufferMs * 1000L; + targetBufferBytesOverwrite = targetBufferBytes; bufferForPlaybackUs = bufferForPlaybackMs * 1000L; bufferForPlaybackAfterRebufferUs = bufferForPlaybackAfterRebufferMs * 1000L; + this.prioritizeTimeOverSizeThresholds = prioritizeTimeOverSizeThresholds; this.priorityTaskManager = priorityTaskManager; } @@ -135,12 +181,10 @@ public final class DefaultLoadControl implements LoadControl { @Override public void onTracksSelected(Renderer[] renderers, TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { - targetBufferSize = 0; - for (int i = 0; i < renderers.length; i++) { - if (trackSelections.get(i) != null) { - targetBufferSize += Util.getDefaultBufferSize(renderers[i].getTrackType()); - } - } + targetBufferSize = + targetBufferBytesOverwrite == C.LENGTH_UNSET + ? calculateTargetBufferSize(renderers, trackSelections) + : targetBufferBytesOverwrite; allocator.setTargetBufferSize(targetBufferSize); } @@ -162,16 +206,28 @@ public final class DefaultLoadControl implements LoadControl { @Override public boolean shouldStartPlayback(long bufferedDurationUs, boolean rebuffering) { long minBufferDurationUs = rebuffering ? bufferForPlaybackAfterRebufferUs : bufferForPlaybackUs; - return minBufferDurationUs <= 0 || bufferedDurationUs >= minBufferDurationUs; + return minBufferDurationUs <= 0 + || bufferedDurationUs >= minBufferDurationUs + || (!prioritizeTimeOverSizeThresholds + && allocator.getTotalBytesAllocated() >= targetBufferSize); } @Override public boolean shouldContinueLoading(long bufferedDurationUs) { boolean targetBufferSizeReached = allocator.getTotalBytesAllocated() >= targetBufferSize; boolean wasBuffering = isBuffering; - isBuffering = bufferedDurationUs < minBufferUs // below low watermark - || (bufferedDurationUs <= maxBufferUs // between watermarks - && isBuffering && !targetBufferSizeReached); + if (prioritizeTimeOverSizeThresholds) { + isBuffering = + bufferedDurationUs < minBufferUs // below low watermark + || (bufferedDurationUs <= maxBufferUs // between watermarks + && isBuffering + && !targetBufferSizeReached); + } else { + isBuffering = + !targetBufferSizeReached + && (bufferedDurationUs < minBufferUs // below low watermark + || (bufferedDurationUs <= maxBufferUs && isBuffering)); // between watermarks + } if (priorityTaskManager != null && isBuffering != wasBuffering) { if (isBuffering) { priorityTaskManager.add(C.PRIORITY_PLAYBACK); @@ -182,6 +238,25 @@ public final class DefaultLoadControl implements LoadControl { return isBuffering; } + /** + * Calculate target buffer size in bytes based on the selected tracks. The player will try not to + * exceed this target buffer. Only used when {@code targetBufferBytes} is {@link C#LENGTH_UNSET}. + * + * @param renderers The renderers for which the track were selected. + * @param trackSelectionArray The selected tracks. + * @return The target buffer size in bytes. + */ + protected int calculateTargetBufferSize( + Renderer[] renderers, TrackSelectionArray trackSelectionArray) { + int targetBufferSize = 0; + for (int i = 0; i < renderers.length; i++) { + if (trackSelectionArray.get(i) != null) { + targetBufferSize += Util.getDefaultBufferSize(renderers[i].getTrackType()); + } + } + return targetBufferSize; + } + private void reset(boolean resetAllocator) { targetBufferSize = 0; if (priorityTaskManager != null && isBuffering) { From 0287f13c0a10de2a97867125b185702e3eed6978 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 15 Dec 2017 11:59:36 -0800 Subject: [PATCH 0974/2472] Fix analyze/lint errors - Lint doesn't like a static import of something not available on the minimum API level. - The method linked to in the Javadoc was incorrect (wrong signature). I couldn't really work out why it was there, so I got rid of it rather than updating. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179222587 --- .../android/exoplayer2/audio/ChannelMappingAudioProcessor.java | 2 -- .../java/com/google/android/exoplayer2/video/DummySurface.java | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java index c3f3e32526..17b90680dd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java @@ -51,8 +51,6 @@ import java.util.Arrays; /** * Resets the channel mapping. After calling this method, call {@link #configure(int, int, int)} * to start using the new channel map. - * - * @see AudioSink#configure(String, int, int, int, int, int[], int, int) */ public void setChannelMap(int[] outputChannels) { pendingOutputChannels = outputChannels; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java index 2c172c086b..9fcf89d628 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java @@ -24,7 +24,6 @@ import static android.opengl.EGL14.EGL_DEPTH_SIZE; import static android.opengl.EGL14.EGL_GREEN_SIZE; import static android.opengl.EGL14.EGL_HEIGHT; import static android.opengl.EGL14.EGL_NONE; -import static android.opengl.EGL14.EGL_NO_SURFACE; import static android.opengl.EGL14.EGL_OPENGL_ES2_BIT; import static android.opengl.EGL14.EGL_RED_SIZE; import static android.opengl.EGL14.EGL_RENDERABLE_TYPE; @@ -326,7 +325,7 @@ public final class DummySurface extends Surface { EGLSurface surface; if (secureMode == SECURE_MODE_SURFACELESS_CONTEXT) { - surface = EGL_NO_SURFACE; + surface = EGL14.EGL_NO_SURFACE; } else { int[] pbufferAttributes; if (secureMode == SECURE_MODE_PROTECTED_PBUFFER) { From 3fae0b8e6e756c1210636071088dd1fe7e9c080e Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 15 Dec 2017 11:59:36 -0800 Subject: [PATCH 0975/2472] Fix analyze/lint errors - Lint doesn't like a static import of something not available on the minimum API level. - The method linked to in the Javadoc was incorrect (wrong signature). I couldn't really work out why it was there, so I got rid of it rather than updating. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179222587 --- .../android/exoplayer2/audio/ChannelMappingAudioProcessor.java | 2 -- .../java/com/google/android/exoplayer2/video/DummySurface.java | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java index c3f3e32526..17b90680dd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java @@ -51,8 +51,6 @@ import java.util.Arrays; /** * Resets the channel mapping. After calling this method, call {@link #configure(int, int, int)} * to start using the new channel map. - * - * @see AudioSink#configure(String, int, int, int, int, int[], int, int) */ public void setChannelMap(int[] outputChannels) { pendingOutputChannels = outputChannels; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java index 2c172c086b..9fcf89d628 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/DummySurface.java @@ -24,7 +24,6 @@ import static android.opengl.EGL14.EGL_DEPTH_SIZE; import static android.opengl.EGL14.EGL_GREEN_SIZE; import static android.opengl.EGL14.EGL_HEIGHT; import static android.opengl.EGL14.EGL_NONE; -import static android.opengl.EGL14.EGL_NO_SURFACE; import static android.opengl.EGL14.EGL_OPENGL_ES2_BIT; import static android.opengl.EGL14.EGL_RED_SIZE; import static android.opengl.EGL14.EGL_RENDERABLE_TYPE; @@ -326,7 +325,7 @@ public final class DummySurface extends Surface { EGLSurface surface; if (secureMode == SECURE_MODE_SURFACELESS_CONTEXT) { - surface = EGL_NO_SURFACE; + surface = EGL14.EGL_NO_SURFACE; } else { int[] pbufferAttributes; if (secureMode == SECURE_MODE_PROTECTED_PBUFFER) { From d9bee4d29c54eb12ca64b64e6f455add96d242ba Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 15 Dec 2017 12:38:05 -0800 Subject: [PATCH 0976/2472] Bump version to 2.6.1 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179227114 --- constants.gradle | 2 +- demos/cast/src/main/AndroidManifest.xml | 4 ++-- demos/ima/src/main/AndroidManifest.xml | 4 ++-- demos/main/src/main/AndroidManifest.xml | 4 ++-- .../com/google/android/exoplayer2/ExoPlayerLibraryInfo.java | 6 +++--- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/constants.gradle b/constants.gradle index bad69389a5..c18fb28d4d 100644 --- a/constants.gradle +++ b/constants.gradle @@ -28,7 +28,7 @@ project.ext { junitVersion = '4.12' truthVersion = '0.35' robolectricVersion = '3.4.2' - releaseVersion = '2.6.0' + releaseVersion = '2.6.1' modulePrefix = ':' if (gradle.ext.has('exoplayerModulePrefix')) { modulePrefix += gradle.ext.exoplayerModulePrefix diff --git a/demos/cast/src/main/AndroidManifest.xml b/demos/cast/src/main/AndroidManifest.xml index 8aaef5f8ce..e12e27fa4c 100644 --- a/demos/cast/src/main/AndroidManifest.xml +++ b/demos/cast/src/main/AndroidManifest.xml @@ -15,8 +15,8 @@ --> + android:versionCode="2601" + android:versionName="2.6.1"> diff --git a/demos/ima/src/main/AndroidManifest.xml b/demos/ima/src/main/AndroidManifest.xml index f14feeda74..0efeaf6f7f 100644 --- a/demos/ima/src/main/AndroidManifest.xml +++ b/demos/ima/src/main/AndroidManifest.xml @@ -15,8 +15,8 @@ --> + android:versionCode="2601" + android:versionName="2.6.1"> diff --git a/demos/main/src/main/AndroidManifest.xml b/demos/main/src/main/AndroidManifest.xml index ec8016e8a3..00326157a2 100644 --- a/demos/main/src/main/AndroidManifest.xml +++ b/demos/main/src/main/AndroidManifest.xml @@ -16,8 +16,8 @@ + android:versionCode="2601" + android:versionName="2.6.1"> diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java index f13a7de0ca..b2200b6671 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java @@ -31,13 +31,13 @@ public final class ExoPlayerLibraryInfo { * The version of the library expressed as a string, for example "1.2.3". */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. - public static final String VERSION = "2.6.0"; + public static final String VERSION = "2.6.1"; /** * The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final String VERSION_SLASHY = "ExoPlayerLib/2.6.0"; + public static final String VERSION_SLASHY = "ExoPlayerLib/2.6.1"; /** * The version of the library expressed as an integer, for example 1002003. @@ -47,7 +47,7 @@ public final class ExoPlayerLibraryInfo { * integer version 123045006 (123-045-006). */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final int VERSION_INT = 2006000; + public static final int VERSION_INT = 2006001; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} From 16ad280817f68376d0017fdeb7cb16508571701a Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 15 Dec 2017 12:38:05 -0800 Subject: [PATCH 0977/2472] Bump version to 2.6.1 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179227114 --- constants.gradle | 2 +- demos/ima/src/main/AndroidManifest.xml | 4 ++-- demos/main/src/main/AndroidManifest.xml | 4 ++-- .../com/google/android/exoplayer2/ExoPlayerLibraryInfo.java | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/constants.gradle b/constants.gradle index bad69389a5..c18fb28d4d 100644 --- a/constants.gradle +++ b/constants.gradle @@ -28,7 +28,7 @@ project.ext { junitVersion = '4.12' truthVersion = '0.35' robolectricVersion = '3.4.2' - releaseVersion = '2.6.0' + releaseVersion = '2.6.1' modulePrefix = ':' if (gradle.ext.has('exoplayerModulePrefix')) { modulePrefix += gradle.ext.exoplayerModulePrefix diff --git a/demos/ima/src/main/AndroidManifest.xml b/demos/ima/src/main/AndroidManifest.xml index f14feeda74..0efeaf6f7f 100644 --- a/demos/ima/src/main/AndroidManifest.xml +++ b/demos/ima/src/main/AndroidManifest.xml @@ -15,8 +15,8 @@ --> + android:versionCode="2601" + android:versionName="2.6.1"> diff --git a/demos/main/src/main/AndroidManifest.xml b/demos/main/src/main/AndroidManifest.xml index ec8016e8a3..00326157a2 100644 --- a/demos/main/src/main/AndroidManifest.xml +++ b/demos/main/src/main/AndroidManifest.xml @@ -16,8 +16,8 @@ + android:versionCode="2601" + android:versionName="2.6.1"> diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java index f13a7de0ca..b2200b6671 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java @@ -31,13 +31,13 @@ public final class ExoPlayerLibraryInfo { * The version of the library expressed as a string, for example "1.2.3". */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. - public static final String VERSION = "2.6.0"; + public static final String VERSION = "2.6.1"; /** * The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final String VERSION_SLASHY = "ExoPlayerLib/2.6.0"; + public static final String VERSION_SLASHY = "ExoPlayerLib/2.6.1"; /** * The version of the library expressed as an integer, for example 1002003. @@ -47,7 +47,7 @@ public final class ExoPlayerLibraryInfo { * integer version 123045006 (123-045-006). */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final int VERSION_INT = 2006000; + public static final int VERSION_INT = 2006001; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} From 539d291fce5928adbb638dea67dd415425ff2d10 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 18 Dec 2017 04:44:58 -0800 Subject: [PATCH 0978/2472] Fix some lint/analyze errors ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179406910 --- .../exoplayer2/drm/OfflineLicenseHelperTest.java | 2 ++ .../exoplayer2/extractor/ts/AdtsReaderTest.java | 1 + .../upstream/cache/CachedContentIndexTest.java | 2 ++ .../upstream/cache/CachedRegionTrackerTest.java | 3 ++- .../exoplayer2/upstream/cache/SimpleCacheSpanTest.java | 2 ++ .../exoplayer2/audio/ChannelMappingAudioProcessor.java | 2 ++ .../exoplayer2/audio/ResamplingAudioProcessor.java | 1 + .../android/exoplayer2/extractor/ts/LatmReader.java | 3 +-- .../trackselection/DefaultTrackSelector.java | 7 ++++--- .../com/google/android/exoplayer2/video/ColorInfo.java | 10 ++++------ .../exoplayer2/testutil/FakeSimpleExoPlayer.java | 8 +++----- 11 files changed, 24 insertions(+), 17 deletions(-) diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java index 02b29a31b5..76730abc6e 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/drm/OfflineLicenseHelperTest.java @@ -38,6 +38,7 @@ public class OfflineLicenseHelperTest extends InstrumentationTestCase { @Override protected void setUp() throws Exception { + super.setUp(); MockitoUtil.setUpMockito(this); when(mediaDrm.openSession()).thenReturn(new byte[] {1, 2, 3}); offlineLicenseHelper = new OfflineLicenseHelper<>(C.WIDEVINE_UUID, mediaDrm, mediaDrmCallback, @@ -48,6 +49,7 @@ public class OfflineLicenseHelperTest extends InstrumentationTestCase { protected void tearDown() throws Exception { offlineLicenseHelper.release(); offlineLicenseHelper = null; + super.tearDown(); } public void testDownloadRenewReleaseKey() throws Exception { diff --git a/library/core/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 index 6a31250e15..1a10d24c94 100644 --- a/library/core/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 @@ -69,6 +69,7 @@ public class AdtsReaderTest extends TestCase { @Override protected void setUp() throws Exception { + super.setUp(); FakeExtractorOutput fakeExtractorOutput = new FakeExtractorOutput(); adtsOutput = fakeExtractorOutput.track(0, C.TRACK_TYPE_AUDIO); id3Output = fakeExtractorOutput.track(1, C.TRACK_TYPE_METADATA); diff --git a/library/core/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 index 7f6e203c20..17b8313500 100644 --- a/library/core/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 @@ -36,6 +36,7 @@ public class CachedContentIndexTest extends InstrumentationTestCase { @Override public void setUp() throws Exception { + super.setUp(); cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest"); index = new CachedContentIndex(cacheDir); } @@ -43,6 +44,7 @@ public class CachedContentIndexTest extends InstrumentationTestCase { @Override protected void tearDown() throws Exception { Util.recursiveDelete(cacheDir); + super.tearDown(); } public void testAddGetRemove() throws Exception { diff --git a/library/core/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 index f40ae0bc7e..fc4a9cfed6 100644 --- a/library/core/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 @@ -46,8 +46,8 @@ public final class CachedRegionTrackerTest extends InstrumentationTestCase { @Override protected void setUp() throws Exception { + super.setUp(); MockitoUtil.setUpMockito(this); - tracker = new CachedRegionTracker(cache, CACHE_KEY, CHUNK_INDEX); cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest"); index = new CachedContentIndex(cacheDir); @@ -56,6 +56,7 @@ public final class CachedRegionTrackerTest extends InstrumentationTestCase { @Override protected void tearDown() throws Exception { Util.recursiveDelete(cacheDir); + super.tearDown(); } public void testGetRegion_noSpansInCache() { diff --git a/library/core/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 index 8c684b1cb3..eb1cfd82a8 100644 --- a/library/core/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 @@ -48,6 +48,7 @@ public class SimpleCacheSpanTest extends InstrumentationTestCase { @Override protected void setUp() throws Exception { + super.setUp(); cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest"); index = new CachedContentIndex(cacheDir); } @@ -55,6 +56,7 @@ public class SimpleCacheSpanTest extends InstrumentationTestCase { @Override protected void tearDown() throws Exception { Util.recursiveDelete(cacheDir); + super.tearDown(); } public void testCacheFile() throws Exception { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java index 17b90680dd..50b484b938 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/ChannelMappingAudioProcessor.java @@ -51,6 +51,8 @@ import java.util.Arrays; /** * Resets the channel mapping. After calling this method, call {@link #configure(int, int, int)} * to start using the new channel map. + * + * @see AudioSink#configure(int, int, int, int, int[], int, int) */ public void setChannelMap(int[] outputChannels) { pendingOutputChannels = outputChannels; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java index a78adbcee3..d5a18c5ebf 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java @@ -102,6 +102,7 @@ import java.nio.ByteOrder; resampledSize = size / 2; break; case C.ENCODING_PCM_16BIT: + case C.ENCODING_PCM_FLOAT: case C.ENCODING_INVALID: case Format.NO_VALUE: default: diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java index d06c6f0cb4..313e556764 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/LatmReader.java @@ -61,7 +61,6 @@ public final class LatmReader implements ElementaryStreamReader { // Container data. private boolean streamMuxRead; - private int audioMuxVersion; private int audioMuxVersionA; private int numSubframes; private int frameLengthType; @@ -176,7 +175,7 @@ public final class LatmReader implements ElementaryStreamReader { * Parses a StreamMuxConfig as defined in ISO/IEC 14496-3:2009 Section 1.7.3.1, Table 1.42. */ private void parseStreamMuxConfig(ParsableBitArray data) throws ParserException { - audioMuxVersion = data.readBits(1); + int audioMuxVersion = data.readBits(1); audioMuxVersionA = audioMuxVersion == 1 ? data.readBits(1) : 0; if (audioMuxVersionA == 0) { if (audioMuxVersion == 1) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index 09bd81416c..509e86345e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.trackselection; import android.content.Context; import android.graphics.Point; +import android.support.annotation.NonNull; import android.text.TextUtils; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; @@ -1216,11 +1217,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { * Compares the score of the current track format with another {@link AudioTrackScore}. * * @param other The other score to compare to. - * @return A positive integer if this score is better than the other. Zero if they are - * equal. A negative integer if this score is worse than the other. + * @return A positive integer if this score is better than the other. Zero if they are equal. A + * negative integer if this score is worse than the other. */ @Override - public int compareTo(AudioTrackScore other) { + public int compareTo(@NonNull AudioTrackScore other) { if (this.withinRendererCapabilitiesScore != other.withinRendererCapabilitiesScore) { return compareInts(this.withinRendererCapabilitiesScore, other.withinRendererCapabilitiesScore); 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 index 7bdc43f85c..14e40f8605 100644 --- 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 @@ -91,12 +91,10 @@ public final class ColorInfo implements Parcelable { 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; + return colorSpace == other.colorSpace + && colorRange == other.colorRange + && colorTransfer == other.colorTransfer + && Arrays.equals(hdrStaticInfo, other.hdrStaticInfo); } @Override diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java index d568770219..a8ba3b3420 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java @@ -481,11 +481,9 @@ public class FakeSimpleExoPlayer extends SimpleExoPlayer { private boolean haveSufficientBuffer(boolean rebuffering, long rendererPositionUs, long bufferedPositionUs) { - if (bufferedPositionUs == C.TIME_END_OF_SOURCE) { - return true; - } - return - loadControl.shouldStartPlayback(bufferedPositionUs - rendererPositionUs, 1f, rebuffering); + return bufferedPositionUs == C.TIME_END_OF_SOURCE + || loadControl.shouldStartPlayback( + bufferedPositionUs - rendererPositionUs, 1f, rebuffering); } private void handlePlayerError(final ExoPlaybackException e) { From 6c2d1e1966625cd9132c0629649f8210764b205a Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 19 Dec 2017 09:22:59 -0800 Subject: [PATCH 0979/2472] Add possiblity to send messages at playback position. This adds options to ExoPlayer.sendMessages which allow to specify a window index and position at which the message should be sent. Additionally, the options can be configured to use a custom Handler for the messages and whether the message should be repeated when playback reaches the same position again. The internal player converts these window positions to period index and position at the earliest possibility. The internal player also attempts to update these when the source info is refreshed. A sorted list of pending posts is kept and the player triggers these posts when the playback position moves over the specified position. Issue:#2189 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179563355 --- RELEASENOTES.md | 4 + .../exoplayer2/ext/vp9/VpxPlaybackTest.java | 8 +- .../android/exoplayer2/ExoPlayerTest.java | 403 ++++++++++++++++++ .../android/exoplayer2/BaseRenderer.java | 2 +- .../google/android/exoplayer2/ExoPlayer.java | 164 ++++--- .../android/exoplayer2/ExoPlayerImpl.java | 44 +- .../exoplayer2/ExoPlayerImplInternal.java | 283 +++++++++--- .../android/exoplayer2/NoSampleRenderer.java | 2 +- .../android/exoplayer2/PlayerMessage.java | 295 +++++++++++++ .../google/android/exoplayer2/Renderer.java | 12 +- .../android/exoplayer2/SimpleExoPlayer.java | 70 ++- .../DynamicConcatenatingMediaSource.java | 36 +- .../google/android/exoplayer2/util/Util.java | 12 + .../android/exoplayer2/testutil/Action.java | 60 +++ .../exoplayer2/testutil/ActionSchedule.java | 63 +++ .../exoplayer2/testutil/FakeTimeline.java | 13 +- .../testutil/MediaSourceTestRunner.java | 35 +- .../exoplayer2/testutil/StubExoPlayer.java | 6 + 18 files changed, 1280 insertions(+), 232 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java diff --git a/RELEASENOTES.md b/RELEASENOTES.md index c7f7ed7bbd..3c45c3449a 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -6,6 +6,10 @@ * Add optional parameter to `stop` to reset the player when stopping. * Add a reason to `EventListener.onTimelineChanged` to distinguish between initial preparation, reset and dynamic updates. + * Replaced `ExoPlayer.sendMessages` with `ExoPlayer.createMessage` to allow + more customization of the message. Now supports setting a message delivery + playback position and/or a delivery handler. + ([#2189](https://github.com/google/ExoPlayer/issues/2189)). * Buffering: * Allow a back-buffer of media to be retained behind the current playback position, for fast backward seeking. The back-buffer can be configured by 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 0a902e2efe..0f8df65959 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 @@ -119,9 +119,11 @@ public class VpxPlaybackTest extends InstrumentationTestCase { new DefaultDataSourceFactory(context, "ExoPlayerExtVp9Test")) .setExtractorsFactory(MatroskaExtractor.FACTORY) .createMediaSource(uri); - player.sendMessages(new ExoPlayer.ExoPlayerMessage(videoRenderer, - LibvpxVideoRenderer.MSG_SET_OUTPUT_BUFFER_RENDERER, - new VpxVideoSurfaceView(context))); + player + .createMessage(videoRenderer) + .setType(LibvpxVideoRenderer.MSG_SET_OUTPUT_BUFFER_RENDERER) + .setMessage(new VpxVideoSurfaceView(context)) + .send(); player.prepare(mediaSource); player.setPlayWhenReady(true); Looper.loop(); diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index 40b4b2d383..9d8e2dcd9d 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -17,11 +17,13 @@ package com.google.android.exoplayer2; import com.google.android.exoplayer2.Player.DefaultEventListener; import com.google.android.exoplayer2.Player.EventListener; +import com.google.android.exoplayer2.Timeline.Window; import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.testutil.ActionSchedule; +import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerTarget; import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner; import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner.Builder; import com.google.android.exoplayer2.testutil.FakeMediaClockRenderer; @@ -942,4 +944,405 @@ public final class ExoPlayerTest extends TestCase { testRunner.assertTimelinesEqual(timeline); testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); } + + public void testSendMessagesDuringPreparation() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForPlaybackState(Player.STATE_BUFFERING) + .sendMessage(target, /* positionMs= */ 50) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertTrue(target.positionMs >= 50); + } + + public void testSendMessagesAfterPreparation() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForTimelineChanged(timeline) + .sendMessage(target, /* positionMs= */ 50) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertTrue(target.positionMs >= 50); + } + + public void testMultipleSendMessages() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + PositionGrabbingMessageTarget target50 = new PositionGrabbingMessageTarget(); + PositionGrabbingMessageTarget target80 = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForPlaybackState(Player.STATE_BUFFERING) + .sendMessage(target80, /* positionMs= */ 80) + .sendMessage(target50, /* positionMs= */ 50) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertTrue(target50.positionMs >= 50); + assertTrue(target80.positionMs >= 80); + assertTrue(target80.positionMs >= target50.positionMs); + } + + public void testMultipleSendMessagesAtSameTime() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + PositionGrabbingMessageTarget target1 = new PositionGrabbingMessageTarget(); + PositionGrabbingMessageTarget target2 = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForPlaybackState(Player.STATE_BUFFERING) + .sendMessage(target1, /* positionMs= */ 50) + .sendMessage(target2, /* positionMs= */ 50) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertTrue(target1.positionMs >= 50); + assertTrue(target2.positionMs >= 50); + } + + public void testSendMessagesMultiPeriodResolution() throws Exception { + Timeline timeline = + new FakeTimeline(new TimelineWindowDefinition(/* periodCount= */ 10, /* id= */ 0)); + PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForPlaybackState(Player.STATE_BUFFERING) + .sendMessage(target, /* positionMs= */ 50) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertTrue(target.positionMs >= 50); + } + + public void testSendMessagesAtStartAndEndOfPeriod() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 2); + PositionGrabbingMessageTarget targetStartFirstPeriod = new PositionGrabbingMessageTarget(); + PositionGrabbingMessageTarget targetEndMiddlePeriod = new PositionGrabbingMessageTarget(); + PositionGrabbingMessageTarget targetStartMiddlePeriod = new PositionGrabbingMessageTarget(); + PositionGrabbingMessageTarget targetEndLastPeriod = new PositionGrabbingMessageTarget(); + long duration1Ms = timeline.getWindow(0, new Window()).getDurationMs(); + long duration2Ms = timeline.getWindow(1, new Window()).getDurationMs(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForPlaybackState(Player.STATE_BUFFERING) + .sendMessage(targetStartFirstPeriod, /* windowIndex= */ 0, /* positionMs= */ 0) + .sendMessage(targetEndMiddlePeriod, /* windowIndex= */ 0, /* positionMs= */ duration1Ms) + .sendMessage(targetStartMiddlePeriod, /* windowIndex= */ 1, /* positionMs= */ 0) + .sendMessage(targetEndLastPeriod, /* windowIndex= */ 1, /* positionMs= */ duration2Ms) + // Add additional prepare at end and wait until it's processed to ensure that + // messages sent at end of playback are received before test ends. + .waitForPlaybackState(Player.STATE_ENDED) + .prepareSource( + new FakeMediaSource(timeline, null), + /* resetPosition= */ false, + /* resetState= */ true) + .waitForPlaybackState(Player.STATE_READY) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + assertEquals(0, targetStartFirstPeriod.windowIndex); + assertTrue(targetStartFirstPeriod.positionMs >= 0); + assertEquals(0, targetEndMiddlePeriod.windowIndex); + assertTrue(targetEndMiddlePeriod.positionMs >= duration1Ms); + assertEquals(1, targetStartMiddlePeriod.windowIndex); + assertTrue(targetStartMiddlePeriod.positionMs >= 0); + assertEquals(1, targetEndLastPeriod.windowIndex); + assertTrue(targetEndLastPeriod.positionMs >= duration2Ms); + } + + public void testSendMessagesSeekOnDeliveryTimeDuringPreparation() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForPlaybackState(Player.STATE_BUFFERING) + .sendMessage(target, /* positionMs= */ 50) + .seek(/* positionMs= */ 50) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertTrue(target.positionMs >= 50); + } + + public void testSendMessagesSeekOnDeliveryTimeAfterPreparation() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForPlaybackState(Player.STATE_BUFFERING) + .sendMessage(target, /* positionMs= */ 50) + .waitForTimelineChanged(timeline) + .seek(/* positionMs= */ 50) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertTrue(target.positionMs >= 50); + } + + public void testSendMessagesSeekAfterDeliveryTimeDuringPreparation() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForPlaybackState(Player.STATE_BUFFERING) + .sendMessage(target, /* positionMs= */ 50) + .seek(/* positionMs= */ 51) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertEquals(C.POSITION_UNSET, target.positionMs); + } + + public void testSendMessagesSeekAfterDeliveryTimeAfterPreparation() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .sendMessage(target, /* positionMs= */ 50) + .waitForTimelineChanged(timeline) + .seek(/* positionMs= */ 51) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertEquals(C.POSITION_UNSET, target.positionMs); + } + + public void testSendMessagesRepeatDoesNotRepost() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForPlaybackState(Player.STATE_BUFFERING) + .sendMessage(target, /* positionMs= */ 50) + .setRepeatMode(Player.REPEAT_MODE_ALL) + .waitForPositionDiscontinuity() + .setRepeatMode(Player.REPEAT_MODE_OFF) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertEquals(1, target.messageCount); + assertTrue(target.positionMs >= 50); + } + + public void testSendMessagesRepeatWithoutDeletingDoesRepost() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForPlaybackState(Player.STATE_BUFFERING) + .sendMessage( + target, + /* windowIndex= */ 0, + /* positionMs= */ 50, + /* deleteAfterDelivery= */ false) + .setRepeatMode(Player.REPEAT_MODE_ALL) + .waitForPositionDiscontinuity() + .setRepeatMode(Player.REPEAT_MODE_OFF) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertEquals(2, target.messageCount); + assertTrue(target.positionMs >= 50); + } + + public void testSendMessagesMoveCurrentWindowIndex() throws Exception { + Timeline timeline = + new FakeTimeline(new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 0)); + final Timeline secondTimeline = + new FakeTimeline( + new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 1), + new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 0)); + final FakeMediaSource mediaSource = new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT); + PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForTimelineChanged(timeline) + .sendMessage(target, /* positionMs= */ 50) + .executeRunnable( + new Runnable() { + @Override + public void run() { + mediaSource.setNewSourceInfo(secondTimeline, null); + } + }) + .build(); + new ExoPlayerTestRunner.Builder() + .setMediaSource(mediaSource) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertTrue(target.positionMs >= 50); + assertEquals(1, target.windowIndex); + } + + public void testSendMessagesMultiWindowDuringPreparation() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 3); + PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForPlaybackState(Player.STATE_BUFFERING) + .sendMessage(target, /* windowIndex = */ 2, /* positionMs= */ 50) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertEquals(2, target.windowIndex); + assertTrue(target.positionMs >= 50); + } + + public void testSendMessagesMultiWindowAfterPreparation() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 3); + PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForTimelineChanged(timeline) + .sendMessage(target, /* windowIndex = */ 2, /* positionMs= */ 50) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertEquals(2, target.windowIndex); + assertTrue(target.positionMs >= 50); + } + + public void testSendMessagesMoveWindowIndex() throws Exception { + Timeline timeline = + new FakeTimeline( + new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 0), + new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 1)); + final Timeline secondTimeline = + new FakeTimeline( + new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 1), + new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 0)); + final FakeMediaSource mediaSource = new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT); + PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForTimelineChanged(timeline) + .sendMessage(target, /* windowIndex = */ 1, /* positionMs= */ 50) + .executeRunnable( + new Runnable() { + @Override + public void run() { + mediaSource.setNewSourceInfo(secondTimeline, null); + } + }) + .waitForTimelineChanged(secondTimeline) + .seek(/* windowIndex= */ 0, /* positionMs= */ 0) + .build(); + new ExoPlayerTestRunner.Builder() + .setMediaSource(mediaSource) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertTrue(target.positionMs >= 50); + assertEquals(0, target.windowIndex); + } + + public void testSendMessagesNonLinearPeriodOrder() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 3); + PositionGrabbingMessageTarget target1 = new PositionGrabbingMessageTarget(); + PositionGrabbingMessageTarget target2 = new PositionGrabbingMessageTarget(); + PositionGrabbingMessageTarget target3 = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForPlaybackState(Player.STATE_BUFFERING) + .sendMessage(target1, /* windowIndex = */ 0, /* positionMs= */ 50) + .sendMessage(target2, /* windowIndex = */ 1, /* positionMs= */ 50) + .sendMessage(target3, /* windowIndex = */ 2, /* positionMs= */ 50) + .waitForTimelineChanged(timeline) + .seek(/* windowIndex= */ 1, /* positionMs= */ 0) + .waitForPositionDiscontinuity() + .seek(/* windowIndex= */ 0, /* positionMs= */ 0) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertEquals(0, target1.windowIndex); + assertEquals(1, target2.windowIndex); + assertEquals(2, target3.windowIndex); + } + + private static final class PositionGrabbingMessageTarget extends PlayerTarget { + + public int windowIndex; + public long positionMs; + public int messageCount; + + public PositionGrabbingMessageTarget() { + windowIndex = C.INDEX_UNSET; + positionMs = C.POSITION_UNSET; + } + + @Override + public void handleMessage(SimpleExoPlayer player, int messageType, Object message) { + windowIndex = player.getCurrentWindowIndex(); + positionMs = player.getCurrentPosition(); + messageCount++; + } + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java index a4103787d1..8ee9a13c55 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java @@ -157,7 +157,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { return ADAPTIVE_NOT_SUPPORTED; } - // ExoPlayerComponent implementation. + // PlayerMessage.Target implementation. @Override public void handleMessage(int what, Object object) throws ExoPlaybackException { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java index cc767752be..4bd28150bc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java @@ -34,40 +34,43 @@ import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; /** - * An extensible media player that plays {@link MediaSource}s. Instances can be obtained from - * {@link ExoPlayerFactory}. + * An extensible media player that plays {@link MediaSource}s. Instances can be obtained from {@link + * ExoPlayerFactory}. * *

          Player components

          + * *

          ExoPlayer is designed to make few assumptions about (and hence impose few restrictions on) the * type of the media being played, how and where it is stored, and how it is rendered. Rather than * implementing the loading and rendering of media directly, ExoPlayer implementations delegate this * work to components that are injected when a player is created or when it's prepared for playback. * Components common to all ExoPlayer implementations are: + * *

            *
          • 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(MediaSource)} - * at the start of playback. The library modules provide default implementations for regular media - * files ({@link ExtractorMediaSource}), DASH (DashMediaSource), SmoothStreaming (SsMediaSource) - * and HLS (HlsMediaSource), an implementation for loading single media samples - * ({@link SingleSampleMediaSource}) that's most often used for side-loaded subtitle files, and - * implementations for building more complex MediaSources from simpler ones - * ({@link MergingMediaSource}, {@link ConcatenatingMediaSource}, - * {@link DynamicConcatenatingMediaSource}, {@link LoopingMediaSource} and - * {@link ClippingMediaSource}).
          • + * which the loaded media can be read. A MediaSource is injected via {@link + * #prepare(MediaSource)} at the start of playback. The library modules provide default + * implementations for regular media files ({@link ExtractorMediaSource}), DASH + * (DashMediaSource), SmoothStreaming (SsMediaSource) and HLS (HlsMediaSource), an + * implementation for loading single media samples ({@link SingleSampleMediaSource}) that's + * most often used for side-loaded subtitle files, and implementations for building more + * complex MediaSources from simpler ones ({@link MergingMediaSource}, {@link + * ConcatenatingMediaSource}, {@link DynamicConcatenatingMediaSource}, {@link + * LoopingMediaSource} and {@link ClippingMediaSource}). *
          • {@link Renderer}s that render individual components of the media. The library - * provides default implementations for common media types ({@link MediaCodecVideoRenderer}, - * {@link MediaCodecAudioRenderer}, {@link TextRenderer} and {@link MetadataRenderer}). A Renderer - * consumes media from the MediaSource being played. Renderers are injected when the player is - * created.
          • + * provides default implementations for common media types ({@link MediaCodecVideoRenderer}, + * {@link MediaCodecAudioRenderer}, {@link TextRenderer} and {@link MetadataRenderer}). A + * Renderer consumes media from the MediaSource being played. Renderers are injected when the + * player is created. *
          • A {@link TrackSelector} that selects tracks provided by the MediaSource to be - * consumed by each of the available Renderers. The library provides a default implementation - * ({@link DefaultTrackSelector}) suitable for most use cases. A TrackSelector is injected when - * the player is created.
          • + * consumed by each of the available Renderers. The library provides a default implementation + * ({@link DefaultTrackSelector}) suitable for most use cases. A TrackSelector is injected + * when the player is created. *
          • A {@link LoadControl} that controls when the MediaSource buffers more media, and how - * much media is buffered. The library provides a default implementation - * ({@link DefaultLoadControl}) suitable for most use cases. A LoadControl is injected when the - * player is created.
          • + * much media is buffered. The library provides a default implementation ({@link + * DefaultLoadControl}) suitable for most use cases. A LoadControl is injected when the player + * is created. *
          + * *

          An ExoPlayer can be built using the default components provided by the library, but may also * be built using custom implementations if non-standard behaviors are required. For example a * custom LoadControl could be injected to change the player's buffering strategy, or a custom @@ -81,30 +84,32 @@ import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; * it's possible to load data from a non-standard source, or through a different network stack. * *

          Threading model

          - *

          The figure below shows ExoPlayer's threading model.

          - *

          - * ExoPlayer's threading model - *

          + * + *

          The figure below shows ExoPlayer's threading model. + * + *

          ExoPlayer's threading
+ * model * *

            - *
          • It is recommended that ExoPlayer instances are created and accessed from a single application - * thread. The application's main thread is ideal. Accessing an instance from multiple threads is - * discouraged, however if an application does wish to do this then it may do so provided that it - * ensures accesses are synchronized.
          • - *
          • Registered listeners are called on the thread that created the ExoPlayer instance, unless - * the thread that created the ExoPlayer instance does not have a {@link Looper}. In that case, - * registered listeners will be called on the application's main thread.
          • - *
          • An internal playback thread is responsible for playback. Injected player components such as - * Renderers, MediaSources, TrackSelectors and LoadControls are called by the player on this - * thread.
          • - *
          • When the application performs an operation on the player, for example a seek, a message is - * delivered to the internal playback thread via a message queue. The internal playback thread - * consumes messages from the queue and performs the corresponding operations. Similarly, when a - * playback event occurs on the internal playback thread, a message is delivered to the application - * thread via a second message queue. The application thread consumes messages from the queue, - * updating the application visible state and calling corresponding listener methods.
          • - *
          • Injected player components may use additional background threads. For example a MediaSource - * may use background threads to load data. These are implementation specific.
          • + *
          • It is recommended that ExoPlayer instances are created and accessed from a single + * application thread. The application's main thread is ideal. Accessing an instance from + * multiple threads is discouraged, however if an application does wish to do this then it may + * do so provided that it ensures accesses are synchronized. + *
          • Registered listeners are called on the thread that created the ExoPlayer instance, unless + * the thread that created the ExoPlayer instance does not have a {@link Looper}. In that + * case, registered listeners will be called on the application's main thread. + *
          • An internal playback thread is responsible for playback. Injected player components such as + * Renderers, MediaSources, TrackSelectors and LoadControls are called by the player on this + * thread. + *
          • When the application performs an operation on the player, for example a seek, a message is + * delivered to the internal playback thread via a message queue. The internal playback thread + * consumes messages from the queue and performs the corresponding operations. Similarly, when + * a playback event occurs on the internal playback thread, a message is delivered to the + * application thread via a second message queue. The application thread consumes messages + * from the queue, updating the application visible state and calling corresponding listener + * methods. + *
          • Injected player components may use additional background threads. For example a MediaSource + * may use background threads to load data. These are implementation specific. *
          */ public interface ExoPlayer extends Player { @@ -115,54 +120,28 @@ public interface ExoPlayer extends Player { @Deprecated interface EventListener extends Player.EventListener {} - /** - * A component of an {@link ExoPlayer} that can receive messages on the playback thread. - *

          - * Messages can be delivered to a component via {@link #sendMessages} and - * {@link #blockingSendMessages}. - */ - interface ExoPlayerComponent { + /** @deprecated Use {@link PlayerMessage.Target} instead. */ + @Deprecated + interface ExoPlayerComponent extends PlayerMessage.Target {} - /** - * Handles a message delivered to the component. Called on the playback thread. - * - * @param messageType The message type. - * @param message The message. - * @throws ExoPlaybackException If an error occurred whilst handling the message. - */ - void handleMessage(int messageType, Object message) throws ExoPlaybackException; - - } - - /** - * Defines a message and a target {@link ExoPlayerComponent} to receive it. - */ + /** @deprecated Use {@link PlayerMessage} instead. */ + @Deprecated final class ExoPlayerMessage { - /** - * The target to receive the message. - */ - public final ExoPlayerComponent target; - /** - * The type of the message. - */ + /** The target to receive the message. */ + public final PlayerMessage.Target target; + /** The type of the message. */ public final int messageType; - /** - * The message. - */ + /** The message. */ public final Object message; - /** - * @param target The target of the message. - * @param messageType The message type. - * @param message The message. - */ - public ExoPlayerMessage(ExoPlayerComponent target, int messageType, Object message) { + /** @deprecated Use {@link ExoPlayer#createMessage(PlayerMessage.Target)} instead. */ + @Deprecated + public ExoPlayerMessage(PlayerMessage.Target target, int messageType, Object message) { this.target = target; this.messageType = messageType; this.message = message; } - } /** @@ -236,20 +215,25 @@ public interface ExoPlayer extends Player { void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState); /** - * Sends messages to their target components. The messages are delivered on the playback thread. - * If a component throws an {@link ExoPlaybackException} then it is propagated out of the player - * as an error. - * - * @param messages The messages to be sent. + * Creates a message that can be sent to a {@link PlayerMessage.Target}. By default, the message + * will be delivered immediately without blocking on the playback thread. The default {@link + * PlayerMessage#getType()} is 0 and the default {@link PlayerMessage#getMessage()} is null. If a + * position is specified with {@link PlayerMessage#setPosition(long)}, the message will be + * delivered at this position in the current window defined by {@link #getCurrentWindowIndex()}. + * Alternatively, the message can be sent at a specific window using {@link + * PlayerMessage#setPosition(int, long)}. */ + PlayerMessage createMessage(PlayerMessage.Target target); + + /** @deprecated Use {@link #createMessage(PlayerMessage.Target)} instead. */ + @Deprecated void sendMessages(ExoPlayerMessage... messages); /** - * Variant of {@link #sendMessages(ExoPlayerMessage...)} that blocks until after the messages have - * been delivered. - * - * @param messages The messages to be sent. + * @deprecated Use {@link #createMessage(PlayerMessage.Target)} with {@link + * PlayerMessage#blockUntilDelivered()}. */ + @Deprecated void blockingSendMessages(ExoPlayerMessage... messages); /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index 2869a7668e..afb6428fa5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -22,6 +22,7 @@ import android.os.Message; import android.support.annotation.Nullable; import android.util.Log; import android.util.Pair; +import com.google.android.exoplayer2.PlayerMessage.Target; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.source.TrackGroupArray; @@ -31,6 +32,8 @@ 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.Util; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.CopyOnWriteArraySet; /** @@ -45,6 +48,7 @@ import java.util.concurrent.CopyOnWriteArraySet; private final TrackSelectorResult emptyTrackSelectorResult; private final Handler eventHandler; private final ExoPlayerImplInternal internalPlayer; + private final Handler internalPlayerHandler; private final CopyOnWriteArraySet listeners; private final Timeline.Window window; private final Timeline.Period period; @@ -113,6 +117,7 @@ import java.util.concurrent.CopyOnWriteArraySet; shuffleModeEnabled, eventHandler, this); + internalPlayerHandler = new Handler(internalPlayer.getPlaybackLooper()); } @Override @@ -326,12 +331,47 @@ import java.util.concurrent.CopyOnWriteArraySet; @Override public void sendMessages(ExoPlayerMessage... messages) { - internalPlayer.sendMessages(messages); + for (ExoPlayerMessage message : messages) { + createMessage(message.target).setType(message.messageType).setMessage(message.message).send(); + } + } + + @Override + public PlayerMessage createMessage(Target target) { + return new PlayerMessage( + internalPlayer, + target, + playbackInfo.timeline, + getCurrentWindowIndex(), + internalPlayerHandler); } @Override public void blockingSendMessages(ExoPlayerMessage... messages) { - internalPlayer.blockingSendMessages(messages); + List playerMessages = new ArrayList<>(); + for (ExoPlayerMessage message : messages) { + playerMessages.add( + createMessage(message.target) + .setType(message.messageType) + .setMessage(message.message) + .send()); + } + boolean wasInterrupted = false; + for (PlayerMessage message : playerMessages) { + boolean blockMessage = true; + while (blockMessage) { + try { + message.blockUntilDelivered(); + blockMessage = false; + } catch (InterruptedException e) { + wasInterrupted = true; + } + } + } + if (wasInterrupted) { + // Restore the interrupted status. + Thread.currentThread().interrupt(); + } } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 09b3231467..f3d0e1794b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -22,10 +22,10 @@ import android.os.Message; import android.os.Process; import android.os.SystemClock; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.util.Log; import android.util.Pair; import com.google.android.exoplayer2.DefaultMediaClock.PlaybackParameterListener; -import com.google.android.exoplayer2.ExoPlayer.ExoPlayerMessage; import com.google.android.exoplayer2.MediaPeriodInfoSequence.MediaPeriodInfo; import com.google.android.exoplayer2.Player.DiscontinuityReason; import com.google.android.exoplayer2.source.ClippingMediaPeriod; @@ -40,14 +40,19 @@ 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.TraceUtil; +import com.google.android.exoplayer2.util.Util; import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; -/** - * Implements the internal behavior of {@link ExoPlayerImpl}. - */ -/* package */ final class ExoPlayerImplInternal implements Handler.Callback, - MediaPeriod.Callback, TrackSelector.InvalidationListener, MediaSource.Listener, - PlaybackParameterListener { +/** Implements the internal behavior of {@link ExoPlayerImpl}. */ +/* package */ final class ExoPlayerImplInternal + implements Handler.Callback, + MediaPeriod.Callback, + TrackSelector.InvalidationListener, + MediaSource.Listener, + PlaybackParameterListener, + PlayerMessage.Sender { private static final String TAG = "ExoPlayerImplInternal"; @@ -108,6 +113,7 @@ import java.io.IOException; private final boolean retainBackBufferFromKeyframe; private final DefaultMediaClock mediaClock; private final PlaybackInfoUpdate playbackInfoUpdate; + private final ArrayList customMessageInfos; @SuppressWarnings("unused") private SeekParameters seekParameters; @@ -120,13 +126,12 @@ import java.io.IOException; private boolean rebuffering; private @Player.RepeatMode int repeatMode; private boolean shuffleModeEnabled; - private int customMessagesSent; - private int customMessagesProcessed; private long elapsedRealtimeUs; private int pendingPrepareCount; private SeekPosition pendingInitialSeekPosition; private long rendererPositionUs; + private int nextCustomMessageInfoIndex; private MediaPeriodHolder loadingPeriodHolder; private MediaPeriodHolder readingPeriodHolder; @@ -166,6 +171,7 @@ import java.io.IOException; rendererCapabilities[i] = renderers[i].getCapabilities(); } mediaClock = new DefaultMediaClock(this); + customMessageInfos = new ArrayList<>(); enabledRenderers = new Renderer[0]; window = new Timeline.Window(); period = new Timeline.Period(); @@ -214,34 +220,15 @@ import java.io.IOException; handler.obtainMessage(MSG_STOP, reset ? 1 : 0, 0).sendToTarget(); } - public void sendMessages(ExoPlayerMessage... messages) { + @Override + public synchronized void sendMessage( + PlayerMessage message, PlayerMessage.Sender.Listener listener) { if (released) { Log.w(TAG, "Ignoring messages sent after release."); + listener.onMessageDeleted(); return; } - customMessagesSent++; - handler.obtainMessage(MSG_CUSTOM, messages).sendToTarget(); - } - - public synchronized void blockingSendMessages(ExoPlayerMessage... messages) { - if (released) { - Log.w(TAG, "Ignoring messages sent after release."); - return; - } - int messageNumber = customMessagesSent++; - handler.obtainMessage(MSG_CUSTOM, messages).sendToTarget(); - boolean wasInterrupted = false; - while (customMessagesProcessed <= messageNumber) { - try { - wait(); - } catch (InterruptedException e) { - wasInterrupted = true; - } - } - if (wasInterrupted) { - // Restore the interrupted status. - Thread.currentThread().interrupt(); - } + handler.obtainMessage(MSG_CUSTOM, new CustomMessageInfo(message, listener)).sendToTarget(); } public synchronized void release() { @@ -349,7 +336,7 @@ import java.io.IOException; reselectTracksInternal(); break; case MSG_CUSTOM: - sendMessagesInternal((ExoPlayerMessage[]) msg.obj); + sendMessageInternal((CustomMessageInfo) msg.obj); break; case MSG_RELEASE: releaseInternal(); @@ -537,8 +524,9 @@ import java.io.IOException; } else { rendererPositionUs = mediaClock.syncAndGetPositionUs(); periodPositionUs = playingPeriodHolder.toPeriodTime(rendererPositionUs); + maybeTriggerCustomMessages(playbackInfo.positionUs, periodPositionUs); + playbackInfo.positionUs = periodPositionUs; } - playbackInfo.positionUs = periodPositionUs; elapsedRealtimeUs = SystemClock.elapsedRealtime() * 1000; // Update the buffered position. @@ -656,7 +644,8 @@ import java.io.IOException; boolean seekPositionAdjusted = seekPosition.windowPositionUs == C.TIME_UNSET; try { - Pair periodPosition = resolveSeekPosition(seekPosition); + Pair periodPosition = + resolveSeekPosition(seekPosition, /* trySubsequentPeriods= */ true); if (periodPosition == null) { // The seek position was valid for the timeline that it was performed into, but the // timeline has changed and a suitable seek position could not be resolved in the new one. @@ -850,6 +839,11 @@ import java.io.IOException; } if (resetState) { mediaPeriodInfoSequence.setTimeline(null); + for (CustomMessageInfo customMessageInfo : customMessageInfos) { + customMessageInfo.listener.onMessageDeleted(); + } + customMessageInfos.clear(); + nextCustomMessageInfoIndex = 0; } playbackInfo = new PlaybackInfo( @@ -870,21 +864,153 @@ import java.io.IOException; } } - private void sendMessagesInternal(ExoPlayerMessage[] messages) throws ExoPlaybackException { - try { - for (ExoPlayerMessage message : messages) { - message.target.handleMessage(message.messageType, message.message); + private void sendMessageInternal(CustomMessageInfo customMessageInfo) { + if (customMessageInfo.message.getPositionMs() == C.TIME_UNSET) { + // If no delivery time is specified, trigger immediate message delivery. + sendCustomMessagesToTarget(customMessageInfo); + } else if (playbackInfo.timeline == null) { + // Still waiting for initial timeline to resolve position. + customMessageInfos.add(customMessageInfo); + } else { + if (resolveCustomMessagePosition(customMessageInfo)) { + customMessageInfos.add(customMessageInfo); + // Ensure new message is inserted according to playback order. + Collections.sort(customMessageInfos); + } else { + customMessageInfo.listener.onMessageDeleted(); } - if (playbackInfo.playbackState == Player.STATE_READY - || playbackInfo.playbackState == Player.STATE_BUFFERING) { - // The message may have caused something to change that now requires us to do work. - handler.sendEmptyMessage(MSG_DO_SOME_WORK); + } + } + + private void sendCustomMessagesToTarget(final CustomMessageInfo customMessageInfo) { + final Runnable handleMessageRunnable = + new Runnable() { + @Override + public void run() { + try { + customMessageInfo + .message + .getTarget() + .handleMessage( + customMessageInfo.message.getType(), customMessageInfo.message.getMessage()); + } catch (ExoPlaybackException e) { + eventHandler.obtainMessage(MSG_ERROR, e).sendToTarget(); + } finally { + customMessageInfo.listener.onMessageDelivered(); + if (customMessageInfo.message.getDeleteAfterDelivery()) { + customMessageInfo.listener.onMessageDeleted(); + } + // The message may have caused something to change that now requires us to do + // work. + handler.sendEmptyMessage(MSG_DO_SOME_WORK); + } + } + }; + handler.post( + new Runnable() { + @Override + public void run() { + customMessageInfo.message.getHandler().post(handleMessageRunnable); + } + }); + } + + private void resolveCustomMessagePositions() { + for (int i = customMessageInfos.size() - 1; i >= 0; i--) { + if (!resolveCustomMessagePosition(customMessageInfos.get(i))) { + // Remove messages if new position can't be resolved. + customMessageInfos.get(i).listener.onMessageDeleted(); + customMessageInfos.remove(i); } - } finally { - synchronized (this) { - customMessagesProcessed++; - notifyAll(); + } + // Re-sort messages by playback order. + Collections.sort(customMessageInfos); + } + + private boolean resolveCustomMessagePosition(CustomMessageInfo customMessageInfo) { + if (customMessageInfo.resolvedPeriodUid == null) { + // Position is still unresolved. Try to find window in current timeline. + Pair periodPosition = + resolveSeekPosition( + new SeekPosition( + customMessageInfo.message.getTimeline(), + customMessageInfo.message.getWindowIndex(), + C.msToUs(customMessageInfo.message.getPositionMs())), + /* trySubsequentPeriods= */ false); + if (periodPosition == null) { + return false; } + customMessageInfo.setResolvedPosition( + periodPosition.first, + periodPosition.second, + playbackInfo.timeline.getPeriod(periodPosition.first, period, true).uid); + } else { + // Position has been resolved for a previous timeline. Try to find the updated period index. + int index = playbackInfo.timeline.getIndexOfPeriod(customMessageInfo.resolvedPeriodUid); + if (index == C.INDEX_UNSET) { + return false; + } + customMessageInfo.resolvedPeriodIndex = index; + } + return true; + } + + private void maybeTriggerCustomMessages(long oldPeriodPositionUs, long newPeriodPositionUs) { + if (customMessageInfos.isEmpty() || playbackInfo.periodId.isAd()) { + return; + } + // If this is the first call from the start position, include oldPeriodPositionUs in potential + // trigger positions. + if (playbackInfo.startPositionUs == oldPeriodPositionUs) { + oldPeriodPositionUs--; + } + // Correct next index if necessary (e.g. after seeking, timeline changes, or new messages) + int currentPeriodIndex = playbackInfo.periodId.periodIndex; + CustomMessageInfo prevInfo = + nextCustomMessageInfoIndex > 0 + ? customMessageInfos.get(nextCustomMessageInfoIndex - 1) + : null; + while (prevInfo != null + && (prevInfo.resolvedPeriodIndex > currentPeriodIndex + || (prevInfo.resolvedPeriodIndex == currentPeriodIndex + && prevInfo.resolvedPeriodTimeUs > oldPeriodPositionUs))) { + nextCustomMessageInfoIndex--; + prevInfo = + nextCustomMessageInfoIndex > 0 + ? customMessageInfos.get(nextCustomMessageInfoIndex - 1) + : null; + } + CustomMessageInfo nextInfo = + nextCustomMessageInfoIndex < customMessageInfos.size() + ? customMessageInfos.get(nextCustomMessageInfoIndex) + : null; + while (nextInfo != null + && nextInfo.resolvedPeriodUid != null + && (nextInfo.resolvedPeriodIndex < currentPeriodIndex + || (nextInfo.resolvedPeriodIndex == currentPeriodIndex + && nextInfo.resolvedPeriodTimeUs <= oldPeriodPositionUs))) { + nextCustomMessageInfoIndex++; + nextInfo = + nextCustomMessageInfoIndex < customMessageInfos.size() + ? customMessageInfos.get(nextCustomMessageInfoIndex) + : null; + } + // Check if any message falls within the covered time span. + while (nextInfo != null + && nextInfo.resolvedPeriodUid != null + && nextInfo.resolvedPeriodIndex == currentPeriodIndex + && nextInfo.resolvedPeriodTimeUs > oldPeriodPositionUs + && nextInfo.resolvedPeriodTimeUs <= newPeriodPositionUs) { + sendCustomMessagesToTarget(nextInfo); + if (nextInfo.message.getDeleteAfterDelivery()) { + customMessageInfos.remove(nextCustomMessageInfoIndex); + } else { + nextCustomMessageInfoIndex++; + } + nextInfo = + nextCustomMessageInfoIndex < customMessageInfos.size() + ? customMessageInfos.get(nextCustomMessageInfoIndex) + : null; } } @@ -1034,12 +1160,14 @@ import java.io.IOException; Object manifest = sourceRefreshInfo.manifest; mediaPeriodInfoSequence.setTimeline(timeline); playbackInfo = playbackInfo.copyWithTimeline(timeline, manifest); + resolveCustomMessagePositions(); if (oldTimeline == null) { playbackInfoUpdate.incrementPendingOperationAcks(pendingPrepareCount); pendingPrepareCount = 0; if (pendingInitialSeekPosition != null) { - Pair periodPosition = resolveSeekPosition(pendingInitialSeekPosition); + Pair periodPosition = + resolveSeekPosition(pendingInitialSeekPosition, /* trySubsequentPeriods= */ true); pendingInitialSeekPosition = null; if (periodPosition == null) { // The seek position was valid for the timeline that it was performed into, but the @@ -1224,11 +1352,14 @@ import java.io.IOException; * internal timeline. * * @param seekPosition The position to resolve. + * @param trySubsequentPeriods Whether the position can be resolved to a subsequent matching + * period if the original period is no longer available. * @return The resolved position, or null if resolution was not successful. * @throws IllegalSeekPositionException If the window index of the seek position is outside the * bounds of the timeline. */ - private Pair resolveSeekPosition(SeekPosition seekPosition) { + private Pair resolveSeekPosition( + SeekPosition seekPosition, boolean trySubsequentPeriods) { Timeline timeline = playbackInfo.timeline; Timeline seekTimeline = seekPosition.timeline; if (seekTimeline.isEmpty()) { @@ -1257,12 +1388,14 @@ import java.io.IOException; // We successfully located the period in the internal timeline. return Pair.create(periodIndex, periodPosition.second); } - // Try and find a subsequent period from the seek timeline in the internal timeline. - periodIndex = resolveSubsequentPeriod(periodPosition.first, seekTimeline, timeline); - if (periodIndex != C.INDEX_UNSET) { - // We found one. Map the SeekPosition onto the corresponding default position. - return getPeriodPosition(timeline, timeline.getPeriod(periodIndex, period).windowIndex, - C.TIME_UNSET); + if (trySubsequentPeriods) { + // Try and find a subsequent period from the seek timeline in the internal timeline. + periodIndex = resolveSubsequentPeriod(periodPosition.first, seekTimeline, timeline); + if (periodIndex != C.INDEX_UNSET) { + // We found one. Map the SeekPosition onto the corresponding default position. + return getPeriodPosition( + timeline, timeline.getPeriod(periodIndex, period).windowIndex, C.TIME_UNSET); + } } // We didn't find one. Give up. return null; @@ -1802,7 +1935,45 @@ import java.io.IOException; this.windowIndex = windowIndex; this.windowPositionUs = windowPositionUs; } + } + private static final class CustomMessageInfo implements Comparable { + + public final PlayerMessage message; + public final PlayerMessage.Sender.Listener listener; + + public int resolvedPeriodIndex; + public long resolvedPeriodTimeUs; + public @Nullable Object resolvedPeriodUid; + + public CustomMessageInfo(PlayerMessage message, PlayerMessage.Sender.Listener listener) { + this.message = message; + this.listener = listener; + } + + public void setResolvedPosition(int periodIndex, long periodTimeUs, Object periodUid) { + resolvedPeriodIndex = periodIndex; + resolvedPeriodTimeUs = periodTimeUs; + resolvedPeriodUid = periodUid; + } + + @Override + public int compareTo(@NonNull CustomMessageInfo other) { + if ((resolvedPeriodUid == null) != (other.resolvedPeriodUid == null)) { + // CustomMessageInfos with a resolved period position are always smaller. + return resolvedPeriodUid != null ? -1 : 1; + } + if (resolvedPeriodUid == null) { + // Don't sort message with unresolved positions. + return 0; + } + // Sort resolved media times by period index and then by period position. + int comparePeriodIndex = resolvedPeriodIndex - other.resolvedPeriodIndex; + if (comparePeriodIndex != 0) { + return comparePeriodIndex; + } + return Util.compareLong(resolvedPeriodTimeUs, other.resolvedPeriodTimeUs); + } } private static final class MediaSourceRefreshInfo { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java index 978f4f7a97..593d3d1fce 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java @@ -179,7 +179,7 @@ public abstract class NoSampleRenderer implements Renderer, RendererCapabilities return ADAPTIVE_NOT_SUPPORTED; } - // ExoPlayerComponent implementation. + // PlayerMessage.Target implementation. @Override public void handleMessage(int what, Object object) throws ExoPlaybackException { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java b/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java new file mode 100644 index 0000000000..44a4b0c7c2 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java @@ -0,0 +1,295 @@ +/* + * 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.os.Handler; +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.util.Assertions; + +/** + * Defines a player message which can be sent with a {@link Sender} and received by a {@link + * Target}. + */ +public final class PlayerMessage { + + /** A target for messages. */ + public interface Target { + + /** + * Handles a message delivered to the target. + * + * @param messageType The message type. + * @param message The message. + * @throws ExoPlaybackException If an error occurred whilst handling the message. + */ + void handleMessage(int messageType, Object message) throws ExoPlaybackException; + } + + /** A sender for messages. */ + public interface Sender { + + /** A listener for message events triggered by the sender. */ + interface Listener { + + /** Called when the message has been delivered. */ + void onMessageDelivered(); + + /** Called when the message has been deleted. */ + void onMessageDeleted(); + } + + /** + * Sends a message. + * + * @param message The message to be sent. + * @param listener The listener to listen to message events. + */ + void sendMessage(PlayerMessage message, Listener listener); + } + + private final Target target; + private final Sender sender; + private final Timeline timeline; + + private int type; + private Object message; + private Handler handler; + private int windowIndex; + private long positionMs; + private boolean deleteAfterDelivery; + private boolean isSent; + private boolean isDelivered; + private boolean isDeleted; + + /** + * Creates a new message. + * + * @param sender The {@link Sender} used to send the message. + * @param target The {@link Target} the message is sent to. + * @param timeline The timeline used when setting the position with {@link #setPosition(long)}. If + * set to {@link Timeline#EMPTY}, any position can be specified. + * @param defaultWindowIndex The default window index in the {@code timeline} when no other window + * index is specified. + * @param defaultHandler The default handler to send the message on when no other handler is + * specified. + */ + public PlayerMessage( + Sender sender, + Target target, + Timeline timeline, + int defaultWindowIndex, + Handler defaultHandler) { + this.sender = sender; + this.target = target; + this.timeline = timeline; + this.handler = defaultHandler; + this.windowIndex = defaultWindowIndex; + this.positionMs = C.TIME_UNSET; + this.deleteAfterDelivery = true; + } + + /** Returns the timeline used for setting the position with {@link #setPosition(long)}. */ + public Timeline getTimeline() { + return timeline; + } + + /** Returns the target the message is sent to. */ + public Target getTarget() { + return target; + } + + /** + * Sets a custom message type forwarded to the {@link Target#handleMessage(int, Object)}. + * + * @param messageType The custom message type. + * @return This message. + * @throws IllegalStateException If {@link #send()} has already been called. + */ + public PlayerMessage setType(int messageType) { + Assertions.checkState(!isSent); + this.type = messageType; + return this; + } + + /** Returns custom message type forwarded to the {@link Target#handleMessage(int, Object)}. */ + public int getType() { + return type; + } + + /** + * Sets a custom message forwarded to the {@link Target#handleMessage(int, Object)}. + * + * @param message The custom message. + * @return This message. + * @throws IllegalStateException If {@link #send()} has already been called. + */ + public PlayerMessage setMessage(@Nullable Object message) { + Assertions.checkState(!isSent); + this.message = message; + return this; + } + + /** Returns custom message forwarded to the {@link Target#handleMessage(int, Object)}. */ + public Object getMessage() { + return message; + } + + /** + * Sets the handler the message is delivered on. + * + * @param handler A {@link Handler}. + * @return This message. + * @throws IllegalStateException If {@link #send()} has already been called. + */ + public PlayerMessage setHandler(Handler handler) { + Assertions.checkState(!isSent); + this.handler = handler; + return this; + } + + /** Returns the handler the message is delivered on. */ + public Handler getHandler() { + return handler; + } + + /** + * Sets a position in the current window at which the message will be delivered. + * + * @param positionMs The position in the current window at which the message will be sent, in + * milliseconds. + * @return This message. + * @throws IllegalStateException If {@link #send()} has already been called. + */ + public PlayerMessage setPosition(long positionMs) { + Assertions.checkState(!isSent); + this.positionMs = positionMs; + return this; + } + + /** + * Returns position in window at {@link #getWindowIndex()} at which the message will be delivered, + * in milliseconds. If {@link C#TIME_UNSET}, the message will be delivered immediately. + */ + public long getPositionMs() { + return positionMs; + } + + /** + * Sets a position in a window at which the message will be delivered. + * + * @param windowIndex The index of the window at which the message will be sent. + * @param positionMs The position in the window with index {@code windowIndex} at which the + * message will be sent, in milliseconds. + * @return This message. + * @throws IllegalSeekPositionException If the timeline returned by {@link #getTimeline()} is not + * empty and the provided window index is not within the bounds of the timeline. + * @throws IllegalStateException If {@link #send()} has already been called. + */ + public PlayerMessage setPosition(int windowIndex, long positionMs) { + Assertions.checkState(!isSent); + Assertions.checkArgument(positionMs != C.TIME_UNSET); + if (windowIndex < 0 || (!timeline.isEmpty() && windowIndex >= timeline.getWindowCount())) { + throw new IllegalSeekPositionException(timeline, windowIndex, positionMs); + } + this.windowIndex = windowIndex; + this.positionMs = positionMs; + return this; + } + + /** Returns window index at which the message will be delivered. */ + public int getWindowIndex() { + return windowIndex; + } + + /** + * Sets whether the message will be deleted after delivery. If false, the message will be resent + * if playback reaches the specified position again. Only allowed to be false if a position is set + * with {@link #setPosition(long)}. + * + * @param deleteAfterDelivery Whether the message is deleted after delivery. + * @return This message. + * @throws IllegalStateException If {@link #send()} has already been called. + */ + public PlayerMessage setDeleteAfterDelivery(boolean deleteAfterDelivery) { + Assertions.checkState(!isSent); + this.deleteAfterDelivery = deleteAfterDelivery; + return this; + } + + /** Returns whether the message will be deleted after delivery. */ + public boolean getDeleteAfterDelivery() { + return deleteAfterDelivery; + } + + /** + * Sends the message. If the target throws an {@link ExoPlaybackException} then it is propagated + * out of the player as an error using {@link + * Player.EventListener#onPlayerError(ExoPlaybackException)}. + * + * @return This message. + * @throws IllegalStateException If {@link #send()} has already been called. + */ + public PlayerMessage send() { + Assertions.checkState(!isSent); + if (positionMs == C.TIME_UNSET) { + Assertions.checkArgument(deleteAfterDelivery); + } + isSent = true; + sender.sendMessage( + this, + new Sender.Listener() { + @Override + public void onMessageDelivered() { + synchronized (PlayerMessage.this) { + isDelivered = true; + PlayerMessage.this.notifyAll(); + } + } + + @Override + public void onMessageDeleted() { + synchronized (PlayerMessage.this) { + isDeleted = true; + PlayerMessage.this.notifyAll(); + } + } + }); + return this; + } + + /** + * Blocks until after the message has been delivered or the player is no longer able to deliver + * the message. + * + *

          Note that this method can't be called if the current thread is the same thread used by the + * message handler set with {@link #setHandler(Handler)} as it would cause a deadlock. + * + * @return Whether the message was delivered successfully. + * @throws IllegalStateException If this method is called before {@link #send()}. + * @throws IllegalStateException If this method is called on the same thread used by the message + * handler set with {@link #setHandler(Handler)}. + * @throws InterruptedException If the current thread is interrupted while waiting for the message + * to be delivered. + */ + public synchronized boolean blockUntilDelivered() throws InterruptedException { + Assertions.checkState(!isSent); + Assertions.checkState(handler.getLooper().getThread() != Thread.currentThread()); + while (!isDelivered && !isDeleted) { + wait(); + } + return isDelivered; + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java b/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java index 6def1591da..d0a07930e0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java @@ -15,22 +15,20 @@ */ package com.google.android.exoplayer2; -import com.google.android.exoplayer2.ExoPlayer.ExoPlayerComponent; import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.util.MediaClock; import java.io.IOException; /** * Renders media read from a {@link SampleStream}. - *

          - * Internally, a renderer's lifecycle is managed by the owning {@link ExoPlayer}. The renderer is + * + *

          Internally, a renderer's lifecycle is managed by the owning {@link ExoPlayer}. The renderer is * transitioned through various states as the overall playback state changes. The valid state * transitions are shown below, annotated with the methods that are called during each transition. - *

          - * Renderer state transitions - *

          + * + *

          Renderer state transitions */ -public interface Renderer extends ExoPlayerComponent { +public interface Renderer extends PlayerMessage.Target { /** * The renderer is disabled. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 69369d4229..e2d0ed1422 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -93,8 +93,6 @@ public class SimpleExoPlayer implements ExoPlayer { private final CopyOnWriteArraySet metadataOutputs; private final CopyOnWriteArraySet videoDebugListeners; private final CopyOnWriteArraySet audioDebugListeners; - private final int videoRendererCount; - private final int audioRendererCount; private Format videoFormat; private Format audioFormat; @@ -124,25 +122,6 @@ public class SimpleExoPlayer implements ExoPlayer { renderers = renderersFactory.createRenderers(eventHandler, componentListener, componentListener, componentListener, componentListener); - // Obtain counts of video and audio renderers. - int videoRendererCount = 0; - int audioRendererCount = 0; - for (Renderer renderer : renderers) { - switch (renderer.getTrackType()) { - case C.TRACK_TYPE_VIDEO: - videoRendererCount++; - break; - case C.TRACK_TYPE_AUDIO: - audioRendererCount++; - break; - default: - // Don't count other track types. - break; - } - } - this.videoRendererCount = videoRendererCount; - this.audioRendererCount = audioRendererCount; - // Set initial values. audioVolume = 1; audioSessionId = C.AUDIO_SESSION_ID_UNSET; @@ -163,15 +142,15 @@ public class SimpleExoPlayer implements ExoPlayer { */ public void setVideoScalingMode(@C.VideoScalingMode int videoScalingMode) { this.videoScalingMode = videoScalingMode; - ExoPlayerMessage[] messages = new ExoPlayerMessage[videoRendererCount]; - int count = 0; for (Renderer renderer : renderers) { if (renderer.getTrackType() == C.TRACK_TYPE_VIDEO) { - messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_SCALING_MODE, - videoScalingMode); + player + .createMessage(renderer) + .setType(C.MSG_SET_SCALING_MODE) + .setMessage(videoScalingMode) + .send(); } } - player.sendMessages(messages); } /** @@ -352,15 +331,15 @@ public class SimpleExoPlayer implements ExoPlayer { */ public void setAudioAttributes(AudioAttributes audioAttributes) { this.audioAttributes = audioAttributes; - 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_AUDIO_ATTRIBUTES, - audioAttributes); + player + .createMessage(renderer) + .setType(C.MSG_SET_AUDIO_ATTRIBUTES) + .setMessage(audioAttributes) + .send(); } } - player.sendMessages(messages); } /** @@ -377,14 +356,11 @@ public class SimpleExoPlayer implements ExoPlayer { */ public void setVolume(float audioVolume) { this.audioVolume = audioVolume; - 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_VOLUME, audioVolume); + player.createMessage(renderer).setType(C.MSG_SET_VOLUME).setMessage(audioVolume).send(); } } - player.sendMessages(messages); } /** @@ -770,6 +746,11 @@ public class SimpleExoPlayer implements ExoPlayer { player.sendMessages(messages); } + @Override + public PlayerMessage createMessage(PlayerMessage.Target target) { + return player.createMessage(target); + } + @Override public void blockingSendMessages(ExoPlayerMessage... messages) { player.blockingSendMessages(messages); @@ -908,22 +889,25 @@ public class SimpleExoPlayer implements ExoPlayer { private void setVideoSurfaceInternal(Surface surface, boolean ownsSurface) { // Note: We don't turn this method into a no-op if the surface is being replaced with itself // so as to ensure onRenderedFirstFrame callbacks are still called in this case. - ExoPlayerMessage[] messages = new ExoPlayerMessage[videoRendererCount]; - int count = 0; + boolean surfaceReplaced = this.surface != null && this.surface != surface; for (Renderer renderer : renderers) { if (renderer.getTrackType() == C.TRACK_TYPE_VIDEO) { - messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_SURFACE, surface); + PlayerMessage message = + player.createMessage(renderer).setType(C.MSG_SET_SURFACE).setMessage(surface).send(); + if (surfaceReplaced) { + try { + message.blockUntilDelivered(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } } } - if (this.surface != null && this.surface != surface) { - // We're replacing a surface. Block to ensure that it's not accessed after the method returns. - player.blockingSendMessages(messages); + if (surfaceReplaced) { // If we created the previous surface, we are responsible for releasing it. if (this.ownsSurface) { this.surface.release(); } - } else { - player.sendMessages(messages); } this.surface = surface; this.ownsSurface = ownsSurface; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java index c410456e7b..54537ba548 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java @@ -23,8 +23,7 @@ import android.util.SparseIntArray; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; -import com.google.android.exoplayer2.ExoPlayer.ExoPlayerComponent; -import com.google.android.exoplayer2.ExoPlayer.ExoPlayerMessage; +import com.google.android.exoplayer2.PlayerMessage; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder; import com.google.android.exoplayer2.upstream.Allocator; @@ -42,7 +41,7 @@ import java.util.Map; * Concatenates multiple {@link MediaSource}s. The list of {@link MediaSource}s can be modified * during playback. Access to this class is thread-safe. */ -public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPlayerComponent { +public final class DynamicConcatenatingMediaSource implements MediaSource, PlayerMessage.Target { private static final int MSG_ADD = 0; private static final int MSG_ADD_MULTIPLE = 1; @@ -147,8 +146,11 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl Assertions.checkArgument(!mediaSourcesPublic.contains(mediaSource)); mediaSourcesPublic.add(index, mediaSource); if (player != null) { - player.sendMessages(new ExoPlayerMessage(this, MSG_ADD, - new MessageData<>(index, mediaSource, actionOnCompletion))); + player + .createMessage(this) + .setType(MSG_ADD) + .setMessage(new MessageData<>(index, mediaSource, actionOnCompletion)) + .send(); } else if (actionOnCompletion != null) { actionOnCompletion.run(); } @@ -220,8 +222,11 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl } mediaSourcesPublic.addAll(index, mediaSources); if (player != null && !mediaSources.isEmpty()) { - player.sendMessages(new ExoPlayerMessage(this, MSG_ADD_MULTIPLE, - new MessageData<>(index, mediaSources, actionOnCompletion))); + player + .createMessage(this) + .setType(MSG_ADD_MULTIPLE) + .setMessage(new MessageData<>(index, mediaSources, actionOnCompletion)) + .send(); } else if (actionOnCompletion != null){ actionOnCompletion.run(); } @@ -256,8 +261,11 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl public synchronized void removeMediaSource(int index, @Nullable Runnable actionOnCompletion) { mediaSourcesPublic.remove(index); if (player != null) { - player.sendMessages(new ExoPlayerMessage(this, MSG_REMOVE, - new MessageData<>(index, null, actionOnCompletion))); + player + .createMessage(this) + .setType(MSG_REMOVE) + .setMessage(new MessageData<>(index, null, actionOnCompletion)) + .send(); } else if (actionOnCompletion != null) { actionOnCompletion.run(); } @@ -293,8 +301,11 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl } mediaSourcesPublic.add(newIndex, mediaSourcesPublic.remove(currentIndex)); if (player != null) { - player.sendMessages(new ExoPlayerMessage(this, MSG_MOVE, - new MessageData<>(currentIndex, newIndex, actionOnCompletion))); + player + .createMessage(this) + .setType(MSG_MOVE) + .setMessage(new MessageData<>(currentIndex, newIndex, actionOnCompletion)) + .send(); } else if (actionOnCompletion != null) { actionOnCompletion.run(); } @@ -427,8 +438,7 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl new ConcatenatedTimeline(mediaSourceHolders, windowCount, periodCount, shuffleOrder), null); if (actionOnCompletion != null) { - player.sendMessages( - new ExoPlayerMessage(this, MSG_ON_COMPLETION, actionOnCompletion)); + player.createMessage(this).setType(MSG_ON_COMPLETION).setMessage(actionOnCompletion).send(); } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java index d796e6936f..a5f5222820 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -561,6 +561,18 @@ public final class Util { return stayInBounds ? Math.min(list.size() - 1, index) : index; } + /** + * Compares two long values and returns the same value as {@code Long.compare(long, long)}. + * + * @param left The left operand. + * @param right The right operand. + * @return 0, if left == right, a negative value if left < right, or a positive value if left + * > right. + */ + public static int compareLong(long left, long right) { + return left < right ? -1 : left == right ? 0 : 1; + } + /** * Parses an xs:duration attribute value, returning the parsed duration in milliseconds. * diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java index ff0b8a6bc0..5ec45af29f 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java @@ -18,13 +18,17 @@ package com.google.android.exoplayer2.testutil; import android.os.Handler; import android.util.Log; import android.view.Surface; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.PlayerMessage; +import com.google.android.exoplayer2.PlayerMessage.Target; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.testutil.ActionSchedule.ActionNode; +import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerTarget; import com.google.android.exoplayer2.trackselection.MappingTrackSelector; /** @@ -345,7 +349,63 @@ public abstract class Action { Surface surface) { player.setShuffleModeEnabled(shuffleModeEnabled); } + } + /** Calls {@link ExoPlayer#createMessage(Target)} and {@link PlayerMessage#send()}. */ + public static final class SendMessages extends Action { + + private final Target target; + private final int windowIndex; + private final long positionMs; + private final boolean deleteAfterDelivery; + + /** + * @param tag A tag to use for logging. + * @param target A message target. + * @param positionMs The position at which the message should be sent, in milliseconds. + */ + public SendMessages(String tag, Target target, long positionMs) { + this( + tag, + target, + /* windowIndex= */ C.INDEX_UNSET, + positionMs, + /* deleteAfterDelivery= */ true); + } + + /** + * @param tag A tag to use for logging. + * @param target A message target. + * @param windowIndex The window index at which the message should be sent, or {@link + * C#INDEX_UNSET} for the current window. + * @param positionMs The position at which the message should be sent, in milliseconds. + * @param deleteAfterDelivery Whether the message will be deleted after delivery. + */ + public SendMessages( + String tag, Target target, int windowIndex, long positionMs, boolean deleteAfterDelivery) { + super(tag, "SendMessages"); + this.target = target; + this.windowIndex = windowIndex; + this.positionMs = positionMs; + this.deleteAfterDelivery = deleteAfterDelivery; + } + + @Override + protected void doActionImpl( + final SimpleExoPlayer player, MappingTrackSelector trackSelector, Surface surface) { + if (target instanceof PlayerTarget) { + ((PlayerTarget) target).setPlayer(player); + } + PlayerMessage message = player.createMessage(target); + if (windowIndex != C.INDEX_UNSET) { + message.setPosition(windowIndex, positionMs); + } else { + message.setPosition(positionMs); + } + message.setHandler(new Handler()); + message.setDeleteAfterDelivery(deleteAfterDelivery); + message.send(); + } } /** diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java index 477071f91f..2ac487c98e 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java @@ -20,8 +20,11 @@ import android.os.Looper; import android.support.annotation.Nullable; import android.view.Surface; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.PlayerMessage; +import com.google.android.exoplayer2.PlayerMessage.Target; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaSource; @@ -29,6 +32,7 @@ import com.google.android.exoplayer2.testutil.Action.ClearVideoSurface; import com.google.android.exoplayer2.testutil.Action.ExecuteRunnable; import com.google.android.exoplayer2.testutil.Action.PrepareSource; import com.google.android.exoplayer2.testutil.Action.Seek; +import com.google.android.exoplayer2.testutil.Action.SendMessages; import com.google.android.exoplayer2.testutil.Action.SetPlayWhenReady; import com.google.android.exoplayer2.testutil.Action.SetPlaybackParameters; import com.google.android.exoplayer2.testutil.Action.SetRendererDisabled; @@ -315,6 +319,44 @@ public final class ActionSchedule { return apply(new SetShuffleModeEnabled(tag, shuffleModeEnabled)); } + /** + * Schedules sending a {@link PlayerMessage}. + * + * @param positionMs The position in the current window at which the message should be sent, in + * milliseconds. + * @return The builder, for convenience. + */ + public Builder sendMessage(Target target, long positionMs) { + return apply(new SendMessages(tag, target, positionMs)); + } + + /** + * Schedules sending a {@link PlayerMessage}. + * + * @param target A message target. + * @param windowIndex The window index at which the message should be sent. + * @param positionMs The position at which the message should be sent, in milliseconds. + * @return The builder, for convenience. + */ + public Builder sendMessage(Target target, int windowIndex, long positionMs) { + return apply( + new SendMessages(tag, target, windowIndex, positionMs, /* deleteAfterDelivery= */ true)); + } + + /** + * Schedules to send a {@link PlayerMessage}. + * + * @param target A message target. + * @param windowIndex The window index at which the message should be sent. + * @param positionMs The position at which the message should be sent, in milliseconds. + * @param deleteAfterDelivery Whether the message will be deleted after delivery. + * @return The builder, for convenience. + */ + public Builder sendMessage( + Target target, int windowIndex, long positionMs, boolean deleteAfterDelivery) { + return apply(new SendMessages(tag, target, windowIndex, positionMs, deleteAfterDelivery)); + } + /** * Schedules a delay until the timeline changed to a specified expected timeline. * @@ -365,7 +407,28 @@ public final class ActionSchedule { currentDelayMs = 0; return this; } + } + /** + * Provides a wrapper for a {@link Target} which has access to the player when handling messages. + * Can be used with {@link Builder#sendMessage(Target, long)}. + */ + public abstract static class PlayerTarget implements Target { + + private SimpleExoPlayer player; + + /** Handles the message send to the component and additionally provides access to the player. */ + public abstract void handleMessage(SimpleExoPlayer player, int messageType, Object message); + + /** Sets the player to be passed to {@link #handleMessage(SimpleExoPlayer, int, Object)}. */ + /* package */ void setPlayer(SimpleExoPlayer player) { + this.player = player; + } + + @Override + public final void handleMessage(int messageType, Object message) throws ExoPlaybackException { + handleMessage(player, messageType, message); + } } /** diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java index 4a9d79f906..797c09d6b6 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.testutil; +import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.util.Util; @@ -170,7 +171,7 @@ public final class FakeTimeline extends Timeline { int windowPeriodIndex = periodIndex - periodOffsets[windowIndex]; TimelineWindowDefinition windowDefinition = windowDefinitions[windowIndex]; Object id = setIds ? windowPeriodIndex : null; - Object uid = setIds ? periodIndex : null; + Object uid = setIds ? Pair.create(windowDefinition.id, windowPeriodIndex) : null; long periodDurationUs = windowDefinition.durationUs / windowDefinition.periodCount; long positionInWindowUs = periodDurationUs * windowPeriodIndex; if (windowDefinition.adGroupsPerPeriodCount == 0) { @@ -198,11 +199,13 @@ public final class FakeTimeline extends Timeline { @Override public int getIndexOfPeriod(Object uid) { - if (!(uid instanceof Integer)) { - return C.INDEX_UNSET; + Period period = new Period(); + for (int i = 0; i < getPeriodCount(); i++) { + if (getPeriod(i, period, true).uid.equals(uid)) { + return i; + } } - int index = (Integer) uid; - return index >= 0 && index < getPeriodCount() ? index : C.INDEX_UNSET; + return C.INDEX_UNSET; } private static TimelineWindowDefinition[] createDefaultWindowDefinitions(int windowCount) { diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java index 4f31a8b027..93c14afc8f 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java @@ -24,7 +24,9 @@ import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Message; +import android.util.Pair; import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.PlayerMessage; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; @@ -281,7 +283,8 @@ public class MediaSourceTestRunner { } - private static class EventHandlingExoPlayer extends StubExoPlayer implements Handler.Callback { + private static class EventHandlingExoPlayer extends StubExoPlayer + implements Handler.Callback, PlayerMessage.Sender { private final Handler handler; @@ -290,23 +293,33 @@ public class MediaSourceTestRunner { } @Override - public void sendMessages(ExoPlayerMessage... messages) { - handler.obtainMessage(0, messages).sendToTarget(); + public PlayerMessage createMessage(PlayerMessage.Target target) { + return new PlayerMessage( + /* sender= */ this, target, Timeline.EMPTY, /* defaultWindowIndex= */ 0, handler); } @Override + public void sendMessage(PlayerMessage message, Listener listener) { + handler.obtainMessage(0, Pair.create(message, listener)).sendToTarget(); + } + + @Override + @SuppressWarnings("unchecked") public boolean handleMessage(Message msg) { - ExoPlayerMessage[] messages = (ExoPlayerMessage[]) msg.obj; - for (ExoPlayerMessage message : messages) { - try { - message.target.handleMessage(message.messageType, message.message); - } catch (ExoPlaybackException e) { - fail("Unexpected ExoPlaybackException."); - } + Pair messageAndListener = (Pair) msg.obj; + try { + messageAndListener + .first + .getTarget() + .handleMessage( + messageAndListener.first.getType(), messageAndListener.first.getMessage()); + messageAndListener.second.onMessageDelivered(); + messageAndListener.second.onMessageDeleted(); + } catch (ExoPlaybackException e) { + fail("Unexpected ExoPlaybackException."); } return true; } - } } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java index 1ea83bf1ec..7164fa13ab 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java @@ -19,6 +19,7 @@ import android.os.Looper; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.PlayerMessage; import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaSource; @@ -146,6 +147,11 @@ public abstract class StubExoPlayer implements ExoPlayer { throw new UnsupportedOperationException(); } + @Override + public PlayerMessage createMessage(PlayerMessage.Target target) { + throw new UnsupportedOperationException(); + } + @Override public void sendMessages(ExoPlayerMessage... messages) { throw new UnsupportedOperationException(); From 3b633f81b2e84f91d02dd68c8f1e0656ff757a6a Mon Sep 17 00:00:00 2001 From: tiffanywong Date: Wed, 20 Dec 2017 03:12:51 -0800 Subject: [PATCH 0980/2472] Automated g4 rollback of changelist 179563355. *** Original change description *** Add possiblity to send messages at playback position. This adds options to ExoPlayer.sendMessages which allow to specify a window index and position at which the message should be sent. Additionally, the options can be configured to use a custom Handler for the messages and whether the message should be repeated when playback reaches the same position again. The internal player converts these window positions to period index and position at the earliest possibility. The internal player also at... *** ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179666357 --- RELEASENOTES.md | 4 - .../exoplayer2/ext/vp9/VpxPlaybackTest.java | 8 +- .../android/exoplayer2/ExoPlayerTest.java | 403 ------------------ .../android/exoplayer2/BaseRenderer.java | 2 +- .../google/android/exoplayer2/ExoPlayer.java | 164 +++---- .../android/exoplayer2/ExoPlayerImpl.java | 44 +- .../exoplayer2/ExoPlayerImplInternal.java | 283 +++--------- .../android/exoplayer2/NoSampleRenderer.java | 2 +- .../android/exoplayer2/PlayerMessage.java | 295 ------------- .../google/android/exoplayer2/Renderer.java | 12 +- .../android/exoplayer2/SimpleExoPlayer.java | 70 +-- .../DynamicConcatenatingMediaSource.java | 36 +- .../google/android/exoplayer2/util/Util.java | 12 - .../android/exoplayer2/testutil/Action.java | 60 --- .../exoplayer2/testutil/ActionSchedule.java | 63 --- .../exoplayer2/testutil/FakeTimeline.java | 13 +- .../testutil/MediaSourceTestRunner.java | 35 +- .../exoplayer2/testutil/StubExoPlayer.java | 6 - 18 files changed, 232 insertions(+), 1280 deletions(-) delete mode 100644 library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 3c45c3449a..c7f7ed7bbd 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -6,10 +6,6 @@ * Add optional parameter to `stop` to reset the player when stopping. * Add a reason to `EventListener.onTimelineChanged` to distinguish between initial preparation, reset and dynamic updates. - * Replaced `ExoPlayer.sendMessages` with `ExoPlayer.createMessage` to allow - more customization of the message. Now supports setting a message delivery - playback position and/or a delivery handler. - ([#2189](https://github.com/google/ExoPlayer/issues/2189)). * Buffering: * Allow a back-buffer of media to be retained behind the current playback position, for fast backward seeking. The back-buffer can be configured by 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 0f8df65959..0a902e2efe 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 @@ -119,11 +119,9 @@ public class VpxPlaybackTest extends InstrumentationTestCase { new DefaultDataSourceFactory(context, "ExoPlayerExtVp9Test")) .setExtractorsFactory(MatroskaExtractor.FACTORY) .createMediaSource(uri); - player - .createMessage(videoRenderer) - .setType(LibvpxVideoRenderer.MSG_SET_OUTPUT_BUFFER_RENDERER) - .setMessage(new VpxVideoSurfaceView(context)) - .send(); + player.sendMessages(new ExoPlayer.ExoPlayerMessage(videoRenderer, + LibvpxVideoRenderer.MSG_SET_OUTPUT_BUFFER_RENDERER, + new VpxVideoSurfaceView(context))); player.prepare(mediaSource); player.setPlayWhenReady(true); Looper.loop(); diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index 9d8e2dcd9d..40b4b2d383 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -17,13 +17,11 @@ package com.google.android.exoplayer2; import com.google.android.exoplayer2.Player.DefaultEventListener; import com.google.android.exoplayer2.Player.EventListener; -import com.google.android.exoplayer2.Timeline.Window; import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.testutil.ActionSchedule; -import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerTarget; import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner; import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner.Builder; import com.google.android.exoplayer2.testutil.FakeMediaClockRenderer; @@ -944,405 +942,4 @@ public final class ExoPlayerTest extends TestCase { testRunner.assertTimelinesEqual(timeline); testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); } - - public void testSendMessagesDuringPreparation() throws Exception { - Timeline timeline = new FakeTimeline(/* windowCount= */ 1); - PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); - ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSendMessages") - .waitForPlaybackState(Player.STATE_BUFFERING) - .sendMessage(target, /* positionMs= */ 50) - .build(); - new ExoPlayerTestRunner.Builder() - .setTimeline(timeline) - .setActionSchedule(actionSchedule) - .build() - .start() - .blockUntilEnded(TIMEOUT_MS); - assertTrue(target.positionMs >= 50); - } - - public void testSendMessagesAfterPreparation() throws Exception { - Timeline timeline = new FakeTimeline(/* windowCount= */ 1); - PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); - ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSendMessages") - .waitForTimelineChanged(timeline) - .sendMessage(target, /* positionMs= */ 50) - .build(); - new ExoPlayerTestRunner.Builder() - .setTimeline(timeline) - .setActionSchedule(actionSchedule) - .build() - .start() - .blockUntilEnded(TIMEOUT_MS); - assertTrue(target.positionMs >= 50); - } - - public void testMultipleSendMessages() throws Exception { - Timeline timeline = new FakeTimeline(/* windowCount= */ 1); - PositionGrabbingMessageTarget target50 = new PositionGrabbingMessageTarget(); - PositionGrabbingMessageTarget target80 = new PositionGrabbingMessageTarget(); - ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSendMessages") - .waitForPlaybackState(Player.STATE_BUFFERING) - .sendMessage(target80, /* positionMs= */ 80) - .sendMessage(target50, /* positionMs= */ 50) - .build(); - new ExoPlayerTestRunner.Builder() - .setTimeline(timeline) - .setActionSchedule(actionSchedule) - .build() - .start() - .blockUntilEnded(TIMEOUT_MS); - assertTrue(target50.positionMs >= 50); - assertTrue(target80.positionMs >= 80); - assertTrue(target80.positionMs >= target50.positionMs); - } - - public void testMultipleSendMessagesAtSameTime() throws Exception { - Timeline timeline = new FakeTimeline(/* windowCount= */ 1); - PositionGrabbingMessageTarget target1 = new PositionGrabbingMessageTarget(); - PositionGrabbingMessageTarget target2 = new PositionGrabbingMessageTarget(); - ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSendMessages") - .waitForPlaybackState(Player.STATE_BUFFERING) - .sendMessage(target1, /* positionMs= */ 50) - .sendMessage(target2, /* positionMs= */ 50) - .build(); - new ExoPlayerTestRunner.Builder() - .setTimeline(timeline) - .setActionSchedule(actionSchedule) - .build() - .start() - .blockUntilEnded(TIMEOUT_MS); - assertTrue(target1.positionMs >= 50); - assertTrue(target2.positionMs >= 50); - } - - public void testSendMessagesMultiPeriodResolution() throws Exception { - Timeline timeline = - new FakeTimeline(new TimelineWindowDefinition(/* periodCount= */ 10, /* id= */ 0)); - PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); - ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSendMessages") - .waitForPlaybackState(Player.STATE_BUFFERING) - .sendMessage(target, /* positionMs= */ 50) - .build(); - new ExoPlayerTestRunner.Builder() - .setTimeline(timeline) - .setActionSchedule(actionSchedule) - .build() - .start() - .blockUntilEnded(TIMEOUT_MS); - assertTrue(target.positionMs >= 50); - } - - public void testSendMessagesAtStartAndEndOfPeriod() throws Exception { - Timeline timeline = new FakeTimeline(/* windowCount= */ 2); - PositionGrabbingMessageTarget targetStartFirstPeriod = new PositionGrabbingMessageTarget(); - PositionGrabbingMessageTarget targetEndMiddlePeriod = new PositionGrabbingMessageTarget(); - PositionGrabbingMessageTarget targetStartMiddlePeriod = new PositionGrabbingMessageTarget(); - PositionGrabbingMessageTarget targetEndLastPeriod = new PositionGrabbingMessageTarget(); - long duration1Ms = timeline.getWindow(0, new Window()).getDurationMs(); - long duration2Ms = timeline.getWindow(1, new Window()).getDurationMs(); - ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSendMessages") - .waitForPlaybackState(Player.STATE_BUFFERING) - .sendMessage(targetStartFirstPeriod, /* windowIndex= */ 0, /* positionMs= */ 0) - .sendMessage(targetEndMiddlePeriod, /* windowIndex= */ 0, /* positionMs= */ duration1Ms) - .sendMessage(targetStartMiddlePeriod, /* windowIndex= */ 1, /* positionMs= */ 0) - .sendMessage(targetEndLastPeriod, /* windowIndex= */ 1, /* positionMs= */ duration2Ms) - // Add additional prepare at end and wait until it's processed to ensure that - // messages sent at end of playback are received before test ends. - .waitForPlaybackState(Player.STATE_ENDED) - .prepareSource( - new FakeMediaSource(timeline, null), - /* resetPosition= */ false, - /* resetState= */ true) - .waitForPlaybackState(Player.STATE_READY) - .build(); - new ExoPlayerTestRunner.Builder() - .setTimeline(timeline) - .setActionSchedule(actionSchedule) - .build() - .start() - .blockUntilActionScheduleFinished(TIMEOUT_MS) - .blockUntilEnded(TIMEOUT_MS); - assertEquals(0, targetStartFirstPeriod.windowIndex); - assertTrue(targetStartFirstPeriod.positionMs >= 0); - assertEquals(0, targetEndMiddlePeriod.windowIndex); - assertTrue(targetEndMiddlePeriod.positionMs >= duration1Ms); - assertEquals(1, targetStartMiddlePeriod.windowIndex); - assertTrue(targetStartMiddlePeriod.positionMs >= 0); - assertEquals(1, targetEndLastPeriod.windowIndex); - assertTrue(targetEndLastPeriod.positionMs >= duration2Ms); - } - - public void testSendMessagesSeekOnDeliveryTimeDuringPreparation() throws Exception { - Timeline timeline = new FakeTimeline(/* windowCount= */ 1); - PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); - ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSendMessages") - .waitForPlaybackState(Player.STATE_BUFFERING) - .sendMessage(target, /* positionMs= */ 50) - .seek(/* positionMs= */ 50) - .build(); - new ExoPlayerTestRunner.Builder() - .setTimeline(timeline) - .setActionSchedule(actionSchedule) - .build() - .start() - .blockUntilEnded(TIMEOUT_MS); - assertTrue(target.positionMs >= 50); - } - - public void testSendMessagesSeekOnDeliveryTimeAfterPreparation() throws Exception { - Timeline timeline = new FakeTimeline(/* windowCount= */ 1); - PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); - ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSendMessages") - .waitForPlaybackState(Player.STATE_BUFFERING) - .sendMessage(target, /* positionMs= */ 50) - .waitForTimelineChanged(timeline) - .seek(/* positionMs= */ 50) - .build(); - new ExoPlayerTestRunner.Builder() - .setTimeline(timeline) - .setActionSchedule(actionSchedule) - .build() - .start() - .blockUntilEnded(TIMEOUT_MS); - assertTrue(target.positionMs >= 50); - } - - public void testSendMessagesSeekAfterDeliveryTimeDuringPreparation() throws Exception { - Timeline timeline = new FakeTimeline(/* windowCount= */ 1); - PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); - ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSendMessages") - .waitForPlaybackState(Player.STATE_BUFFERING) - .sendMessage(target, /* positionMs= */ 50) - .seek(/* positionMs= */ 51) - .build(); - new ExoPlayerTestRunner.Builder() - .setTimeline(timeline) - .setActionSchedule(actionSchedule) - .build() - .start() - .blockUntilEnded(TIMEOUT_MS); - assertEquals(C.POSITION_UNSET, target.positionMs); - } - - public void testSendMessagesSeekAfterDeliveryTimeAfterPreparation() throws Exception { - Timeline timeline = new FakeTimeline(/* windowCount= */ 1); - PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); - ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSendMessages") - .sendMessage(target, /* positionMs= */ 50) - .waitForTimelineChanged(timeline) - .seek(/* positionMs= */ 51) - .build(); - new ExoPlayerTestRunner.Builder() - .setTimeline(timeline) - .setActionSchedule(actionSchedule) - .build() - .start() - .blockUntilEnded(TIMEOUT_MS); - assertEquals(C.POSITION_UNSET, target.positionMs); - } - - public void testSendMessagesRepeatDoesNotRepost() throws Exception { - Timeline timeline = new FakeTimeline(/* windowCount= */ 1); - PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); - ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSendMessages") - .waitForPlaybackState(Player.STATE_BUFFERING) - .sendMessage(target, /* positionMs= */ 50) - .setRepeatMode(Player.REPEAT_MODE_ALL) - .waitForPositionDiscontinuity() - .setRepeatMode(Player.REPEAT_MODE_OFF) - .build(); - new ExoPlayerTestRunner.Builder() - .setTimeline(timeline) - .setActionSchedule(actionSchedule) - .build() - .start() - .blockUntilEnded(TIMEOUT_MS); - assertEquals(1, target.messageCount); - assertTrue(target.positionMs >= 50); - } - - public void testSendMessagesRepeatWithoutDeletingDoesRepost() throws Exception { - Timeline timeline = new FakeTimeline(/* windowCount= */ 1); - PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); - ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSendMessages") - .waitForPlaybackState(Player.STATE_BUFFERING) - .sendMessage( - target, - /* windowIndex= */ 0, - /* positionMs= */ 50, - /* deleteAfterDelivery= */ false) - .setRepeatMode(Player.REPEAT_MODE_ALL) - .waitForPositionDiscontinuity() - .setRepeatMode(Player.REPEAT_MODE_OFF) - .build(); - new ExoPlayerTestRunner.Builder() - .setTimeline(timeline) - .setActionSchedule(actionSchedule) - .build() - .start() - .blockUntilEnded(TIMEOUT_MS); - assertEquals(2, target.messageCount); - assertTrue(target.positionMs >= 50); - } - - public void testSendMessagesMoveCurrentWindowIndex() throws Exception { - Timeline timeline = - new FakeTimeline(new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 0)); - final Timeline secondTimeline = - new FakeTimeline( - new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 1), - new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 0)); - final FakeMediaSource mediaSource = new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT); - PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); - ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSendMessages") - .waitForTimelineChanged(timeline) - .sendMessage(target, /* positionMs= */ 50) - .executeRunnable( - new Runnable() { - @Override - public void run() { - mediaSource.setNewSourceInfo(secondTimeline, null); - } - }) - .build(); - new ExoPlayerTestRunner.Builder() - .setMediaSource(mediaSource) - .setActionSchedule(actionSchedule) - .build() - .start() - .blockUntilEnded(TIMEOUT_MS); - assertTrue(target.positionMs >= 50); - assertEquals(1, target.windowIndex); - } - - public void testSendMessagesMultiWindowDuringPreparation() throws Exception { - Timeline timeline = new FakeTimeline(/* windowCount= */ 3); - PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); - ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSendMessages") - .waitForPlaybackState(Player.STATE_BUFFERING) - .sendMessage(target, /* windowIndex = */ 2, /* positionMs= */ 50) - .build(); - new ExoPlayerTestRunner.Builder() - .setTimeline(timeline) - .setActionSchedule(actionSchedule) - .build() - .start() - .blockUntilEnded(TIMEOUT_MS); - assertEquals(2, target.windowIndex); - assertTrue(target.positionMs >= 50); - } - - public void testSendMessagesMultiWindowAfterPreparation() throws Exception { - Timeline timeline = new FakeTimeline(/* windowCount= */ 3); - PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); - ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSendMessages") - .waitForTimelineChanged(timeline) - .sendMessage(target, /* windowIndex = */ 2, /* positionMs= */ 50) - .build(); - new ExoPlayerTestRunner.Builder() - .setTimeline(timeline) - .setActionSchedule(actionSchedule) - .build() - .start() - .blockUntilEnded(TIMEOUT_MS); - assertEquals(2, target.windowIndex); - assertTrue(target.positionMs >= 50); - } - - public void testSendMessagesMoveWindowIndex() throws Exception { - Timeline timeline = - new FakeTimeline( - new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 0), - new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 1)); - final Timeline secondTimeline = - new FakeTimeline( - new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 1), - new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 0)); - final FakeMediaSource mediaSource = new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT); - PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); - ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSendMessages") - .waitForTimelineChanged(timeline) - .sendMessage(target, /* windowIndex = */ 1, /* positionMs= */ 50) - .executeRunnable( - new Runnable() { - @Override - public void run() { - mediaSource.setNewSourceInfo(secondTimeline, null); - } - }) - .waitForTimelineChanged(secondTimeline) - .seek(/* windowIndex= */ 0, /* positionMs= */ 0) - .build(); - new ExoPlayerTestRunner.Builder() - .setMediaSource(mediaSource) - .setActionSchedule(actionSchedule) - .build() - .start() - .blockUntilEnded(TIMEOUT_MS); - assertTrue(target.positionMs >= 50); - assertEquals(0, target.windowIndex); - } - - public void testSendMessagesNonLinearPeriodOrder() throws Exception { - Timeline timeline = new FakeTimeline(/* windowCount= */ 3); - PositionGrabbingMessageTarget target1 = new PositionGrabbingMessageTarget(); - PositionGrabbingMessageTarget target2 = new PositionGrabbingMessageTarget(); - PositionGrabbingMessageTarget target3 = new PositionGrabbingMessageTarget(); - ActionSchedule actionSchedule = - new ActionSchedule.Builder("testSendMessages") - .waitForPlaybackState(Player.STATE_BUFFERING) - .sendMessage(target1, /* windowIndex = */ 0, /* positionMs= */ 50) - .sendMessage(target2, /* windowIndex = */ 1, /* positionMs= */ 50) - .sendMessage(target3, /* windowIndex = */ 2, /* positionMs= */ 50) - .waitForTimelineChanged(timeline) - .seek(/* windowIndex= */ 1, /* positionMs= */ 0) - .waitForPositionDiscontinuity() - .seek(/* windowIndex= */ 0, /* positionMs= */ 0) - .build(); - new ExoPlayerTestRunner.Builder() - .setTimeline(timeline) - .setActionSchedule(actionSchedule) - .build() - .start() - .blockUntilEnded(TIMEOUT_MS); - assertEquals(0, target1.windowIndex); - assertEquals(1, target2.windowIndex); - assertEquals(2, target3.windowIndex); - } - - private static final class PositionGrabbingMessageTarget extends PlayerTarget { - - public int windowIndex; - public long positionMs; - public int messageCount; - - public PositionGrabbingMessageTarget() { - windowIndex = C.INDEX_UNSET; - positionMs = C.POSITION_UNSET; - } - - @Override - public void handleMessage(SimpleExoPlayer player, int messageType, Object message) { - windowIndex = player.getCurrentWindowIndex(); - positionMs = player.getCurrentPosition(); - messageCount++; - } - } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java index 8ee9a13c55..a4103787d1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java @@ -157,7 +157,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { return ADAPTIVE_NOT_SUPPORTED; } - // PlayerMessage.Target implementation. + // ExoPlayerComponent implementation. @Override public void handleMessage(int what, Object object) throws ExoPlaybackException { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java index 4bd28150bc..cc767752be 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java @@ -34,43 +34,40 @@ import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; /** - * An extensible media player that plays {@link MediaSource}s. Instances can be obtained from {@link - * ExoPlayerFactory}. + * An extensible media player that plays {@link MediaSource}s. Instances can be obtained from + * {@link ExoPlayerFactory}. * *

          Player components

          - * *

          ExoPlayer is designed to make few assumptions about (and hence impose few restrictions on) the * type of the media being played, how and where it is stored, and how it is rendered. Rather than * implementing the loading and rendering of media directly, ExoPlayer implementations delegate this * work to components that are injected when a player is created or when it's prepared for playback. * Components common to all ExoPlayer implementations are: - * *

            *
          • 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(MediaSource)} at the start of playback. The library modules provide default - * implementations for regular media files ({@link ExtractorMediaSource}), DASH - * (DashMediaSource), SmoothStreaming (SsMediaSource) and HLS (HlsMediaSource), an - * implementation for loading single media samples ({@link SingleSampleMediaSource}) that's - * most often used for side-loaded subtitle files, and implementations for building more - * complex MediaSources from simpler ones ({@link MergingMediaSource}, {@link - * ConcatenatingMediaSource}, {@link DynamicConcatenatingMediaSource}, {@link - * LoopingMediaSource} and {@link ClippingMediaSource}). + * which the loaded media can be read. A MediaSource is injected via {@link #prepare(MediaSource)} + * at the start of playback. The library modules provide default implementations for regular media + * files ({@link ExtractorMediaSource}), DASH (DashMediaSource), SmoothStreaming (SsMediaSource) + * and HLS (HlsMediaSource), an implementation for loading single media samples + * ({@link SingleSampleMediaSource}) that's most often used for side-loaded subtitle files, and + * implementations for building more complex MediaSources from simpler ones + * ({@link MergingMediaSource}, {@link ConcatenatingMediaSource}, + * {@link DynamicConcatenatingMediaSource}, {@link LoopingMediaSource} and + * {@link ClippingMediaSource}).
          • *
          • {@link Renderer}s that render individual components of the media. The library - * provides default implementations for common media types ({@link MediaCodecVideoRenderer}, - * {@link MediaCodecAudioRenderer}, {@link TextRenderer} and {@link MetadataRenderer}). A - * Renderer consumes media from the MediaSource being played. Renderers are injected when the - * player is created. + * provides default implementations for common media types ({@link MediaCodecVideoRenderer}, + * {@link MediaCodecAudioRenderer}, {@link TextRenderer} and {@link MetadataRenderer}). A Renderer + * consumes media from the MediaSource being played. Renderers are injected when the player is + * created.
          • *
          • A {@link TrackSelector} that selects tracks provided by the MediaSource to be - * consumed by each of the available Renderers. The library provides a default implementation - * ({@link DefaultTrackSelector}) suitable for most use cases. A TrackSelector is injected - * when the player is created. + * consumed by each of the available Renderers. The library provides a default implementation + * ({@link DefaultTrackSelector}) suitable for most use cases. A TrackSelector is injected when + * the player is created.
          • *
          • A {@link LoadControl} that controls when the MediaSource buffers more media, and how - * much media is buffered. The library provides a default implementation ({@link - * DefaultLoadControl}) suitable for most use cases. A LoadControl is injected when the player - * is created. + * much media is buffered. The library provides a default implementation + * ({@link DefaultLoadControl}) suitable for most use cases. A LoadControl is injected when the + * player is created.
          • *
          - * *

          An ExoPlayer can be built using the default components provided by the library, but may also * be built using custom implementations if non-standard behaviors are required. For example a * custom LoadControl could be injected to change the player's buffering strategy, or a custom @@ -84,32 +81,30 @@ import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; * it's possible to load data from a non-standard source, or through a different network stack. * *

          Threading model

          - * - *

          The figure below shows ExoPlayer's threading model. - * - *

          ExoPlayer's threading
- * model + *

          The figure below shows ExoPlayer's threading model.

          + *

          + * ExoPlayer's threading model + *

          * *
            - *
          • It is recommended that ExoPlayer instances are created and accessed from a single - * application thread. The application's main thread is ideal. Accessing an instance from - * multiple threads is discouraged, however if an application does wish to do this then it may - * do so provided that it ensures accesses are synchronized. - *
          • Registered listeners are called on the thread that created the ExoPlayer instance, unless - * the thread that created the ExoPlayer instance does not have a {@link Looper}. In that - * case, registered listeners will be called on the application's main thread. - *
          • An internal playback thread is responsible for playback. Injected player components such as - * Renderers, MediaSources, TrackSelectors and LoadControls are called by the player on this - * thread. - *
          • When the application performs an operation on the player, for example a seek, a message is - * delivered to the internal playback thread via a message queue. The internal playback thread - * consumes messages from the queue and performs the corresponding operations. Similarly, when - * a playback event occurs on the internal playback thread, a message is delivered to the - * application thread via a second message queue. The application thread consumes messages - * from the queue, updating the application visible state and calling corresponding listener - * methods. - *
          • Injected player components may use additional background threads. For example a MediaSource - * may use background threads to load data. These are implementation specific. + *
          • It is recommended that ExoPlayer instances are created and accessed from a single application + * thread. The application's main thread is ideal. Accessing an instance from multiple threads is + * discouraged, however if an application does wish to do this then it may do so provided that it + * ensures accesses are synchronized.
          • + *
          • Registered listeners are called on the thread that created the ExoPlayer instance, unless + * the thread that created the ExoPlayer instance does not have a {@link Looper}. In that case, + * registered listeners will be called on the application's main thread.
          • + *
          • An internal playback thread is responsible for playback. Injected player components such as + * Renderers, MediaSources, TrackSelectors and LoadControls are called by the player on this + * thread.
          • + *
          • When the application performs an operation on the player, for example a seek, a message is + * delivered to the internal playback thread via a message queue. The internal playback thread + * consumes messages from the queue and performs the corresponding operations. Similarly, when a + * playback event occurs on the internal playback thread, a message is delivered to the application + * thread via a second message queue. The application thread consumes messages from the queue, + * updating the application visible state and calling corresponding listener methods.
          • + *
          • Injected player components may use additional background threads. For example a MediaSource + * may use background threads to load data. These are implementation specific.
          • *
          */ public interface ExoPlayer extends Player { @@ -120,28 +115,54 @@ public interface ExoPlayer extends Player { @Deprecated interface EventListener extends Player.EventListener {} - /** @deprecated Use {@link PlayerMessage.Target} instead. */ - @Deprecated - interface ExoPlayerComponent extends PlayerMessage.Target {} + /** + * A component of an {@link ExoPlayer} that can receive messages on the playback thread. + *

          + * Messages can be delivered to a component via {@link #sendMessages} and + * {@link #blockingSendMessages}. + */ + interface ExoPlayerComponent { - /** @deprecated Use {@link PlayerMessage} instead. */ - @Deprecated + /** + * Handles a message delivered to the component. Called on the playback thread. + * + * @param messageType The message type. + * @param message The message. + * @throws ExoPlaybackException If an error occurred whilst handling the message. + */ + void handleMessage(int messageType, Object message) throws ExoPlaybackException; + + } + + /** + * Defines a message and a target {@link ExoPlayerComponent} to receive it. + */ final class ExoPlayerMessage { - /** The target to receive the message. */ - public final PlayerMessage.Target target; - /** The type of the message. */ + /** + * The target to receive the message. + */ + public final ExoPlayerComponent target; + /** + * The type of the message. + */ public final int messageType; - /** The message. */ + /** + * The message. + */ public final Object message; - /** @deprecated Use {@link ExoPlayer#createMessage(PlayerMessage.Target)} instead. */ - @Deprecated - public ExoPlayerMessage(PlayerMessage.Target target, int messageType, Object message) { + /** + * @param target The target of the message. + * @param messageType The message type. + * @param message The message. + */ + public ExoPlayerMessage(ExoPlayerComponent target, int messageType, Object message) { this.target = target; this.messageType = messageType; this.message = message; } + } /** @@ -215,25 +236,20 @@ public interface ExoPlayer extends Player { void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState); /** - * Creates a message that can be sent to a {@link PlayerMessage.Target}. By default, the message - * will be delivered immediately without blocking on the playback thread. The default {@link - * PlayerMessage#getType()} is 0 and the default {@link PlayerMessage#getMessage()} is null. If a - * position is specified with {@link PlayerMessage#setPosition(long)}, the message will be - * delivered at this position in the current window defined by {@link #getCurrentWindowIndex()}. - * Alternatively, the message can be sent at a specific window using {@link - * PlayerMessage#setPosition(int, long)}. + * Sends messages to their target components. The messages are delivered on the playback thread. + * If a component throws an {@link ExoPlaybackException} then it is propagated out of the player + * as an error. + * + * @param messages The messages to be sent. */ - PlayerMessage createMessage(PlayerMessage.Target target); - - /** @deprecated Use {@link #createMessage(PlayerMessage.Target)} instead. */ - @Deprecated void sendMessages(ExoPlayerMessage... messages); /** - * @deprecated Use {@link #createMessage(PlayerMessage.Target)} with {@link - * PlayerMessage#blockUntilDelivered()}. + * Variant of {@link #sendMessages(ExoPlayerMessage...)} that blocks until after the messages have + * been delivered. + * + * @param messages The messages to be sent. */ - @Deprecated void blockingSendMessages(ExoPlayerMessage... messages); /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index afb6428fa5..2869a7668e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -22,7 +22,6 @@ import android.os.Message; import android.support.annotation.Nullable; import android.util.Log; import android.util.Pair; -import com.google.android.exoplayer2.PlayerMessage.Target; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.source.TrackGroupArray; @@ -32,8 +31,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.Util; -import java.util.ArrayList; -import java.util.List; import java.util.concurrent.CopyOnWriteArraySet; /** @@ -48,7 +45,6 @@ import java.util.concurrent.CopyOnWriteArraySet; private final TrackSelectorResult emptyTrackSelectorResult; private final Handler eventHandler; private final ExoPlayerImplInternal internalPlayer; - private final Handler internalPlayerHandler; private final CopyOnWriteArraySet listeners; private final Timeline.Window window; private final Timeline.Period period; @@ -117,7 +113,6 @@ import java.util.concurrent.CopyOnWriteArraySet; shuffleModeEnabled, eventHandler, this); - internalPlayerHandler = new Handler(internalPlayer.getPlaybackLooper()); } @Override @@ -331,47 +326,12 @@ import java.util.concurrent.CopyOnWriteArraySet; @Override public void sendMessages(ExoPlayerMessage... messages) { - for (ExoPlayerMessage message : messages) { - createMessage(message.target).setType(message.messageType).setMessage(message.message).send(); - } - } - - @Override - public PlayerMessage createMessage(Target target) { - return new PlayerMessage( - internalPlayer, - target, - playbackInfo.timeline, - getCurrentWindowIndex(), - internalPlayerHandler); + internalPlayer.sendMessages(messages); } @Override public void blockingSendMessages(ExoPlayerMessage... messages) { - List playerMessages = new ArrayList<>(); - for (ExoPlayerMessage message : messages) { - playerMessages.add( - createMessage(message.target) - .setType(message.messageType) - .setMessage(message.message) - .send()); - } - boolean wasInterrupted = false; - for (PlayerMessage message : playerMessages) { - boolean blockMessage = true; - while (blockMessage) { - try { - message.blockUntilDelivered(); - blockMessage = false; - } catch (InterruptedException e) { - wasInterrupted = true; - } - } - } - if (wasInterrupted) { - // Restore the interrupted status. - Thread.currentThread().interrupt(); - } + internalPlayer.blockingSendMessages(messages); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index f3d0e1794b..09b3231467 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -22,10 +22,10 @@ import android.os.Message; import android.os.Process; import android.os.SystemClock; import android.support.annotation.NonNull; -import android.support.annotation.Nullable; import android.util.Log; import android.util.Pair; import com.google.android.exoplayer2.DefaultMediaClock.PlaybackParameterListener; +import com.google.android.exoplayer2.ExoPlayer.ExoPlayerMessage; import com.google.android.exoplayer2.MediaPeriodInfoSequence.MediaPeriodInfo; import com.google.android.exoplayer2.Player.DiscontinuityReason; import com.google.android.exoplayer2.source.ClippingMediaPeriod; @@ -40,19 +40,14 @@ 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.TraceUtil; -import com.google.android.exoplayer2.util.Util; import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -/** Implements the internal behavior of {@link ExoPlayerImpl}. */ -/* package */ final class ExoPlayerImplInternal - implements Handler.Callback, - MediaPeriod.Callback, - TrackSelector.InvalidationListener, - MediaSource.Listener, - PlaybackParameterListener, - PlayerMessage.Sender { +/** + * Implements the internal behavior of {@link ExoPlayerImpl}. + */ +/* package */ final class ExoPlayerImplInternal implements Handler.Callback, + MediaPeriod.Callback, TrackSelector.InvalidationListener, MediaSource.Listener, + PlaybackParameterListener { private static final String TAG = "ExoPlayerImplInternal"; @@ -113,7 +108,6 @@ import java.util.Collections; private final boolean retainBackBufferFromKeyframe; private final DefaultMediaClock mediaClock; private final PlaybackInfoUpdate playbackInfoUpdate; - private final ArrayList customMessageInfos; @SuppressWarnings("unused") private SeekParameters seekParameters; @@ -126,12 +120,13 @@ import java.util.Collections; private boolean rebuffering; private @Player.RepeatMode int repeatMode; private boolean shuffleModeEnabled; + private int customMessagesSent; + private int customMessagesProcessed; private long elapsedRealtimeUs; private int pendingPrepareCount; private SeekPosition pendingInitialSeekPosition; private long rendererPositionUs; - private int nextCustomMessageInfoIndex; private MediaPeriodHolder loadingPeriodHolder; private MediaPeriodHolder readingPeriodHolder; @@ -171,7 +166,6 @@ import java.util.Collections; rendererCapabilities[i] = renderers[i].getCapabilities(); } mediaClock = new DefaultMediaClock(this); - customMessageInfos = new ArrayList<>(); enabledRenderers = new Renderer[0]; window = new Timeline.Window(); period = new Timeline.Period(); @@ -220,15 +214,34 @@ import java.util.Collections; handler.obtainMessage(MSG_STOP, reset ? 1 : 0, 0).sendToTarget(); } - @Override - public synchronized void sendMessage( - PlayerMessage message, PlayerMessage.Sender.Listener listener) { + public void sendMessages(ExoPlayerMessage... messages) { if (released) { Log.w(TAG, "Ignoring messages sent after release."); - listener.onMessageDeleted(); return; } - handler.obtainMessage(MSG_CUSTOM, new CustomMessageInfo(message, listener)).sendToTarget(); + customMessagesSent++; + handler.obtainMessage(MSG_CUSTOM, messages).sendToTarget(); + } + + public synchronized void blockingSendMessages(ExoPlayerMessage... messages) { + if (released) { + Log.w(TAG, "Ignoring messages sent after release."); + return; + } + int messageNumber = customMessagesSent++; + handler.obtainMessage(MSG_CUSTOM, messages).sendToTarget(); + boolean wasInterrupted = false; + while (customMessagesProcessed <= messageNumber) { + try { + wait(); + } catch (InterruptedException e) { + wasInterrupted = true; + } + } + if (wasInterrupted) { + // Restore the interrupted status. + Thread.currentThread().interrupt(); + } } public synchronized void release() { @@ -336,7 +349,7 @@ import java.util.Collections; reselectTracksInternal(); break; case MSG_CUSTOM: - sendMessageInternal((CustomMessageInfo) msg.obj); + sendMessagesInternal((ExoPlayerMessage[]) msg.obj); break; case MSG_RELEASE: releaseInternal(); @@ -524,9 +537,8 @@ import java.util.Collections; } else { rendererPositionUs = mediaClock.syncAndGetPositionUs(); periodPositionUs = playingPeriodHolder.toPeriodTime(rendererPositionUs); - maybeTriggerCustomMessages(playbackInfo.positionUs, periodPositionUs); - playbackInfo.positionUs = periodPositionUs; } + playbackInfo.positionUs = periodPositionUs; elapsedRealtimeUs = SystemClock.elapsedRealtime() * 1000; // Update the buffered position. @@ -644,8 +656,7 @@ import java.util.Collections; boolean seekPositionAdjusted = seekPosition.windowPositionUs == C.TIME_UNSET; try { - Pair periodPosition = - resolveSeekPosition(seekPosition, /* trySubsequentPeriods= */ true); + Pair periodPosition = resolveSeekPosition(seekPosition); if (periodPosition == null) { // The seek position was valid for the timeline that it was performed into, but the // timeline has changed and a suitable seek position could not be resolved in the new one. @@ -839,11 +850,6 @@ import java.util.Collections; } if (resetState) { mediaPeriodInfoSequence.setTimeline(null); - for (CustomMessageInfo customMessageInfo : customMessageInfos) { - customMessageInfo.listener.onMessageDeleted(); - } - customMessageInfos.clear(); - nextCustomMessageInfoIndex = 0; } playbackInfo = new PlaybackInfo( @@ -864,153 +870,21 @@ import java.util.Collections; } } - private void sendMessageInternal(CustomMessageInfo customMessageInfo) { - if (customMessageInfo.message.getPositionMs() == C.TIME_UNSET) { - // If no delivery time is specified, trigger immediate message delivery. - sendCustomMessagesToTarget(customMessageInfo); - } else if (playbackInfo.timeline == null) { - // Still waiting for initial timeline to resolve position. - customMessageInfos.add(customMessageInfo); - } else { - if (resolveCustomMessagePosition(customMessageInfo)) { - customMessageInfos.add(customMessageInfo); - // Ensure new message is inserted according to playback order. - Collections.sort(customMessageInfos); - } else { - customMessageInfo.listener.onMessageDeleted(); + private void sendMessagesInternal(ExoPlayerMessage[] messages) throws ExoPlaybackException { + try { + for (ExoPlayerMessage message : messages) { + message.target.handleMessage(message.messageType, message.message); } - } - } - - private void sendCustomMessagesToTarget(final CustomMessageInfo customMessageInfo) { - final Runnable handleMessageRunnable = - new Runnable() { - @Override - public void run() { - try { - customMessageInfo - .message - .getTarget() - .handleMessage( - customMessageInfo.message.getType(), customMessageInfo.message.getMessage()); - } catch (ExoPlaybackException e) { - eventHandler.obtainMessage(MSG_ERROR, e).sendToTarget(); - } finally { - customMessageInfo.listener.onMessageDelivered(); - if (customMessageInfo.message.getDeleteAfterDelivery()) { - customMessageInfo.listener.onMessageDeleted(); - } - // The message may have caused something to change that now requires us to do - // work. - handler.sendEmptyMessage(MSG_DO_SOME_WORK); - } - } - }; - handler.post( - new Runnable() { - @Override - public void run() { - customMessageInfo.message.getHandler().post(handleMessageRunnable); - } - }); - } - - private void resolveCustomMessagePositions() { - for (int i = customMessageInfos.size() - 1; i >= 0; i--) { - if (!resolveCustomMessagePosition(customMessageInfos.get(i))) { - // Remove messages if new position can't be resolved. - customMessageInfos.get(i).listener.onMessageDeleted(); - customMessageInfos.remove(i); + if (playbackInfo.playbackState == Player.STATE_READY + || playbackInfo.playbackState == Player.STATE_BUFFERING) { + // The message may have caused something to change that now requires us to do work. + handler.sendEmptyMessage(MSG_DO_SOME_WORK); } - } - // Re-sort messages by playback order. - Collections.sort(customMessageInfos); - } - - private boolean resolveCustomMessagePosition(CustomMessageInfo customMessageInfo) { - if (customMessageInfo.resolvedPeriodUid == null) { - // Position is still unresolved. Try to find window in current timeline. - Pair periodPosition = - resolveSeekPosition( - new SeekPosition( - customMessageInfo.message.getTimeline(), - customMessageInfo.message.getWindowIndex(), - C.msToUs(customMessageInfo.message.getPositionMs())), - /* trySubsequentPeriods= */ false); - if (periodPosition == null) { - return false; + } finally { + synchronized (this) { + customMessagesProcessed++; + notifyAll(); } - customMessageInfo.setResolvedPosition( - periodPosition.first, - periodPosition.second, - playbackInfo.timeline.getPeriod(periodPosition.first, period, true).uid); - } else { - // Position has been resolved for a previous timeline. Try to find the updated period index. - int index = playbackInfo.timeline.getIndexOfPeriod(customMessageInfo.resolvedPeriodUid); - if (index == C.INDEX_UNSET) { - return false; - } - customMessageInfo.resolvedPeriodIndex = index; - } - return true; - } - - private void maybeTriggerCustomMessages(long oldPeriodPositionUs, long newPeriodPositionUs) { - if (customMessageInfos.isEmpty() || playbackInfo.periodId.isAd()) { - return; - } - // If this is the first call from the start position, include oldPeriodPositionUs in potential - // trigger positions. - if (playbackInfo.startPositionUs == oldPeriodPositionUs) { - oldPeriodPositionUs--; - } - // Correct next index if necessary (e.g. after seeking, timeline changes, or new messages) - int currentPeriodIndex = playbackInfo.periodId.periodIndex; - CustomMessageInfo prevInfo = - nextCustomMessageInfoIndex > 0 - ? customMessageInfos.get(nextCustomMessageInfoIndex - 1) - : null; - while (prevInfo != null - && (prevInfo.resolvedPeriodIndex > currentPeriodIndex - || (prevInfo.resolvedPeriodIndex == currentPeriodIndex - && prevInfo.resolvedPeriodTimeUs > oldPeriodPositionUs))) { - nextCustomMessageInfoIndex--; - prevInfo = - nextCustomMessageInfoIndex > 0 - ? customMessageInfos.get(nextCustomMessageInfoIndex - 1) - : null; - } - CustomMessageInfo nextInfo = - nextCustomMessageInfoIndex < customMessageInfos.size() - ? customMessageInfos.get(nextCustomMessageInfoIndex) - : null; - while (nextInfo != null - && nextInfo.resolvedPeriodUid != null - && (nextInfo.resolvedPeriodIndex < currentPeriodIndex - || (nextInfo.resolvedPeriodIndex == currentPeriodIndex - && nextInfo.resolvedPeriodTimeUs <= oldPeriodPositionUs))) { - nextCustomMessageInfoIndex++; - nextInfo = - nextCustomMessageInfoIndex < customMessageInfos.size() - ? customMessageInfos.get(nextCustomMessageInfoIndex) - : null; - } - // Check if any message falls within the covered time span. - while (nextInfo != null - && nextInfo.resolvedPeriodUid != null - && nextInfo.resolvedPeriodIndex == currentPeriodIndex - && nextInfo.resolvedPeriodTimeUs > oldPeriodPositionUs - && nextInfo.resolvedPeriodTimeUs <= newPeriodPositionUs) { - sendCustomMessagesToTarget(nextInfo); - if (nextInfo.message.getDeleteAfterDelivery()) { - customMessageInfos.remove(nextCustomMessageInfoIndex); - } else { - nextCustomMessageInfoIndex++; - } - nextInfo = - nextCustomMessageInfoIndex < customMessageInfos.size() - ? customMessageInfos.get(nextCustomMessageInfoIndex) - : null; } } @@ -1160,14 +1034,12 @@ import java.util.Collections; Object manifest = sourceRefreshInfo.manifest; mediaPeriodInfoSequence.setTimeline(timeline); playbackInfo = playbackInfo.copyWithTimeline(timeline, manifest); - resolveCustomMessagePositions(); if (oldTimeline == null) { playbackInfoUpdate.incrementPendingOperationAcks(pendingPrepareCount); pendingPrepareCount = 0; if (pendingInitialSeekPosition != null) { - Pair periodPosition = - resolveSeekPosition(pendingInitialSeekPosition, /* trySubsequentPeriods= */ true); + Pair periodPosition = resolveSeekPosition(pendingInitialSeekPosition); pendingInitialSeekPosition = null; if (periodPosition == null) { // The seek position was valid for the timeline that it was performed into, but the @@ -1352,14 +1224,11 @@ import java.util.Collections; * internal timeline. * * @param seekPosition The position to resolve. - * @param trySubsequentPeriods Whether the position can be resolved to a subsequent matching - * period if the original period is no longer available. * @return The resolved position, or null if resolution was not successful. * @throws IllegalSeekPositionException If the window index of the seek position is outside the * bounds of the timeline. */ - private Pair resolveSeekPosition( - SeekPosition seekPosition, boolean trySubsequentPeriods) { + private Pair resolveSeekPosition(SeekPosition seekPosition) { Timeline timeline = playbackInfo.timeline; Timeline seekTimeline = seekPosition.timeline; if (seekTimeline.isEmpty()) { @@ -1388,14 +1257,12 @@ import java.util.Collections; // We successfully located the period in the internal timeline. return Pair.create(periodIndex, periodPosition.second); } - if (trySubsequentPeriods) { - // Try and find a subsequent period from the seek timeline in the internal timeline. - periodIndex = resolveSubsequentPeriod(periodPosition.first, seekTimeline, timeline); - if (periodIndex != C.INDEX_UNSET) { - // We found one. Map the SeekPosition onto the corresponding default position. - return getPeriodPosition( - timeline, timeline.getPeriod(periodIndex, period).windowIndex, C.TIME_UNSET); - } + // Try and find a subsequent period from the seek timeline in the internal timeline. + periodIndex = resolveSubsequentPeriod(periodPosition.first, seekTimeline, timeline); + if (periodIndex != C.INDEX_UNSET) { + // We found one. Map the SeekPosition onto the corresponding default position. + return getPeriodPosition(timeline, timeline.getPeriod(periodIndex, period).windowIndex, + C.TIME_UNSET); } // We didn't find one. Give up. return null; @@ -1935,45 +1802,7 @@ import java.util.Collections; this.windowIndex = windowIndex; this.windowPositionUs = windowPositionUs; } - } - private static final class CustomMessageInfo implements Comparable { - - public final PlayerMessage message; - public final PlayerMessage.Sender.Listener listener; - - public int resolvedPeriodIndex; - public long resolvedPeriodTimeUs; - public @Nullable Object resolvedPeriodUid; - - public CustomMessageInfo(PlayerMessage message, PlayerMessage.Sender.Listener listener) { - this.message = message; - this.listener = listener; - } - - public void setResolvedPosition(int periodIndex, long periodTimeUs, Object periodUid) { - resolvedPeriodIndex = periodIndex; - resolvedPeriodTimeUs = periodTimeUs; - resolvedPeriodUid = periodUid; - } - - @Override - public int compareTo(@NonNull CustomMessageInfo other) { - if ((resolvedPeriodUid == null) != (other.resolvedPeriodUid == null)) { - // CustomMessageInfos with a resolved period position are always smaller. - return resolvedPeriodUid != null ? -1 : 1; - } - if (resolvedPeriodUid == null) { - // Don't sort message with unresolved positions. - return 0; - } - // Sort resolved media times by period index and then by period position. - int comparePeriodIndex = resolvedPeriodIndex - other.resolvedPeriodIndex; - if (comparePeriodIndex != 0) { - return comparePeriodIndex; - } - return Util.compareLong(resolvedPeriodTimeUs, other.resolvedPeriodTimeUs); - } } private static final class MediaSourceRefreshInfo { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java index 593d3d1fce..978f4f7a97 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java @@ -179,7 +179,7 @@ public abstract class NoSampleRenderer implements Renderer, RendererCapabilities return ADAPTIVE_NOT_SUPPORTED; } - // PlayerMessage.Target implementation. + // ExoPlayerComponent implementation. @Override public void handleMessage(int what, Object object) throws ExoPlaybackException { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java b/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java deleted file mode 100644 index 44a4b0c7c2..0000000000 --- a/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java +++ /dev/null @@ -1,295 +0,0 @@ -/* - * 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.os.Handler; -import android.support.annotation.Nullable; -import com.google.android.exoplayer2.util.Assertions; - -/** - * Defines a player message which can be sent with a {@link Sender} and received by a {@link - * Target}. - */ -public final class PlayerMessage { - - /** A target for messages. */ - public interface Target { - - /** - * Handles a message delivered to the target. - * - * @param messageType The message type. - * @param message The message. - * @throws ExoPlaybackException If an error occurred whilst handling the message. - */ - void handleMessage(int messageType, Object message) throws ExoPlaybackException; - } - - /** A sender for messages. */ - public interface Sender { - - /** A listener for message events triggered by the sender. */ - interface Listener { - - /** Called when the message has been delivered. */ - void onMessageDelivered(); - - /** Called when the message has been deleted. */ - void onMessageDeleted(); - } - - /** - * Sends a message. - * - * @param message The message to be sent. - * @param listener The listener to listen to message events. - */ - void sendMessage(PlayerMessage message, Listener listener); - } - - private final Target target; - private final Sender sender; - private final Timeline timeline; - - private int type; - private Object message; - private Handler handler; - private int windowIndex; - private long positionMs; - private boolean deleteAfterDelivery; - private boolean isSent; - private boolean isDelivered; - private boolean isDeleted; - - /** - * Creates a new message. - * - * @param sender The {@link Sender} used to send the message. - * @param target The {@link Target} the message is sent to. - * @param timeline The timeline used when setting the position with {@link #setPosition(long)}. If - * set to {@link Timeline#EMPTY}, any position can be specified. - * @param defaultWindowIndex The default window index in the {@code timeline} when no other window - * index is specified. - * @param defaultHandler The default handler to send the message on when no other handler is - * specified. - */ - public PlayerMessage( - Sender sender, - Target target, - Timeline timeline, - int defaultWindowIndex, - Handler defaultHandler) { - this.sender = sender; - this.target = target; - this.timeline = timeline; - this.handler = defaultHandler; - this.windowIndex = defaultWindowIndex; - this.positionMs = C.TIME_UNSET; - this.deleteAfterDelivery = true; - } - - /** Returns the timeline used for setting the position with {@link #setPosition(long)}. */ - public Timeline getTimeline() { - return timeline; - } - - /** Returns the target the message is sent to. */ - public Target getTarget() { - return target; - } - - /** - * Sets a custom message type forwarded to the {@link Target#handleMessage(int, Object)}. - * - * @param messageType The custom message type. - * @return This message. - * @throws IllegalStateException If {@link #send()} has already been called. - */ - public PlayerMessage setType(int messageType) { - Assertions.checkState(!isSent); - this.type = messageType; - return this; - } - - /** Returns custom message type forwarded to the {@link Target#handleMessage(int, Object)}. */ - public int getType() { - return type; - } - - /** - * Sets a custom message forwarded to the {@link Target#handleMessage(int, Object)}. - * - * @param message The custom message. - * @return This message. - * @throws IllegalStateException If {@link #send()} has already been called. - */ - public PlayerMessage setMessage(@Nullable Object message) { - Assertions.checkState(!isSent); - this.message = message; - return this; - } - - /** Returns custom message forwarded to the {@link Target#handleMessage(int, Object)}. */ - public Object getMessage() { - return message; - } - - /** - * Sets the handler the message is delivered on. - * - * @param handler A {@link Handler}. - * @return This message. - * @throws IllegalStateException If {@link #send()} has already been called. - */ - public PlayerMessage setHandler(Handler handler) { - Assertions.checkState(!isSent); - this.handler = handler; - return this; - } - - /** Returns the handler the message is delivered on. */ - public Handler getHandler() { - return handler; - } - - /** - * Sets a position in the current window at which the message will be delivered. - * - * @param positionMs The position in the current window at which the message will be sent, in - * milliseconds. - * @return This message. - * @throws IllegalStateException If {@link #send()} has already been called. - */ - public PlayerMessage setPosition(long positionMs) { - Assertions.checkState(!isSent); - this.positionMs = positionMs; - return this; - } - - /** - * Returns position in window at {@link #getWindowIndex()} at which the message will be delivered, - * in milliseconds. If {@link C#TIME_UNSET}, the message will be delivered immediately. - */ - public long getPositionMs() { - return positionMs; - } - - /** - * Sets a position in a window at which the message will be delivered. - * - * @param windowIndex The index of the window at which the message will be sent. - * @param positionMs The position in the window with index {@code windowIndex} at which the - * message will be sent, in milliseconds. - * @return This message. - * @throws IllegalSeekPositionException If the timeline returned by {@link #getTimeline()} is not - * empty and the provided window index is not within the bounds of the timeline. - * @throws IllegalStateException If {@link #send()} has already been called. - */ - public PlayerMessage setPosition(int windowIndex, long positionMs) { - Assertions.checkState(!isSent); - Assertions.checkArgument(positionMs != C.TIME_UNSET); - if (windowIndex < 0 || (!timeline.isEmpty() && windowIndex >= timeline.getWindowCount())) { - throw new IllegalSeekPositionException(timeline, windowIndex, positionMs); - } - this.windowIndex = windowIndex; - this.positionMs = positionMs; - return this; - } - - /** Returns window index at which the message will be delivered. */ - public int getWindowIndex() { - return windowIndex; - } - - /** - * Sets whether the message will be deleted after delivery. If false, the message will be resent - * if playback reaches the specified position again. Only allowed to be false if a position is set - * with {@link #setPosition(long)}. - * - * @param deleteAfterDelivery Whether the message is deleted after delivery. - * @return This message. - * @throws IllegalStateException If {@link #send()} has already been called. - */ - public PlayerMessage setDeleteAfterDelivery(boolean deleteAfterDelivery) { - Assertions.checkState(!isSent); - this.deleteAfterDelivery = deleteAfterDelivery; - return this; - } - - /** Returns whether the message will be deleted after delivery. */ - public boolean getDeleteAfterDelivery() { - return deleteAfterDelivery; - } - - /** - * Sends the message. If the target throws an {@link ExoPlaybackException} then it is propagated - * out of the player as an error using {@link - * Player.EventListener#onPlayerError(ExoPlaybackException)}. - * - * @return This message. - * @throws IllegalStateException If {@link #send()} has already been called. - */ - public PlayerMessage send() { - Assertions.checkState(!isSent); - if (positionMs == C.TIME_UNSET) { - Assertions.checkArgument(deleteAfterDelivery); - } - isSent = true; - sender.sendMessage( - this, - new Sender.Listener() { - @Override - public void onMessageDelivered() { - synchronized (PlayerMessage.this) { - isDelivered = true; - PlayerMessage.this.notifyAll(); - } - } - - @Override - public void onMessageDeleted() { - synchronized (PlayerMessage.this) { - isDeleted = true; - PlayerMessage.this.notifyAll(); - } - } - }); - return this; - } - - /** - * Blocks until after the message has been delivered or the player is no longer able to deliver - * the message. - * - *

          Note that this method can't be called if the current thread is the same thread used by the - * message handler set with {@link #setHandler(Handler)} as it would cause a deadlock. - * - * @return Whether the message was delivered successfully. - * @throws IllegalStateException If this method is called before {@link #send()}. - * @throws IllegalStateException If this method is called on the same thread used by the message - * handler set with {@link #setHandler(Handler)}. - * @throws InterruptedException If the current thread is interrupted while waiting for the message - * to be delivered. - */ - public synchronized boolean blockUntilDelivered() throws InterruptedException { - Assertions.checkState(!isSent); - Assertions.checkState(handler.getLooper().getThread() != Thread.currentThread()); - while (!isDelivered && !isDeleted) { - wait(); - } - return isDelivered; - } -} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java b/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java index d0a07930e0..6def1591da 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java @@ -15,20 +15,22 @@ */ package com.google.android.exoplayer2; +import com.google.android.exoplayer2.ExoPlayer.ExoPlayerComponent; import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.util.MediaClock; import java.io.IOException; /** * Renders media read from a {@link SampleStream}. - * - *

          Internally, a renderer's lifecycle is managed by the owning {@link ExoPlayer}. The renderer is + *

          + * Internally, a renderer's lifecycle is managed by the owning {@link ExoPlayer}. The renderer is * transitioned through various states as the overall playback state changes. The valid state * transitions are shown below, annotated with the methods that are called during each transition. - * - *

          Renderer state transitions + *

          + * Renderer state transitions + *

          */ -public interface Renderer extends PlayerMessage.Target { +public interface Renderer extends ExoPlayerComponent { /** * The renderer is disabled. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index e2d0ed1422..69369d4229 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -93,6 +93,8 @@ public class SimpleExoPlayer implements ExoPlayer { private final CopyOnWriteArraySet metadataOutputs; private final CopyOnWriteArraySet videoDebugListeners; private final CopyOnWriteArraySet audioDebugListeners; + private final int videoRendererCount; + private final int audioRendererCount; private Format videoFormat; private Format audioFormat; @@ -122,6 +124,25 @@ public class SimpleExoPlayer implements ExoPlayer { renderers = renderersFactory.createRenderers(eventHandler, componentListener, componentListener, componentListener, componentListener); + // Obtain counts of video and audio renderers. + int videoRendererCount = 0; + int audioRendererCount = 0; + for (Renderer renderer : renderers) { + switch (renderer.getTrackType()) { + case C.TRACK_TYPE_VIDEO: + videoRendererCount++; + break; + case C.TRACK_TYPE_AUDIO: + audioRendererCount++; + break; + default: + // Don't count other track types. + break; + } + } + this.videoRendererCount = videoRendererCount; + this.audioRendererCount = audioRendererCount; + // Set initial values. audioVolume = 1; audioSessionId = C.AUDIO_SESSION_ID_UNSET; @@ -142,15 +163,15 @@ public class SimpleExoPlayer implements ExoPlayer { */ public void setVideoScalingMode(@C.VideoScalingMode int videoScalingMode) { this.videoScalingMode = videoScalingMode; + ExoPlayerMessage[] messages = new ExoPlayerMessage[videoRendererCount]; + int count = 0; for (Renderer renderer : renderers) { if (renderer.getTrackType() == C.TRACK_TYPE_VIDEO) { - player - .createMessage(renderer) - .setType(C.MSG_SET_SCALING_MODE) - .setMessage(videoScalingMode) - .send(); + messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_SCALING_MODE, + videoScalingMode); } } + player.sendMessages(messages); } /** @@ -331,15 +352,15 @@ public class SimpleExoPlayer implements ExoPlayer { */ public void setAudioAttributes(AudioAttributes audioAttributes) { this.audioAttributes = audioAttributes; + ExoPlayerMessage[] messages = new ExoPlayerMessage[audioRendererCount]; + int count = 0; for (Renderer renderer : renderers) { if (renderer.getTrackType() == C.TRACK_TYPE_AUDIO) { - player - .createMessage(renderer) - .setType(C.MSG_SET_AUDIO_ATTRIBUTES) - .setMessage(audioAttributes) - .send(); + messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_AUDIO_ATTRIBUTES, + audioAttributes); } } + player.sendMessages(messages); } /** @@ -356,11 +377,14 @@ public class SimpleExoPlayer implements ExoPlayer { */ public void setVolume(float audioVolume) { this.audioVolume = audioVolume; + ExoPlayerMessage[] messages = new ExoPlayerMessage[audioRendererCount]; + int count = 0; for (Renderer renderer : renderers) { if (renderer.getTrackType() == C.TRACK_TYPE_AUDIO) { - player.createMessage(renderer).setType(C.MSG_SET_VOLUME).setMessage(audioVolume).send(); + messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_VOLUME, audioVolume); } } + player.sendMessages(messages); } /** @@ -746,11 +770,6 @@ public class SimpleExoPlayer implements ExoPlayer { player.sendMessages(messages); } - @Override - public PlayerMessage createMessage(PlayerMessage.Target target) { - return player.createMessage(target); - } - @Override public void blockingSendMessages(ExoPlayerMessage... messages) { player.blockingSendMessages(messages); @@ -889,25 +908,22 @@ public class SimpleExoPlayer implements ExoPlayer { private void setVideoSurfaceInternal(Surface surface, boolean ownsSurface) { // Note: We don't turn this method into a no-op if the surface is being replaced with itself // so as to ensure onRenderedFirstFrame callbacks are still called in this case. - boolean surfaceReplaced = this.surface != null && this.surface != surface; + ExoPlayerMessage[] messages = new ExoPlayerMessage[videoRendererCount]; + int count = 0; for (Renderer renderer : renderers) { if (renderer.getTrackType() == C.TRACK_TYPE_VIDEO) { - PlayerMessage message = - player.createMessage(renderer).setType(C.MSG_SET_SURFACE).setMessage(surface).send(); - if (surfaceReplaced) { - try { - message.blockUntilDelivered(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } + messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_SURFACE, surface); } } - if (surfaceReplaced) { + if (this.surface != null && this.surface != surface) { + // We're replacing a surface. Block to ensure that it's not accessed after the method returns. + player.blockingSendMessages(messages); // If we created the previous surface, we are responsible for releasing it. if (this.ownsSurface) { this.surface.release(); } + } else { + player.sendMessages(messages); } this.surface = surface; this.ownsSurface = ownsSurface; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java index 54537ba548..c410456e7b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java @@ -23,7 +23,8 @@ import android.util.SparseIntArray; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; -import com.google.android.exoplayer2.PlayerMessage; +import com.google.android.exoplayer2.ExoPlayer.ExoPlayerComponent; +import com.google.android.exoplayer2.ExoPlayer.ExoPlayerMessage; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder; import com.google.android.exoplayer2.upstream.Allocator; @@ -41,7 +42,7 @@ import java.util.Map; * Concatenates multiple {@link MediaSource}s. The list of {@link MediaSource}s can be modified * during playback. Access to this class is thread-safe. */ -public final class DynamicConcatenatingMediaSource implements MediaSource, PlayerMessage.Target { +public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPlayerComponent { private static final int MSG_ADD = 0; private static final int MSG_ADD_MULTIPLE = 1; @@ -146,11 +147,8 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, Playe Assertions.checkArgument(!mediaSourcesPublic.contains(mediaSource)); mediaSourcesPublic.add(index, mediaSource); if (player != null) { - player - .createMessage(this) - .setType(MSG_ADD) - .setMessage(new MessageData<>(index, mediaSource, actionOnCompletion)) - .send(); + player.sendMessages(new ExoPlayerMessage(this, MSG_ADD, + new MessageData<>(index, mediaSource, actionOnCompletion))); } else if (actionOnCompletion != null) { actionOnCompletion.run(); } @@ -222,11 +220,8 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, Playe } mediaSourcesPublic.addAll(index, mediaSources); if (player != null && !mediaSources.isEmpty()) { - player - .createMessage(this) - .setType(MSG_ADD_MULTIPLE) - .setMessage(new MessageData<>(index, mediaSources, actionOnCompletion)) - .send(); + player.sendMessages(new ExoPlayerMessage(this, MSG_ADD_MULTIPLE, + new MessageData<>(index, mediaSources, actionOnCompletion))); } else if (actionOnCompletion != null){ actionOnCompletion.run(); } @@ -261,11 +256,8 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, Playe public synchronized void removeMediaSource(int index, @Nullable Runnable actionOnCompletion) { mediaSourcesPublic.remove(index); if (player != null) { - player - .createMessage(this) - .setType(MSG_REMOVE) - .setMessage(new MessageData<>(index, null, actionOnCompletion)) - .send(); + player.sendMessages(new ExoPlayerMessage(this, MSG_REMOVE, + new MessageData<>(index, null, actionOnCompletion))); } else if (actionOnCompletion != null) { actionOnCompletion.run(); } @@ -301,11 +293,8 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, Playe } mediaSourcesPublic.add(newIndex, mediaSourcesPublic.remove(currentIndex)); if (player != null) { - player - .createMessage(this) - .setType(MSG_MOVE) - .setMessage(new MessageData<>(currentIndex, newIndex, actionOnCompletion)) - .send(); + player.sendMessages(new ExoPlayerMessage(this, MSG_MOVE, + new MessageData<>(currentIndex, newIndex, actionOnCompletion))); } else if (actionOnCompletion != null) { actionOnCompletion.run(); } @@ -438,7 +427,8 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, Playe new ConcatenatedTimeline(mediaSourceHolders, windowCount, periodCount, shuffleOrder), null); if (actionOnCompletion != null) { - player.createMessage(this).setType(MSG_ON_COMPLETION).setMessage(actionOnCompletion).send(); + player.sendMessages( + new ExoPlayerMessage(this, MSG_ON_COMPLETION, actionOnCompletion)); } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java index a5f5222820..d796e6936f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -561,18 +561,6 @@ public final class Util { return stayInBounds ? Math.min(list.size() - 1, index) : index; } - /** - * Compares two long values and returns the same value as {@code Long.compare(long, long)}. - * - * @param left The left operand. - * @param right The right operand. - * @return 0, if left == right, a negative value if left < right, or a positive value if left - * > right. - */ - public static int compareLong(long left, long right) { - return left < right ? -1 : left == right ? 0 : 1; - } - /** * Parses an xs:duration attribute value, returning the parsed duration in milliseconds. * diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java index 5ec45af29f..ff0b8a6bc0 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java @@ -18,17 +18,13 @@ package com.google.android.exoplayer2.testutil; import android.os.Handler; import android.util.Log; import android.view.Surface; -import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.PlayerMessage; -import com.google.android.exoplayer2.PlayerMessage.Target; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.testutil.ActionSchedule.ActionNode; -import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerTarget; import com.google.android.exoplayer2.trackselection.MappingTrackSelector; /** @@ -349,63 +345,7 @@ public abstract class Action { Surface surface) { player.setShuffleModeEnabled(shuffleModeEnabled); } - } - /** Calls {@link ExoPlayer#createMessage(Target)} and {@link PlayerMessage#send()}. */ - public static final class SendMessages extends Action { - - private final Target target; - private final int windowIndex; - private final long positionMs; - private final boolean deleteAfterDelivery; - - /** - * @param tag A tag to use for logging. - * @param target A message target. - * @param positionMs The position at which the message should be sent, in milliseconds. - */ - public SendMessages(String tag, Target target, long positionMs) { - this( - tag, - target, - /* windowIndex= */ C.INDEX_UNSET, - positionMs, - /* deleteAfterDelivery= */ true); - } - - /** - * @param tag A tag to use for logging. - * @param target A message target. - * @param windowIndex The window index at which the message should be sent, or {@link - * C#INDEX_UNSET} for the current window. - * @param positionMs The position at which the message should be sent, in milliseconds. - * @param deleteAfterDelivery Whether the message will be deleted after delivery. - */ - public SendMessages( - String tag, Target target, int windowIndex, long positionMs, boolean deleteAfterDelivery) { - super(tag, "SendMessages"); - this.target = target; - this.windowIndex = windowIndex; - this.positionMs = positionMs; - this.deleteAfterDelivery = deleteAfterDelivery; - } - - @Override - protected void doActionImpl( - final SimpleExoPlayer player, MappingTrackSelector trackSelector, Surface surface) { - if (target instanceof PlayerTarget) { - ((PlayerTarget) target).setPlayer(player); - } - PlayerMessage message = player.createMessage(target); - if (windowIndex != C.INDEX_UNSET) { - message.setPosition(windowIndex, positionMs); - } else { - message.setPosition(positionMs); - } - message.setHandler(new Handler()); - message.setDeleteAfterDelivery(deleteAfterDelivery); - message.send(); - } } /** diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java index 2ac487c98e..477071f91f 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java @@ -20,11 +20,8 @@ import android.os.Looper; import android.support.annotation.Nullable; import android.view.Surface; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.PlayerMessage; -import com.google.android.exoplayer2.PlayerMessage.Target; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaSource; @@ -32,7 +29,6 @@ import com.google.android.exoplayer2.testutil.Action.ClearVideoSurface; import com.google.android.exoplayer2.testutil.Action.ExecuteRunnable; import com.google.android.exoplayer2.testutil.Action.PrepareSource; import com.google.android.exoplayer2.testutil.Action.Seek; -import com.google.android.exoplayer2.testutil.Action.SendMessages; import com.google.android.exoplayer2.testutil.Action.SetPlayWhenReady; import com.google.android.exoplayer2.testutil.Action.SetPlaybackParameters; import com.google.android.exoplayer2.testutil.Action.SetRendererDisabled; @@ -319,44 +315,6 @@ public final class ActionSchedule { return apply(new SetShuffleModeEnabled(tag, shuffleModeEnabled)); } - /** - * Schedules sending a {@link PlayerMessage}. - * - * @param positionMs The position in the current window at which the message should be sent, in - * milliseconds. - * @return The builder, for convenience. - */ - public Builder sendMessage(Target target, long positionMs) { - return apply(new SendMessages(tag, target, positionMs)); - } - - /** - * Schedules sending a {@link PlayerMessage}. - * - * @param target A message target. - * @param windowIndex The window index at which the message should be sent. - * @param positionMs The position at which the message should be sent, in milliseconds. - * @return The builder, for convenience. - */ - public Builder sendMessage(Target target, int windowIndex, long positionMs) { - return apply( - new SendMessages(tag, target, windowIndex, positionMs, /* deleteAfterDelivery= */ true)); - } - - /** - * Schedules to send a {@link PlayerMessage}. - * - * @param target A message target. - * @param windowIndex The window index at which the message should be sent. - * @param positionMs The position at which the message should be sent, in milliseconds. - * @param deleteAfterDelivery Whether the message will be deleted after delivery. - * @return The builder, for convenience. - */ - public Builder sendMessage( - Target target, int windowIndex, long positionMs, boolean deleteAfterDelivery) { - return apply(new SendMessages(tag, target, windowIndex, positionMs, deleteAfterDelivery)); - } - /** * Schedules a delay until the timeline changed to a specified expected timeline. * @@ -407,28 +365,7 @@ public final class ActionSchedule { currentDelayMs = 0; return this; } - } - /** - * Provides a wrapper for a {@link Target} which has access to the player when handling messages. - * Can be used with {@link Builder#sendMessage(Target, long)}. - */ - public abstract static class PlayerTarget implements Target { - - private SimpleExoPlayer player; - - /** Handles the message send to the component and additionally provides access to the player. */ - public abstract void handleMessage(SimpleExoPlayer player, int messageType, Object message); - - /** Sets the player to be passed to {@link #handleMessage(SimpleExoPlayer, int, Object)}. */ - /* package */ void setPlayer(SimpleExoPlayer player) { - this.player = player; - } - - @Override - public final void handleMessage(int messageType, Object message) throws ExoPlaybackException { - handleMessage(player, messageType, message); - } } /** diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java index 797c09d6b6..4a9d79f906 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.testutil; -import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.util.Util; @@ -171,7 +170,7 @@ public final class FakeTimeline extends Timeline { int windowPeriodIndex = periodIndex - periodOffsets[windowIndex]; TimelineWindowDefinition windowDefinition = windowDefinitions[windowIndex]; Object id = setIds ? windowPeriodIndex : null; - Object uid = setIds ? Pair.create(windowDefinition.id, windowPeriodIndex) : null; + Object uid = setIds ? periodIndex : null; long periodDurationUs = windowDefinition.durationUs / windowDefinition.periodCount; long positionInWindowUs = periodDurationUs * windowPeriodIndex; if (windowDefinition.adGroupsPerPeriodCount == 0) { @@ -199,13 +198,11 @@ public final class FakeTimeline extends Timeline { @Override public int getIndexOfPeriod(Object uid) { - Period period = new Period(); - for (int i = 0; i < getPeriodCount(); i++) { - if (getPeriod(i, period, true).uid.equals(uid)) { - return i; - } + if (!(uid instanceof Integer)) { + return C.INDEX_UNSET; } - return C.INDEX_UNSET; + int index = (Integer) uid; + return index >= 0 && index < getPeriodCount() ? index : C.INDEX_UNSET; } private static TimelineWindowDefinition[] createDefaultWindowDefinitions(int windowCount) { diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java index 93c14afc8f..4f31a8b027 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java @@ -24,9 +24,7 @@ import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Message; -import android.util.Pair; import com.google.android.exoplayer2.ExoPlaybackException; -import com.google.android.exoplayer2.PlayerMessage; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; @@ -283,8 +281,7 @@ public class MediaSourceTestRunner { } - private static class EventHandlingExoPlayer extends StubExoPlayer - implements Handler.Callback, PlayerMessage.Sender { + private static class EventHandlingExoPlayer extends StubExoPlayer implements Handler.Callback { private final Handler handler; @@ -293,33 +290,23 @@ public class MediaSourceTestRunner { } @Override - public PlayerMessage createMessage(PlayerMessage.Target target) { - return new PlayerMessage( - /* sender= */ this, target, Timeline.EMPTY, /* defaultWindowIndex= */ 0, handler); + public void sendMessages(ExoPlayerMessage... messages) { + handler.obtainMessage(0, messages).sendToTarget(); } @Override - public void sendMessage(PlayerMessage message, Listener listener) { - handler.obtainMessage(0, Pair.create(message, listener)).sendToTarget(); - } - - @Override - @SuppressWarnings("unchecked") public boolean handleMessage(Message msg) { - Pair messageAndListener = (Pair) msg.obj; - try { - messageAndListener - .first - .getTarget() - .handleMessage( - messageAndListener.first.getType(), messageAndListener.first.getMessage()); - messageAndListener.second.onMessageDelivered(); - messageAndListener.second.onMessageDeleted(); - } catch (ExoPlaybackException e) { - fail("Unexpected ExoPlaybackException."); + ExoPlayerMessage[] messages = (ExoPlayerMessage[]) msg.obj; + for (ExoPlayerMessage message : messages) { + try { + message.target.handleMessage(message.messageType, message.message); + } catch (ExoPlaybackException e) { + fail("Unexpected ExoPlaybackException."); + } } return true; } + } } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java index 7164fa13ab..1ea83bf1ec 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java @@ -19,7 +19,6 @@ import android.os.Looper; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.PlayerMessage; import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaSource; @@ -147,11 +146,6 @@ public abstract class StubExoPlayer implements ExoPlayer { throw new UnsupportedOperationException(); } - @Override - public PlayerMessage createMessage(PlayerMessage.Target target) { - throw new UnsupportedOperationException(); - } - @Override public void sendMessages(ExoPlayerMessage... messages) { throw new UnsupportedOperationException(); From ec71c05e8bef510e91996ed24273dcd9bfda90eb Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 20 Dec 2017 07:26:49 -0800 Subject: [PATCH 0981/2472] Add possiblity to send messages at playback position. This adds options to ExoPlayer.sendMessages which allow to specify a window index and position at which the message should be sent. Additionally, the options can be configured to use a custom Handler for the messages and whether the message should be repeated when playback reaches the same position again. The internal player converts these window positions to period index and position at the earliest possibility. The internal player also attempts to update these when the source info is refreshed. A sorted list of pending posts is kept and the player triggers these posts when the playback position moves over the specified position. Issue:#2189 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179683841 --- RELEASENOTES.md | 4 + .../exoplayer2/ext/vp9/VpxPlaybackTest.java | 8 +- .../android/exoplayer2/ExoPlayerTest.java | 446 ++++++++++++++++++ .../android/exoplayer2/BaseRenderer.java | 2 +- .../google/android/exoplayer2/ExoPlayer.java | 164 +++---- .../android/exoplayer2/ExoPlayerImpl.java | 44 +- .../exoplayer2/ExoPlayerImplInternal.java | 283 ++++++++--- .../android/exoplayer2/NoSampleRenderer.java | 2 +- .../android/exoplayer2/PlayerMessage.java | 295 ++++++++++++ .../google/android/exoplayer2/Renderer.java | 12 +- .../android/exoplayer2/SimpleExoPlayer.java | 70 ++- .../DynamicConcatenatingMediaSource.java | 36 +- .../google/android/exoplayer2/util/Util.java | 12 + .../android/exoplayer2/testutil/Action.java | 64 +++ .../exoplayer2/testutil/ActionSchedule.java | 85 ++++ .../exoplayer2/testutil/FakeTimeline.java | 13 +- .../testutil/MediaSourceTestRunner.java | 35 +- .../exoplayer2/testutil/StubExoPlayer.java | 6 + 18 files changed, 1349 insertions(+), 232 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java diff --git a/RELEASENOTES.md b/RELEASENOTES.md index c7f7ed7bbd..3c45c3449a 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -6,6 +6,10 @@ * Add optional parameter to `stop` to reset the player when stopping. * Add a reason to `EventListener.onTimelineChanged` to distinguish between initial preparation, reset and dynamic updates. + * Replaced `ExoPlayer.sendMessages` with `ExoPlayer.createMessage` to allow + more customization of the message. Now supports setting a message delivery + playback position and/or a delivery handler. + ([#2189](https://github.com/google/ExoPlayer/issues/2189)). * Buffering: * Allow a back-buffer of media to be retained behind the current playback position, for fast backward seeking. The back-buffer can be configured by 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 0a902e2efe..0f8df65959 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 @@ -119,9 +119,11 @@ public class VpxPlaybackTest extends InstrumentationTestCase { new DefaultDataSourceFactory(context, "ExoPlayerExtVp9Test")) .setExtractorsFactory(MatroskaExtractor.FACTORY) .createMediaSource(uri); - player.sendMessages(new ExoPlayer.ExoPlayerMessage(videoRenderer, - LibvpxVideoRenderer.MSG_SET_OUTPUT_BUFFER_RENDERER, - new VpxVideoSurfaceView(context))); + player + .createMessage(videoRenderer) + .setType(LibvpxVideoRenderer.MSG_SET_OUTPUT_BUFFER_RENDERER) + .setMessage(new VpxVideoSurfaceView(context)) + .send(); player.prepare(mediaSource); player.setPlayWhenReady(true); Looper.loop(); diff --git a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java index 40b4b2d383..70ff878e35 100644 --- a/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/androidTest/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -15,13 +15,17 @@ */ package com.google.android.exoplayer2; +import android.view.Surface; import com.google.android.exoplayer2.Player.DefaultEventListener; import com.google.android.exoplayer2.Player.EventListener; +import com.google.android.exoplayer2.Timeline.Window; import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.testutil.ActionSchedule; +import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerRunnable; +import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerTarget; import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner; import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner.Builder; import com.google.android.exoplayer2.testutil.FakeMediaClockRenderer; @@ -34,8 +38,10 @@ import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinit import com.google.android.exoplayer2.testutil.FakeTrackSelection; import com.google.android.exoplayer2.testutil.FakeTrackSelector; import com.google.android.exoplayer2.upstream.Allocator; +import com.google.android.exoplayer2.video.DummySurface; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.concurrent.CountDownLatch; import junit.framework.TestCase; @@ -942,4 +948,444 @@ public final class ExoPlayerTest extends TestCase { testRunner.assertTimelinesEqual(timeline); testRunner.assertTimelineChangeReasonsEqual(Player.TIMELINE_CHANGE_REASON_PREPARED); } + + public void testSendMessagesDuringPreparation() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForPlaybackState(Player.STATE_BUFFERING) + .sendMessage(target, /* positionMs= */ 50) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertTrue(target.positionMs >= 50); + } + + public void testSendMessagesAfterPreparation() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForTimelineChanged(timeline) + .sendMessage(target, /* positionMs= */ 50) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertTrue(target.positionMs >= 50); + } + + public void testMultipleSendMessages() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + PositionGrabbingMessageTarget target50 = new PositionGrabbingMessageTarget(); + PositionGrabbingMessageTarget target80 = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForPlaybackState(Player.STATE_BUFFERING) + .sendMessage(target80, /* positionMs= */ 80) + .sendMessage(target50, /* positionMs= */ 50) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertTrue(target50.positionMs >= 50); + assertTrue(target80.positionMs >= 80); + assertTrue(target80.positionMs >= target50.positionMs); + } + + public void testMultipleSendMessagesAtSameTime() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + PositionGrabbingMessageTarget target1 = new PositionGrabbingMessageTarget(); + PositionGrabbingMessageTarget target2 = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForPlaybackState(Player.STATE_BUFFERING) + .sendMessage(target1, /* positionMs= */ 50) + .sendMessage(target2, /* positionMs= */ 50) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertTrue(target1.positionMs >= 50); + assertTrue(target2.positionMs >= 50); + } + + public void testSendMessagesMultiPeriodResolution() throws Exception { + Timeline timeline = + new FakeTimeline(new TimelineWindowDefinition(/* periodCount= */ 10, /* id= */ 0)); + PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForPlaybackState(Player.STATE_BUFFERING) + .sendMessage(target, /* positionMs= */ 50) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertTrue(target.positionMs >= 50); + } + + public void testSendMessagesAtStartAndEndOfPeriod() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 2); + PositionGrabbingMessageTarget targetStartFirstPeriod = new PositionGrabbingMessageTarget(); + PositionGrabbingMessageTarget targetEndMiddlePeriod = new PositionGrabbingMessageTarget(); + PositionGrabbingMessageTarget targetStartMiddlePeriod = new PositionGrabbingMessageTarget(); + PositionGrabbingMessageTarget targetEndLastPeriod = new PositionGrabbingMessageTarget(); + long duration1Ms = timeline.getWindow(0, new Window()).getDurationMs(); + long duration2Ms = timeline.getWindow(1, new Window()).getDurationMs(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForPlaybackState(Player.STATE_BUFFERING) + .sendMessage(targetStartFirstPeriod, /* windowIndex= */ 0, /* positionMs= */ 0) + .sendMessage(targetEndMiddlePeriod, /* windowIndex= */ 0, /* positionMs= */ duration1Ms) + .sendMessage(targetStartMiddlePeriod, /* windowIndex= */ 1, /* positionMs= */ 0) + .sendMessage(targetEndLastPeriod, /* windowIndex= */ 1, /* positionMs= */ duration2Ms) + // Add additional prepare at end and wait until it's processed to ensure that + // messages sent at end of playback are received before test ends. + .waitForPlaybackState(Player.STATE_ENDED) + .prepareSource( + new FakeMediaSource(timeline, null), + /* resetPosition= */ false, + /* resetState= */ true) + .waitForPlaybackState(Player.STATE_READY) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + assertEquals(0, targetStartFirstPeriod.windowIndex); + assertTrue(targetStartFirstPeriod.positionMs >= 0); + assertEquals(0, targetEndMiddlePeriod.windowIndex); + assertTrue(targetEndMiddlePeriod.positionMs >= duration1Ms); + assertEquals(1, targetStartMiddlePeriod.windowIndex); + assertTrue(targetStartMiddlePeriod.positionMs >= 0); + assertEquals(1, targetEndLastPeriod.windowIndex); + assertTrue(targetEndLastPeriod.positionMs >= duration2Ms); + } + + public void testSendMessagesSeekOnDeliveryTimeDuringPreparation() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForPlaybackState(Player.STATE_BUFFERING) + .sendMessage(target, /* positionMs= */ 50) + .seek(/* positionMs= */ 50) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertTrue(target.positionMs >= 50); + } + + public void testSendMessagesSeekOnDeliveryTimeAfterPreparation() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForPlaybackState(Player.STATE_BUFFERING) + .sendMessage(target, /* positionMs= */ 50) + .waitForTimelineChanged(timeline) + .seek(/* positionMs= */ 50) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertTrue(target.positionMs >= 50); + } + + public void testSendMessagesSeekAfterDeliveryTimeDuringPreparation() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForPlaybackState(Player.STATE_BUFFERING) + .sendMessage(target, /* positionMs= */ 50) + .seek(/* positionMs= */ 51) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertEquals(C.POSITION_UNSET, target.positionMs); + } + + public void testSendMessagesSeekAfterDeliveryTimeAfterPreparation() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .sendMessage(target, /* positionMs= */ 50) + .waitForTimelineChanged(timeline) + .seek(/* positionMs= */ 51) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertEquals(C.POSITION_UNSET, target.positionMs); + } + + public void testSendMessagesRepeatDoesNotRepost() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForPlaybackState(Player.STATE_BUFFERING) + .sendMessage(target, /* positionMs= */ 50) + .setRepeatMode(Player.REPEAT_MODE_ALL) + .waitForPositionDiscontinuity() + .setRepeatMode(Player.REPEAT_MODE_OFF) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertEquals(1, target.messageCount); + assertTrue(target.positionMs >= 50); + } + + public void testSendMessagesRepeatWithoutDeletingDoesRepost() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForPlaybackState(Player.STATE_BUFFERING) + .sendMessage( + target, + /* windowIndex= */ 0, + /* positionMs= */ 50, + /* deleteAfterDelivery= */ false) + .setRepeatMode(Player.REPEAT_MODE_ALL) + .waitForPositionDiscontinuity() + .setRepeatMode(Player.REPEAT_MODE_OFF) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertEquals(2, target.messageCount); + assertTrue(target.positionMs >= 50); + } + + public void testSendMessagesMoveCurrentWindowIndex() throws Exception { + Timeline timeline = + new FakeTimeline(new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 0)); + final Timeline secondTimeline = + new FakeTimeline( + new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 1), + new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 0)); + final FakeMediaSource mediaSource = new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT); + PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForTimelineChanged(timeline) + .sendMessage(target, /* positionMs= */ 50) + .executeRunnable( + new Runnable() { + @Override + public void run() { + mediaSource.setNewSourceInfo(secondTimeline, null); + } + }) + .build(); + new ExoPlayerTestRunner.Builder() + .setMediaSource(mediaSource) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertTrue(target.positionMs >= 50); + assertEquals(1, target.windowIndex); + } + + public void testSendMessagesMultiWindowDuringPreparation() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 3); + PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForPlaybackState(Player.STATE_BUFFERING) + .sendMessage(target, /* windowIndex = */ 2, /* positionMs= */ 50) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertEquals(2, target.windowIndex); + assertTrue(target.positionMs >= 50); + } + + public void testSendMessagesMultiWindowAfterPreparation() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 3); + PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForTimelineChanged(timeline) + .sendMessage(target, /* windowIndex = */ 2, /* positionMs= */ 50) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertEquals(2, target.windowIndex); + assertTrue(target.positionMs >= 50); + } + + public void testSendMessagesMoveWindowIndex() throws Exception { + Timeline timeline = + new FakeTimeline( + new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 0), + new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 1)); + final Timeline secondTimeline = + new FakeTimeline( + new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 1), + new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 0)); + final FakeMediaSource mediaSource = new FakeMediaSource(timeline, null, Builder.VIDEO_FORMAT); + PositionGrabbingMessageTarget target = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForTimelineChanged(timeline) + .sendMessage(target, /* windowIndex = */ 1, /* positionMs= */ 50) + .executeRunnable( + new Runnable() { + @Override + public void run() { + mediaSource.setNewSourceInfo(secondTimeline, null); + } + }) + .waitForTimelineChanged(secondTimeline) + .seek(/* windowIndex= */ 0, /* positionMs= */ 0) + .build(); + new ExoPlayerTestRunner.Builder() + .setMediaSource(mediaSource) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertTrue(target.positionMs >= 50); + assertEquals(0, target.windowIndex); + } + + public void testSendMessagesNonLinearPeriodOrder() throws Exception { + Timeline timeline = new FakeTimeline(/* windowCount= */ 3); + PositionGrabbingMessageTarget target1 = new PositionGrabbingMessageTarget(); + PositionGrabbingMessageTarget target2 = new PositionGrabbingMessageTarget(); + PositionGrabbingMessageTarget target3 = new PositionGrabbingMessageTarget(); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("testSendMessages") + .waitForPlaybackState(Player.STATE_BUFFERING) + .sendMessage(target1, /* windowIndex = */ 0, /* positionMs= */ 50) + .sendMessage(target2, /* windowIndex = */ 1, /* positionMs= */ 50) + .sendMessage(target3, /* windowIndex = */ 2, /* positionMs= */ 50) + .waitForTimelineChanged(timeline) + .seek(/* windowIndex= */ 1, /* positionMs= */ 0) + .waitForPositionDiscontinuity() + .seek(/* windowIndex= */ 0, /* positionMs= */ 0) + .build(); + new ExoPlayerTestRunner.Builder() + .setTimeline(timeline) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilEnded(TIMEOUT_MS); + assertEquals(0, target1.windowIndex); + assertEquals(1, target2.windowIndex); + assertEquals(2, target3.windowIndex); + } + + public void testSetAndSwitchSurfaceTest() throws Exception { + final List rendererMessages = new ArrayList<>(); + Renderer videoRenderer = + new FakeRenderer(Builder.VIDEO_FORMAT) { + @Override + public void handleMessage(int what, Object object) throws ExoPlaybackException { + super.handleMessage(what, object); + rendererMessages.add(what); + } + }; + final Surface surface1 = DummySurface.newInstanceV17(/* context= */ null, /* secure= */ false); + final Surface surface2 = DummySurface.newInstanceV17(/* context= */ null, /* secure= */ false); + ActionSchedule actionSchedule = + new ActionSchedule.Builder("setAndSwitchSurfaceTest") + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + player.setVideoSurface(surface1); + } + }) + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + player.setVideoSurface(surface2); + } + }) + .build(); + new ExoPlayerTestRunner.Builder() + .setRenderers(videoRenderer) + .setActionSchedule(actionSchedule) + .build() + .start() + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + assertEquals(2, Collections.frequency(rendererMessages, C.MSG_SET_SURFACE)); + } + + private static final class PositionGrabbingMessageTarget extends PlayerTarget { + + public int windowIndex; + public long positionMs; + public int messageCount; + + public PositionGrabbingMessageTarget() { + windowIndex = C.INDEX_UNSET; + positionMs = C.POSITION_UNSET; + } + + @Override + public void handleMessage(SimpleExoPlayer player, int messageType, Object message) { + windowIndex = player.getCurrentWindowIndex(); + positionMs = player.getCurrentPosition(); + messageCount++; + } + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java index a4103787d1..8ee9a13c55 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java @@ -157,7 +157,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { return ADAPTIVE_NOT_SUPPORTED; } - // ExoPlayerComponent implementation. + // PlayerMessage.Target implementation. @Override public void handleMessage(int what, Object object) throws ExoPlaybackException { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java index cc767752be..4bd28150bc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java @@ -34,40 +34,43 @@ import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; /** - * An extensible media player that plays {@link MediaSource}s. Instances can be obtained from - * {@link ExoPlayerFactory}. + * An extensible media player that plays {@link MediaSource}s. Instances can be obtained from {@link + * ExoPlayerFactory}. * *

          Player components

          + * *

          ExoPlayer is designed to make few assumptions about (and hence impose few restrictions on) the * type of the media being played, how and where it is stored, and how it is rendered. Rather than * implementing the loading and rendering of media directly, ExoPlayer implementations delegate this * work to components that are injected when a player is created or when it's prepared for playback. * Components common to all ExoPlayer implementations are: + * *

            *
          • 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(MediaSource)} - * at the start of playback. The library modules provide default implementations for regular media - * files ({@link ExtractorMediaSource}), DASH (DashMediaSource), SmoothStreaming (SsMediaSource) - * and HLS (HlsMediaSource), an implementation for loading single media samples - * ({@link SingleSampleMediaSource}) that's most often used for side-loaded subtitle files, and - * implementations for building more complex MediaSources from simpler ones - * ({@link MergingMediaSource}, {@link ConcatenatingMediaSource}, - * {@link DynamicConcatenatingMediaSource}, {@link LoopingMediaSource} and - * {@link ClippingMediaSource}).
          • + * which the loaded media can be read. A MediaSource is injected via {@link + * #prepare(MediaSource)} at the start of playback. The library modules provide default + * implementations for regular media files ({@link ExtractorMediaSource}), DASH + * (DashMediaSource), SmoothStreaming (SsMediaSource) and HLS (HlsMediaSource), an + * implementation for loading single media samples ({@link SingleSampleMediaSource}) that's + * most often used for side-loaded subtitle files, and implementations for building more + * complex MediaSources from simpler ones ({@link MergingMediaSource}, {@link + * ConcatenatingMediaSource}, {@link DynamicConcatenatingMediaSource}, {@link + * LoopingMediaSource} and {@link ClippingMediaSource}). *
          • {@link Renderer}s that render individual components of the media. The library - * provides default implementations for common media types ({@link MediaCodecVideoRenderer}, - * {@link MediaCodecAudioRenderer}, {@link TextRenderer} and {@link MetadataRenderer}). A Renderer - * consumes media from the MediaSource being played. Renderers are injected when the player is - * created.
          • + * provides default implementations for common media types ({@link MediaCodecVideoRenderer}, + * {@link MediaCodecAudioRenderer}, {@link TextRenderer} and {@link MetadataRenderer}). A + * Renderer consumes media from the MediaSource being played. Renderers are injected when the + * player is created. *
          • A {@link TrackSelector} that selects tracks provided by the MediaSource to be - * consumed by each of the available Renderers. The library provides a default implementation - * ({@link DefaultTrackSelector}) suitable for most use cases. A TrackSelector is injected when - * the player is created.
          • + * consumed by each of the available Renderers. The library provides a default implementation + * ({@link DefaultTrackSelector}) suitable for most use cases. A TrackSelector is injected + * when the player is created. *
          • A {@link LoadControl} that controls when the MediaSource buffers more media, and how - * much media is buffered. The library provides a default implementation - * ({@link DefaultLoadControl}) suitable for most use cases. A LoadControl is injected when the - * player is created.
          • + * much media is buffered. The library provides a default implementation ({@link + * DefaultLoadControl}) suitable for most use cases. A LoadControl is injected when the player + * is created. *
          + * *

          An ExoPlayer can be built using the default components provided by the library, but may also * be built using custom implementations if non-standard behaviors are required. For example a * custom LoadControl could be injected to change the player's buffering strategy, or a custom @@ -81,30 +84,32 @@ import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; * it's possible to load data from a non-standard source, or through a different network stack. * *

          Threading model

          - *

          The figure below shows ExoPlayer's threading model.

          - *

          - * ExoPlayer's threading model - *

          + * + *

          The figure below shows ExoPlayer's threading model. + * + *

          ExoPlayer's threading
+ * model * *

            - *
          • It is recommended that ExoPlayer instances are created and accessed from a single application - * thread. The application's main thread is ideal. Accessing an instance from multiple threads is - * discouraged, however if an application does wish to do this then it may do so provided that it - * ensures accesses are synchronized.
          • - *
          • Registered listeners are called on the thread that created the ExoPlayer instance, unless - * the thread that created the ExoPlayer instance does not have a {@link Looper}. In that case, - * registered listeners will be called on the application's main thread.
          • - *
          • An internal playback thread is responsible for playback. Injected player components such as - * Renderers, MediaSources, TrackSelectors and LoadControls are called by the player on this - * thread.
          • - *
          • When the application performs an operation on the player, for example a seek, a message is - * delivered to the internal playback thread via a message queue. The internal playback thread - * consumes messages from the queue and performs the corresponding operations. Similarly, when a - * playback event occurs on the internal playback thread, a message is delivered to the application - * thread via a second message queue. The application thread consumes messages from the queue, - * updating the application visible state and calling corresponding listener methods.
          • - *
          • Injected player components may use additional background threads. For example a MediaSource - * may use background threads to load data. These are implementation specific.
          • + *
          • It is recommended that ExoPlayer instances are created and accessed from a single + * application thread. The application's main thread is ideal. Accessing an instance from + * multiple threads is discouraged, however if an application does wish to do this then it may + * do so provided that it ensures accesses are synchronized. + *
          • Registered listeners are called on the thread that created the ExoPlayer instance, unless + * the thread that created the ExoPlayer instance does not have a {@link Looper}. In that + * case, registered listeners will be called on the application's main thread. + *
          • An internal playback thread is responsible for playback. Injected player components such as + * Renderers, MediaSources, TrackSelectors and LoadControls are called by the player on this + * thread. + *
          • When the application performs an operation on the player, for example a seek, a message is + * delivered to the internal playback thread via a message queue. The internal playback thread + * consumes messages from the queue and performs the corresponding operations. Similarly, when + * a playback event occurs on the internal playback thread, a message is delivered to the + * application thread via a second message queue. The application thread consumes messages + * from the queue, updating the application visible state and calling corresponding listener + * methods. + *
          • Injected player components may use additional background threads. For example a MediaSource + * may use background threads to load data. These are implementation specific. *
          */ public interface ExoPlayer extends Player { @@ -115,54 +120,28 @@ public interface ExoPlayer extends Player { @Deprecated interface EventListener extends Player.EventListener {} - /** - * A component of an {@link ExoPlayer} that can receive messages on the playback thread. - *

          - * Messages can be delivered to a component via {@link #sendMessages} and - * {@link #blockingSendMessages}. - */ - interface ExoPlayerComponent { + /** @deprecated Use {@link PlayerMessage.Target} instead. */ + @Deprecated + interface ExoPlayerComponent extends PlayerMessage.Target {} - /** - * Handles a message delivered to the component. Called on the playback thread. - * - * @param messageType The message type. - * @param message The message. - * @throws ExoPlaybackException If an error occurred whilst handling the message. - */ - void handleMessage(int messageType, Object message) throws ExoPlaybackException; - - } - - /** - * Defines a message and a target {@link ExoPlayerComponent} to receive it. - */ + /** @deprecated Use {@link PlayerMessage} instead. */ + @Deprecated final class ExoPlayerMessage { - /** - * The target to receive the message. - */ - public final ExoPlayerComponent target; - /** - * The type of the message. - */ + /** The target to receive the message. */ + public final PlayerMessage.Target target; + /** The type of the message. */ public final int messageType; - /** - * The message. - */ + /** The message. */ public final Object message; - /** - * @param target The target of the message. - * @param messageType The message type. - * @param message The message. - */ - public ExoPlayerMessage(ExoPlayerComponent target, int messageType, Object message) { + /** @deprecated Use {@link ExoPlayer#createMessage(PlayerMessage.Target)} instead. */ + @Deprecated + public ExoPlayerMessage(PlayerMessage.Target target, int messageType, Object message) { this.target = target; this.messageType = messageType; this.message = message; } - } /** @@ -236,20 +215,25 @@ public interface ExoPlayer extends Player { void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState); /** - * Sends messages to their target components. The messages are delivered on the playback thread. - * If a component throws an {@link ExoPlaybackException} then it is propagated out of the player - * as an error. - * - * @param messages The messages to be sent. + * Creates a message that can be sent to a {@link PlayerMessage.Target}. By default, the message + * will be delivered immediately without blocking on the playback thread. The default {@link + * PlayerMessage#getType()} is 0 and the default {@link PlayerMessage#getMessage()} is null. If a + * position is specified with {@link PlayerMessage#setPosition(long)}, the message will be + * delivered at this position in the current window defined by {@link #getCurrentWindowIndex()}. + * Alternatively, the message can be sent at a specific window using {@link + * PlayerMessage#setPosition(int, long)}. */ + PlayerMessage createMessage(PlayerMessage.Target target); + + /** @deprecated Use {@link #createMessage(PlayerMessage.Target)} instead. */ + @Deprecated void sendMessages(ExoPlayerMessage... messages); /** - * Variant of {@link #sendMessages(ExoPlayerMessage...)} that blocks until after the messages have - * been delivered. - * - * @param messages The messages to be sent. + * @deprecated Use {@link #createMessage(PlayerMessage.Target)} with {@link + * PlayerMessage#blockUntilDelivered()}. */ + @Deprecated void blockingSendMessages(ExoPlayerMessage... messages); /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index 2869a7668e..afb6428fa5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -22,6 +22,7 @@ import android.os.Message; import android.support.annotation.Nullable; import android.util.Log; import android.util.Pair; +import com.google.android.exoplayer2.PlayerMessage.Target; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.source.TrackGroupArray; @@ -31,6 +32,8 @@ 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.Util; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.CopyOnWriteArraySet; /** @@ -45,6 +48,7 @@ import java.util.concurrent.CopyOnWriteArraySet; private final TrackSelectorResult emptyTrackSelectorResult; private final Handler eventHandler; private final ExoPlayerImplInternal internalPlayer; + private final Handler internalPlayerHandler; private final CopyOnWriteArraySet listeners; private final Timeline.Window window; private final Timeline.Period period; @@ -113,6 +117,7 @@ import java.util.concurrent.CopyOnWriteArraySet; shuffleModeEnabled, eventHandler, this); + internalPlayerHandler = new Handler(internalPlayer.getPlaybackLooper()); } @Override @@ -326,12 +331,47 @@ import java.util.concurrent.CopyOnWriteArraySet; @Override public void sendMessages(ExoPlayerMessage... messages) { - internalPlayer.sendMessages(messages); + for (ExoPlayerMessage message : messages) { + createMessage(message.target).setType(message.messageType).setMessage(message.message).send(); + } + } + + @Override + public PlayerMessage createMessage(Target target) { + return new PlayerMessage( + internalPlayer, + target, + playbackInfo.timeline, + getCurrentWindowIndex(), + internalPlayerHandler); } @Override public void blockingSendMessages(ExoPlayerMessage... messages) { - internalPlayer.blockingSendMessages(messages); + List playerMessages = new ArrayList<>(); + for (ExoPlayerMessage message : messages) { + playerMessages.add( + createMessage(message.target) + .setType(message.messageType) + .setMessage(message.message) + .send()); + } + boolean wasInterrupted = false; + for (PlayerMessage message : playerMessages) { + boolean blockMessage = true; + while (blockMessage) { + try { + message.blockUntilDelivered(); + blockMessage = false; + } catch (InterruptedException e) { + wasInterrupted = true; + } + } + } + if (wasInterrupted) { + // Restore the interrupted status. + Thread.currentThread().interrupt(); + } } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 09b3231467..f3d0e1794b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -22,10 +22,10 @@ import android.os.Message; import android.os.Process; import android.os.SystemClock; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.util.Log; import android.util.Pair; import com.google.android.exoplayer2.DefaultMediaClock.PlaybackParameterListener; -import com.google.android.exoplayer2.ExoPlayer.ExoPlayerMessage; import com.google.android.exoplayer2.MediaPeriodInfoSequence.MediaPeriodInfo; import com.google.android.exoplayer2.Player.DiscontinuityReason; import com.google.android.exoplayer2.source.ClippingMediaPeriod; @@ -40,14 +40,19 @@ 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.TraceUtil; +import com.google.android.exoplayer2.util.Util; import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; -/** - * Implements the internal behavior of {@link ExoPlayerImpl}. - */ -/* package */ final class ExoPlayerImplInternal implements Handler.Callback, - MediaPeriod.Callback, TrackSelector.InvalidationListener, MediaSource.Listener, - PlaybackParameterListener { +/** Implements the internal behavior of {@link ExoPlayerImpl}. */ +/* package */ final class ExoPlayerImplInternal + implements Handler.Callback, + MediaPeriod.Callback, + TrackSelector.InvalidationListener, + MediaSource.Listener, + PlaybackParameterListener, + PlayerMessage.Sender { private static final String TAG = "ExoPlayerImplInternal"; @@ -108,6 +113,7 @@ import java.io.IOException; private final boolean retainBackBufferFromKeyframe; private final DefaultMediaClock mediaClock; private final PlaybackInfoUpdate playbackInfoUpdate; + private final ArrayList customMessageInfos; @SuppressWarnings("unused") private SeekParameters seekParameters; @@ -120,13 +126,12 @@ import java.io.IOException; private boolean rebuffering; private @Player.RepeatMode int repeatMode; private boolean shuffleModeEnabled; - private int customMessagesSent; - private int customMessagesProcessed; private long elapsedRealtimeUs; private int pendingPrepareCount; private SeekPosition pendingInitialSeekPosition; private long rendererPositionUs; + private int nextCustomMessageInfoIndex; private MediaPeriodHolder loadingPeriodHolder; private MediaPeriodHolder readingPeriodHolder; @@ -166,6 +171,7 @@ import java.io.IOException; rendererCapabilities[i] = renderers[i].getCapabilities(); } mediaClock = new DefaultMediaClock(this); + customMessageInfos = new ArrayList<>(); enabledRenderers = new Renderer[0]; window = new Timeline.Window(); period = new Timeline.Period(); @@ -214,34 +220,15 @@ import java.io.IOException; handler.obtainMessage(MSG_STOP, reset ? 1 : 0, 0).sendToTarget(); } - public void sendMessages(ExoPlayerMessage... messages) { + @Override + public synchronized void sendMessage( + PlayerMessage message, PlayerMessage.Sender.Listener listener) { if (released) { Log.w(TAG, "Ignoring messages sent after release."); + listener.onMessageDeleted(); return; } - customMessagesSent++; - handler.obtainMessage(MSG_CUSTOM, messages).sendToTarget(); - } - - public synchronized void blockingSendMessages(ExoPlayerMessage... messages) { - if (released) { - Log.w(TAG, "Ignoring messages sent after release."); - return; - } - int messageNumber = customMessagesSent++; - handler.obtainMessage(MSG_CUSTOM, messages).sendToTarget(); - boolean wasInterrupted = false; - while (customMessagesProcessed <= messageNumber) { - try { - wait(); - } catch (InterruptedException e) { - wasInterrupted = true; - } - } - if (wasInterrupted) { - // Restore the interrupted status. - Thread.currentThread().interrupt(); - } + handler.obtainMessage(MSG_CUSTOM, new CustomMessageInfo(message, listener)).sendToTarget(); } public synchronized void release() { @@ -349,7 +336,7 @@ import java.io.IOException; reselectTracksInternal(); break; case MSG_CUSTOM: - sendMessagesInternal((ExoPlayerMessage[]) msg.obj); + sendMessageInternal((CustomMessageInfo) msg.obj); break; case MSG_RELEASE: releaseInternal(); @@ -537,8 +524,9 @@ import java.io.IOException; } else { rendererPositionUs = mediaClock.syncAndGetPositionUs(); periodPositionUs = playingPeriodHolder.toPeriodTime(rendererPositionUs); + maybeTriggerCustomMessages(playbackInfo.positionUs, periodPositionUs); + playbackInfo.positionUs = periodPositionUs; } - playbackInfo.positionUs = periodPositionUs; elapsedRealtimeUs = SystemClock.elapsedRealtime() * 1000; // Update the buffered position. @@ -656,7 +644,8 @@ import java.io.IOException; boolean seekPositionAdjusted = seekPosition.windowPositionUs == C.TIME_UNSET; try { - Pair periodPosition = resolveSeekPosition(seekPosition); + Pair periodPosition = + resolveSeekPosition(seekPosition, /* trySubsequentPeriods= */ true); if (periodPosition == null) { // The seek position was valid for the timeline that it was performed into, but the // timeline has changed and a suitable seek position could not be resolved in the new one. @@ -850,6 +839,11 @@ import java.io.IOException; } if (resetState) { mediaPeriodInfoSequence.setTimeline(null); + for (CustomMessageInfo customMessageInfo : customMessageInfos) { + customMessageInfo.listener.onMessageDeleted(); + } + customMessageInfos.clear(); + nextCustomMessageInfoIndex = 0; } playbackInfo = new PlaybackInfo( @@ -870,21 +864,153 @@ import java.io.IOException; } } - private void sendMessagesInternal(ExoPlayerMessage[] messages) throws ExoPlaybackException { - try { - for (ExoPlayerMessage message : messages) { - message.target.handleMessage(message.messageType, message.message); + private void sendMessageInternal(CustomMessageInfo customMessageInfo) { + if (customMessageInfo.message.getPositionMs() == C.TIME_UNSET) { + // If no delivery time is specified, trigger immediate message delivery. + sendCustomMessagesToTarget(customMessageInfo); + } else if (playbackInfo.timeline == null) { + // Still waiting for initial timeline to resolve position. + customMessageInfos.add(customMessageInfo); + } else { + if (resolveCustomMessagePosition(customMessageInfo)) { + customMessageInfos.add(customMessageInfo); + // Ensure new message is inserted according to playback order. + Collections.sort(customMessageInfos); + } else { + customMessageInfo.listener.onMessageDeleted(); } - if (playbackInfo.playbackState == Player.STATE_READY - || playbackInfo.playbackState == Player.STATE_BUFFERING) { - // The message may have caused something to change that now requires us to do work. - handler.sendEmptyMessage(MSG_DO_SOME_WORK); + } + } + + private void sendCustomMessagesToTarget(final CustomMessageInfo customMessageInfo) { + final Runnable handleMessageRunnable = + new Runnable() { + @Override + public void run() { + try { + customMessageInfo + .message + .getTarget() + .handleMessage( + customMessageInfo.message.getType(), customMessageInfo.message.getMessage()); + } catch (ExoPlaybackException e) { + eventHandler.obtainMessage(MSG_ERROR, e).sendToTarget(); + } finally { + customMessageInfo.listener.onMessageDelivered(); + if (customMessageInfo.message.getDeleteAfterDelivery()) { + customMessageInfo.listener.onMessageDeleted(); + } + // The message may have caused something to change that now requires us to do + // work. + handler.sendEmptyMessage(MSG_DO_SOME_WORK); + } + } + }; + handler.post( + new Runnable() { + @Override + public void run() { + customMessageInfo.message.getHandler().post(handleMessageRunnable); + } + }); + } + + private void resolveCustomMessagePositions() { + for (int i = customMessageInfos.size() - 1; i >= 0; i--) { + if (!resolveCustomMessagePosition(customMessageInfos.get(i))) { + // Remove messages if new position can't be resolved. + customMessageInfos.get(i).listener.onMessageDeleted(); + customMessageInfos.remove(i); } - } finally { - synchronized (this) { - customMessagesProcessed++; - notifyAll(); + } + // Re-sort messages by playback order. + Collections.sort(customMessageInfos); + } + + private boolean resolveCustomMessagePosition(CustomMessageInfo customMessageInfo) { + if (customMessageInfo.resolvedPeriodUid == null) { + // Position is still unresolved. Try to find window in current timeline. + Pair periodPosition = + resolveSeekPosition( + new SeekPosition( + customMessageInfo.message.getTimeline(), + customMessageInfo.message.getWindowIndex(), + C.msToUs(customMessageInfo.message.getPositionMs())), + /* trySubsequentPeriods= */ false); + if (periodPosition == null) { + return false; } + customMessageInfo.setResolvedPosition( + periodPosition.first, + periodPosition.second, + playbackInfo.timeline.getPeriod(periodPosition.first, period, true).uid); + } else { + // Position has been resolved for a previous timeline. Try to find the updated period index. + int index = playbackInfo.timeline.getIndexOfPeriod(customMessageInfo.resolvedPeriodUid); + if (index == C.INDEX_UNSET) { + return false; + } + customMessageInfo.resolvedPeriodIndex = index; + } + return true; + } + + private void maybeTriggerCustomMessages(long oldPeriodPositionUs, long newPeriodPositionUs) { + if (customMessageInfos.isEmpty() || playbackInfo.periodId.isAd()) { + return; + } + // If this is the first call from the start position, include oldPeriodPositionUs in potential + // trigger positions. + if (playbackInfo.startPositionUs == oldPeriodPositionUs) { + oldPeriodPositionUs--; + } + // Correct next index if necessary (e.g. after seeking, timeline changes, or new messages) + int currentPeriodIndex = playbackInfo.periodId.periodIndex; + CustomMessageInfo prevInfo = + nextCustomMessageInfoIndex > 0 + ? customMessageInfos.get(nextCustomMessageInfoIndex - 1) + : null; + while (prevInfo != null + && (prevInfo.resolvedPeriodIndex > currentPeriodIndex + || (prevInfo.resolvedPeriodIndex == currentPeriodIndex + && prevInfo.resolvedPeriodTimeUs > oldPeriodPositionUs))) { + nextCustomMessageInfoIndex--; + prevInfo = + nextCustomMessageInfoIndex > 0 + ? customMessageInfos.get(nextCustomMessageInfoIndex - 1) + : null; + } + CustomMessageInfo nextInfo = + nextCustomMessageInfoIndex < customMessageInfos.size() + ? customMessageInfos.get(nextCustomMessageInfoIndex) + : null; + while (nextInfo != null + && nextInfo.resolvedPeriodUid != null + && (nextInfo.resolvedPeriodIndex < currentPeriodIndex + || (nextInfo.resolvedPeriodIndex == currentPeriodIndex + && nextInfo.resolvedPeriodTimeUs <= oldPeriodPositionUs))) { + nextCustomMessageInfoIndex++; + nextInfo = + nextCustomMessageInfoIndex < customMessageInfos.size() + ? customMessageInfos.get(nextCustomMessageInfoIndex) + : null; + } + // Check if any message falls within the covered time span. + while (nextInfo != null + && nextInfo.resolvedPeriodUid != null + && nextInfo.resolvedPeriodIndex == currentPeriodIndex + && nextInfo.resolvedPeriodTimeUs > oldPeriodPositionUs + && nextInfo.resolvedPeriodTimeUs <= newPeriodPositionUs) { + sendCustomMessagesToTarget(nextInfo); + if (nextInfo.message.getDeleteAfterDelivery()) { + customMessageInfos.remove(nextCustomMessageInfoIndex); + } else { + nextCustomMessageInfoIndex++; + } + nextInfo = + nextCustomMessageInfoIndex < customMessageInfos.size() + ? customMessageInfos.get(nextCustomMessageInfoIndex) + : null; } } @@ -1034,12 +1160,14 @@ import java.io.IOException; Object manifest = sourceRefreshInfo.manifest; mediaPeriodInfoSequence.setTimeline(timeline); playbackInfo = playbackInfo.copyWithTimeline(timeline, manifest); + resolveCustomMessagePositions(); if (oldTimeline == null) { playbackInfoUpdate.incrementPendingOperationAcks(pendingPrepareCount); pendingPrepareCount = 0; if (pendingInitialSeekPosition != null) { - Pair periodPosition = resolveSeekPosition(pendingInitialSeekPosition); + Pair periodPosition = + resolveSeekPosition(pendingInitialSeekPosition, /* trySubsequentPeriods= */ true); pendingInitialSeekPosition = null; if (periodPosition == null) { // The seek position was valid for the timeline that it was performed into, but the @@ -1224,11 +1352,14 @@ import java.io.IOException; * internal timeline. * * @param seekPosition The position to resolve. + * @param trySubsequentPeriods Whether the position can be resolved to a subsequent matching + * period if the original period is no longer available. * @return The resolved position, or null if resolution was not successful. * @throws IllegalSeekPositionException If the window index of the seek position is outside the * bounds of the timeline. */ - private Pair resolveSeekPosition(SeekPosition seekPosition) { + private Pair resolveSeekPosition( + SeekPosition seekPosition, boolean trySubsequentPeriods) { Timeline timeline = playbackInfo.timeline; Timeline seekTimeline = seekPosition.timeline; if (seekTimeline.isEmpty()) { @@ -1257,12 +1388,14 @@ import java.io.IOException; // We successfully located the period in the internal timeline. return Pair.create(periodIndex, periodPosition.second); } - // Try and find a subsequent period from the seek timeline in the internal timeline. - periodIndex = resolveSubsequentPeriod(periodPosition.first, seekTimeline, timeline); - if (periodIndex != C.INDEX_UNSET) { - // We found one. Map the SeekPosition onto the corresponding default position. - return getPeriodPosition(timeline, timeline.getPeriod(periodIndex, period).windowIndex, - C.TIME_UNSET); + if (trySubsequentPeriods) { + // Try and find a subsequent period from the seek timeline in the internal timeline. + periodIndex = resolveSubsequentPeriod(periodPosition.first, seekTimeline, timeline); + if (periodIndex != C.INDEX_UNSET) { + // We found one. Map the SeekPosition onto the corresponding default position. + return getPeriodPosition( + timeline, timeline.getPeriod(periodIndex, period).windowIndex, C.TIME_UNSET); + } } // We didn't find one. Give up. return null; @@ -1802,7 +1935,45 @@ import java.io.IOException; this.windowIndex = windowIndex; this.windowPositionUs = windowPositionUs; } + } + private static final class CustomMessageInfo implements Comparable { + + public final PlayerMessage message; + public final PlayerMessage.Sender.Listener listener; + + public int resolvedPeriodIndex; + public long resolvedPeriodTimeUs; + public @Nullable Object resolvedPeriodUid; + + public CustomMessageInfo(PlayerMessage message, PlayerMessage.Sender.Listener listener) { + this.message = message; + this.listener = listener; + } + + public void setResolvedPosition(int periodIndex, long periodTimeUs, Object periodUid) { + resolvedPeriodIndex = periodIndex; + resolvedPeriodTimeUs = periodTimeUs; + resolvedPeriodUid = periodUid; + } + + @Override + public int compareTo(@NonNull CustomMessageInfo other) { + if ((resolvedPeriodUid == null) != (other.resolvedPeriodUid == null)) { + // CustomMessageInfos with a resolved period position are always smaller. + return resolvedPeriodUid != null ? -1 : 1; + } + if (resolvedPeriodUid == null) { + // Don't sort message with unresolved positions. + return 0; + } + // Sort resolved media times by period index and then by period position. + int comparePeriodIndex = resolvedPeriodIndex - other.resolvedPeriodIndex; + if (comparePeriodIndex != 0) { + return comparePeriodIndex; + } + return Util.compareLong(resolvedPeriodTimeUs, other.resolvedPeriodTimeUs); + } } private static final class MediaSourceRefreshInfo { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java index 978f4f7a97..593d3d1fce 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java @@ -179,7 +179,7 @@ public abstract class NoSampleRenderer implements Renderer, RendererCapabilities return ADAPTIVE_NOT_SUPPORTED; } - // ExoPlayerComponent implementation. + // PlayerMessage.Target implementation. @Override public void handleMessage(int what, Object object) throws ExoPlaybackException { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java b/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java new file mode 100644 index 0000000000..420eb60a48 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java @@ -0,0 +1,295 @@ +/* + * 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.os.Handler; +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.util.Assertions; + +/** + * Defines a player message which can be sent with a {@link Sender} and received by a {@link + * Target}. + */ +public final class PlayerMessage { + + /** A target for messages. */ + public interface Target { + + /** + * Handles a message delivered to the target. + * + * @param messageType The message type. + * @param message The message. + * @throws ExoPlaybackException If an error occurred whilst handling the message. + */ + void handleMessage(int messageType, Object message) throws ExoPlaybackException; + } + + /** A sender for messages. */ + public interface Sender { + + /** A listener for message events triggered by the sender. */ + interface Listener { + + /** Called when the message has been delivered. */ + void onMessageDelivered(); + + /** Called when the message has been deleted. */ + void onMessageDeleted(); + } + + /** + * Sends a message. + * + * @param message The message to be sent. + * @param listener The listener to listen to message events. + */ + void sendMessage(PlayerMessage message, Listener listener); + } + + private final Target target; + private final Sender sender; + private final Timeline timeline; + + private int type; + private Object message; + private Handler handler; + private int windowIndex; + private long positionMs; + private boolean deleteAfterDelivery; + private boolean isSent; + private boolean isDelivered; + private boolean isDeleted; + + /** + * Creates a new message. + * + * @param sender The {@link Sender} used to send the message. + * @param target The {@link Target} the message is sent to. + * @param timeline The timeline used when setting the position with {@link #setPosition(long)}. If + * set to {@link Timeline#EMPTY}, any position can be specified. + * @param defaultWindowIndex The default window index in the {@code timeline} when no other window + * index is specified. + * @param defaultHandler The default handler to send the message on when no other handler is + * specified. + */ + public PlayerMessage( + Sender sender, + Target target, + Timeline timeline, + int defaultWindowIndex, + Handler defaultHandler) { + this.sender = sender; + this.target = target; + this.timeline = timeline; + this.handler = defaultHandler; + this.windowIndex = defaultWindowIndex; + this.positionMs = C.TIME_UNSET; + this.deleteAfterDelivery = true; + } + + /** Returns the timeline used for setting the position with {@link #setPosition(long)}. */ + public Timeline getTimeline() { + return timeline; + } + + /** Returns the target the message is sent to. */ + public Target getTarget() { + return target; + } + + /** + * Sets a custom message type forwarded to the {@link Target#handleMessage(int, Object)}. + * + * @param messageType The custom message type. + * @return This message. + * @throws IllegalStateException If {@link #send()} has already been called. + */ + public PlayerMessage setType(int messageType) { + Assertions.checkState(!isSent); + this.type = messageType; + return this; + } + + /** Returns custom message type forwarded to the {@link Target#handleMessage(int, Object)}. */ + public int getType() { + return type; + } + + /** + * Sets a custom message forwarded to the {@link Target#handleMessage(int, Object)}. + * + * @param message The custom message. + * @return This message. + * @throws IllegalStateException If {@link #send()} has already been called. + */ + public PlayerMessage setMessage(@Nullable Object message) { + Assertions.checkState(!isSent); + this.message = message; + return this; + } + + /** Returns custom message forwarded to the {@link Target#handleMessage(int, Object)}. */ + public Object getMessage() { + return message; + } + + /** + * Sets the handler the message is delivered on. + * + * @param handler A {@link Handler}. + * @return This message. + * @throws IllegalStateException If {@link #send()} has already been called. + */ + public PlayerMessage setHandler(Handler handler) { + Assertions.checkState(!isSent); + this.handler = handler; + return this; + } + + /** Returns the handler the message is delivered on. */ + public Handler getHandler() { + return handler; + } + + /** + * Sets a position in the current window at which the message will be delivered. + * + * @param positionMs The position in the current window at which the message will be sent, in + * milliseconds. + * @return This message. + * @throws IllegalStateException If {@link #send()} has already been called. + */ + public PlayerMessage setPosition(long positionMs) { + Assertions.checkState(!isSent); + this.positionMs = positionMs; + return this; + } + + /** + * Returns position in window at {@link #getWindowIndex()} at which the message will be delivered, + * in milliseconds. If {@link C#TIME_UNSET}, the message will be delivered immediately. + */ + public long getPositionMs() { + return positionMs; + } + + /** + * Sets a position in a window at which the message will be delivered. + * + * @param windowIndex The index of the window at which the message will be sent. + * @param positionMs The position in the window with index {@code windowIndex} at which the + * message will be sent, in milliseconds. + * @return This message. + * @throws IllegalSeekPositionException If the timeline returned by {@link #getTimeline()} is not + * empty and the provided window index is not within the bounds of the timeline. + * @throws IllegalStateException If {@link #send()} has already been called. + */ + public PlayerMessage setPosition(int windowIndex, long positionMs) { + Assertions.checkState(!isSent); + Assertions.checkArgument(positionMs != C.TIME_UNSET); + if (windowIndex < 0 || (!timeline.isEmpty() && windowIndex >= timeline.getWindowCount())) { + throw new IllegalSeekPositionException(timeline, windowIndex, positionMs); + } + this.windowIndex = windowIndex; + this.positionMs = positionMs; + return this; + } + + /** Returns window index at which the message will be delivered. */ + public int getWindowIndex() { + return windowIndex; + } + + /** + * Sets whether the message will be deleted after delivery. If false, the message will be resent + * if playback reaches the specified position again. Only allowed to be false if a position is set + * with {@link #setPosition(long)}. + * + * @param deleteAfterDelivery Whether the message is deleted after delivery. + * @return This message. + * @throws IllegalStateException If {@link #send()} has already been called. + */ + public PlayerMessage setDeleteAfterDelivery(boolean deleteAfterDelivery) { + Assertions.checkState(!isSent); + this.deleteAfterDelivery = deleteAfterDelivery; + return this; + } + + /** Returns whether the message will be deleted after delivery. */ + public boolean getDeleteAfterDelivery() { + return deleteAfterDelivery; + } + + /** + * Sends the message. If the target throws an {@link ExoPlaybackException} then it is propagated + * out of the player as an error using {@link + * Player.EventListener#onPlayerError(ExoPlaybackException)}. + * + * @return This message. + * @throws IllegalStateException If {@link #send()} has already been called. + */ + public PlayerMessage send() { + Assertions.checkState(!isSent); + if (positionMs == C.TIME_UNSET) { + Assertions.checkArgument(deleteAfterDelivery); + } + isSent = true; + sender.sendMessage( + this, + new Sender.Listener() { + @Override + public void onMessageDelivered() { + synchronized (PlayerMessage.this) { + isDelivered = true; + PlayerMessage.this.notifyAll(); + } + } + + @Override + public void onMessageDeleted() { + synchronized (PlayerMessage.this) { + isDeleted = true; + PlayerMessage.this.notifyAll(); + } + } + }); + return this; + } + + /** + * Blocks until after the message has been delivered or the player is no longer able to deliver + * the message. + * + *

          Note that this method can't be called if the current thread is the same thread used by the + * message handler set with {@link #setHandler(Handler)} as it would cause a deadlock. + * + * @return Whether the message was delivered successfully. + * @throws IllegalStateException If this method is called before {@link #send()}. + * @throws IllegalStateException If this method is called on the same thread used by the message + * handler set with {@link #setHandler(Handler)}. + * @throws InterruptedException If the current thread is interrupted while waiting for the message + * to be delivered. + */ + public synchronized boolean blockUntilDelivered() throws InterruptedException { + Assertions.checkState(isSent); + Assertions.checkState(handler.getLooper().getThread() != Thread.currentThread()); + while (!isDelivered && !isDeleted) { + wait(); + } + return isDelivered; + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java b/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java index 6def1591da..d0a07930e0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java @@ -15,22 +15,20 @@ */ package com.google.android.exoplayer2; -import com.google.android.exoplayer2.ExoPlayer.ExoPlayerComponent; import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.util.MediaClock; import java.io.IOException; /** * Renders media read from a {@link SampleStream}. - *

          - * Internally, a renderer's lifecycle is managed by the owning {@link ExoPlayer}. The renderer is + * + *

          Internally, a renderer's lifecycle is managed by the owning {@link ExoPlayer}. The renderer is * transitioned through various states as the overall playback state changes. The valid state * transitions are shown below, annotated with the methods that are called during each transition. - *

          - * Renderer state transitions - *

          + * + *

          Renderer state transitions */ -public interface Renderer extends ExoPlayerComponent { +public interface Renderer extends PlayerMessage.Target { /** * The renderer is disabled. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 69369d4229..e2d0ed1422 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -93,8 +93,6 @@ public class SimpleExoPlayer implements ExoPlayer { private final CopyOnWriteArraySet metadataOutputs; private final CopyOnWriteArraySet videoDebugListeners; private final CopyOnWriteArraySet audioDebugListeners; - private final int videoRendererCount; - private final int audioRendererCount; private Format videoFormat; private Format audioFormat; @@ -124,25 +122,6 @@ public class SimpleExoPlayer implements ExoPlayer { renderers = renderersFactory.createRenderers(eventHandler, componentListener, componentListener, componentListener, componentListener); - // Obtain counts of video and audio renderers. - int videoRendererCount = 0; - int audioRendererCount = 0; - for (Renderer renderer : renderers) { - switch (renderer.getTrackType()) { - case C.TRACK_TYPE_VIDEO: - videoRendererCount++; - break; - case C.TRACK_TYPE_AUDIO: - audioRendererCount++; - break; - default: - // Don't count other track types. - break; - } - } - this.videoRendererCount = videoRendererCount; - this.audioRendererCount = audioRendererCount; - // Set initial values. audioVolume = 1; audioSessionId = C.AUDIO_SESSION_ID_UNSET; @@ -163,15 +142,15 @@ public class SimpleExoPlayer implements ExoPlayer { */ public void setVideoScalingMode(@C.VideoScalingMode int videoScalingMode) { this.videoScalingMode = videoScalingMode; - ExoPlayerMessage[] messages = new ExoPlayerMessage[videoRendererCount]; - int count = 0; for (Renderer renderer : renderers) { if (renderer.getTrackType() == C.TRACK_TYPE_VIDEO) { - messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_SCALING_MODE, - videoScalingMode); + player + .createMessage(renderer) + .setType(C.MSG_SET_SCALING_MODE) + .setMessage(videoScalingMode) + .send(); } } - player.sendMessages(messages); } /** @@ -352,15 +331,15 @@ public class SimpleExoPlayer implements ExoPlayer { */ public void setAudioAttributes(AudioAttributes audioAttributes) { this.audioAttributes = audioAttributes; - 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_AUDIO_ATTRIBUTES, - audioAttributes); + player + .createMessage(renderer) + .setType(C.MSG_SET_AUDIO_ATTRIBUTES) + .setMessage(audioAttributes) + .send(); } } - player.sendMessages(messages); } /** @@ -377,14 +356,11 @@ public class SimpleExoPlayer implements ExoPlayer { */ public void setVolume(float audioVolume) { this.audioVolume = audioVolume; - 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_VOLUME, audioVolume); + player.createMessage(renderer).setType(C.MSG_SET_VOLUME).setMessage(audioVolume).send(); } } - player.sendMessages(messages); } /** @@ -770,6 +746,11 @@ public class SimpleExoPlayer implements ExoPlayer { player.sendMessages(messages); } + @Override + public PlayerMessage createMessage(PlayerMessage.Target target) { + return player.createMessage(target); + } + @Override public void blockingSendMessages(ExoPlayerMessage... messages) { player.blockingSendMessages(messages); @@ -908,22 +889,25 @@ public class SimpleExoPlayer implements ExoPlayer { private void setVideoSurfaceInternal(Surface surface, boolean ownsSurface) { // Note: We don't turn this method into a no-op if the surface is being replaced with itself // so as to ensure onRenderedFirstFrame callbacks are still called in this case. - ExoPlayerMessage[] messages = new ExoPlayerMessage[videoRendererCount]; - int count = 0; + boolean surfaceReplaced = this.surface != null && this.surface != surface; for (Renderer renderer : renderers) { if (renderer.getTrackType() == C.TRACK_TYPE_VIDEO) { - messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_SURFACE, surface); + PlayerMessage message = + player.createMessage(renderer).setType(C.MSG_SET_SURFACE).setMessage(surface).send(); + if (surfaceReplaced) { + try { + message.blockUntilDelivered(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } } } - if (this.surface != null && this.surface != surface) { - // We're replacing a surface. Block to ensure that it's not accessed after the method returns. - player.blockingSendMessages(messages); + if (surfaceReplaced) { // If we created the previous surface, we are responsible for releasing it. if (this.ownsSurface) { this.surface.release(); } - } else { - player.sendMessages(messages); } this.surface = surface; this.ownsSurface = ownsSurface; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java index c410456e7b..54537ba548 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java @@ -23,8 +23,7 @@ import android.util.SparseIntArray; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; -import com.google.android.exoplayer2.ExoPlayer.ExoPlayerComponent; -import com.google.android.exoplayer2.ExoPlayer.ExoPlayerMessage; +import com.google.android.exoplayer2.PlayerMessage; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder; import com.google.android.exoplayer2.upstream.Allocator; @@ -42,7 +41,7 @@ import java.util.Map; * Concatenates multiple {@link MediaSource}s. The list of {@link MediaSource}s can be modified * during playback. Access to this class is thread-safe. */ -public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPlayerComponent { +public final class DynamicConcatenatingMediaSource implements MediaSource, PlayerMessage.Target { private static final int MSG_ADD = 0; private static final int MSG_ADD_MULTIPLE = 1; @@ -147,8 +146,11 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl Assertions.checkArgument(!mediaSourcesPublic.contains(mediaSource)); mediaSourcesPublic.add(index, mediaSource); if (player != null) { - player.sendMessages(new ExoPlayerMessage(this, MSG_ADD, - new MessageData<>(index, mediaSource, actionOnCompletion))); + player + .createMessage(this) + .setType(MSG_ADD) + .setMessage(new MessageData<>(index, mediaSource, actionOnCompletion)) + .send(); } else if (actionOnCompletion != null) { actionOnCompletion.run(); } @@ -220,8 +222,11 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl } mediaSourcesPublic.addAll(index, mediaSources); if (player != null && !mediaSources.isEmpty()) { - player.sendMessages(new ExoPlayerMessage(this, MSG_ADD_MULTIPLE, - new MessageData<>(index, mediaSources, actionOnCompletion))); + player + .createMessage(this) + .setType(MSG_ADD_MULTIPLE) + .setMessage(new MessageData<>(index, mediaSources, actionOnCompletion)) + .send(); } else if (actionOnCompletion != null){ actionOnCompletion.run(); } @@ -256,8 +261,11 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl public synchronized void removeMediaSource(int index, @Nullable Runnable actionOnCompletion) { mediaSourcesPublic.remove(index); if (player != null) { - player.sendMessages(new ExoPlayerMessage(this, MSG_REMOVE, - new MessageData<>(index, null, actionOnCompletion))); + player + .createMessage(this) + .setType(MSG_REMOVE) + .setMessage(new MessageData<>(index, null, actionOnCompletion)) + .send(); } else if (actionOnCompletion != null) { actionOnCompletion.run(); } @@ -293,8 +301,11 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl } mediaSourcesPublic.add(newIndex, mediaSourcesPublic.remove(currentIndex)); if (player != null) { - player.sendMessages(new ExoPlayerMessage(this, MSG_MOVE, - new MessageData<>(currentIndex, newIndex, actionOnCompletion))); + player + .createMessage(this) + .setType(MSG_MOVE) + .setMessage(new MessageData<>(currentIndex, newIndex, actionOnCompletion)) + .send(); } else if (actionOnCompletion != null) { actionOnCompletion.run(); } @@ -427,8 +438,7 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, ExoPl new ConcatenatedTimeline(mediaSourceHolders, windowCount, periodCount, shuffleOrder), null); if (actionOnCompletion != null) { - player.sendMessages( - new ExoPlayerMessage(this, MSG_ON_COMPLETION, actionOnCompletion)); + player.createMessage(this).setType(MSG_ON_COMPLETION).setMessage(actionOnCompletion).send(); } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java index d796e6936f..a5f5222820 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -561,6 +561,18 @@ public final class Util { return stayInBounds ? Math.min(list.size() - 1, index) : index; } + /** + * Compares two long values and returns the same value as {@code Long.compare(long, long)}. + * + * @param left The left operand. + * @param right The right operand. + * @return 0, if left == right, a negative value if left < right, or a positive value if left + * > right. + */ + public static int compareLong(long left, long right) { + return left < right ? -1 : left == right ? 0 : 1; + } + /** * Parses an xs:duration attribute value, returning the parsed duration in milliseconds. * diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java index ff0b8a6bc0..8145aa0c56 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java @@ -18,13 +18,18 @@ package com.google.android.exoplayer2.testutil; import android.os.Handler; import android.util.Log; import android.view.Surface; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.PlayerMessage; +import com.google.android.exoplayer2.PlayerMessage.Target; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.testutil.ActionSchedule.ActionNode; +import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerRunnable; +import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerTarget; import com.google.android.exoplayer2.trackselection.MappingTrackSelector; /** @@ -345,7 +350,63 @@ public abstract class Action { Surface surface) { player.setShuffleModeEnabled(shuffleModeEnabled); } + } + /** Calls {@link ExoPlayer#createMessage(Target)} and {@link PlayerMessage#send()}. */ + public static final class SendMessages extends Action { + + private final Target target; + private final int windowIndex; + private final long positionMs; + private final boolean deleteAfterDelivery; + + /** + * @param tag A tag to use for logging. + * @param target A message target. + * @param positionMs The position at which the message should be sent, in milliseconds. + */ + public SendMessages(String tag, Target target, long positionMs) { + this( + tag, + target, + /* windowIndex= */ C.INDEX_UNSET, + positionMs, + /* deleteAfterDelivery= */ true); + } + + /** + * @param tag A tag to use for logging. + * @param target A message target. + * @param windowIndex The window index at which the message should be sent, or {@link + * C#INDEX_UNSET} for the current window. + * @param positionMs The position at which the message should be sent, in milliseconds. + * @param deleteAfterDelivery Whether the message will be deleted after delivery. + */ + public SendMessages( + String tag, Target target, int windowIndex, long positionMs, boolean deleteAfterDelivery) { + super(tag, "SendMessages"); + this.target = target; + this.windowIndex = windowIndex; + this.positionMs = positionMs; + this.deleteAfterDelivery = deleteAfterDelivery; + } + + @Override + protected void doActionImpl( + final SimpleExoPlayer player, MappingTrackSelector trackSelector, Surface surface) { + if (target instanceof PlayerTarget) { + ((PlayerTarget) target).setPlayer(player); + } + PlayerMessage message = player.createMessage(target); + if (windowIndex != C.INDEX_UNSET) { + message.setPosition(windowIndex, positionMs); + } else { + message.setPosition(positionMs); + } + message.setHandler(new Handler()); + message.setDeleteAfterDelivery(deleteAfterDelivery); + message.send(); + } } /** @@ -555,6 +616,9 @@ public abstract class Action { @Override protected void doActionImpl(SimpleExoPlayer player, MappingTrackSelector trackSelector, Surface surface) { + if (runnable instanceof PlayerRunnable) { + ((PlayerRunnable) runnable).setPlayer(player); + } runnable.run(); } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java index 477071f91f..33ce846751 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java @@ -20,8 +20,11 @@ import android.os.Looper; import android.support.annotation.Nullable; import android.view.Surface; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.PlayerMessage; +import com.google.android.exoplayer2.PlayerMessage.Target; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaSource; @@ -29,6 +32,7 @@ import com.google.android.exoplayer2.testutil.Action.ClearVideoSurface; import com.google.android.exoplayer2.testutil.Action.ExecuteRunnable; import com.google.android.exoplayer2.testutil.Action.PrepareSource; import com.google.android.exoplayer2.testutil.Action.Seek; +import com.google.android.exoplayer2.testutil.Action.SendMessages; import com.google.android.exoplayer2.testutil.Action.SetPlayWhenReady; import com.google.android.exoplayer2.testutil.Action.SetPlaybackParameters; import com.google.android.exoplayer2.testutil.Action.SetRendererDisabled; @@ -315,6 +319,44 @@ public final class ActionSchedule { return apply(new SetShuffleModeEnabled(tag, shuffleModeEnabled)); } + /** + * Schedules sending a {@link PlayerMessage}. + * + * @param positionMs The position in the current window at which the message should be sent, in + * milliseconds. + * @return The builder, for convenience. + */ + public Builder sendMessage(Target target, long positionMs) { + return apply(new SendMessages(tag, target, positionMs)); + } + + /** + * Schedules sending a {@link PlayerMessage}. + * + * @param target A message target. + * @param windowIndex The window index at which the message should be sent. + * @param positionMs The position at which the message should be sent, in milliseconds. + * @return The builder, for convenience. + */ + public Builder sendMessage(Target target, int windowIndex, long positionMs) { + return apply( + new SendMessages(tag, target, windowIndex, positionMs, /* deleteAfterDelivery= */ true)); + } + + /** + * Schedules to send a {@link PlayerMessage}. + * + * @param target A message target. + * @param windowIndex The window index at which the message should be sent. + * @param positionMs The position at which the message should be sent, in milliseconds. + * @param deleteAfterDelivery Whether the message will be deleted after delivery. + * @return The builder, for convenience. + */ + public Builder sendMessage( + Target target, int windowIndex, long positionMs, boolean deleteAfterDelivery) { + return apply(new SendMessages(tag, target, windowIndex, positionMs, deleteAfterDelivery)); + } + /** * Schedules a delay until the timeline changed to a specified expected timeline. * @@ -365,7 +407,50 @@ public final class ActionSchedule { currentDelayMs = 0; return this; } + } + /** + * Provides a wrapper for a {@link Target} which has access to the player when handling messages. + * Can be used with {@link Builder#sendMessage(Target, long)}. + */ + public abstract static class PlayerTarget implements Target { + + private SimpleExoPlayer player; + + /** Handles the message send to the component and additionally provides access to the player. */ + public abstract void handleMessage(SimpleExoPlayer player, int messageType, Object message); + + /** Sets the player to be passed to {@link #handleMessage(SimpleExoPlayer, int, Object)}. */ + /* package */ void setPlayer(SimpleExoPlayer player) { + this.player = player; + } + + @Override + public final void handleMessage(int messageType, Object message) throws ExoPlaybackException { + handleMessage(player, messageType, message); + } + } + + /** + * Provides a wrapper for a {@link Runnable} which has access to the player. Can be used with + * {@link Builder#executeRunnable(Runnable)}. + */ + public abstract static class PlayerRunnable implements Runnable { + + private SimpleExoPlayer player; + + /** Executes Runnable with reference to player. */ + public abstract void run(SimpleExoPlayer player); + + /** Sets the player to be passed to {@link #run(SimpleExoPlayer)} . */ + /* package */ void setPlayer(SimpleExoPlayer player) { + this.player = player; + } + + @Override + public final void run() { + run(player); + } } /** diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java index 4a9d79f906..797c09d6b6 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.testutil; +import android.util.Pair; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.util.Util; @@ -170,7 +171,7 @@ public final class FakeTimeline extends Timeline { int windowPeriodIndex = periodIndex - periodOffsets[windowIndex]; TimelineWindowDefinition windowDefinition = windowDefinitions[windowIndex]; Object id = setIds ? windowPeriodIndex : null; - Object uid = setIds ? periodIndex : null; + Object uid = setIds ? Pair.create(windowDefinition.id, windowPeriodIndex) : null; long periodDurationUs = windowDefinition.durationUs / windowDefinition.periodCount; long positionInWindowUs = periodDurationUs * windowPeriodIndex; if (windowDefinition.adGroupsPerPeriodCount == 0) { @@ -198,11 +199,13 @@ public final class FakeTimeline extends Timeline { @Override public int getIndexOfPeriod(Object uid) { - if (!(uid instanceof Integer)) { - return C.INDEX_UNSET; + Period period = new Period(); + for (int i = 0; i < getPeriodCount(); i++) { + if (getPeriod(i, period, true).uid.equals(uid)) { + return i; + } } - int index = (Integer) uid; - return index >= 0 && index < getPeriodCount() ? index : C.INDEX_UNSET; + return C.INDEX_UNSET; } private static TimelineWindowDefinition[] createDefaultWindowDefinitions(int windowCount) { diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java index 4f31a8b027..93c14afc8f 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java @@ -24,7 +24,9 @@ import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Message; +import android.util.Pair; import com.google.android.exoplayer2.ExoPlaybackException; +import com.google.android.exoplayer2.PlayerMessage; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; @@ -281,7 +283,8 @@ public class MediaSourceTestRunner { } - private static class EventHandlingExoPlayer extends StubExoPlayer implements Handler.Callback { + private static class EventHandlingExoPlayer extends StubExoPlayer + implements Handler.Callback, PlayerMessage.Sender { private final Handler handler; @@ -290,23 +293,33 @@ public class MediaSourceTestRunner { } @Override - public void sendMessages(ExoPlayerMessage... messages) { - handler.obtainMessage(0, messages).sendToTarget(); + public PlayerMessage createMessage(PlayerMessage.Target target) { + return new PlayerMessage( + /* sender= */ this, target, Timeline.EMPTY, /* defaultWindowIndex= */ 0, handler); } @Override + public void sendMessage(PlayerMessage message, Listener listener) { + handler.obtainMessage(0, Pair.create(message, listener)).sendToTarget(); + } + + @Override + @SuppressWarnings("unchecked") public boolean handleMessage(Message msg) { - ExoPlayerMessage[] messages = (ExoPlayerMessage[]) msg.obj; - for (ExoPlayerMessage message : messages) { - try { - message.target.handleMessage(message.messageType, message.message); - } catch (ExoPlaybackException e) { - fail("Unexpected ExoPlaybackException."); - } + Pair messageAndListener = (Pair) msg.obj; + try { + messageAndListener + .first + .getTarget() + .handleMessage( + messageAndListener.first.getType(), messageAndListener.first.getMessage()); + messageAndListener.second.onMessageDelivered(); + messageAndListener.second.onMessageDeleted(); + } catch (ExoPlaybackException e) { + fail("Unexpected ExoPlaybackException."); } return true; } - } } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java index 1ea83bf1ec..7164fa13ab 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java @@ -19,6 +19,7 @@ import android.os.Looper; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.PlayerMessage; import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.MediaSource; @@ -146,6 +147,11 @@ public abstract class StubExoPlayer implements ExoPlayer { throw new UnsupportedOperationException(); } + @Override + public PlayerMessage createMessage(PlayerMessage.Target target) { + throw new UnsupportedOperationException(); + } + @Override public void sendMessages(ExoPlayerMessage... messages) { throw new UnsupportedOperationException(); From 7fb296dab98c007718227fc50ad47d689e3c40d1 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 22 Dec 2017 03:25:13 -0800 Subject: [PATCH 0982/2472] Initialize sample streams in FakeAdaptiveMediaPeriod. This prevents NPE when release or stop is called before tracks have been selected. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179911907 --- .../testutil/FakeAdaptiveMediaPeriod.java | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java index ff2a9b23cd..1a3e69f029 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java @@ -44,13 +44,18 @@ public class FakeAdaptiveMediaPeriod extends FakeMediaPeriod private ChunkSampleStream[] sampleStreams; private SequenceableLoader sequenceableLoader; - public FakeAdaptiveMediaPeriod(TrackGroupArray trackGroupArray, EventDispatcher eventDispatcher, - Allocator allocator, FakeChunkSource.Factory chunkSourceFactory, long durationUs) { + public FakeAdaptiveMediaPeriod( + TrackGroupArray trackGroupArray, + EventDispatcher eventDispatcher, + Allocator allocator, + FakeChunkSource.Factory chunkSourceFactory, + long durationUs) { super(trackGroupArray); this.eventDispatcher = eventDispatcher; this.allocator = allocator; this.chunkSourceFactory = chunkSourceFactory; this.durationUs = durationUs; + this.sampleStreams = newSampleStreamArray(0); } @Override @@ -62,13 +67,12 @@ public class FakeAdaptiveMediaPeriod extends FakeMediaPeriod } @Override - public void prepare(Callback callback, long positionUs) { + public synchronized void prepare(Callback callback, long positionUs) { super.prepare(callback, positionUs); this.callback = callback; } @Override - @SuppressWarnings("unchecked") public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { long returnPositionUs = super.selectTracks(selections, mayRetainStreamFlags, streams, @@ -79,7 +83,7 @@ public class FakeAdaptiveMediaPeriod extends FakeMediaPeriod validStreams.add((ChunkSampleStream) stream); } } - this.sampleStreams = validStreams.toArray(new ChunkSampleStream[validStreams.size()]); + this.sampleStreams = validStreams.toArray(newSampleStreamArray(validStreams.size())); this.sequenceableLoader = new CompositeSequenceableLoader(sampleStreams); return returnPositionUs; } @@ -131,4 +135,8 @@ public class FakeAdaptiveMediaPeriod extends FakeMediaPeriod callback.onContinueLoadingRequested(this); } + @SuppressWarnings("unchecked") + private static ChunkSampleStream[] newSampleStreamArray(int length) { + return new ChunkSampleStream[length]; + } } From 6f4110f3f873c79e9369537e23f1b3479d0d5700 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 22 Dec 2017 05:28:52 -0800 Subject: [PATCH 0983/2472] Fix buffer re-evaluation edge cases ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179917833 --- .../source/chunk/ChunkSampleStream.java | 58 ++++++++----------- 1 file changed, 25 insertions(+), 33 deletions(-) diff --git a/library/core/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 index 85c4b12241..a96bc2dcd0 100644 --- a/library/core/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 @@ -408,8 +408,31 @@ public class ChunkSampleStream implements SampleStream, S if (loader.isLoading() || isPendingReset()) { return; } - int queueSize = chunkSource.getPreferredQueueSize(positionUs, readOnlyMediaChunks); - discardUpstreamMediaChunks(queueSize); + + int currentQueueSize = mediaChunks.size(); + int preferredQueueSize = chunkSource.getPreferredQueueSize(positionUs, readOnlyMediaChunks); + if (currentQueueSize <= preferredQueueSize) { + return; + } + + int newQueueSize = currentQueueSize; + for (int i = preferredQueueSize; i < currentQueueSize; i++) { + if (!haveReadFromMediaChunk(i)) { + newQueueSize = i; + break; + } + } + if (newQueueSize == currentQueueSize) { + return; + } + + long endTimeUs = getLastMediaChunk().endTimeUs; + BaseMediaChunk firstRemovedChunk = discardUpstreamMediaChunksFromIndex(newQueueSize); + if (mediaChunks.isEmpty()) { + pendingResetPositionUs = lastSeekPositionUs; + } + loadingFinished = false; + eventDispatcher.upstreamDiscarded(primaryTrackType, firstRemovedChunk.startTimeUs, endTimeUs); } // Internal methods @@ -483,37 +506,6 @@ public class ChunkSampleStream implements SampleStream, S return mediaChunks.get(mediaChunks.size() - 1); } - /** - * Discard upstream media chunks until the queue length is equal to the length specified, but - * avoid discarding any chunk whose samples have been read by either primary sample stream or - * embedded sample streams. - * - * @param desiredQueueSize The desired length of the queue. The final queue size after discarding - * maybe larger than this if there are chunks after the specified position that have been read - * by either primary sample stream or embedded sample streams. - */ - private void discardUpstreamMediaChunks(int desiredQueueSize) { - if (mediaChunks.size() <= desiredQueueSize) { - return; - } - - int firstIndexToRemove = desiredQueueSize; - for (int i = firstIndexToRemove; i < mediaChunks.size(); i++) { - if (!haveReadFromMediaChunk(i)) { - firstIndexToRemove = i; - break; - } - } - - if (firstIndexToRemove == mediaChunks.size()) { - return; - } - long endTimeUs = getLastMediaChunk().endTimeUs; - BaseMediaChunk firstRemovedChunk = discardUpstreamMediaChunksFromIndex(firstIndexToRemove); - loadingFinished = false; - eventDispatcher.upstreamDiscarded(primaryTrackType, firstRemovedChunk.startTimeUs, endTimeUs); - } - /** * Discard upstream media chunks from {@code chunkIndex} and corresponding samples from sample * queues. From 35d4cbf99f6dae832a5a8e0ff3f46e03474376e2 Mon Sep 17 00:00:00 2001 From: hoangtc Date: Fri, 22 Dec 2017 05:33:11 -0800 Subject: [PATCH 0984/2472] Fix a bug that makes ClippingMediaSource not stop in some occasions. If ClippingMediaSource contains a child MediaSource with embedded metadata stream, and the embedded stream is being used, it can lead to ClippingMediaSource not be able to stop after the clipping end point. The reason being the metadata stream cannot read anymore sample, but it's also not end of source at that point. This CL fix this by changing the condition to check if the child stream cannot read anymore sample and it has read past the clipping end point. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179918038 --- .../android/exoplayer2/source/ClippingMediaPeriod.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java index 5685b8b70b..3f2c5ec894 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java @@ -299,9 +299,10 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb formatHolder.format = format.copyWithGaplessInfo(encoderDelay, encoderPadding); return C.RESULT_FORMAT_READ; } - if (endUs != C.TIME_END_OF_SOURCE && ((result == C.RESULT_BUFFER_READ - && buffer.timeUs >= endUs) || (result == C.RESULT_NOTHING_READ - && mediaPeriod.getBufferedPositionUs() == C.TIME_END_OF_SOURCE))) { + if (endUs != C.TIME_END_OF_SOURCE + && ((result == C.RESULT_BUFFER_READ && buffer.timeUs >= endUs) + || (result == C.RESULT_NOTHING_READ + && getBufferedPositionUs() == C.TIME_END_OF_SOURCE))) { buffer.clear(); buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); sentEos = true; From c6529344db8128314ff77980bf66275fa743cb27 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 22 Dec 2017 05:37:39 -0800 Subject: [PATCH 0985/2472] Introduce Handler interface allowing to switch to other Handlers. Especially this removes the need for the Clock interface to directly implement Handler methods. Instead, we have a separate Handler interface and the FakeClock is able to construct such a Handler. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179918255 --- .../google/android/exoplayer2/util/Clock.java | 20 +-- .../exoplayer2/util/HandlerWrapper.java | 76 +++++++++ .../android/exoplayer2/util/SystemClock.java | 9 +- .../exoplayer2/util/SystemHandler.java | 93 +++++++++++ .../android/exoplayer2/testutil/Action.java | 56 ++++--- .../exoplayer2/testutil/ActionSchedule.java | 70 ++++---- .../exoplayer2/testutil/ExoHostedTest.java | 8 +- .../testutil/ExoPlayerTestRunner.java | 46 ++++-- .../exoplayer2/testutil/FakeClock.java | 149 +++++++++++++++--- 9 files changed, 414 insertions(+), 113 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/util/HandlerWrapper.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/util/SystemHandler.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Clock.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Clock.java index 9619ed53ea..7731cca68c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Clock.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Clock.java @@ -15,13 +15,12 @@ */ package com.google.android.exoplayer2.util; -import android.os.Handler; - /** - * An interface through which system clocks can be read. The {@link #DEFAULT} implementation - * must be used for all non-test cases. + * An interface through which system clocks can be read. The {@link #DEFAULT} implementation must be + * used for all non-test cases. Implementations must also be able to create a {@link HandlerWrapper} + * which uses the underlying clock to schedule delayed messages. */ -public interface Clock { +public interface Clock extends HandlerWrapper.Factory { /** * Default {@link Clock} to use for all non-test cases. @@ -37,15 +36,4 @@ public interface Clock { * @see android.os.SystemClock#sleep(long) */ void sleep(long sleepTimeMs); - - /** - * Post a {@link Runnable} on a {@link Handler} thread with a delay measured by this clock. - * @see Handler#postDelayed(Runnable, long) - * - * @param handler The {@link Handler} to post the {@code runnable} on. - * @param runnable A {@link Runnable} to be posted. - * @param delayMs The delay in milliseconds as measured by this clock. - */ - void postDelayed(Handler handler, Runnable runnable, long delayMs); - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/HandlerWrapper.java b/library/core/src/main/java/com/google/android/exoplayer2/util/HandlerWrapper.java new file mode 100644 index 0000000000..25f9c9bb38 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/HandlerWrapper.java @@ -0,0 +1,76 @@ +/* + * 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.util; + +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.support.annotation.Nullable; + +/** + * An interface to call through to an {@link Handler}. The {@link Factory#DEFAULT} factory must be + * used for all non-test cases. + */ +public interface HandlerWrapper { + + /** A factory for handler instances. */ + interface Factory { + + /** Default HandlerWrapper factory to use for all non-test cases. */ + Factory DEFAULT = new SystemHandler.Factory(); + + /** + * Creates a HandlerWrapper running a specified looper and using a specified callback for + * messages. + * + * @see Handler#Handler(Looper, Handler.Callback). + */ + HandlerWrapper createHandler(Looper looper, @Nullable Handler.Callback callback); + } + + /** @see Handler#getLooper(). */ + Looper getLooper(); + + /** @see Handler#obtainMessage(int). */ + Message obtainMessage(int what); + + /** @see Handler#obtainMessage(int, Object). */ + Message obtainMessage(int what, Object obj); + + /** @see Handler#obtainMessage(int, int, int). */ + Message obtainMessage(int what, int arg1, int arg2); + + /** @see Handler#obtainMessage(int, int, int, Object). */ + Message obtainMessage(int what, int arg1, int arg2, Object obj); + + /** @see Handler#sendEmptyMessage(int). */ + boolean sendEmptyMessage(int what); + + /** @see Handler#sendEmptyMessageDelayed(int, long). */ + boolean sendEmptyMessageDelayed(int what, long delayMs); + + /** @see Handler#removeMessages(int). */ + void removeMessages(int what); + + /** @see Handler#removeCallbacksAndMessages(Object). */ + void removeCallbacksAndMessages(Object token); + + /** @see Handler#post(Runnable). */ + boolean post(Runnable runnable); + + /** @see Handler#postDelayed(Runnable, long). */ + boolean postDelayed(Runnable runnable, long delayMs); +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/SystemClock.java b/library/core/src/main/java/com/google/android/exoplayer2/util/SystemClock.java index 272c3f43ec..8a5bdf549f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/SystemClock.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/SystemClock.java @@ -15,7 +15,9 @@ */ package com.google.android.exoplayer2.util; -import android.os.Handler; +import android.os.Handler.Callback; +import android.os.Looper; +import android.support.annotation.Nullable; /** * The standard implementation of {@link Clock}. @@ -33,8 +35,7 @@ import android.os.Handler; } @Override - public void postDelayed(Handler handler, Runnable runnable, long delayMs) { - handler.postDelayed(runnable, delayMs); + public HandlerWrapper createHandler(Looper looper, @Nullable Callback callback) { + return HandlerWrapper.Factory.DEFAULT.createHandler(looper, callback); } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/SystemHandler.java b/library/core/src/main/java/com/google/android/exoplayer2/util/SystemHandler.java new file mode 100644 index 0000000000..b9fe771053 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/SystemHandler.java @@ -0,0 +1,93 @@ +/* + * 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.util; + +import android.os.Handler.Callback; +import android.os.Looper; +import android.os.Message; + +/** The standard implementation of {@link HandlerWrapper}. */ +/* package */ final class SystemHandler implements HandlerWrapper { + + /* package */ static final class Factory implements HandlerWrapper.Factory { + + @Override + public HandlerWrapper createHandler(Looper looper, Callback callback) { + return new SystemHandler(new android.os.Handler(looper, callback)); + } + } + + private final android.os.Handler handler; + + private SystemHandler(android.os.Handler handler) { + this.handler = handler; + } + + @Override + public Looper getLooper() { + return handler.getLooper(); + } + + @Override + public Message obtainMessage(int what) { + return handler.obtainMessage(what); + } + + @Override + public Message obtainMessage(int what, Object obj) { + return handler.obtainMessage(what, obj); + } + + @Override + public Message obtainMessage(int what, int arg1, int arg2) { + return handler.obtainMessage(what, arg1, arg2); + } + + @Override + public Message obtainMessage(int what, int arg1, int arg2, Object obj) { + return handler.obtainMessage(what, arg1, arg2, obj); + } + + @Override + public boolean sendEmptyMessage(int what) { + return handler.sendEmptyMessage(what); + } + + @Override + public boolean sendEmptyMessageDelayed(int what, long delayMs) { + return handler.sendEmptyMessageDelayed(what, delayMs); + } + + @Override + public void removeMessages(int what) { + handler.removeMessages(what); + } + + @Override + public void removeCallbacksAndMessages(Object token) { + handler.removeCallbacksAndMessages(token); + } + + @Override + public boolean post(Runnable runnable) { + return handler.post(runnable); + } + + @Override + public boolean postDelayed(Runnable runnable, long delayMs) { + return handler.postDelayed(runnable, delayMs); + } +} diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java index 8145aa0c56..7d2a1fd03f 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/Action.java @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.testutil; -import android.os.Handler; import android.util.Log; import android.view.Surface; import com.google.android.exoplayer2.C; @@ -31,6 +30,7 @@ import com.google.android.exoplayer2.testutil.ActionSchedule.ActionNode; import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerRunnable; import com.google.android.exoplayer2.testutil.ActionSchedule.PlayerTarget; import com.google.android.exoplayer2.trackselection.MappingTrackSelector; +import com.google.android.exoplayer2.util.HandlerWrapper; /** * Base class for actions to perform during playback tests. @@ -58,15 +58,19 @@ public abstract class Action { * @param handler The handler to use to pass to the next action. * @param nextAction The next action to schedule immediately after this action finished. */ - public final void doActionAndScheduleNext(SimpleExoPlayer player, - MappingTrackSelector trackSelector, Surface surface, Handler handler, ActionNode nextAction) { + public final void doActionAndScheduleNext( + SimpleExoPlayer player, + MappingTrackSelector trackSelector, + Surface surface, + HandlerWrapper handler, + ActionNode nextAction) { Log.i(tag, description); doActionAndScheduleNextImpl(player, trackSelector, surface, handler, nextAction); } /** * Called by {@link #doActionAndScheduleNext(SimpleExoPlayer, MappingTrackSelector, Surface, - * Handler, ActionNode)} to perform the action and to schedule the next action node. + * HandlerWrapper, ActionNode)} to perform the action and to schedule the next action node. * * @param player The player to which the action should be applied. * @param trackSelector The track selector to which the action should be applied. @@ -74,8 +78,12 @@ public abstract class Action { * @param handler The handler to use to pass to the next action. * @param nextAction The next action to schedule immediately after this action finished. */ - protected void doActionAndScheduleNextImpl(SimpleExoPlayer player, - MappingTrackSelector trackSelector, Surface surface, Handler handler, ActionNode nextAction) { + protected void doActionAndScheduleNextImpl( + SimpleExoPlayer player, + MappingTrackSelector trackSelector, + Surface surface, + HandlerWrapper handler, + ActionNode nextAction) { doActionImpl(player, trackSelector, surface); if (nextAction != null) { nextAction.schedule(player, trackSelector, surface, handler); @@ -84,14 +92,14 @@ public abstract class Action { /** * Called by {@link #doActionAndScheduleNextImpl(SimpleExoPlayer, MappingTrackSelector, Surface, - * Handler, ActionNode)} to perform the action. + * HandlerWrapper, ActionNode)} to perform the action. * * @param player The player to which the action should be applied. * @param trackSelector The track selector to which the action should be applied. * @param surface The surface to use when applying actions. */ - protected abstract void doActionImpl(SimpleExoPlayer player, MappingTrackSelector trackSelector, - Surface surface); + protected abstract void doActionImpl( + SimpleExoPlayer player, MappingTrackSelector trackSelector, Surface surface); /** * Calls {@link Player#seekTo(long)} or {@link Player#seekTo(int, long)}. @@ -403,7 +411,7 @@ public abstract class Action { } else { message.setPosition(positionMs); } - message.setHandler(new Handler()); + message.setHandler(new android.os.Handler()); message.setDeleteAfterDelivery(deleteAfterDelivery); message.send(); } @@ -449,8 +457,11 @@ public abstract class Action { } @Override - protected void doActionAndScheduleNextImpl(final SimpleExoPlayer player, - final MappingTrackSelector trackSelector, final Surface surface, final Handler handler, + protected void doActionAndScheduleNextImpl( + final SimpleExoPlayer player, + final MappingTrackSelector trackSelector, + final Surface surface, + final HandlerWrapper handler, final ActionNode nextAction) { if (nextAction == null) { return; @@ -493,8 +504,11 @@ public abstract class Action { } @Override - protected void doActionAndScheduleNextImpl(final SimpleExoPlayer player, - final MappingTrackSelector trackSelector, final Surface surface, final Handler handler, + protected void doActionAndScheduleNextImpl( + final SimpleExoPlayer player, + final MappingTrackSelector trackSelector, + final Surface surface, + final HandlerWrapper handler, final ActionNode nextAction) { if (nextAction == null) { return; @@ -533,8 +547,11 @@ public abstract class Action { } @Override - protected void doActionAndScheduleNextImpl(final SimpleExoPlayer player, - final MappingTrackSelector trackSelector, final Surface surface, final Handler handler, + protected void doActionAndScheduleNextImpl( + final SimpleExoPlayer player, + final MappingTrackSelector trackSelector, + final Surface surface, + final HandlerWrapper handler, final ActionNode nextAction) { if (nextAction == null) { return; @@ -575,8 +592,11 @@ public abstract class Action { } @Override - protected void doActionAndScheduleNextImpl(final SimpleExoPlayer player, - final MappingTrackSelector trackSelector, final Surface surface, final Handler handler, + protected void doActionAndScheduleNextImpl( + final SimpleExoPlayer player, + final MappingTrackSelector trackSelector, + final Surface surface, + final HandlerWrapper handler, final ActionNode nextAction) { if (nextAction == null) { return; diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java index 33ce846751..f152bb4eb8 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ActionSchedule.java @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.testutil; -import android.os.Handler; import android.os.Looper; import android.support.annotation.Nullable; import android.view.Surface; @@ -46,7 +45,7 @@ import com.google.android.exoplayer2.testutil.Action.WaitForSeekProcessed; import com.google.android.exoplayer2.testutil.Action.WaitForTimelineChanged; import com.google.android.exoplayer2.trackselection.MappingTrackSelector; import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.Clock; +import com.google.android.exoplayer2.util.HandlerWrapper; /** * Schedules a sequence of {@link Action}s for execution during a test. @@ -87,8 +86,12 @@ public final class ActionSchedule { * @param callback A {@link Callback} to notify when the action schedule finishes, or null if no * notification is needed. */ - /* package */ void start(SimpleExoPlayer player, MappingTrackSelector trackSelector, - Surface surface, Handler mainHandler, @Nullable Callback callback) { + /* package */ void start( + SimpleExoPlayer player, + MappingTrackSelector trackSelector, + Surface surface, + HandlerWrapper mainHandler, + @Nullable Callback callback) { callbackAction.setCallback(callback); rootNode.schedule(player, trackSelector, surface, mainHandler); } @@ -99,7 +102,6 @@ public final class ActionSchedule { public static final class Builder { private final String tag; - private final Clock clock; private final ActionNode rootNode; private long currentDelayMs; @@ -109,17 +111,8 @@ public final class ActionSchedule { * @param tag A tag to use for logging. */ public Builder(String tag) { - this(tag, Clock.DEFAULT); - } - - /** - * @param tag A tag to use for logging. - * @param clock A clock to use for measuring delays. - */ - public Builder(String tag, Clock clock) { this.tag = tag; - this.clock = clock; - rootNode = new ActionNode(new RootAction(tag), clock, 0); + rootNode = new ActionNode(new RootAction(tag), 0); previousNode = rootNode; } @@ -141,7 +134,7 @@ public final class ActionSchedule { * @return The builder, for convenience. */ public Builder apply(Action action) { - return appendActionNode(new ActionNode(action, clock, currentDelayMs)); + return appendActionNode(new ActionNode(action, currentDelayMs)); } /** @@ -152,7 +145,7 @@ public final class ActionSchedule { * @return The builder, for convenience. */ public Builder repeat(Action action, long intervalMs) { - return appendActionNode(new ActionNode(action, clock, currentDelayMs, intervalMs)); + return appendActionNode(new ActionNode(action, currentDelayMs, intervalMs)); } /** @@ -459,7 +452,6 @@ public final class ActionSchedule { /* package */ static final class ActionNode implements Runnable { private final Action action; - private final Clock clock; private final long delayMs; private final long repeatIntervalMs; @@ -468,27 +460,24 @@ public final class ActionSchedule { private SimpleExoPlayer player; private MappingTrackSelector trackSelector; private Surface surface; - private Handler mainHandler; + private HandlerWrapper mainHandler; /** * @param action The wrapped action. - * @param clock The clock to use for measuring the delay. * @param delayMs The delay between the node being scheduled and the action being executed. */ - public ActionNode(Action action, Clock clock, long delayMs) { - this(action, clock, delayMs, C.TIME_UNSET); + public ActionNode(Action action, long delayMs) { + this(action, delayMs, C.TIME_UNSET); } /** * @param action The wrapped action. - * @param clock The clock to use for measuring the delay. * @param delayMs The delay between the node being scheduled and the action being executed. * @param repeatIntervalMs The interval between one execution and the next repetition. If set to * {@link C#TIME_UNSET}, the action is executed once only. */ - public ActionNode(Action action, Clock clock, long delayMs, long repeatIntervalMs) { + public ActionNode(Action action, long delayMs, long repeatIntervalMs) { this.action = action; - this.clock = clock; this.delayMs = delayMs; this.repeatIntervalMs = repeatIntervalMs; } @@ -503,16 +492,19 @@ public final class ActionSchedule { } /** - * Schedules {@link #action} to be executed after {@link #delayMs}. The {@link #next} node - * will be scheduled immediately after {@link #action} is executed. + * Schedules {@link #action} to be executed after {@link #delayMs}. The {@link #next} node will + * be scheduled immediately after {@link #action} is executed. * * @param player The player to which actions should be applied. * @param trackSelector The track selector to which actions should be applied. * @param surface The surface to use when applying actions. * @param mainHandler A handler associated with the main thread of the host activity. */ - public void schedule(SimpleExoPlayer player, MappingTrackSelector trackSelector, - Surface surface, Handler mainHandler) { + public void schedule( + SimpleExoPlayer player, + MappingTrackSelector trackSelector, + Surface surface, + HandlerWrapper mainHandler) { this.player = player; this.trackSelector = trackSelector; this.surface = surface; @@ -520,7 +512,7 @@ public final class ActionSchedule { if (delayMs == 0 && Looper.myLooper() == mainHandler.getLooper()) { run(); } else { - clock.postDelayed(mainHandler, this, delayMs); + mainHandler.postDelayed(this, delayMs); } } @@ -528,13 +520,15 @@ public final class ActionSchedule { public void run() { action.doActionAndScheduleNext(player, trackSelector, surface, mainHandler, next); if (repeatIntervalMs != C.TIME_UNSET) { - clock.postDelayed(mainHandler, new Runnable() { - @Override - public void run() { - action.doActionAndScheduleNext(player, trackSelector, surface, mainHandler, null); - clock.postDelayed(mainHandler, this, repeatIntervalMs); - } - }, repeatIntervalMs); + mainHandler.postDelayed( + new Runnable() { + @Override + public void run() { + action.doActionAndScheduleNext(player, trackSelector, surface, mainHandler, null); + mainHandler.postDelayed(this, repeatIntervalMs); + } + }, + repeatIntervalMs); } } @@ -577,7 +571,7 @@ public final class ActionSchedule { SimpleExoPlayer player, MappingTrackSelector trackSelector, Surface surface, - Handler handler, + HandlerWrapper handler, ActionNode nextAction) { Assertions.checkArgument(nextAction == null); if (callback != null) { diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java index ab63087f95..3a5f3ccd7a 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java @@ -16,7 +16,7 @@ package com.google.android.exoplayer2.testutil; import android.os.ConditionVariable; -import android.os.Handler; +import android.os.Looper; import android.os.SystemClock; import android.util.Log; import android.view.Surface; @@ -42,6 +42,7 @@ import com.google.android.exoplayer2.upstream.BandwidthMeter; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.upstream.TransferListener; +import com.google.android.exoplayer2.util.HandlerWrapper; import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.video.VideoRendererEventListener; import junit.framework.Assert; @@ -72,7 +73,7 @@ public abstract class ExoHostedTest extends Player.DefaultEventListener implemen private final ConditionVariable testFinished; private ActionSchedule pendingSchedule; - private Handler actionHandler; + private HandlerWrapper actionHandler; private MappingTrackSelector trackSelector; private SimpleExoPlayer player; private Surface surface; @@ -187,7 +188,8 @@ public abstract class ExoHostedTest extends Player.DefaultEventListener implemen player.addAudioDebugListener(this); player.addVideoDebugListener(this); player.setPlayWhenReady(true); - actionHandler = new Handler(); + actionHandler = + HandlerWrapper.Factory.DEFAULT.createHandler(Looper.myLooper(), /* callback= */ null); // Schedule any pending actions. if (pendingSchedule != null) { pendingSchedule.start(player, trackSelector, surface, actionHandler, /* callback= */ null); diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java index 4905fc2233..f100b4fac1 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.testutil; -import android.os.Handler; import android.os.HandlerThread; import android.support.annotation.Nullable; import com.google.android.exoplayer2.DefaultLoadControl; @@ -38,6 +37,8 @@ import com.google.android.exoplayer2.text.TextOutput; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.MappingTrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import com.google.android.exoplayer2.util.Clock; +import com.google.android.exoplayer2.util.HandlerWrapper; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.video.VideoRendererEventListener; import java.util.ArrayList; @@ -91,6 +92,7 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener public static final Format AUDIO_FORMAT = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, Format.NO_VALUE, 2, 44100, null, null, 0, null); + private Clock clock; private PlayerFactory playerFactory; private Timeline timeline; private Object manifest; @@ -236,6 +238,18 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener return this; } + /** + * Sets the {@link Clock} to be used by the test runner. The default value is {@link + * Clock#DEFAULT}. + * + * @param clock A {@link Clock} to be used by the test runner. + * @return This builder. + */ + public Builder setClock(Clock clock) { + this.clock = clock; + return this; + } + /** * Sets an {@link ActionSchedule} to be run by the test runner. The first action will be * executed immediately before {@link SimpleExoPlayer#prepare(MediaSource)}. @@ -312,19 +326,25 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener if (renderers == null) { renderers = new Renderer[] {new FakeRenderer(supportedFormats)}; } - renderersFactory = new RenderersFactory() { - @Override - public Renderer[] createRenderers(Handler eventHandler, - VideoRendererEventListener videoRendererEventListener, - AudioRendererEventListener audioRendererEventListener, TextOutput textRendererOutput, - MetadataOutput metadataRendererOutput) { - return renderers; - } - }; + renderersFactory = + new RenderersFactory() { + @Override + public Renderer[] createRenderers( + android.os.Handler eventHandler, + VideoRendererEventListener videoRendererEventListener, + AudioRendererEventListener audioRendererEventListener, + TextOutput textRendererOutput, + MetadataOutput metadataRendererOutput) { + return renderers; + } + }; } if (loadControl == null) { loadControl = new DefaultLoadControl(); } + if (clock == null) { + clock = Clock.DEFAULT; + } if (playerFactory == null) { playerFactory = new PlayerFactory() { @Override @@ -344,6 +364,7 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener expectedPlayerEndedCount = 1; } return new ExoPlayerTestRunner( + clock, playerFactory, mediaSource, renderersFactory, @@ -368,7 +389,7 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener private final @Nullable AudioRendererEventListener audioRendererEventListener; private final HandlerThread playerThread; - private final Handler handler; + private final HandlerWrapper handler; private final CountDownLatch endedCountDownLatch; private final CountDownLatch actionScheduleFinishedCountDownLatch; private final ArrayList timelines; @@ -383,6 +404,7 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener private boolean playerWasPrepared; private ExoPlayerTestRunner( + Clock clock, PlayerFactory playerFactory, MediaSource mediaSource, RenderersFactory renderersFactory, @@ -411,7 +433,7 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener this.actionScheduleFinishedCountDownLatch = new CountDownLatch(actionSchedule != null ? 1 : 0); this.playerThread = new HandlerThread("ExoPlayerTest thread"); playerThread.start(); - this.handler = new Handler(playerThread.getLooper()); + this.handler = clock.createHandler(playerThread.getLooper(), /* callback= */ null); } // Called on the test thread to run the test. diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeClock.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeClock.java index 843e5858d8..c78a8c03e0 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeClock.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeClock.java @@ -15,19 +15,21 @@ */ package com.google.android.exoplayer2.testutil; -import android.os.Handler; +import android.os.Handler.Callback; +import android.os.Looper; +import android.os.Message; import com.google.android.exoplayer2.util.Clock; +import com.google.android.exoplayer2.util.HandlerWrapper; import java.util.ArrayList; import java.util.List; -/** - * Fake {@link Clock} implementation independent of {@link android.os.SystemClock}. - */ +/** Fake {@link Clock} implementation independent of {@link android.os.SystemClock}. */ public final class FakeClock implements Clock { - private long currentTimeMs; private final List wakeUpTimes; - private final List handlerPosts; + private final List handlerMessages; + + private long currentTimeMs; /** * Create {@link FakeClock} with an arbitrary initial timestamp. @@ -37,7 +39,7 @@ public final class FakeClock implements Clock { public FakeClock(long initialTimeMs) { this.currentTimeMs = initialTimeMs; this.wakeUpTimes = new ArrayList<>(); - this.handlerPosts = new ArrayList<>(); + this.handlerMessages = new ArrayList<>(); } /** @@ -53,10 +55,9 @@ public final class FakeClock implements Clock { break; } } - for (int i = handlerPosts.size() - 1; i >= 0; i--) { - if (handlerPosts.get(i).postTime <= currentTimeMs) { - HandlerPostData postData = handlerPosts.remove(i); - postData.handler.post(postData.runnable); + for (int i = handlerMessages.size() - 1; i >= 0; i--) { + if (handlerMessages.get(i).maybeSendToTarget(currentTimeMs)) { + handlerMessages.remove(i); } } } @@ -84,27 +85,131 @@ public final class FakeClock implements Clock { } @Override - public synchronized void postDelayed(Handler handler, Runnable runnable, long delayMs) { - if (delayMs <= 0) { - handler.post(runnable); - } else { - handlerPosts.add(new HandlerPostData(currentTimeMs + delayMs, handler, runnable)); - } + public HandlerWrapper createHandler(Looper looper, Callback callback) { + return new ClockHandler(looper, callback); } - private static final class HandlerPostData { + /** Adds a handler post to list of pending messages. */ + protected synchronized void addDelayedHandlerMessage( + HandlerWrapper handler, Runnable runnable, long delayMs) { + handlerMessages.add(new HandlerMessageData(currentTimeMs + delayMs, handler, runnable)); + } - public final long postTime; - public final Handler handler; - public final Runnable runnable; + /** Adds an empty handler message to list of pending messages. */ + protected synchronized void addDelayedHandlerMessage( + HandlerWrapper handler, int message, long delayMs) { + handlerMessages.add(new HandlerMessageData(currentTimeMs + delayMs, handler, message)); + } - public HandlerPostData(long postTime, Handler handler, Runnable runnable) { + /** Message data saved to send messages or execute runnables at a later time on a Handler. */ + private static final class HandlerMessageData { + + private final long postTime; + private final HandlerWrapper handler; + private final Runnable runnable; + private final int message; + + public HandlerMessageData(long postTime, HandlerWrapper handler, Runnable runnable) { this.postTime = postTime; this.handler = handler; this.runnable = runnable; + this.message = 0; } + public HandlerMessageData(long postTime, HandlerWrapper handler, int message) { + this.postTime = postTime; + this.handler = handler; + this.runnable = null; + this.message = message; + } + + /** Sends the message and returns whether the message was sent to its target. */ + public boolean maybeSendToTarget(long currentTimeMs) { + if (postTime <= currentTimeMs) { + if (runnable != null) { + handler.post(runnable); + } else { + handler.sendEmptyMessage(message); + } + return true; + } + return false; + } } + /** HandlerWrapper implementation using the enclosing Clock to schedule delayed messages. */ + private final class ClockHandler implements HandlerWrapper { + + private final android.os.Handler handler; + + public ClockHandler(Looper looper, Callback callback) { + handler = new android.os.Handler(looper, callback); + } + + @Override + public Looper getLooper() { + return handler.getLooper(); + } + + @Override + public Message obtainMessage(int what) { + return handler.obtainMessage(what); + } + + @Override + public Message obtainMessage(int what, Object obj) { + return handler.obtainMessage(what, obj); + } + + @Override + public Message obtainMessage(int what, int arg1, int arg2) { + return handler.obtainMessage(what, arg1, arg2); + } + + @Override + public Message obtainMessage(int what, int arg1, int arg2, Object obj) { + return handler.obtainMessage(what, arg1, arg2, obj); + } + + @Override + public boolean sendEmptyMessage(int what) { + return handler.sendEmptyMessage(what); + } + + @Override + public boolean sendEmptyMessageDelayed(int what, long delayMs) { + if (delayMs <= 0) { + return handler.sendEmptyMessage(what); + } else { + addDelayedHandlerMessage(this, what, delayMs); + return true; + } + } + + @Override + public void removeMessages(int what) { + handler.removeMessages(what); + } + + @Override + public void removeCallbacksAndMessages(Object token) { + handler.removeCallbacksAndMessages(token); + } + + @Override + public boolean post(Runnable runnable) { + return handler.post(runnable); + } + + @Override + public boolean postDelayed(Runnable runnable, long delayMs) { + if (delayMs <= 0) { + return handler.post(runnable); + } else { + addDelayedHandlerMessage(this, runnable, delayMs); + return true; + } + } + } } From 61b9e846a86fde4b97b2cda27b3850ba475262b5 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 22 Dec 2017 06:36:46 -0800 Subject: [PATCH 0986/2472] Allow setting a Clock for the main playback thread. This allows to inject a FakeClock for tests. Other playback components (e.g. some media sources) still use SystemClock but they can be amended in the future if needed. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179921889 --- .../android/exoplayer2/DefaultMediaClock.java | 10 ------ .../android/exoplayer2/ExoPlayerFactory.java | 3 +- .../android/exoplayer2/ExoPlayerImpl.java | 8 +++-- .../exoplayer2/ExoPlayerImplInternal.java | 26 +++++++-------- .../android/exoplayer2/SimpleExoPlayer.java | 32 ++++++++++++++++--- .../exoplayer2/util/HandlerWrapper.java | 14 ++++++-- .../exoplayer2/util/StandaloneMediaClock.java | 7 ---- .../exoplayer2/util/SystemHandler.java | 11 +++++-- .../exoplayer2/testutil/FakeClock.java | 3 +- .../testutil/FakeSimpleExoPlayer.java | 7 ++-- 10 files changed, 74 insertions(+), 47 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultMediaClock.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultMediaClock.java index 5f342bc722..ed57cec70c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultMediaClock.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultMediaClock.java @@ -46,16 +46,6 @@ import com.google.android.exoplayer2.util.StandaloneMediaClock; private @Nullable Renderer rendererClockSource; private @Nullable MediaClock rendererClock; - /** - * Creates a new instance with listener for playback parameter changes. - * - * @param listener A {@link PlaybackParameterListener} to listen for playback parameter - * changes. - */ - public DefaultMediaClock(PlaybackParameterListener listener) { - this(listener, Clock.DEFAULT); - } - /** * Creates a new instance with listener for playback parameter changes and a {@link Clock} to use * for the standalone clock implementation. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java index b647e541bc..821671e34e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java @@ -20,6 +20,7 @@ import android.support.annotation.Nullable; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.trackselection.TrackSelector; +import com.google.android.exoplayer2.util.Clock; /** * A factory for {@link ExoPlayer} instances. @@ -160,7 +161,7 @@ public final class ExoPlayerFactory { */ public static ExoPlayer newInstance(Renderer[] renderers, TrackSelector trackSelector, LoadControl loadControl) { - return new ExoPlayerImpl(renderers, trackSelector, loadControl); + return new ExoPlayerImpl(renderers, trackSelector, loadControl, Clock.DEFAULT); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index afb6428fa5..4e97a47924 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -31,6 +31,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectionArray; 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.Clock; import com.google.android.exoplayer2.util.Util; import java.util.ArrayList; import java.util.List; @@ -75,9 +76,11 @@ import java.util.concurrent.CopyOnWriteArraySet; * @param renderers The {@link Renderer}s that will 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. + * @param clock The {@link Clock} that will be used by the instance. */ @SuppressLint("HandlerLeak") - public ExoPlayerImpl(Renderer[] renderers, TrackSelector trackSelector, LoadControl loadControl) { + public ExoPlayerImpl( + Renderer[] renderers, TrackSelector trackSelector, LoadControl loadControl, Clock clock) { Log.i(TAG, "Init " + Integer.toHexString(System.identityHashCode(this)) + " [" + ExoPlayerLibraryInfo.VERSION_SLASHY + "] [" + Util.DEVICE_DEBUG_INFO + "]"); Assertions.checkState(renderers.length > 0); @@ -116,7 +119,8 @@ import java.util.concurrent.CopyOnWriteArraySet; repeatMode, shuffleModeEnabled, eventHandler, - this); + this, + clock); internalPlayerHandler = new Handler(internalPlayer.getPlaybackLooper()); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index f3d0e1794b..c5fdf38bfa 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -39,6 +39,8 @@ import com.google.android.exoplayer2.trackselection.TrackSelectionArray; 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.Clock; +import com.google.android.exoplayer2.util.HandlerWrapper; import com.google.android.exoplayer2.util.TraceUtil; import com.google.android.exoplayer2.util.Util; import java.io.IOException; @@ -102,7 +104,7 @@ import java.util.Collections; private final TrackSelector trackSelector; private final TrackSelectorResult emptyTrackSelectorResult; private final LoadControl loadControl; - private final Handler handler; + private final HandlerWrapper handler; private final HandlerThread internalPlaybackThread; private final Handler eventHandler; private final ExoPlayer player; @@ -126,7 +128,6 @@ import java.util.Collections; private boolean rebuffering; private @Player.RepeatMode int repeatMode; private boolean shuffleModeEnabled; - private long elapsedRealtimeUs; private int pendingPrepareCount; private SeekPosition pendingInitialSeekPosition; @@ -146,7 +147,8 @@ import java.util.Collections; @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled, Handler eventHandler, - ExoPlayer player) { + ExoPlayer player, + Clock clock) { this.renderers = renderers; this.trackSelector = trackSelector; this.emptyTrackSelectorResult = emptyTrackSelectorResult; @@ -170,7 +172,7 @@ import java.util.Collections; renderers[i].setIndex(i); rendererCapabilities[i] = renderers[i].getCapabilities(); } - mediaClock = new DefaultMediaClock(this); + mediaClock = new DefaultMediaClock(this, clock); customMessageInfos = new ArrayList<>(); enabledRenderers = new Renderer[0]; window = new Timeline.Window(); @@ -183,7 +185,7 @@ import java.util.Collections; internalPlaybackThread = new HandlerThread("ExoPlayerImplInternal:Handler", Process.THREAD_PRIORITY_AUDIO); internalPlaybackThread.start(); - handler = new Handler(internalPlaybackThread.getLooper(), this); + handler = clock.createHandler(internalPlaybackThread.getLooper(), this); } public void prepare(MediaSource mediaSource, boolean resetPosition) { @@ -527,7 +529,6 @@ import java.util.Collections; maybeTriggerCustomMessages(playbackInfo.positionUs, periodPositionUs); playbackInfo.positionUs = periodPositionUs; } - elapsedRealtimeUs = SystemClock.elapsedRealtime() * 1000; // Update the buffered position. long bufferedPositionUs = enabledRenderers.length == 0 ? C.TIME_END_OF_SOURCE @@ -549,16 +550,19 @@ import java.util.Collections; TraceUtil.beginSection("doSomeWork"); updatePlaybackPositions(); + long rendererPositionElapsedRealtimeUs = SystemClock.elapsedRealtime() * 1000; + playingPeriodHolder.mediaPeriod.discardBuffer(playbackInfo.positionUs - backBufferDurationUs, retainBackBufferFromKeyframe); boolean allRenderersEnded = true; boolean allRenderersReadyOrEnded = true; + for (Renderer renderer : enabledRenderers) { // TODO: Each renderer should return the maximum delay before which it wishes to be called // again. The minimum of these values should then be used as the delay before the next // invocation of this method. - renderer.render(rendererPositionUs, elapsedRealtimeUs); + renderer.render(rendererPositionUs, rendererPositionElapsedRealtimeUs); allRenderersEnded = allRenderersEnded && renderer.isEnded(); // Determine whether the renderer is ready (or ended). We override to assume the renderer is // ready if it needs the next sample stream. This is necessary to avoid getting stuck if @@ -625,13 +629,7 @@ import java.util.Collections; private void scheduleNextWork(long thisOperationStartTimeMs, long intervalMs) { handler.removeMessages(MSG_DO_SOME_WORK); - long nextOperationStartTimeMs = thisOperationStartTimeMs + intervalMs; - long nextOperationDelayMs = nextOperationStartTimeMs - SystemClock.elapsedRealtime(); - if (nextOperationDelayMs <= 0) { - handler.sendEmptyMessage(MSG_DO_SOME_WORK); - } else { - handler.sendEmptyMessageDelayed(MSG_DO_SOME_WORK, nextOperationDelayMs); - } + handler.sendEmptyMessageDelayed(MSG_DO_SOME_WORK, intervalMs, thisOperationStartTimeMs); } private void seekToInternal(SeekPosition seekPosition) throws ExoPlaybackException { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index e2d0ed1422..d4346a65e1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -38,6 +38,7 @@ import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.TextOutput; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.trackselection.TrackSelector; +import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.video.VideoRendererEventListener; import java.util.List; @@ -109,8 +110,28 @@ public class SimpleExoPlayer implements ExoPlayer { private AudioAttributes audioAttributes; private float audioVolume; - protected SimpleExoPlayer(RenderersFactory renderersFactory, TrackSelector trackSelector, - LoadControl loadControl) { + /** + * @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. + */ + protected SimpleExoPlayer( + RenderersFactory renderersFactory, TrackSelector trackSelector, LoadControl loadControl) { + this(renderersFactory, trackSelector, loadControl, Clock.DEFAULT); + } + + /** + * @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. + * @param clock The {@link Clock} that will be used by the instance. Should always be {@link + * Clock#DEFAULT}, unless the player is being used from a test. + */ + protected SimpleExoPlayer( + RenderersFactory renderersFactory, + TrackSelector trackSelector, + LoadControl loadControl, + Clock clock) { componentListener = new ComponentListener(); videoListeners = new CopyOnWriteArraySet<>(); textOutputs = new CopyOnWriteArraySet<>(); @@ -129,7 +150,7 @@ public class SimpleExoPlayer implements ExoPlayer { videoScalingMode = C.VIDEO_SCALING_MODE_DEFAULT; // Build the player and associated objects. - player = createExoPlayerImpl(renderers, trackSelector, loadControl); + player = createExoPlayerImpl(renderers, trackSelector, loadControl, clock); } /** @@ -864,11 +885,12 @@ public class SimpleExoPlayer implements ExoPlayer { * @param renderers The {@link Renderer}s that will 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. + * @param clock The {@link Clock} that will be used by this instance. * @return A new {@link ExoPlayer} instance. */ protected ExoPlayer createExoPlayerImpl( - Renderer[] renderers, TrackSelector trackSelector, LoadControl loadControl) { - return new ExoPlayerImpl(renderers, trackSelector, loadControl); + Renderer[] renderers, TrackSelector trackSelector, LoadControl loadControl, Clock clock) { + return new ExoPlayerImpl(renderers, trackSelector, loadControl, clock); } private void removeSurfaceCallbacks() { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/HandlerWrapper.java b/library/core/src/main/java/com/google/android/exoplayer2/util/HandlerWrapper.java index 25f9c9bb38..b9f3a750d7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/HandlerWrapper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/HandlerWrapper.java @@ -59,8 +59,18 @@ public interface HandlerWrapper { /** @see Handler#sendEmptyMessage(int). */ boolean sendEmptyMessage(int what); - /** @see Handler#sendEmptyMessageDelayed(int, long). */ - boolean sendEmptyMessageDelayed(int what, long delayMs); + /** + * Variant of {@code Handler#sendEmptyMessageDelayed(int, long)} which also takes a reference time + * measured by {@code android.os.SystemClock#elapsedRealtime()} to which the delay is added. + * + * @param what The message identifier. + * @param delayMs The delay in milliseconds to send the message. This delay is added to the {@code + * referenceTimeMs}. + * @param referenceTimeMs The time which the delay is added to. Always measured with {@code + * android.os.SystemClock#elapsedRealtime()}. + * @return Whether the message was successfully enqueued on the Handler thread. + */ + boolean sendEmptyMessageDelayed(int what, long delayMs, long referenceTimeMs); /** @see Handler#removeMessages(int). */ void removeMessages(int what); 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 index 3c0ec2a854..b1f53416fb 100644 --- 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 @@ -31,13 +31,6 @@ public final class StandaloneMediaClock implements MediaClock { private long baseElapsedMs; private PlaybackParameters playbackParameters; - /** - * Creates a new standalone media clock. - */ - public StandaloneMediaClock() { - this(Clock.DEFAULT); - } - /** * Creates a new standalone media clock using the given {@link Clock} implementation. * diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/SystemHandler.java b/library/core/src/main/java/com/google/android/exoplayer2/util/SystemHandler.java index b9fe771053..e99c626057 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/SystemHandler.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/SystemHandler.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.util; import android.os.Handler.Callback; import android.os.Looper; import android.os.Message; +import android.os.SystemClock; /** The standard implementation of {@link HandlerWrapper}. */ /* package */ final class SystemHandler implements HandlerWrapper { @@ -67,8 +68,14 @@ import android.os.Message; } @Override - public boolean sendEmptyMessageDelayed(int what, long delayMs) { - return handler.sendEmptyMessageDelayed(what, delayMs); + public boolean sendEmptyMessageDelayed(int what, long delayMs, long referenceTimeMs) { + long targetMessageTime = referenceTimeMs + delayMs; + long remainingDelayMs = targetMessageTime - SystemClock.elapsedRealtime(); + if (remainingDelayMs <= 0) { + return handler.sendEmptyMessage(what); + } else { + return handler.sendEmptyMessageDelayed(what, remainingDelayMs); + } } @Override diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeClock.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeClock.java index c78a8c03e0..83ecbacdde 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeClock.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeClock.java @@ -177,7 +177,8 @@ public final class FakeClock implements Clock { } @Override - public boolean sendEmptyMessageDelayed(int what, long delayMs) { + public boolean sendEmptyMessageDelayed(int what, long delayMs, long referenceTimeMs) { + // Ignore referenceTimeMs measured by SystemClock and just send with requested delay. if (delayMs <= 0) { return handler.sendEmptyMessage(what); } else { diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java index a8ba3b3420..591b94a9cd 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java @@ -44,6 +44,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelector.InvalidationListener; import com.google.android.exoplayer2.trackselection.TrackSelectorResult; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Clock; import java.util.Arrays; import java.util.concurrent.CopyOnWriteArraySet; @@ -58,13 +59,13 @@ public class FakeSimpleExoPlayer extends SimpleExoPlayer { public FakeSimpleExoPlayer(RenderersFactory renderersFactory, TrackSelector trackSelector, LoadControl loadControl, FakeClock clock) { - super (renderersFactory, trackSelector, loadControl); + super(renderersFactory, trackSelector, loadControl, clock); player.setFakeClock(clock); } @Override - protected ExoPlayer createExoPlayerImpl(Renderer[] renderers, TrackSelector trackSelector, - LoadControl loadControl) { + protected ExoPlayer createExoPlayerImpl( + Renderer[] renderers, TrackSelector trackSelector, LoadControl loadControl, Clock clock) { this.player = new FakeExoPlayer(renderers, trackSelector, loadControl); return player; } From f2bb2d27be3cd34654d555970910bd8d459ff77a Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 22 Dec 2017 07:35:30 -0800 Subject: [PATCH 0987/2472] Add support for extracting 32-bit float WAVE Issue: #3379 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179925320 --- RELEASENOTES.md | 2 ++ .../extractor/wav/WavHeaderReader.java | 22 ++++++++++++++----- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 3c45c3449a..3bc55476c2 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -36,6 +36,8 @@ * DefaultTrackSelector: Support disabling of individual text track selection flags. * New Cast extension: Simplifies toggling between local and Cast playbacks. +* Add support for extracting 32-bit WAVE files + ([#3379](https://github.com/google/ExoPlayer/issues/3379)). ### 2.6.1 ### diff --git a/library/core/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 index 0e99380a1c..d0810a0629 100644 --- a/library/core/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 @@ -31,6 +31,8 @@ import java.io.IOException; /** Integer PCM audio data. */ private static final int TYPE_PCM = 0x0001; + /** Float PCM audio data. */ + private static final int TYPE_FLOAT = 0x0003; /** Extended WAVE format. */ private static final int TYPE_WAVE_FORMAT_EXTENSIBLE = 0xFFFE; @@ -87,14 +89,22 @@ import java.io.IOException; + blockAlignment); } - @C.PcmEncoding int encoding = Util.getPcmEncoding(bitsPerSample); - if (encoding == C.ENCODING_INVALID) { - Log.e(TAG, "Unsupported WAV bit depth: " + bitsPerSample); - return null; + @C.PcmEncoding int encoding; + switch (type) { + case TYPE_PCM: + case TYPE_WAVE_FORMAT_EXTENSIBLE: + encoding = Util.getPcmEncoding(bitsPerSample); + break; + case TYPE_FLOAT: + encoding = bitsPerSample == 32 ? C.ENCODING_PCM_FLOAT : C.ENCODING_INVALID; + break; + default: + Log.e(TAG, "Unsupported WAV format type: " + type); + return null; } - if (type != TYPE_PCM && type != TYPE_WAVE_FORMAT_EXTENSIBLE) { - Log.e(TAG, "Unsupported WAV format type: " + type); + if (encoding == C.ENCODING_INVALID) { + Log.e(TAG, "Unsupported WAV bit depth " + bitsPerSample + " for type " + type); return null; } From 410e614cfd1c42299bce27dbcbf4d695b2943511 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 22 Dec 2017 07:43:36 -0800 Subject: [PATCH 0988/2472] Run custom messages executed on playback thread immediately. This ensures message order if multiple custom messages running on the playback thread and direct player commands are called immedately after each other. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179925852 --- .../exoplayer2/ExoPlayerImplInternal.java | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index c5fdf38bfa..1c680d4aba 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -904,13 +904,17 @@ import java.util.Collections; } } }; - handler.post( - new Runnable() { - @Override - public void run() { - customMessageInfo.message.getHandler().post(handleMessageRunnable); - } - }); + if (customMessageInfo.message.getHandler().getLooper() == handler.getLooper()) { + handleMessageRunnable.run(); + } else { + handler.post( + new Runnable() { + @Override + public void run() { + customMessageInfo.message.getHandler().post(handleMessageRunnable); + } + }); + } } private void resolveCustomMessagePositions() { From f279f3c843ff9825b2e323d58feb0096a29e64a7 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 22 Dec 2017 08:39:21 -0800 Subject: [PATCH 0989/2472] Replace FakeExoPlayer with real player running with fake clock. This ensures that simulated playbacks always use the current player implementation. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179929911 --- .../testutil/ExoPlayerTestRunner.java | 67 +-- .../exoplayer2/testutil/FakeClock.java | 2 +- .../testutil/FakeSimpleExoPlayer.java | 541 ------------------ 3 files changed, 16 insertions(+), 594 deletions(-) delete mode 100644 testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java index f100b4fac1..cefe94b6c7 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.java @@ -19,7 +19,6 @@ import android.os.HandlerThread; import android.support.annotation.Nullable; import com.google.android.exoplayer2.DefaultLoadControl; import com.google.android.exoplayer2.ExoPlaybackException; -import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.LoadControl; import com.google.android.exoplayer2.Player; @@ -32,11 +31,11 @@ import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.metadata.MetadataOutput; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner.Builder.PlayerFactory; import com.google.android.exoplayer2.text.TextOutput; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.MappingTrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.HandlerWrapper; import com.google.android.exoplayer2.util.MimeTypes; @@ -59,26 +58,6 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener */ public static final class Builder { - /** - * Factory to create an {@link SimpleExoPlayer} instance. The player will be created on its own - * {@link HandlerThread}. - */ - public interface PlayerFactory { - - /** - * Creates a new {@link SimpleExoPlayer} using the provided renderers factory, track selector, - * and load control. - * - * @param renderersFactory A {@link RenderersFactory} to be used for the new player. - * @param trackSelector A {@link MappingTrackSelector} to be used for the new player. - * @param loadControl A {@link LoadControl} to be used for the new player. - * @return A new {@link SimpleExoPlayer}. - */ - SimpleExoPlayer createExoPlayer(RenderersFactory renderersFactory, - MappingTrackSelector trackSelector, LoadControl loadControl); - - } - /** * A generic video {@link Format} which can be used to set up media sources and renderers. */ @@ -93,7 +72,6 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, Format.NO_VALUE, 2, 44100, null, null, 0, null); private Clock clock; - private PlayerFactory playerFactory; private Timeline timeline; private Object manifest; private MediaSource mediaSource; @@ -223,21 +201,6 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener return this; } - /** - * Sets the {@link PlayerFactory} which creates the {@link SimpleExoPlayer} to be used by the - * test runner. The default value is a {@link SimpleExoPlayer} with the renderers provided by - * {@link #setRenderers(Renderer...)} or {@link #setRenderersFactory(RenderersFactory)}, the - * track selector provided by {@link #setTrackSelector(MappingTrackSelector)} and the load - * control provided by {@link #setLoadControl(LoadControl)}. - * - * @param playerFactory A {@link PlayerFactory} to create the player. - * @return This builder. - */ - public Builder setExoPlayer(PlayerFactory playerFactory) { - this.playerFactory = playerFactory; - return this; - } - /** * Sets the {@link Clock} to be used by the test runner. The default value is {@link * Clock#DEFAULT}. @@ -345,15 +308,6 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener if (clock == null) { clock = Clock.DEFAULT; } - if (playerFactory == null) { - playerFactory = new PlayerFactory() { - @Override - public SimpleExoPlayer createExoPlayer(RenderersFactory renderersFactory, - MappingTrackSelector trackSelector, LoadControl loadControl) { - return ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector, loadControl); - } - }; - } if (mediaSource == null) { if (timeline == null) { timeline = new FakeTimeline(1); @@ -365,7 +319,6 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener } return new ExoPlayerTestRunner( clock, - playerFactory, mediaSource, renderersFactory, trackSelector, @@ -378,7 +331,7 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener } } - private final PlayerFactory playerFactory; + private final Clock clock; private final MediaSource mediaSource; private final RenderersFactory renderersFactory; private final MappingTrackSelector trackSelector; @@ -405,7 +358,6 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener private ExoPlayerTestRunner( Clock clock, - PlayerFactory playerFactory, MediaSource mediaSource, RenderersFactory renderersFactory, MappingTrackSelector trackSelector, @@ -415,7 +367,7 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener @Nullable VideoRendererEventListener videoRendererEventListener, @Nullable AudioRendererEventListener audioRendererEventListener, int expectedPlayerEndedCount) { - this.playerFactory = playerFactory; + this.clock = clock; this.mediaSource = mediaSource; this.renderersFactory = renderersFactory; this.trackSelector = trackSelector; @@ -451,7 +403,7 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener @Override public void run() { try { - player = playerFactory.createExoPlayer(renderersFactory, trackSelector, loadControl); + player = new TestSimpleExoPlayer(renderersFactory, trackSelector, loadControl, clock); player.addListener(ExoPlayerTestRunner.this); if (eventListener != null) { player.addListener(eventListener); @@ -685,4 +637,15 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener actionScheduleFinishedCountDownLatch.countDown(); } + /** SimpleExoPlayer implementation using a custom Clock. */ + private static final class TestSimpleExoPlayer extends SimpleExoPlayer { + + public TestSimpleExoPlayer( + RenderersFactory renderersFactory, + TrackSelector trackSelector, + LoadControl loadControl, + Clock clock) { + super(renderersFactory, trackSelector, loadControl, clock); + } + } } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeClock.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeClock.java index 83ecbacdde..49656eef99 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeClock.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeClock.java @@ -24,7 +24,7 @@ import java.util.ArrayList; import java.util.List; /** Fake {@link Clock} implementation independent of {@link android.os.SystemClock}. */ -public final class FakeClock implements Clock { +public class FakeClock implements Clock { private final List wakeUpTimes; private final List handlerMessages; diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java deleted file mode 100644 index 591b94a9cd..0000000000 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSimpleExoPlayer.java +++ /dev/null @@ -1,541 +0,0 @@ -/* - * 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.testutil; - -import android.os.ConditionVariable; -import android.os.Handler; -import android.os.HandlerThread; -import android.os.Looper; -import android.support.annotation.Nullable; -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.LoadControl; -import com.google.android.exoplayer2.PlaybackParameters; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.Renderer; -import com.google.android.exoplayer2.RendererCapabilities; -import com.google.android.exoplayer2.RenderersFactory; -import com.google.android.exoplayer2.SimpleExoPlayer; -import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.Timeline.Period; -import com.google.android.exoplayer2.source.MediaPeriod; -import com.google.android.exoplayer2.source.MediaSource; -import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; -import com.google.android.exoplayer2.source.SampleStream; -import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.trackselection.TrackSelection; -import com.google.android.exoplayer2.trackselection.TrackSelectionArray; -import com.google.android.exoplayer2.trackselection.TrackSelector; -import com.google.android.exoplayer2.trackselection.TrackSelector.InvalidationListener; -import com.google.android.exoplayer2.trackselection.TrackSelectorResult; -import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.Clock; -import java.util.Arrays; -import java.util.concurrent.CopyOnWriteArraySet; - -/** - * Fake {@link SimpleExoPlayer} which runs a simplified copy of the playback loop as fast as - * possible without waiting. It does only support single period timelines and does not support - * updates during playback (like seek, timeline changes, repeat mode changes). - */ -public class FakeSimpleExoPlayer extends SimpleExoPlayer { - - private FakeExoPlayer player; - - public FakeSimpleExoPlayer(RenderersFactory renderersFactory, TrackSelector trackSelector, - LoadControl loadControl, FakeClock clock) { - super(renderersFactory, trackSelector, loadControl, clock); - player.setFakeClock(clock); - } - - @Override - protected ExoPlayer createExoPlayerImpl( - Renderer[] renderers, TrackSelector trackSelector, LoadControl loadControl, Clock clock) { - this.player = new FakeExoPlayer(renderers, trackSelector, loadControl); - return player; - } - - private static class FakeExoPlayer extends StubExoPlayer implements MediaSource.Listener, - MediaPeriod.Callback, Runnable { - - private final Renderer[] renderers; - private final TrackSelector trackSelector; - private final LoadControl loadControl; - private final CopyOnWriteArraySet eventListeners; - private final HandlerThread playbackThread; - private final Handler playbackHandler; - private final Handler eventListenerHandler; - - private FakeClock clock; - private MediaSource mediaSource; - private Timeline timeline; - private Object manifest; - private MediaPeriod mediaPeriod; - private TrackSelectorResult selectorResult; - - private boolean isStartingUp; - private boolean isLoading; - private int playbackState; - private long rendererPositionUs; - private long durationUs; - private volatile long currentPositionMs; - private volatile long bufferedPositionMs; - - public FakeExoPlayer(Renderer[] renderers, TrackSelector trackSelector, - LoadControl loadControl) { - this.renderers = renderers; - this.trackSelector = trackSelector; - this.loadControl = loadControl; - this.eventListeners = new CopyOnWriteArraySet<>(); - Looper eventListenerLooper = Looper.myLooper(); - this.eventListenerHandler = new Handler(eventListenerLooper != null ? eventListenerLooper - : Looper.getMainLooper()); - this.playbackThread = new HandlerThread("FakeExoPlayer Thread"); - playbackThread.start(); - this.playbackHandler = new Handler(playbackThread.getLooper()); - this.isStartingUp = true; - this.isLoading = false; - this.playbackState = Player.STATE_IDLE; - this.durationUs = C.TIME_UNSET; - } - - public void setFakeClock(FakeClock clock) { - this.clock = clock; - } - - @Override - public void addListener(Player.EventListener listener) { - eventListeners.add(listener); - } - - @Override - public void removeListener(Player.EventListener listener) { - eventListeners.remove(listener); - } - - @Override - public int getPlaybackState() { - return playbackState; - } - - @Override - public void setPlayWhenReady(boolean playWhenReady) { - if (!playWhenReady) { - throw new UnsupportedOperationException(); - } - } - - @Override - public boolean getPlayWhenReady() { - return true; - } - - @Override - public int getRepeatMode() { - return Player.REPEAT_MODE_OFF; - } - - @Override - public boolean getShuffleModeEnabled() { - return false; - } - - @Override - public boolean isLoading() { - return isLoading; - } - - @Override - public PlaybackParameters getPlaybackParameters() { - return PlaybackParameters.DEFAULT; - } - - @Override - public void stop() { - stop(/* reset= */ false); - } - - @Override - public void stop(boolean reset) { - stopPlayback(/* quitPlaybackThread= */ false); - } - - @Override - @SuppressWarnings("ThreadJoinLoop") - public void release() { - stopPlayback(/* quitPlaybackThread= */ true); - while (playbackThread.isAlive()) { - try { - playbackThread.join(); - } catch (InterruptedException e) { - // Ignore interrupt. - } - } - } - - @Override - public int getRendererCount() { - return renderers.length; - } - - @Override - public int getRendererType(int index) { - return renderers[index].getTrackType(); - } - - @Override - public TrackGroupArray getCurrentTrackGroups() { - return selectorResult != null ? selectorResult.groups : null; - } - - @Override - public TrackSelectionArray getCurrentTrackSelections() { - return selectorResult != null ? selectorResult.selections : null; - } - - @Nullable - @Override - public Object getCurrentManifest() { - return manifest; - } - - @Override - public Timeline getCurrentTimeline() { - return timeline; - } - - @Override - public int getCurrentPeriodIndex() { - return 0; - } - - @Override - public int getCurrentWindowIndex() { - return 0; - } - - @Override - public int getNextWindowIndex() { - return C.INDEX_UNSET; - } - - @Override - public int getPreviousWindowIndex() { - return C.INDEX_UNSET; - } - - @Override - public long getDuration() { - return C.usToMs(durationUs); - } - - @Override - public long getCurrentPosition() { - return currentPositionMs; - } - - @Override - public long getBufferedPosition() { - return bufferedPositionMs == C.TIME_END_OF_SOURCE ? getDuration() : bufferedPositionMs; - } - - @Override - public int getBufferedPercentage() { - long duration = getDuration(); - return duration == C.TIME_UNSET ? 0 : (int) (getBufferedPosition() * 100 / duration); - } - - @Override - public boolean isCurrentWindowDynamic() { - return false; - } - - @Override - public boolean isCurrentWindowSeekable() { - return false; - } - - @Override - public boolean isPlayingAd() { - return false; - } - - @Override - public int getCurrentAdGroupIndex() { - return 0; - } - - @Override - public int getCurrentAdIndexInAdGroup() { - return 0; - } - - @Override - public long getContentPosition() { - return getCurrentPosition(); - } - - @Override - public Looper getPlaybackLooper() { - return playbackThread.getLooper(); - } - - @Override - public void prepare(MediaSource mediaSource) { - prepare(mediaSource, true, true); - } - - @Override - public void prepare(final MediaSource mediaSource, boolean resetPosition, boolean resetState) { - if (!resetPosition || !resetState) { - throw new UnsupportedOperationException(); - } - this.mediaSource = mediaSource; - playbackHandler.post(new Runnable() { - @Override - public void run() { - mediaSource.prepareSource(FakeExoPlayer.this, true, FakeExoPlayer.this); - } - }); - } - - // MediaSource.Listener - - @Override - public void onSourceInfoRefreshed(MediaSource source, final Timeline timeline, - final @Nullable Object manifest) { - if (this.timeline != null) { - throw new UnsupportedOperationException(); - } - Assertions.checkArgument(timeline.getPeriodCount() == 1); - Assertions.checkArgument(timeline.getWindowCount() == 1); - final ConditionVariable waitForNotification = new ConditionVariable(); - eventListenerHandler.post(new Runnable() { - @Override - public void run() { - for (Player.EventListener eventListener : eventListeners) { - FakeExoPlayer.this.durationUs = timeline.getPeriod(0, new Period()).durationUs; - FakeExoPlayer.this.timeline = timeline; - FakeExoPlayer.this.manifest = manifest; - eventListener.onTimelineChanged(timeline, manifest, - Player.TIMELINE_CHANGE_REASON_PREPARED); - waitForNotification.open(); - } - } - }); - waitForNotification.block(); - this.mediaPeriod = mediaSource.createPeriod(new MediaPeriodId(0), loadControl.getAllocator()); - mediaPeriod.prepare(this, 0); - } - - // MediaPeriod.Callback - - @Override - public void onContinueLoadingRequested(MediaPeriod source) { - maybeContinueLoading(); - } - - @Override - public void onPrepared(MediaPeriod mediaPeriod) { - try { - initializePlaybackLoop(); - } catch (ExoPlaybackException e) { - handlePlayerError(e); - } - } - - // Runnable (Playback loop). - - @Override - public void run() { - try { - maybeContinueLoading(); - mediaPeriod.discardBuffer(rendererPositionUs, /* toKeyframe= */ false); - boolean allRenderersEnded = true; - boolean allRenderersReadyOrEnded = true; - if (playbackState == Player.STATE_READY) { - for (Renderer renderer : renderers) { - renderer.render(rendererPositionUs, C.msToUs(clock.elapsedRealtime())); - if (!renderer.isEnded()) { - allRenderersEnded = false; - } - if (!(renderer.isReady() || renderer.isEnded())) { - allRenderersReadyOrEnded = false; - } - } - } - if (rendererPositionUs >= durationUs && allRenderersEnded) { - changePlaybackState(Player.STATE_ENDED); - return; - } - long bufferedPositionUs = mediaPeriod.getBufferedPositionUs(); - if (playbackState == Player.STATE_BUFFERING && allRenderersReadyOrEnded - && haveSufficientBuffer(!isStartingUp, rendererPositionUs, bufferedPositionUs)) { - changePlaybackState(Player.STATE_READY); - isStartingUp = false; - } else if (playbackState == Player.STATE_READY && !allRenderersReadyOrEnded) { - changePlaybackState(Player.STATE_BUFFERING); - } - // Advance simulated time by 10ms. - clock.advanceTime(10); - if (playbackState == Player.STATE_READY) { - rendererPositionUs += 10000; - } - this.currentPositionMs = C.usToMs(rendererPositionUs); - this.bufferedPositionMs = C.usToMs(bufferedPositionUs); - playbackHandler.post(this); - } catch (ExoPlaybackException e) { - handlePlayerError(e); - } - } - - // Internal logic - - private void initializePlaybackLoop() throws ExoPlaybackException { - Assertions.checkNotNull(clock); - trackSelector.init(new InvalidationListener() { - @Override - public void onTrackSelectionsInvalidated() { - throw new IllegalStateException(); - } - }); - RendererCapabilities[] rendererCapabilities = new RendererCapabilities[renderers.length]; - for (int i = 0; i < renderers.length; i++) { - rendererCapabilities[i] = renderers[i].getCapabilities(); - } - selectorResult = trackSelector.selectTracks(rendererCapabilities, - mediaPeriod.getTrackGroups()); - SampleStream[] sampleStreams = new SampleStream[renderers.length]; - boolean[] mayRetainStreamFlags = new boolean[renderers.length]; - Arrays.fill(mayRetainStreamFlags, true); - mediaPeriod.selectTracks( - selectorResult.selections.getAll(), - mayRetainStreamFlags, - sampleStreams, - new boolean[renderers.length], - /* positionUs = */ 0); - eventListenerHandler.post(new Runnable() { - @Override - public void run() { - for (Player.EventListener eventListener : eventListeners) { - eventListener.onTracksChanged(selectorResult.groups, selectorResult.selections); - } - } - }); - - loadControl.onPrepared(); - loadControl.onTracksSelected(renderers, selectorResult.groups, selectorResult.selections); - - for (int i = 0; i < renderers.length; i++) { - TrackSelection selection = selectorResult.selections.get(i); - Format[] formats = new Format[selection.length()]; - for (int j = 0; j < formats.length; j++) { - formats[j] = selection.getFormat(j); - } - renderers[i].enable(selectorResult.rendererConfigurations[i], formats, sampleStreams[i], 0, - false, 0); - renderers[i].setCurrentStreamFinal(); - } - - rendererPositionUs = 0; - changePlaybackState(Player.STATE_BUFFERING); - playbackHandler.post(this); - } - - private void maybeContinueLoading() { - boolean newIsLoading = false; - long nextLoadPositionUs = mediaPeriod.getNextLoadPositionUs(); - if (nextLoadPositionUs != C.TIME_END_OF_SOURCE) { - long bufferedDurationUs = nextLoadPositionUs - rendererPositionUs; - if (loadControl.shouldContinueLoading(bufferedDurationUs, 1f)) { - newIsLoading = true; - mediaPeriod.continueLoading(rendererPositionUs); - } - } - if (newIsLoading != isLoading) { - isLoading = newIsLoading; - eventListenerHandler.post(new Runnable() { - @Override - public void run() { - for (Player.EventListener eventListener : eventListeners) { - eventListener.onLoadingChanged(isLoading); - } - } - }); - } - } - - private boolean haveSufficientBuffer(boolean rebuffering, long rendererPositionUs, - long bufferedPositionUs) { - return bufferedPositionUs == C.TIME_END_OF_SOURCE - || loadControl.shouldStartPlayback( - bufferedPositionUs - rendererPositionUs, 1f, rebuffering); - } - - private void handlePlayerError(final ExoPlaybackException e) { - eventListenerHandler.post(new Runnable() { - @Override - public void run() { - for (Player.EventListener listener : eventListeners) { - listener.onPlayerError(e); - } - } - }); - changePlaybackState(Player.STATE_ENDED); - } - - private void changePlaybackState(final int playbackState) { - this.playbackState = playbackState; - eventListenerHandler.post(new Runnable() { - @Override - public void run() { - for (Player.EventListener listener : eventListeners) { - listener.onPlayerStateChanged(true, playbackState); - } - } - }); - } - - private void releaseMedia() { - if (mediaSource != null) { - if (mediaPeriod != null) { - mediaSource.releasePeriod(mediaPeriod); - mediaPeriod = null; - } - mediaSource.releaseSource(); - mediaSource = null; - } - } - - private void stopPlayback(final boolean quitPlaybackThread) { - playbackHandler.post(new Runnable() { - @Override - public void run () { - playbackHandler.removeCallbacksAndMessages(null); - releaseMedia(); - changePlaybackState(Player.STATE_IDLE); - if (quitPlaybackThread) { - playbackThread.quit(); - } - } - }); - } - - } - -} From 8f1ef6a29aeedfdf8934e85b9bf9089fe06c6dfb Mon Sep 17 00:00:00 2001 From: eguven Date: Wed, 27 Dec 2017 09:02:40 -0800 Subject: [PATCH 0990/2472] Typo fixes Issue:#3631 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=180197723 --- README.md | 2 +- RELEASENOTES.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ecfe3eb96f..7f35329516 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ individually. In addition to library modules, ExoPlayer has multiple extension modules that depend on external libraries to provide additional functionality. Some -extensions are available from JCenter, whereas others must be built manaully. +extensions are available from JCenter, whereas others must be built manually. Browse the [extensions directory][] and their individual READMEs for details. More information on the library and extension modules that are available from diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 3bc55476c2..43e860b000 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -221,7 +221,7 @@ easy and seamless way of incorporating display ads into ExoPlayer playbacks. You can read more about the IMA extension [here](https://medium.com/google-exoplayer/playing-ads-with-exoplayer-and-ima-868dfd767ea). -* MediaSession extension: Provides an easy to to connect ExoPlayer with +* MediaSession extension: Provides an easy to connect ExoPlayer with MediaSessionCompat in the Android Support Library. * RTMP extension: An extension for playing streams over RTMP. * Build: Made it easier for application developers to depend on a local checkout From ad80784c19981eb95ad22550cd41f445b5b7d101 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 28 Dec 2017 02:14:20 -0800 Subject: [PATCH 0991/2472] Fix parameter order in DefaultLoadControl constructor. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=180254437 --- .../java/com/google/android/exoplayer2/DefaultLoadControl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java index 3708500d9f..26873fcf2e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java @@ -90,8 +90,8 @@ public class DefaultLoadControl implements LoadControl { allocator, DEFAULT_MIN_BUFFER_MS, DEFAULT_MAX_BUFFER_MS, - DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS, DEFAULT_BUFFER_FOR_PLAYBACK_MS, + DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS, DEFAULT_TARGET_BUFFER_BYTES, DEFAULT_PRIORITIZE_TIME_OVER_SIZE_THRESHOLDS); } From 2bd704d8335292e36f0bcd84767282881c3df05b Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 2 Jan 2018 05:35:14 -0800 Subject: [PATCH 0992/2472] Add missing override for reevaluateBuffer in FakeAdaptiveMediaPeriod. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=180538379 --- .../exoplayer2/testutil/FakeAdaptiveMediaPeriod.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java index 1a3e69f029..7b9fe3db07 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java @@ -96,6 +96,12 @@ public class FakeAdaptiveMediaPeriod extends FakeMediaPeriod } } + @Override + public void reevaluateBuffer(long positionUs) { + super.reevaluateBuffer(positionUs); + sequenceableLoader.reevaluateBuffer(positionUs); + } + @Override public long getBufferedPositionUs() { super.getBufferedPositionUs(); From 88abb153bb18c9b15be0d8e15ea673cfd9dccf3e Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 2 Jan 2018 06:15:13 -0800 Subject: [PATCH 0993/2472] Force audio renderers to report same position when not started Whilst the previous behavior was WAI and had the advantage of updating the position to be more exact when known, there were a couple of disadvantages: 1. If seeking to the very end of a period in a playlist when paused, the position adjustment could trigger a position discontinuity to the next period. 2. We de-duplicate seeks to the current playback position. The position adjustment can prevent this from being effective. This is particularly important with the new SeekParameters support. When seeking to nearest sync point it's often possible to de-duplicate seeks, but we cannot do so if the playback position adjusts away from the sync point's time. Issue: #2439 Issue: #2882 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=180540736 --- RELEASENOTES.md | 2 + .../audio/MediaCodecAudioRenderer.java | 19 +++++--- .../audio/SimpleDecoderAudioRenderer.java | 45 +++++++++++-------- 3 files changed, 43 insertions(+), 23 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 43e860b000..b0027d75a1 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -78,6 +78,8 @@ ([#3188](https://github.com/google/ExoPlayer/issues/3188)). * CEA-608: Fix handling of row count changes in roll-up mode ([#3513](https://github.com/google/ExoPlayer/issues/3513)). +* Prevent period transitions when seeking to the end of a period when paused + ([#2439](https://github.com/google/ExoPlayer/issues/2439)). ### 2.6.0 ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index 25ad847f7e..b4459e42aa 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java @@ -364,6 +364,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media @Override protected void onStopped() { audioSink.pause(); + updateCurrentPosition(); super.onStopped(); } @@ -393,11 +394,8 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media @Override public long getPositionUs() { - long newCurrentPositionUs = audioSink.getCurrentPositionUs(isEnded()); - if (newCurrentPositionUs != AudioSink.CURRENT_POSITION_NOT_SET) { - currentPositionUs = allowPositionDiscontinuity ? newCurrentPositionUs - : Math.max(currentPositionUs, newCurrentPositionUs); - allowPositionDiscontinuity = false; + if (getState() == STATE_STARTED) { + updateCurrentPosition(); } return currentPositionUs; } @@ -466,6 +464,17 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } } + private void updateCurrentPosition() { + long newCurrentPositionUs = audioSink.getCurrentPositionUs(isEnded()); + if (newCurrentPositionUs != AudioSink.CURRENT_POSITION_NOT_SET) { + currentPositionUs = + allowPositionDiscontinuity + ? newCurrentPositionUs + : Math.max(currentPositionUs, newCurrentPositionUs); + allowPositionDiscontinuity = false; + } + } + /** * Returns whether the decoder is known to output six audio channels when provided with input with * fewer than six channels. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java index d9ad549104..16a85fe1f4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java @@ -459,11 +459,8 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements @Override public long getPositionUs() { - long newCurrentPositionUs = audioSink.getCurrentPositionUs(isEnded()); - if (newCurrentPositionUs != AudioSink.CURRENT_POSITION_NOT_SET) { - currentPositionUs = allowPositionDiscontinuity ? newCurrentPositionUs - : Math.max(currentPositionUs, newCurrentPositionUs); - allowPositionDiscontinuity = false; + if (getState() == STATE_STARTED) { + updateCurrentPosition(); } return currentPositionUs; } @@ -510,6 +507,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements @Override protected void onStopped() { audioSink.pause(); + updateCurrentPosition(); } @Override @@ -540,6 +538,22 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements } } + @Override + public void handleMessage(int messageType, Object message) throws ExoPlaybackException { + switch (messageType) { + case C.MSG_SET_VOLUME: + audioSink.setVolume((Float) message); + break; + case C.MSG_SET_AUDIO_ATTRIBUTES: + AudioAttributes audioAttributes = (AudioAttributes) message; + audioSink.setAudioAttributes(audioAttributes); + break; + default: + super.handleMessage(messageType, message); + break; + } + } + private void maybeInitDecoder() throws ExoPlaybackException { if (decoder != null) { return; @@ -625,19 +639,14 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements eventDispatcher.inputFormatChanged(newFormat); } - @Override - public void handleMessage(int messageType, Object message) throws ExoPlaybackException { - switch (messageType) { - case C.MSG_SET_VOLUME: - audioSink.setVolume((Float) message); - break; - case C.MSG_SET_AUDIO_ATTRIBUTES: - AudioAttributes audioAttributes = (AudioAttributes) message; - audioSink.setAudioAttributes(audioAttributes); - break; - default: - super.handleMessage(messageType, message); - break; + private void updateCurrentPosition() { + long newCurrentPositionUs = audioSink.getCurrentPositionUs(isEnded()); + if (newCurrentPositionUs != AudioSink.CURRENT_POSITION_NOT_SET) { + currentPositionUs = + allowPositionDiscontinuity + ? newCurrentPositionUs + : Math.max(currentPositionUs, newCurrentPositionUs); + allowPositionDiscontinuity = false; } } From 884f64017fd407aff53bc6e756a73d0f083d9cc1 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 2 Jan 2018 07:00:04 -0800 Subject: [PATCH 0994/2472] Typo fix ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=180543378 --- RELEASENOTES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index b0027d75a1..9f48db0be8 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -223,7 +223,7 @@ easy and seamless way of incorporating display ads into ExoPlayer playbacks. You can read more about the IMA extension [here](https://medium.com/google-exoplayer/playing-ads-with-exoplayer-and-ima-868dfd767ea). -* MediaSession extension: Provides an easy to connect ExoPlayer with +* MediaSession extension: Provides an easy way to connect ExoPlayer with MediaSessionCompat in the Android Support Library. * RTMP extension: An extension for playing streams over RTMP. * Build: Made it easier for application developers to depend on a local checkout From 22f8ee37d429e9ed0304da00d507035d7e7ef887 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 2 Jan 2018 07:12:25 -0800 Subject: [PATCH 0995/2472] Clean-up of player message handling. Some readability fixes for PlayerMessage and the handling in ExoPlayerImplInternal. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=180544294 --- .../exoplayer2/ext/vp9/VpxPlaybackTest.java | 2 +- .../google/android/exoplayer2/ExoPlayer.java | 2 +- .../android/exoplayer2/ExoPlayerImpl.java | 4 +- .../exoplayer2/ExoPlayerImplInternal.java | 95 +++++++++---------- .../android/exoplayer2/PlayerMessage.java | 77 ++++++--------- .../android/exoplayer2/SimpleExoPlayer.java | 30 +++--- .../DynamicConcatenatingMediaSource.java | 10 +- .../testutil/MediaSourceTestRunner.java | 16 +--- 8 files changed, 107 insertions(+), 129 deletions(-) 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 0f8df65959..c5485d3f96 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 @@ -122,7 +122,7 @@ public class VpxPlaybackTest extends InstrumentationTestCase { player .createMessage(videoRenderer) .setType(LibvpxVideoRenderer.MSG_SET_OUTPUT_BUFFER_RENDERER) - .setMessage(new VpxVideoSurfaceView(context)) + .setPayload(new VpxVideoSurfaceView(context)) .send(); player.prepare(mediaSource); player.setPlayWhenReady(true); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java index 4bd28150bc..a9980f9803 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java @@ -217,7 +217,7 @@ public interface ExoPlayer extends Player { /** * Creates a message that can be sent to a {@link PlayerMessage.Target}. By default, the message * will be delivered immediately without blocking on the playback thread. The default {@link - * PlayerMessage#getType()} is 0 and the default {@link PlayerMessage#getMessage()} is null. If a + * PlayerMessage#getType()} is 0 and the default {@link PlayerMessage#getPayload()} is null. If a * position is specified with {@link PlayerMessage#setPosition(long)}, the message will be * delivered at this position in the current window defined by {@link #getCurrentWindowIndex()}. * Alternatively, the message can be sent at a specific window using {@link diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index 4e97a47924..b5f6e623eb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -336,7 +336,7 @@ import java.util.concurrent.CopyOnWriteArraySet; @Override public void sendMessages(ExoPlayerMessage... messages) { for (ExoPlayerMessage message : messages) { - createMessage(message.target).setType(message.messageType).setMessage(message.message).send(); + createMessage(message.target).setType(message.messageType).setPayload(message.message).send(); } } @@ -357,7 +357,7 @@ import java.util.concurrent.CopyOnWriteArraySet; playerMessages.add( createMessage(message.target) .setType(message.messageType) - .setMessage(message.message) + .setPayload(message.message) .send()); } boolean wasInterrupted = false; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 1c680d4aba..65f43ae684 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -79,6 +79,7 @@ import java.util.Collections; private static final int MSG_CUSTOM = 12; private static final int MSG_SET_REPEAT_MODE = 13; private static final int MSG_SET_SHUFFLE_ENABLED = 14; + private static final int MSG_SEND_MESSAGE_TO_TARGET = 15; private static final int PREPARING_SOURCE_INTERVAL_MS = 10; private static final int RENDERING_INTERVAL_MS = 10; @@ -223,14 +224,13 @@ import java.util.Collections; } @Override - public synchronized void sendMessage( - PlayerMessage message, PlayerMessage.Sender.Listener listener) { + public synchronized void sendMessage(PlayerMessage message) { if (released) { Log.w(TAG, "Ignoring messages sent after release."); - listener.onMessageDeleted(); + message.markAsProcessed(/* isDelivered= */ false); return; } - handler.obtainMessage(MSG_CUSTOM, new CustomMessageInfo(message, listener)).sendToTarget(); + handler.obtainMessage(MSG_CUSTOM, message).sendToTarget(); } public synchronized void release() { @@ -338,7 +338,10 @@ import java.util.Collections; reselectTracksInternal(); break; case MSG_CUSTOM: - sendMessageInternal((CustomMessageInfo) msg.obj); + sendMessageInternal((PlayerMessage) msg.obj); + break; + case MSG_SEND_MESSAGE_TO_TARGET: + sendCustomMessageToTargetThread((PlayerMessage) msg.obj); break; case MSG_RELEASE: releaseInternal(); @@ -838,7 +841,7 @@ import java.util.Collections; if (resetState) { mediaPeriodInfoSequence.setTimeline(null); for (CustomMessageInfo customMessageInfo : customMessageInfos) { - customMessageInfo.listener.onMessageDeleted(); + customMessageInfo.message.markAsProcessed(/* isDelivered= */ false); } customMessageInfos.clear(); nextCustomMessageInfoIndex = 0; @@ -862,58 +865,54 @@ import java.util.Collections; } } - private void sendMessageInternal(CustomMessageInfo customMessageInfo) { - if (customMessageInfo.message.getPositionMs() == C.TIME_UNSET) { + private void sendMessageInternal(PlayerMessage message) { + if (message.getPositionMs() == C.TIME_UNSET) { // If no delivery time is specified, trigger immediate message delivery. - sendCustomMessagesToTarget(customMessageInfo); + sendCustomMessageToTarget(message); } else if (playbackInfo.timeline == null) { // Still waiting for initial timeline to resolve position. - customMessageInfos.add(customMessageInfo); + customMessageInfos.add(new CustomMessageInfo(message)); } else { + CustomMessageInfo customMessageInfo = new CustomMessageInfo(message); if (resolveCustomMessagePosition(customMessageInfo)) { customMessageInfos.add(customMessageInfo); // Ensure new message is inserted according to playback order. Collections.sort(customMessageInfos); } else { - customMessageInfo.listener.onMessageDeleted(); + message.markAsProcessed(/* isDelivered= */ false); } } } - private void sendCustomMessagesToTarget(final CustomMessageInfo customMessageInfo) { - final Runnable handleMessageRunnable = - new Runnable() { - @Override - public void run() { - try { - customMessageInfo - .message - .getTarget() - .handleMessage( - customMessageInfo.message.getType(), customMessageInfo.message.getMessage()); - } catch (ExoPlaybackException e) { - eventHandler.obtainMessage(MSG_ERROR, e).sendToTarget(); - } finally { - customMessageInfo.listener.onMessageDelivered(); - if (customMessageInfo.message.getDeleteAfterDelivery()) { - customMessageInfo.listener.onMessageDeleted(); - } - // The message may have caused something to change that now requires us to do - // work. - handler.sendEmptyMessage(MSG_DO_SOME_WORK); - } - } - }; - if (customMessageInfo.message.getHandler().getLooper() == handler.getLooper()) { - handleMessageRunnable.run(); + private void sendCustomMessageToTarget(PlayerMessage message) { + if (message.getHandler().getLooper() == handler.getLooper()) { + deliverCustomMessage(message); + // The message may have caused something to change that now requires us to do work. + handler.sendEmptyMessage(MSG_DO_SOME_WORK); } else { - handler.post( - new Runnable() { - @Override - public void run() { - customMessageInfo.message.getHandler().post(handleMessageRunnable); - } - }); + handler.obtainMessage(MSG_SEND_MESSAGE_TO_TARGET, message).sendToTarget(); + } + } + + private void sendCustomMessageToTargetThread(final PlayerMessage message) { + message + .getHandler() + .post( + new Runnable() { + @Override + public void run() { + deliverCustomMessage(message); + } + }); + } + + private void deliverCustomMessage(PlayerMessage message) { + try { + message.getTarget().handleMessage(message.getType(), message.getPayload()); + } catch (ExoPlaybackException e) { + eventHandler.obtainMessage(MSG_ERROR, e).sendToTarget(); + } finally { + message.markAsProcessed(/* isDelivered= */ true); } } @@ -921,7 +920,7 @@ import java.util.Collections; for (int i = customMessageInfos.size() - 1; i >= 0; i--) { if (!resolveCustomMessagePosition(customMessageInfos.get(i))) { // Remove messages if new position can't be resolved. - customMessageInfos.get(i).listener.onMessageDeleted(); + customMessageInfos.get(i).message.markAsProcessed(/* isDelivered= */ false); customMessageInfos.remove(i); } } @@ -1003,7 +1002,7 @@ import java.util.Collections; && nextInfo.resolvedPeriodIndex == currentPeriodIndex && nextInfo.resolvedPeriodTimeUs > oldPeriodPositionUs && nextInfo.resolvedPeriodTimeUs <= newPeriodPositionUs) { - sendCustomMessagesToTarget(nextInfo); + sendCustomMessageToTarget(nextInfo.message); if (nextInfo.message.getDeleteAfterDelivery()) { customMessageInfos.remove(nextCustomMessageInfoIndex); } else { @@ -1942,15 +1941,13 @@ import java.util.Collections; private static final class CustomMessageInfo implements Comparable { public final PlayerMessage message; - public final PlayerMessage.Sender.Listener listener; public int resolvedPeriodIndex; public long resolvedPeriodTimeUs; public @Nullable Object resolvedPeriodUid; - public CustomMessageInfo(PlayerMessage message, PlayerMessage.Sender.Listener listener) { + public CustomMessageInfo(PlayerMessage message) { this.message = message; - this.listener = listener; } public void setResolvedPosition(int periodIndex, long periodTimeUs, Object periodUid) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java b/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java index 420eb60a48..1e8a89e102 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/PlayerMessage.java @@ -32,32 +32,21 @@ public final class PlayerMessage { * Handles a message delivered to the target. * * @param messageType The message type. - * @param message The message. + * @param payload The message payload. * @throws ExoPlaybackException If an error occurred whilst handling the message. */ - void handleMessage(int messageType, Object message) throws ExoPlaybackException; + void handleMessage(int messageType, Object payload) throws ExoPlaybackException; } /** A sender for messages. */ public interface Sender { - /** A listener for message events triggered by the sender. */ - interface Listener { - - /** Called when the message has been delivered. */ - void onMessageDelivered(); - - /** Called when the message has been deleted. */ - void onMessageDeleted(); - } - /** * Sends a message. * * @param message The message to be sent. - * @param listener The listener to listen to message events. */ - void sendMessage(PlayerMessage message, Listener listener); + void sendMessage(PlayerMessage message); } private final Target target; @@ -65,14 +54,14 @@ public final class PlayerMessage { private final Timeline timeline; private int type; - private Object message; + private Object payload; private Handler handler; private int windowIndex; private long positionMs; private boolean deleteAfterDelivery; private boolean isSent; private boolean isDelivered; - private boolean isDeleted; + private boolean isProcessed; /** * Creates a new message. @@ -112,9 +101,9 @@ public final class PlayerMessage { } /** - * Sets a custom message type forwarded to the {@link Target#handleMessage(int, Object)}. + * Sets the message type forwarded to {@link Target#handleMessage(int, Object)}. * - * @param messageType The custom message type. + * @param messageType The message type. * @return This message. * @throws IllegalStateException If {@link #send()} has already been called. */ @@ -124,27 +113,27 @@ public final class PlayerMessage { return this; } - /** Returns custom message type forwarded to the {@link Target#handleMessage(int, Object)}. */ + /** Returns the message type forwarded to {@link Target#handleMessage(int, Object)}. */ public int getType() { return type; } /** - * Sets a custom message forwarded to the {@link Target#handleMessage(int, Object)}. + * Sets the message payload forwarded to {@link Target#handleMessage(int, Object)}. * - * @param message The custom message. + * @param payload The message payload. * @return This message. * @throws IllegalStateException If {@link #send()} has already been called. */ - public PlayerMessage setMessage(@Nullable Object message) { + public PlayerMessage setPayload(@Nullable Object payload) { Assertions.checkState(!isSent); - this.message = message; + this.payload = payload; return this; } - /** Returns custom message forwarded to the {@link Target#handleMessage(int, Object)}. */ - public Object getMessage() { - return message; + /** Returns the message payload forwarded to {@link Target#handleMessage(int, Object)}. */ + public Object getPayload() { + return payload; } /** @@ -248,25 +237,7 @@ public final class PlayerMessage { Assertions.checkArgument(deleteAfterDelivery); } isSent = true; - sender.sendMessage( - this, - new Sender.Listener() { - @Override - public void onMessageDelivered() { - synchronized (PlayerMessage.this) { - isDelivered = true; - PlayerMessage.this.notifyAll(); - } - } - - @Override - public void onMessageDeleted() { - synchronized (PlayerMessage.this) { - isDeleted = true; - PlayerMessage.this.notifyAll(); - } - } - }); + sender.sendMessage(this); return this; } @@ -287,9 +258,23 @@ public final class PlayerMessage { public synchronized boolean blockUntilDelivered() throws InterruptedException { Assertions.checkState(isSent); Assertions.checkState(handler.getLooper().getThread() != Thread.currentThread()); - while (!isDelivered && !isDeleted) { + while (!isProcessed) { wait(); } return isDelivered; } + + /** + * Marks the message as processed. Should only be called by a {@link Sender} and may be called + * multiple times. + * + * @param isDelivered Whether the message has been delivered to its target. The message is + * considered as being delivered when this method has been called with {@code isDelivered} set + * to true at least once. + */ + public synchronized void markAsProcessed(boolean isDelivered) { + this.isDelivered |= isDelivered; + isProcessed = true; + notifyAll(); + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index d4346a65e1..ec53e5a964 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -41,6 +41,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.video.VideoRendererEventListener; +import java.util.ArrayList; import java.util.List; import java.util.concurrent.CopyOnWriteArraySet; @@ -168,7 +169,7 @@ public class SimpleExoPlayer implements ExoPlayer { player .createMessage(renderer) .setType(C.MSG_SET_SCALING_MODE) - .setMessage(videoScalingMode) + .setPayload(videoScalingMode) .send(); } } @@ -357,7 +358,7 @@ public class SimpleExoPlayer implements ExoPlayer { player .createMessage(renderer) .setType(C.MSG_SET_AUDIO_ATTRIBUTES) - .setMessage(audioAttributes) + .setPayload(audioAttributes) .send(); } } @@ -379,7 +380,7 @@ public class SimpleExoPlayer implements ExoPlayer { this.audioVolume = audioVolume; for (Renderer renderer : renderers) { if (renderer.getTrackType() == C.TRACK_TYPE_AUDIO) { - player.createMessage(renderer).setType(C.MSG_SET_VOLUME).setMessage(audioVolume).send(); + player.createMessage(renderer).setType(C.MSG_SET_VOLUME).setPayload(audioVolume).send(); } } } @@ -911,21 +912,22 @@ public class SimpleExoPlayer implements ExoPlayer { private void setVideoSurfaceInternal(Surface surface, boolean ownsSurface) { // Note: We don't turn this method into a no-op if the surface is being replaced with itself // so as to ensure onRenderedFirstFrame callbacks are still called in this case. - boolean surfaceReplaced = this.surface != null && this.surface != surface; + List messages = new ArrayList<>(); for (Renderer renderer : renderers) { if (renderer.getTrackType() == C.TRACK_TYPE_VIDEO) { - PlayerMessage message = - player.createMessage(renderer).setType(C.MSG_SET_SURFACE).setMessage(surface).send(); - if (surfaceReplaced) { - try { - message.blockUntilDelivered(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } + messages.add( + player.createMessage(renderer).setType(C.MSG_SET_SURFACE).setPayload(surface).send()); } } - if (surfaceReplaced) { + if (this.surface != null && this.surface != surface) { + // We're replacing a surface. Block to ensure that it's not accessed after the method returns. + try { + for (PlayerMessage message : messages) { + message.blockUntilDelivered(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } // If we created the previous surface, we are responsible for releasing it. if (this.ownsSurface) { this.surface.release(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java index 54537ba548..c2e208afbe 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java @@ -149,7 +149,7 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, Playe player .createMessage(this) .setType(MSG_ADD) - .setMessage(new MessageData<>(index, mediaSource, actionOnCompletion)) + .setPayload(new MessageData<>(index, mediaSource, actionOnCompletion)) .send(); } else if (actionOnCompletion != null) { actionOnCompletion.run(); @@ -225,7 +225,7 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, Playe player .createMessage(this) .setType(MSG_ADD_MULTIPLE) - .setMessage(new MessageData<>(index, mediaSources, actionOnCompletion)) + .setPayload(new MessageData<>(index, mediaSources, actionOnCompletion)) .send(); } else if (actionOnCompletion != null){ actionOnCompletion.run(); @@ -264,7 +264,7 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, Playe player .createMessage(this) .setType(MSG_REMOVE) - .setMessage(new MessageData<>(index, null, actionOnCompletion)) + .setPayload(new MessageData<>(index, null, actionOnCompletion)) .send(); } else if (actionOnCompletion != null) { actionOnCompletion.run(); @@ -304,7 +304,7 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, Playe player .createMessage(this) .setType(MSG_MOVE) - .setMessage(new MessageData<>(currentIndex, newIndex, actionOnCompletion)) + .setPayload(new MessageData<>(currentIndex, newIndex, actionOnCompletion)) .send(); } else if (actionOnCompletion != null) { actionOnCompletion.run(); @@ -438,7 +438,7 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, Playe new ConcatenatedTimeline(mediaSourceHolders, windowCount, periodCount, shuffleOrder), null); if (actionOnCompletion != null) { - player.createMessage(this).setType(MSG_ON_COMPLETION).setMessage(actionOnCompletion).send(); + player.createMessage(this).setType(MSG_ON_COMPLETION).setPayload(actionOnCompletion).send(); } } } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java index 93c14afc8f..635d0dd835 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java @@ -24,7 +24,6 @@ import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Message; -import android.util.Pair; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.PlayerMessage; import com.google.android.exoplayer2.Timeline; @@ -299,22 +298,17 @@ public class MediaSourceTestRunner { } @Override - public void sendMessage(PlayerMessage message, Listener listener) { - handler.obtainMessage(0, Pair.create(message, listener)).sendToTarget(); + public void sendMessage(PlayerMessage message) { + handler.obtainMessage(0, message).sendToTarget(); } @Override @SuppressWarnings("unchecked") public boolean handleMessage(Message msg) { - Pair messageAndListener = (Pair) msg.obj; + PlayerMessage message = (PlayerMessage) msg.obj; try { - messageAndListener - .first - .getTarget() - .handleMessage( - messageAndListener.first.getType(), messageAndListener.first.getMessage()); - messageAndListener.second.onMessageDelivered(); - messageAndListener.second.onMessageDeleted(); + message.getTarget().handleMessage(message.getType(), message.getPayload()); + message.markAsProcessed(/* isDelivered= */ true); } catch (ExoPlaybackException e) { fail("Unexpected ExoPlaybackException."); } From 0821f578e8ca51fda23b9cddf901ed1f594bd493 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 2 Jan 2018 10:04:24 -0800 Subject: [PATCH 0996/2472] Remove HandlerWrapper.Factory ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=180558741 --- .../google/android/exoplayer2/util/Clock.java | 19 ++++++++++++++---- .../exoplayer2/util/HandlerWrapper.java | 20 ++----------------- .../android/exoplayer2/util/SystemClock.java | 3 ++- ...Handler.java => SystemHandlerWrapper.java} | 13 ++---------- .../exoplayer2/testutil/ExoHostedTest.java | 4 ++-- 5 files changed, 23 insertions(+), 36 deletions(-) rename library/core/src/main/java/com/google/android/exoplayer2/util/{SystemHandler.java => SystemHandlerWrapper.java} (85%) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Clock.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Clock.java index 7731cca68c..43c01bf53a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Clock.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Clock.java @@ -15,12 +15,15 @@ */ package com.google.android.exoplayer2.util; +import android.os.Handler; +import android.os.Looper; +import android.support.annotation.Nullable; + /** - * An interface through which system clocks can be read. The {@link #DEFAULT} implementation must be - * used for all non-test cases. Implementations must also be able to create a {@link HandlerWrapper} - * which uses the underlying clock to schedule delayed messages. + * An interface through which system clocks can be read and {@link HandlerWrapper}s created. The + * {@link #DEFAULT} implementation must be used for all non-test cases. */ -public interface Clock extends HandlerWrapper.Factory { +public interface Clock { /** * Default {@link Clock} to use for all non-test cases. @@ -36,4 +39,12 @@ public interface Clock extends HandlerWrapper.Factory { * @see android.os.SystemClock#sleep(long) */ void sleep(long sleepTimeMs); + + /** + * Creates a {@link HandlerWrapper} using a specified looper and a specified callback for handling + * messages. + * + * @see Handler#Handler(Looper, Handler.Callback). + */ + HandlerWrapper createHandler(Looper looper, @Nullable Handler.Callback callback); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/HandlerWrapper.java b/library/core/src/main/java/com/google/android/exoplayer2/util/HandlerWrapper.java index b9f3a750d7..b101a5e199 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/HandlerWrapper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/HandlerWrapper.java @@ -18,29 +18,13 @@ package com.google.android.exoplayer2.util; import android.os.Handler; import android.os.Looper; import android.os.Message; -import android.support.annotation.Nullable; /** - * An interface to call through to an {@link Handler}. The {@link Factory#DEFAULT} factory must be - * used for all non-test cases. + * An interface to call through to a {@link Handler}. Instances must be created by calling {@link + * Clock#createHandler(Looper, Handler.Callback)} on {@link Clock#DEFAULT} for all non-test cases. */ public interface HandlerWrapper { - /** A factory for handler instances. */ - interface Factory { - - /** Default HandlerWrapper factory to use for all non-test cases. */ - Factory DEFAULT = new SystemHandler.Factory(); - - /** - * Creates a HandlerWrapper running a specified looper and using a specified callback for - * messages. - * - * @see Handler#Handler(Looper, Handler.Callback). - */ - HandlerWrapper createHandler(Looper looper, @Nullable Handler.Callback callback); - } - /** @see Handler#getLooper(). */ Looper getLooper(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/SystemClock.java b/library/core/src/main/java/com/google/android/exoplayer2/util/SystemClock.java index 8a5bdf549f..b24a38ea3c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/SystemClock.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/SystemClock.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.util; +import android.os.Handler; import android.os.Handler.Callback; import android.os.Looper; import android.support.annotation.Nullable; @@ -36,6 +37,6 @@ import android.support.annotation.Nullable; @Override public HandlerWrapper createHandler(Looper looper, @Nullable Callback callback) { - return HandlerWrapper.Factory.DEFAULT.createHandler(looper, callback); + return new SystemHandlerWrapper(new Handler(looper, callback)); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/SystemHandler.java b/library/core/src/main/java/com/google/android/exoplayer2/util/SystemHandlerWrapper.java similarity index 85% rename from library/core/src/main/java/com/google/android/exoplayer2/util/SystemHandler.java rename to library/core/src/main/java/com/google/android/exoplayer2/util/SystemHandlerWrapper.java index e99c626057..aa290d9313 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/SystemHandler.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/SystemHandlerWrapper.java @@ -15,25 +15,16 @@ */ package com.google.android.exoplayer2.util; -import android.os.Handler.Callback; import android.os.Looper; import android.os.Message; import android.os.SystemClock; /** The standard implementation of {@link HandlerWrapper}. */ -/* package */ final class SystemHandler implements HandlerWrapper { - - /* package */ static final class Factory implements HandlerWrapper.Factory { - - @Override - public HandlerWrapper createHandler(Looper looper, Callback callback) { - return new SystemHandler(new android.os.Handler(looper, callback)); - } - } +/* package */ final class SystemHandlerWrapper implements HandlerWrapper { private final android.os.Handler handler; - private SystemHandler(android.os.Handler handler) { + public SystemHandlerWrapper(android.os.Handler handler) { this.handler = handler; } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java index 3a5f3ccd7a..2298a2f0cc 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java @@ -42,6 +42,7 @@ import com.google.android.exoplayer2.upstream.BandwidthMeter; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.upstream.TransferListener; +import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.HandlerWrapper; import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.video.VideoRendererEventListener; @@ -188,8 +189,7 @@ public abstract class ExoHostedTest extends Player.DefaultEventListener implemen player.addAudioDebugListener(this); player.addVideoDebugListener(this); player.setPlayWhenReady(true); - actionHandler = - HandlerWrapper.Factory.DEFAULT.createHandler(Looper.myLooper(), /* callback= */ null); + actionHandler = Clock.DEFAULT.createHandler(Looper.myLooper(), /* callback= */ null); // Schedule any pending actions. if (pendingSchedule != null) { pendingSchedule.start(player, trackSelector, surface, actionHandler, /* callback= */ null); From bf3d6028fac610ee3de0009abb7c602457f25b68 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 2 Jan 2018 10:07:15 -0800 Subject: [PATCH 0997/2472] Make SsaDecoder more robust against malformed content Issue: #3645 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=180559196 --- .../android/exoplayer2/text/ssa/SsaDecoder.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java index eec4a1269c..0cb6f66898 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java @@ -150,6 +150,12 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { break; } } + if (formatStartIndex == C.INDEX_UNSET + || formatEndIndex == C.INDEX_UNSET + || formatTextIndex == C.INDEX_UNSET) { + // Set to 0 so that parseDialogueLine skips lines until a complete format line is found. + formatKeyCount = 0; + } } /** @@ -161,12 +167,17 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { */ private void parseDialogueLine(String dialogueLine, List cues, LongArray cueTimesUs) { if (formatKeyCount == 0) { - Log.w(TAG, "Skipping dialogue line before format: " + dialogueLine); + Log.w(TAG, "Skipping dialogue line before complete format: " + dialogueLine); return; } String[] lineValues = dialogueLine.substring(DIALOGUE_LINE_PREFIX.length()) .split(",", formatKeyCount); + if (lineValues.length != formatKeyCount) { + Log.w(TAG, "Skipping dialogue line with fewer columns than format: " + dialogueLine); + return; + } + long startTimeUs = SsaDecoder.parseTimecodeUs(lineValues[formatStartIndex]); if (startTimeUs == C.TIME_UNSET) { Log.w(TAG, "Skipping invalid timing: " + dialogueLine); From ad95a147d24d2a16412d70ba5981ea1f8f5fa303 Mon Sep 17 00:00:00 2001 From: hoangtc Date: Fri, 22 Dec 2017 05:33:11 -0800 Subject: [PATCH 0998/2472] Fix a bug that makes ClippingMediaSource not stop in some occasions. If ClippingMediaSource contains a child MediaSource with embedded metadata stream, and the embedded stream is being used, it can lead to ClippingMediaSource not be able to stop after the clipping end point. The reason being the metadata stream cannot read anymore sample, but it's also not end of source at that point. This CL fix this by changing the condition to check if the child stream cannot read anymore sample and it has read past the clipping end point. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179918038 --- .../android/exoplayer2/source/ClippingMediaPeriod.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java index 89af07a3f0..d27c329845 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java @@ -278,9 +278,10 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb formatHolder.format = format.copyWithGaplessInfo(encoderDelay, encoderPadding); return C.RESULT_FORMAT_READ; } - if (endUs != C.TIME_END_OF_SOURCE && ((result == C.RESULT_BUFFER_READ - && buffer.timeUs >= endUs) || (result == C.RESULT_NOTHING_READ - && mediaPeriod.getBufferedPositionUs() == C.TIME_END_OF_SOURCE))) { + if (endUs != C.TIME_END_OF_SOURCE + && ((result == C.RESULT_BUFFER_READ && buffer.timeUs >= endUs) + || (result == C.RESULT_NOTHING_READ + && getBufferedPositionUs() == C.TIME_END_OF_SOURCE))) { buffer.clear(); buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); sentEos = true; From f657893973a27ee57dd1ece40ed19c30b9b33662 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 2 Jan 2018 10:07:15 -0800 Subject: [PATCH 0999/2472] Make SsaDecoder more robust against malformed content Issue: #3645 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=180559196 --- .../android/exoplayer2/text/ssa/SsaDecoder.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java index eec4a1269c..0cb6f66898 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/ssa/SsaDecoder.java @@ -150,6 +150,12 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { break; } } + if (formatStartIndex == C.INDEX_UNSET + || formatEndIndex == C.INDEX_UNSET + || formatTextIndex == C.INDEX_UNSET) { + // Set to 0 so that parseDialogueLine skips lines until a complete format line is found. + formatKeyCount = 0; + } } /** @@ -161,12 +167,17 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { */ private void parseDialogueLine(String dialogueLine, List cues, LongArray cueTimesUs) { if (formatKeyCount == 0) { - Log.w(TAG, "Skipping dialogue line before format: " + dialogueLine); + Log.w(TAG, "Skipping dialogue line before complete format: " + dialogueLine); return; } String[] lineValues = dialogueLine.substring(DIALOGUE_LINE_PREFIX.length()) .split(",", formatKeyCount); + if (lineValues.length != formatKeyCount) { + Log.w(TAG, "Skipping dialogue line with fewer columns than format: " + dialogueLine); + return; + } + long startTimeUs = SsaDecoder.parseTimecodeUs(lineValues[formatStartIndex]); if (startTimeUs == C.TIME_UNSET) { Log.w(TAG, "Skipping invalid timing: " + dialogueLine); From f8c76f62e40fb29913635fc6b0d8225698cd4bf3 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 3 Jan 2018 13:50:24 +0000 Subject: [PATCH 1000/2472] Fix ClippingSampleStream --- .../android/exoplayer2/source/ClippingMediaPeriod.java | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java index d27c329845..1114a563b6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java @@ -112,7 +112,7 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb if (internalStreams[i] == null) { sampleStreams[i] = null; } else if (streams[i] == null || sampleStreams[i].stream != internalStreams[i]) { - sampleStreams[i] = new ClippingSampleStream(this, internalStreams[i], startUs, endUs, + sampleStreams[i] = new ClippingSampleStream(internalStreams[i], startUs, endUs, pendingInitialDiscontinuity); } streams[i] = sampleStreams[i]; @@ -222,9 +222,8 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb /** * Wraps a {@link SampleStream} and clips its samples. */ - private static final class ClippingSampleStream implements SampleStream { + private final class ClippingSampleStream implements SampleStream { - private final MediaPeriod mediaPeriod; private final SampleStream stream; private final long startUs; private final long endUs; @@ -232,9 +231,8 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb private boolean pendingDiscontinuity; private boolean sentEos; - public ClippingSampleStream(MediaPeriod mediaPeriod, SampleStream stream, long startUs, - long endUs, boolean pendingDiscontinuity) { - this.mediaPeriod = mediaPeriod; + public ClippingSampleStream(SampleStream stream, long startUs, long endUs, + boolean pendingDiscontinuity) { this.stream = stream; this.startUs = startUs; this.endUs = endUs; From 42a3e2e9d2c4383c00bca1b2b032fedeb47aadb7 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 22 Dec 2017 07:35:30 -0800 Subject: [PATCH 1001/2472] Add support for extracting 32-bit float WAVE Issue: #3379 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=179925320 --- RELEASENOTES.md | 2 ++ .../extractor/wav/WavHeaderReader.java | 22 ++++++++++++++----- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 2a2347686d..ef0facd6e2 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -23,6 +23,8 @@ * Audio: * Support 32-bit PCM float output from `DefaultAudioSink`, and add an option to use this with `FfmpegAudioRenderer`. + * Add support for extracting 32-bit WAVE files + ([#3379](https://github.com/google/ExoPlayer/issues/3379)). * Support extraction and decoding of Dolby Atmos ([#2465](https://github.com/google/ExoPlayer/issues/2465)). * Fix handling of playback parameter changes while paused when followed by a diff --git a/library/core/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 index 0e99380a1c..d0810a0629 100644 --- a/library/core/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 @@ -31,6 +31,8 @@ import java.io.IOException; /** Integer PCM audio data. */ private static final int TYPE_PCM = 0x0001; + /** Float PCM audio data. */ + private static final int TYPE_FLOAT = 0x0003; /** Extended WAVE format. */ private static final int TYPE_WAVE_FORMAT_EXTENSIBLE = 0xFFFE; @@ -87,14 +89,22 @@ import java.io.IOException; + blockAlignment); } - @C.PcmEncoding int encoding = Util.getPcmEncoding(bitsPerSample); - if (encoding == C.ENCODING_INVALID) { - Log.e(TAG, "Unsupported WAV bit depth: " + bitsPerSample); - return null; + @C.PcmEncoding int encoding; + switch (type) { + case TYPE_PCM: + case TYPE_WAVE_FORMAT_EXTENSIBLE: + encoding = Util.getPcmEncoding(bitsPerSample); + break; + case TYPE_FLOAT: + encoding = bitsPerSample == 32 ? C.ENCODING_PCM_FLOAT : C.ENCODING_INVALID; + break; + default: + Log.e(TAG, "Unsupported WAV format type: " + type); + return null; } - if (type != TYPE_PCM && type != TYPE_WAVE_FORMAT_EXTENSIBLE) { - Log.e(TAG, "Unsupported WAV format type: " + type); + if (encoding == C.ENCODING_INVALID) { + Log.e(TAG, "Unsupported WAV bit depth " + bitsPerSample + " for type " + type); return null; } From 134c494b1b4b6ae03706c3a6859ce264020856ae Mon Sep 17 00:00:00 2001 From: eguven Date: Wed, 27 Dec 2017 09:02:40 -0800 Subject: [PATCH 1002/2472] Typo fixes Issue:#3631 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=180197723 --- README.md | 2 +- RELEASENOTES.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ecfe3eb96f..7f35329516 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ individually. In addition to library modules, ExoPlayer has multiple extension modules that depend on external libraries to provide additional functionality. Some -extensions are available from JCenter, whereas others must be built manaully. +extensions are available from JCenter, whereas others must be built manually. Browse the [extensions directory][] and their individual READMEs for details. More information on the library and extension modules that are available from diff --git a/RELEASENOTES.md b/RELEASENOTES.md index ef0facd6e2..5fe0090727 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -184,7 +184,7 @@ easy and seamless way of incorporating display ads into ExoPlayer playbacks. You can read more about the IMA extension [here](https://medium.com/google-exoplayer/playing-ads-with-exoplayer-and-ima-868dfd767ea). -* MediaSession extension: Provides an easy to to connect ExoPlayer with +* MediaSession extension: Provides an easy to connect ExoPlayer with MediaSessionCompat in the Android Support Library. * RTMP extension: An extension for playing streams over RTMP. * Build: Made it easier for application developers to depend on a local checkout From b704a1d6436ba64a0d6fab2868d36a6b47030fdc Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 2 Jan 2018 07:00:04 -0800 Subject: [PATCH 1003/2472] Typo fix ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=180543378 --- RELEASENOTES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 5fe0090727..9d949570d7 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -184,7 +184,7 @@ easy and seamless way of incorporating display ads into ExoPlayer playbacks. You can read more about the IMA extension [here](https://medium.com/google-exoplayer/playing-ads-with-exoplayer-and-ima-868dfd767ea). -* MediaSession extension: Provides an easy to connect ExoPlayer with +* MediaSession extension: Provides an easy way to connect ExoPlayer with MediaSessionCompat in the Android Support Library. * RTMP extension: An extension for playing streams over RTMP. * Build: Made it easier for application developers to depend on a local checkout From 2cc044ded10f548b5e7dd89922cd3a494f7898aa Mon Sep 17 00:00:00 2001 From: Alex Cohn Date: Thu, 4 Jan 2018 13:22:13 +0200 Subject: [PATCH 1004/2472] minimal fix to support NDK r16 --- extensions/vp9/README.md | 2 -- extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/extensions/vp9/README.md b/extensions/vp9/README.md index 649e4a6ee2..8dc4974430 100644 --- a/extensions/vp9/README.md +++ b/extensions/vp9/README.md @@ -29,8 +29,6 @@ VP9_EXT_PATH="${EXOPLAYER_ROOT}/extensions/vp9/src/main" ``` * Download the [Android NDK][] and set its location in an environment variable. -Only versions up to NDK 15c are supported currently (see [#3520][]). - ``` NDK_PATH="" ``` 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 5f058d0551..4aabf2379e 100755 --- a/extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh +++ b/extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh @@ -102,7 +102,7 @@ for i in $(seq 0 ${limit}); do # configure and make echo "build_android_configs: " echo "configure ${config[${i}]} ${common_params}" - ../../libvpx/configure ${config[${i}]} ${common_params} + ../../libvpx/configure ${config[${i}]} ${common_params} --extra-cflags="-isystem $ndk/sysroot/usr/include/arm-linux-androideabi -isystem $ndk/sysroot/usr/include" rm -f libvpx_srcs.txt for f in ${allowed_files}; do # the build system supports multiple different configurations. avoid From d3ba207a4b425b019423c0a2a6da81e7bdf6046b Mon Sep 17 00:00:00 2001 From: eguven Date: Wed, 3 Jan 2018 02:46:59 -0800 Subject: [PATCH 1005/2472] Refactor CacheDataSource Simplified and clarified the code. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=180649983 --- .../upstream/cache/CacheDataSource.java | 91 +++++++++---------- 1 file changed, 43 insertions(+), 48 deletions(-) diff --git a/library/core/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 index bb2a952b11..5eea140a8b 100644 --- a/library/core/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 @@ -97,7 +97,7 @@ public final class CacheDataSource implements DataSource { private final boolean ignoreCacheForUnsetLengthRequests; private DataSource currentDataSource; - private boolean currentRequestUnbounded; + private boolean readingUnknownLengthDataFromUpstream; private Uri uri; private int flags; private String key; @@ -202,7 +202,7 @@ public final class CacheDataSource implements DataSource { } } } - openNextSource(true); + openNextSource(); return bytesRemaining; } catch (IOException e) { handleBeforeThrow(e); @@ -229,15 +229,21 @@ public final class CacheDataSource implements DataSource { bytesRemaining -= bytesRead; } } else { - if (currentRequestUnbounded) { - // We only do unbounded requests to upstream and only when we don't know the actual stream - // length. So we reached the end of stream. - setContentLength(readPosition); - bytesRemaining = 0; + if (readingUnknownLengthDataFromUpstream) { + setCurrentDataSourceBytesRemaining(0); } closeCurrentSource(); if (bytesRemaining > 0 || bytesRemaining == C.LENGTH_UNSET) { - if (openNextSource(false)) { + try { + openNextSource(); + } catch (IOException e) { + if (readingUnknownLengthDataFromUpstream && isCausedByPositionOutOfRange(e)) { + setCurrentDataSourceBytesRemaining(0); + } else { + throw e; + } + } + if (bytesRemaining != 0) { return read(buffer, offset, readLength); } } @@ -270,9 +276,8 @@ public final class CacheDataSource implements DataSource { * Opens the next source. If the cache contains data spanning the current read position then * {@link #cacheReadDataSource} is opened to read from it. Else {@link #upstreamDataSource} is * opened to read from the upstream source and write into the cache. - * @param initial Whether it is the initial open call. */ - private boolean openNextSource(boolean initial) throws IOException { + private void openNextSource() throws IOException { DataSpec dataSpec; CacheSpan span; if (currentRequestIgnoresCache) { @@ -323,48 +328,38 @@ public final class CacheDataSource implements DataSource { } } - currentRequestUnbounded = dataSpec.length == C.LENGTH_UNSET; - boolean successful = false; - long currentBytesRemaining = 0; - try { - currentBytesRemaining = currentDataSource.open(dataSpec); - successful = true; - } catch (IOException e) { - // if this isn't the initial open call (we had read some bytes) and an unbounded range request - // failed because of POSITION_OUT_OF_RANGE then mute the exception. We are trying to find the - // end of the stream. - if (!initial && currentRequestUnbounded) { - Throwable cause = e; - while (cause != null) { - if (cause instanceof DataSourceException) { - int reason = ((DataSourceException) cause).reason; - if (reason == DataSourceException.POSITION_OUT_OF_RANGE) { - e = null; - break; - } - } - cause = cause.getCause(); - } - } - if (e != null) { - throw e; - } - } + // If the request is unbounded it must be an upstream request. + readingUnknownLengthDataFromUpstream = dataSpec.length == C.LENGTH_UNSET; - // If we did an unbounded request (which means it's to upstream and - // bytesRemaining == C.LENGTH_UNSET) and got a resolved length from open() request - if (currentRequestUnbounded && currentBytesRemaining != C.LENGTH_UNSET) { - bytesRemaining = currentBytesRemaining; - setContentLength(dataSpec.position + bytesRemaining); + long resolvedLength = currentDataSource.open(dataSpec); + if (readingUnknownLengthDataFromUpstream && resolvedLength != C.LENGTH_UNSET) { + setCurrentDataSourceBytesRemaining(resolvedLength); } - return successful; } - private void setContentLength(long length) throws IOException { - // If writing into cache - if (currentDataSource == cacheWriteDataSource) { - cache.setContentLength(key, length); + private static boolean isCausedByPositionOutOfRange(IOException e) { + Throwable cause = e; + while (cause != null) { + if (cause instanceof DataSourceException) { + int reason = ((DataSourceException) cause).reason; + if (reason == DataSourceException.POSITION_OUT_OF_RANGE) { + return true; + } + } + cause = cause.getCause(); } + return false; + } + + private void setCurrentDataSourceBytesRemaining(long bytesRemaining) throws IOException { + this.bytesRemaining = bytesRemaining; + if (isWritingToCache()) { + cache.setContentLength(key, readPosition + bytesRemaining); + } + } + + private boolean isWritingToCache() { + return currentDataSource == cacheWriteDataSource; } private void closeCurrentSource() throws IOException { @@ -374,7 +369,7 @@ public final class CacheDataSource implements DataSource { try { currentDataSource.close(); currentDataSource = null; - currentRequestUnbounded = false; + readingUnknownLengthDataFromUpstream = false; } finally { if (lockedSpan != null) { cache.releaseHoleSpan(lockedSpan); From 7b9f71b44d0d8a290f6d4e6844a0c73e2a5502fe Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 3 Jan 2018 05:30:56 -0800 Subject: [PATCH 1006/2472] Don't kill the process if SimpleDecoder.decode throws. Issue: #3645 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=180659855 --- .../exoplayer2/ext/ffmpeg/FfmpegDecoder.java | 13 +++++++--- .../ext/ffmpeg/FfmpegDecoderException.java | 3 +++ .../exoplayer2/ext/flac/FlacDecoder.java | 13 +++++++--- .../ext/flac/FlacDecoderException.java | 3 +++ .../exoplayer2/ext/opus/OpusDecoder.java | 13 +++++++--- .../exoplayer2/ext/vp9/VpxDecoder.java | 5 ++++ .../ext/vp9/VpxDecoderException.java | 6 ++--- .../audio/AudioDecoderException.java | 26 +++++++------------ .../exoplayer2/audio/DefaultAudioSink.java | 8 +++--- .../exoplayer2/decoder/SimpleDecoder.java | 21 ++++++++++++++- .../text/SimpleSubtitleDecoder.java | 5 ++++ .../audio/SimpleDecoderAudioRendererTest.java | 5 ++++ 12 files changed, 83 insertions(+), 38 deletions(-) diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java index 8807738cfa..91bd82ab2a 100644 --- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java +++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoder.java @@ -69,18 +69,23 @@ import java.util.List; } @Override - public DecoderInputBuffer createInputBuffer() { + protected DecoderInputBuffer createInputBuffer() { return new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DIRECT); } @Override - public SimpleOutputBuffer createOutputBuffer() { + protected SimpleOutputBuffer createOutputBuffer() { return new SimpleOutputBuffer(this); } @Override - public FfmpegDecoderException decode(DecoderInputBuffer inputBuffer, - SimpleOutputBuffer outputBuffer, boolean reset) { + protected FfmpegDecoderException createUnexpectedDecodeException(Throwable error) { + return new FfmpegDecoderException("Unexpected decode error", error); + } + + @Override + protected FfmpegDecoderException decode( + DecoderInputBuffer inputBuffer, SimpleOutputBuffer outputBuffer, boolean reset) { if (reset) { nativeContext = ffmpegReset(nativeContext, extraData); if (nativeContext == 0) { diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoderException.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoderException.java index b4cf327198..d6b5a62450 100644 --- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoderException.java +++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegDecoderException.java @@ -26,4 +26,7 @@ public final class FfmpegDecoderException extends AudioDecoderException { super(message); } + /* package */ FfmpegDecoderException(String message, Throwable cause) { + super(message, cause); + } } diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java index 3ecccd8246..15d294a35a 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoder.java @@ -70,18 +70,23 @@ import java.util.List; } @Override - public DecoderInputBuffer createInputBuffer() { + protected DecoderInputBuffer createInputBuffer() { return new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL); } @Override - public SimpleOutputBuffer createOutputBuffer() { + protected SimpleOutputBuffer createOutputBuffer() { return new SimpleOutputBuffer(this); } @Override - public FlacDecoderException decode(DecoderInputBuffer inputBuffer, - SimpleOutputBuffer outputBuffer, boolean reset) { + protected FlacDecoderException createUnexpectedDecodeException(Throwable error) { + return new FlacDecoderException("Unexpected decode error", error); + } + + @Override + protected FlacDecoderException decode( + DecoderInputBuffer inputBuffer, SimpleOutputBuffer outputBuffer, boolean reset) { if (reset) { decoderJni.flush(); } diff --git a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderException.java b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderException.java index 2bdff62935..95d7f87c05 100644 --- a/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderException.java +++ b/extensions/flac/src/main/java/com/google/android/exoplayer2/ext/flac/FlacDecoderException.java @@ -26,4 +26,7 @@ public final class FlacDecoderException extends AudioDecoderException { super(message); } + /* package */ FlacDecoderException(String message, Throwable cause) { + super(message, cause); + } } 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 b4a4622346..f8ec477b88 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 @@ -135,18 +135,23 @@ import java.util.List; } @Override - public DecoderInputBuffer createInputBuffer() { + protected DecoderInputBuffer createInputBuffer() { return new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DIRECT); } @Override - public SimpleOutputBuffer createOutputBuffer() { + protected SimpleOutputBuffer createOutputBuffer() { return new SimpleOutputBuffer(this); } @Override - public OpusDecoderException decode(DecoderInputBuffer inputBuffer, - SimpleOutputBuffer outputBuffer, boolean reset) { + protected OpusDecoderException createUnexpectedDecodeException(Throwable error) { + return new OpusDecoderException("Unexpected decode error", error); + } + + @Override + protected OpusDecoderException decode( + DecoderInputBuffer inputBuffer, SimpleOutputBuffer outputBuffer, boolean reset) { if (reset) { opusReset(nativeDecoderContext); // When seeking to 0, skip number of samples as specified in opus header. When seeking to diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java index 6a15023c0b..6f8c0a1918 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java @@ -99,6 +99,11 @@ import java.nio.ByteBuffer; super.releaseOutputBuffer(buffer); } + @Override + protected VpxDecoderException createUnexpectedDecodeException(Throwable error) { + return new VpxDecoderException("Unexpected decode error", error); + } + @Override protected VpxDecoderException decode(VpxInputBuffer inputBuffer, VpxOutputBuffer outputBuffer, boolean reset) { diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoderException.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoderException.java index 5f43b503ac..8de14629d3 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoderException.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoderException.java @@ -15,10 +15,8 @@ */ package com.google.android.exoplayer2.ext.vp9; -/** - * Thrown when a libvpx decoder error occurs. - */ -public class VpxDecoderException extends Exception { +/** Thrown when a libvpx decoder error occurs. */ +public final class VpxDecoderException extends Exception { /* package */ VpxDecoderException(String message) { super(message); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioDecoderException.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioDecoderException.java index b5ee052924..ac4f632d62 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioDecoderException.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioDecoderException.java @@ -15,27 +15,21 @@ */ package com.google.android.exoplayer2.audio; -/** - * Thrown when an audio decoder error occurs. - */ -public abstract class AudioDecoderException extends Exception { +/** Thrown when an audio decoder error occurs. */ +public class AudioDecoderException extends Exception { - /** - * @param detailMessage The detail message for this exception. - */ - public AudioDecoderException(String detailMessage) { - super(detailMessage); + /** @param message The detail message for this exception. */ + public AudioDecoderException(String message) { + super(message); } /** - * @param detailMessage The detail message for this exception. - * @param cause the cause (which is saved for later retrieval by the - * {@link #getCause()} method). (A null value is - * permitted, and indicates that the cause is nonexistent or - * unknown.) + * @param message The detail message for this exception. + * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method). + * A null value is permitted, and indicates that the cause is nonexistent or unknown. */ - public AudioDecoderException(String detailMessage, Throwable cause) { - super(detailMessage, cause); + public AudioDecoderException(String message, Throwable cause) { + super(message, cause); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java index ab4564e2c3..b9a0b8236f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java @@ -55,11 +55,9 @@ public final class DefaultAudioSink implements AudioSink { */ public static final class InvalidAudioTrackTimestampException extends RuntimeException { - /** - * @param detailMessage The detail message for this exception. - */ - public InvalidAudioTrackTimestampException(String detailMessage) { - super(detailMessage); + /** @param message The detail message for this exception. */ + public InvalidAudioTrackTimestampException(String message) { + super(message); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/decoder/SimpleDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/decoder/SimpleDecoder.java index 1d380ef858..68089d7b41 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/decoder/SimpleDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/decoder/SimpleDecoder.java @@ -219,7 +219,18 @@ public abstract class SimpleDecoder Date: Wed, 3 Jan 2018 05:39:57 -0800 Subject: [PATCH 1007/2472] Update Cronet extension readme on how to enable Java 8 features. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=180660349 --- extensions/cronet/README.md | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/extensions/cronet/README.md b/extensions/cronet/README.md index 66da774978..ea84b602db 100644 --- a/extensions/cronet/README.md +++ b/extensions/cronet/README.md @@ -19,10 +19,20 @@ and enable the extension: 1. Copy the three jar files into the `libs` directory of this extension 1. Copy the content of the downloaded `libs` directory into the `jniLibs` directory of this extension - -* In your `settings.gradle` file, add - `gradle.ext.exoplayerIncludeCronetExtension = true` before the line that - applies `core_settings.gradle`. +1. In your `settings.gradle` file, add + `gradle.ext.exoplayerIncludeCronetExtension = true` before the line that + applies `core_settings.gradle`. +1. In all `build.gradle` files where this extension is linked as a dependency, + add + ``` + android { + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + } + ``` + to enable Java 8 features required by the Cronet library. [top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md [here]: https://console.cloud.google.com/storage/browser/chromium-cronet/android From a29fb7b989c676967f806cdff572855203097894 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 3 Jan 2018 05:59:18 -0800 Subject: [PATCH 1008/2472] Update release notes to cherry-pick 32-bit WAVE support ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=180661355 --- RELEASENOTES.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 9f48db0be8..5d3d00a544 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -36,8 +36,6 @@ * DefaultTrackSelector: Support disabling of individual text track selection flags. * New Cast extension: Simplifies toggling between local and Cast playbacks. -* Add support for extracting 32-bit WAVE files - ([#3379](https://github.com/google/ExoPlayer/issues/3379)). ### 2.6.1 ### @@ -62,6 +60,8 @@ * Audio: * Support 32-bit PCM float output from `DefaultAudioSink`, and add an option to use this with `FfmpegAudioRenderer`. + * Add support for extracting 32-bit WAVE files + ([#3379](https://github.com/google/ExoPlayer/issues/3379)). * Support extraction and decoding of Dolby Atmos ([#2465](https://github.com/google/ExoPlayer/issues/2465)). * Fix handling of playback parameter changes while paused when followed by a From 2bc734afec7b8f729f19eae495b13de2dd481c40 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 3 Jan 2018 06:52:55 -0800 Subject: [PATCH 1009/2472] Replace message delay with send at time in playback loop. This removes the need to calculate the time needed to run the doSomeWork method. Consequently, we can use both the real Clock/Handler and the FakeClock without changing the way the playback loop works and without violating the interfaces of Clock or Handler. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=180665647 --- .../exoplayer2/ExoPlayerImplInternal.java | 6 ++- .../google/android/exoplayer2/util/Clock.java | 11 +++-- .../exoplayer2/util/HandlerWrapper.java | 14 +------ .../android/exoplayer2/util/SystemClock.java | 5 +++ .../exoplayer2/util/SystemHandlerWrapper.java | 11 +---- .../exoplayer2/testutil/FakeClock.java | 42 ++++++++++--------- 6 files changed, 40 insertions(+), 49 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 65f43ae684..8fd508a2f0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -117,6 +117,7 @@ import java.util.Collections; private final DefaultMediaClock mediaClock; private final PlaybackInfoUpdate playbackInfoUpdate; private final ArrayList customMessageInfos; + private final Clock clock; @SuppressWarnings("unused") private SeekParameters seekParameters; @@ -159,6 +160,7 @@ import java.util.Collections; this.shuffleModeEnabled = shuffleModeEnabled; this.eventHandler = eventHandler; this.player = player; + this.clock = clock; backBufferDurationUs = loadControl.getBackBufferDurationUs(); retainBackBufferFromKeyframe = loadControl.retainBackBufferFromKeyframe(); @@ -541,7 +543,7 @@ import java.util.Collections; } private void doSomeWork() throws ExoPlaybackException, IOException { - long operationStartTimeMs = SystemClock.elapsedRealtime(); + long operationStartTimeMs = clock.uptimeMillis(); updatePeriods(); if (playingPeriodHolder == null) { // We're still waiting for the first period to be prepared. @@ -632,7 +634,7 @@ import java.util.Collections; private void scheduleNextWork(long thisOperationStartTimeMs, long intervalMs) { handler.removeMessages(MSG_DO_SOME_WORK); - handler.sendEmptyMessageDelayed(MSG_DO_SOME_WORK, intervalMs, thisOperationStartTimeMs); + handler.sendEmptyMessageAtTime(MSG_DO_SOME_WORK, thisOperationStartTimeMs + intervalMs); } private void seekToInternal(SeekPosition seekPosition) throws ExoPlaybackException { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Clock.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Clock.java index 43c01bf53a..dced6752eb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Clock.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Clock.java @@ -30,14 +30,13 @@ public interface Clock { */ Clock DEFAULT = new SystemClock(); - /** - * @see android.os.SystemClock#elapsedRealtime() - */ + /** @see android.os.SystemClock#elapsedRealtime() */ long elapsedRealtime(); - /** - * @see android.os.SystemClock#sleep(long) - */ + /** @see android.os.SystemClock#uptimeMillis() */ + long uptimeMillis(); + + /** @see android.os.SystemClock#sleep(long) */ void sleep(long sleepTimeMs); /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/HandlerWrapper.java b/library/core/src/main/java/com/google/android/exoplayer2/util/HandlerWrapper.java index b101a5e199..3ce93f9370 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/HandlerWrapper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/HandlerWrapper.java @@ -43,18 +43,8 @@ public interface HandlerWrapper { /** @see Handler#sendEmptyMessage(int). */ boolean sendEmptyMessage(int what); - /** - * Variant of {@code Handler#sendEmptyMessageDelayed(int, long)} which also takes a reference time - * measured by {@code android.os.SystemClock#elapsedRealtime()} to which the delay is added. - * - * @param what The message identifier. - * @param delayMs The delay in milliseconds to send the message. This delay is added to the {@code - * referenceTimeMs}. - * @param referenceTimeMs The time which the delay is added to. Always measured with {@code - * android.os.SystemClock#elapsedRealtime()}. - * @return Whether the message was successfully enqueued on the Handler thread. - */ - boolean sendEmptyMessageDelayed(int what, long delayMs, long referenceTimeMs); + /** @see Handler#sendEmptyMessageAtTime(int, long). */ + boolean sendEmptyMessageAtTime(int what, long uptimeMs); /** @see Handler#removeMessages(int). */ void removeMessages(int what); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/SystemClock.java b/library/core/src/main/java/com/google/android/exoplayer2/util/SystemClock.java index b24a38ea3c..72d3df46e1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/SystemClock.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/SystemClock.java @@ -30,6 +30,11 @@ import android.support.annotation.Nullable; return android.os.SystemClock.elapsedRealtime(); } + @Override + public long uptimeMillis() { + return android.os.SystemClock.uptimeMillis(); + } + @Override public void sleep(long sleepTimeMs) { android.os.SystemClock.sleep(sleepTimeMs); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/SystemHandlerWrapper.java b/library/core/src/main/java/com/google/android/exoplayer2/util/SystemHandlerWrapper.java index aa290d9313..ee469a5b2a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/SystemHandlerWrapper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/SystemHandlerWrapper.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.util; import android.os.Looper; import android.os.Message; -import android.os.SystemClock; /** The standard implementation of {@link HandlerWrapper}. */ /* package */ final class SystemHandlerWrapper implements HandlerWrapper { @@ -59,14 +58,8 @@ import android.os.SystemClock; } @Override - public boolean sendEmptyMessageDelayed(int what, long delayMs, long referenceTimeMs) { - long targetMessageTime = referenceTimeMs + delayMs; - long remainingDelayMs = targetMessageTime - SystemClock.elapsedRealtime(); - if (remainingDelayMs <= 0) { - return handler.sendEmptyMessage(what); - } else { - return handler.sendEmptyMessageDelayed(what, remainingDelayMs); - } + public boolean sendEmptyMessageAtTime(int what, long uptimeMs) { + return handler.sendEmptyMessageAtTime(what, uptimeMs); } @Override diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeClock.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeClock.java index 49656eef99..a591546613 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeClock.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeClock.java @@ -67,6 +67,11 @@ public class FakeClock implements Clock { return currentTimeMs; } + @Override + public long uptimeMillis() { + return elapsedRealtime(); + } + @Override public synchronized void sleep(long sleepTimeMs) { if (sleepTimeMs <= 0) { @@ -90,15 +95,23 @@ public class FakeClock implements Clock { } /** Adds a handler post to list of pending messages. */ - protected synchronized void addDelayedHandlerMessage( - HandlerWrapper handler, Runnable runnable, long delayMs) { - handlerMessages.add(new HandlerMessageData(currentTimeMs + delayMs, handler, runnable)); + protected synchronized boolean addHandlerMessageAtTime( + HandlerWrapper handler, Runnable runnable, long timeMs) { + if (timeMs <= currentTimeMs) { + return handler.post(runnable); + } + handlerMessages.add(new HandlerMessageData(timeMs, handler, runnable)); + return true; } /** Adds an empty handler message to list of pending messages. */ - protected synchronized void addDelayedHandlerMessage( - HandlerWrapper handler, int message, long delayMs) { - handlerMessages.add(new HandlerMessageData(currentTimeMs + delayMs, handler, message)); + protected synchronized boolean addHandlerMessageAtTime( + HandlerWrapper handler, int message, long timeMs) { + if (timeMs <= currentTimeMs) { + return handler.sendEmptyMessage(message); + } + handlerMessages.add(new HandlerMessageData(timeMs, handler, message)); + return true; } /** Message data saved to send messages or execute runnables at a later time on a Handler. */ @@ -177,14 +190,8 @@ public class FakeClock implements Clock { } @Override - public boolean sendEmptyMessageDelayed(int what, long delayMs, long referenceTimeMs) { - // Ignore referenceTimeMs measured by SystemClock and just send with requested delay. - if (delayMs <= 0) { - return handler.sendEmptyMessage(what); - } else { - addDelayedHandlerMessage(this, what, delayMs); - return true; - } + public boolean sendEmptyMessageAtTime(int what, long uptimeMs) { + return addHandlerMessageAtTime(this, what, uptimeMs); } @Override @@ -204,12 +211,7 @@ public class FakeClock implements Clock { @Override public boolean postDelayed(Runnable runnable, long delayMs) { - if (delayMs <= 0) { - return handler.post(runnable); - } else { - addDelayedHandlerMessage(this, runnable, delayMs); - return true; - } + return addHandlerMessageAtTime(this, runnable, uptimeMillis() + delayMs); } } } From 7314e9bddc2635c9426cbe237b237f35f24bb15b Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 3 Jan 2018 08:44:56 -0800 Subject: [PATCH 1010/2472] DRM fixes - Parse multiple kids from default_KID. It's specified as a whitespace separated list of UUIDs rather than a single UUID. - Opportunistically proceed with playback in cases where the manifest only defines a single SchemeData with the common PSSH UUID. In such cases the manifest isn't saying anything about which specific DRM schemes it supports. Issue: #3630 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=180675056 --- RELEASENOTES.md | 3 +++ .../exoplayer2/drm/DefaultDrmSessionManager.java | 15 ++++++++++++--- .../source/dash/manifest/DashManifestParser.java | 11 ++++++++--- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 5d3d00a544..25e4e841e3 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -26,6 +26,9 @@ positions. * Note: `SeekParameters` are only currently effective when playing `ExtractorMediaSource`s (i.e. progressive streams). +* DRM: Optimistically attempt playback of DRM protected content that does not + declare scheme specific init data + ([#3630](https://github.com/google/ExoPlayer/issues/3630)). * DASH: Support DASH manifest EventStream elements. * HLS: Add opt-in support for chunkless preparation in HLS. This allows an HLS source to finish preparation without downloading any chunks, which can diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java index 08defdccee..9c134970ff 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java @@ -23,6 +23,7 @@ import android.os.Message; import android.support.annotation.IntDef; import android.support.annotation.NonNull; import android.text.TextUtils; +import android.util.Log; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.drm.DefaultDrmSession.ProvisioningManager; @@ -87,7 +88,6 @@ public class DefaultDrmSessionManager implements DrmSe * The key to use when passing CustomData to a PlayReady instance in an optional parameter map. */ public static final String PLAYREADY_CUSTOM_DATA_KEY = "PRCustomData"; - private static final String CENC_SCHEME_MIME_TYPE = "cenc"; /** Determines the action to be done after a session acquired. */ @Retention(RetentionPolicy.SOURCE) @@ -109,6 +109,9 @@ public class DefaultDrmSessionManager implements DrmSe /** Number of times to retry for initial provisioning and key request for reporting error. */ public static final int INITIAL_DRM_REQUEST_RETRY_COUNT = 3; + private static final String TAG = "DrmSessionManager"; + private static final String CENC_SCHEME_MIME_TYPE = "cenc"; + private final UUID uuid; private final ExoMediaDrm mediaDrm; private final MediaDrmCallback callback; @@ -350,8 +353,14 @@ public class DefaultDrmSessionManager implements DrmSe public boolean canAcquireSession(@NonNull DrmInitData drmInitData) { SchemeData schemeData = getSchemeData(drmInitData, uuid, true); if (schemeData == null) { - // No data for this manager's scheme. - return false; + if (drmInitData.schemeDataCount == 1 && drmInitData.get(0).matches(C.COMMON_PSSH_UUID)) { + // Assume scheme specific data will be added before the session is opened. + Log.w( + TAG, "DrmInitData only contains common PSSH SchemeData. Assuming support for: " + uuid); + } else { + // No data for this manager's scheme. + return false; + } } String schemeType = drmInitData.schemeType; if (schemeType == null || C.CENC_TYPE_cenc.equals(schemeType)) { diff --git a/library/dash/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 index 73d234fa72..bda2a1fb85 100644 --- a/library/dash/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 @@ -365,9 +365,14 @@ public class DashManifestParser extends DefaultHandler case "urn:mpeg:dash:mp4protection:2011": schemeType = xpp.getAttributeValue(null, "value"); String defaultKid = xpp.getAttributeValue(null, "cenc:default_KID"); - if (defaultKid != null && !"00000000-0000-0000-0000-000000000000".equals(defaultKid)) { - UUID keyId = UUID.fromString(defaultKid); - data = PsshAtomUtil.buildPsshAtom(C.COMMON_PSSH_UUID, new UUID[] {keyId}, null); + if (!TextUtils.isEmpty(defaultKid) + && !"00000000-0000-0000-0000-000000000000".equals(defaultKid)) { + String[] defaultKidStrings = defaultKid.split("\\s+"); + UUID[] defaultKids = new UUID[defaultKidStrings.length]; + for (int i = 0; i < defaultKidStrings.length; i++) { + defaultKids[i] = UUID.fromString(defaultKidStrings[i]); + } + data = PsshAtomUtil.buildPsshAtom(C.COMMON_PSSH_UUID, defaultKids, null); uuid = C.COMMON_PSSH_UUID; } break; From 8e8e53c42d7a63ee5f6703d86902ebb102c10af5 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 3 Jan 2018 09:18:11 -0800 Subject: [PATCH 1011/2472] Add support for Dolby TrueHD passthrough Issue: #2147 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=180678595 --- RELEASENOTES.md | 2 + .../java/com/google/android/exoplayer2/C.java | 63 ++++----- .../android/exoplayer2/audio/Ac3Util.java | 52 ++++++- .../exoplayer2/audio/DefaultAudioSink.java | 15 +- .../extractor/mkv/MatroskaExtractor.java | 131 ++++++++++++++++-- .../android/exoplayer2/util/MimeTypes.java | 2 + 6 files changed, 211 insertions(+), 54 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 25e4e841e3..4679a0b376 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -39,6 +39,8 @@ * DefaultTrackSelector: Support disabling of individual text track selection flags. * New Cast extension: Simplifies toggling between local and Cast playbacks. +* Audio: Support TrueHD passthrough for rechunked samples in Matroska files + ([#2147](https://github.com/google/ExoPlayer/issues/2147)). ### 2.6.1 ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/C.java b/library/core/src/main/java/com/google/android/exoplayer2/C.java index 6a35c0c5e8..d6e61c12b1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/C.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/C.java @@ -122,13 +122,22 @@ public final class C { */ public static final int AUDIO_SESSION_ID_UNSET = AudioManager.AUDIO_SESSION_ID_GENERATE; - /** - * Represents an audio encoding, or an invalid or unset value. - */ + /** Represents an audio encoding, or an invalid or unset value. */ @Retention(RetentionPolicy.SOURCE) - @IntDef({Format.NO_VALUE, ENCODING_INVALID, ENCODING_PCM_8BIT, ENCODING_PCM_16BIT, - ENCODING_PCM_24BIT, ENCODING_PCM_32BIT, ENCODING_PCM_FLOAT, ENCODING_AC3, ENCODING_E_AC3, - ENCODING_DTS, ENCODING_DTS_HD}) + @IntDef({ + Format.NO_VALUE, + ENCODING_INVALID, + ENCODING_PCM_8BIT, + ENCODING_PCM_16BIT, + ENCODING_PCM_24BIT, + ENCODING_PCM_32BIT, + ENCODING_PCM_FLOAT, + ENCODING_AC3, + ENCODING_E_AC3, + ENCODING_DTS, + ENCODING_DTS_HD, + ENCODING_DOLBY_TRUEHD + }) public @interface Encoding {} /** @@ -138,46 +147,28 @@ public final class C { @IntDef({Format.NO_VALUE, ENCODING_INVALID, ENCODING_PCM_8BIT, ENCODING_PCM_16BIT, ENCODING_PCM_24BIT, ENCODING_PCM_32BIT, ENCODING_PCM_FLOAT}) public @interface PcmEncoding {} - /** - * @see AudioFormat#ENCODING_INVALID - */ + /** @see AudioFormat#ENCODING_INVALID */ public static final int ENCODING_INVALID = AudioFormat.ENCODING_INVALID; - /** - * @see AudioFormat#ENCODING_PCM_8BIT - */ + /** @see AudioFormat#ENCODING_PCM_8BIT */ public static final int ENCODING_PCM_8BIT = AudioFormat.ENCODING_PCM_8BIT; - /** - * @see AudioFormat#ENCODING_PCM_16BIT - */ + /** @see AudioFormat#ENCODING_PCM_16BIT */ public static final int ENCODING_PCM_16BIT = AudioFormat.ENCODING_PCM_16BIT; - /** - * PCM encoding with 24 bits per sample. - */ + /** PCM encoding with 24 bits per sample. */ public static final int ENCODING_PCM_24BIT = 0x80000000; - /** - * PCM encoding with 32 bits per sample. - */ + /** PCM encoding with 32 bits per sample. */ public static final int ENCODING_PCM_32BIT = 0x40000000; - /** - * @see AudioFormat#ENCODING_PCM_FLOAT - */ + /** @see AudioFormat#ENCODING_PCM_FLOAT */ public static final int ENCODING_PCM_FLOAT = AudioFormat.ENCODING_PCM_FLOAT; - /** - * @see AudioFormat#ENCODING_AC3 - */ + /** @see AudioFormat#ENCODING_AC3 */ public static final int ENCODING_AC3 = AudioFormat.ENCODING_AC3; - /** - * @see AudioFormat#ENCODING_E_AC3 - */ + /** @see AudioFormat#ENCODING_E_AC3 */ public static final int ENCODING_E_AC3 = AudioFormat.ENCODING_E_AC3; - /** - * @see AudioFormat#ENCODING_DTS - */ + /** @see AudioFormat#ENCODING_DTS */ public static final int ENCODING_DTS = AudioFormat.ENCODING_DTS; - /** - * @see AudioFormat#ENCODING_DTS_HD - */ + /** @see AudioFormat#ENCODING_DTS_HD */ public static final int ENCODING_DTS_HD = AudioFormat.ENCODING_DTS_HD; + /** @see AudioFormat#ENCODING_DOLBY_TRUEHD */ + public static final int ENCODING_DOLBY_TRUEHD = AudioFormat.ENCODING_DOLBY_TRUEHD; /** * @see AudioFormat#CHANNEL_OUT_7POINT1_SURROUND diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java index e9ffab7ace..5797e73740 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/Ac3Util.java @@ -27,9 +27,7 @@ import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableByteArray; import java.nio.ByteBuffer; -/** - * Utility methods for parsing (E-)AC-3 syncframes, which are access units in (E-)AC-3 bitstreams. - */ +/** Utility methods for parsing Dolby TrueHD and (E-)AC3 syncframes. */ public final class Ac3Util { /** @@ -93,6 +91,17 @@ public final class Ac3Util { } + /** + * The number of samples to store in each output chunk when rechunking TrueHD streams. The number + * of samples extracted from the container corresponding to one syncframe must be an integer + * multiple of this value. + */ + public static final int TRUEHD_RECHUNK_SAMPLE_COUNT = 8; + /** + * The number of bytes that must be parsed from a TrueHD syncframe to calculate the sample count. + */ + public static final int TRUEHD_SYNCFRAME_PREFIX_LENGTH = 12; + /** * The number of new samples per (E-)AC-3 audio block. */ @@ -441,6 +450,43 @@ public final class Ac3Util { : BLOCKS_PER_SYNCFRAME_BY_NUMBLKSCOD[(buffer.get(buffer.position() + 4) & 0x30) >> 4]); } + /** + * Returns the number of audio samples represented by the given TrueHD syncframe, or 0 if the + * buffer is not the start of a syncframe. + * + * @param syncframe The bytes from which to read the syncframe. Must be at least {@link + * #TRUEHD_SYNCFRAME_PREFIX_LENGTH} bytes long. + * @return The number of audio samples represented by the syncframe, or 0 if the buffer doesn't + * contain the start of a syncframe. + */ + public static int parseTrueHdSyncframeAudioSampleCount(byte[] syncframe) { + // TODO: Link to specification if available. + if (syncframe[4] != (byte) 0xF8 + || syncframe[5] != (byte) 0x72 + || syncframe[6] != (byte) 0x6F + || syncframe[7] != (byte) 0xBA) { + return 0; + } + return 40 << (syncframe[8] & 7); + } + + /** + * Reads the number of audio samples represented by the given TrueHD syncframe, or 0 if the buffer + * is not the start of a syncframe. The buffer's position is not modified. + * + * @param buffer The {@link ByteBuffer} from which to read the syncframe. Must have at least + * {@link #TRUEHD_SYNCFRAME_PREFIX_LENGTH} bytes remaining. + * @return The number of audio samples represented by the syncframe, or 0 if the buffer is not the + * start of a syncframe. + */ + public static int parseTrueHdSyncframeAudioSampleCount(ByteBuffer buffer) { + // TODO: Link to specification if available. + if (buffer.getInt(buffer.position() + 4) != 0xBA6F72F8) { + return 0; + } + return 40 << (buffer.get(buffer.position() + 8) & 0x07); + } + private static int getAc3SyncframeSize(int fscod, int frmsizecod) { int halfFrmsizecod = frmsizecod / 2; if (fscod < 0 || fscod >= SAMPLE_RATE_BY_FSCOD.length || frmsizecod < 0 diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java index b9a0b8236f..e3bf72c541 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java @@ -446,9 +446,12 @@ public final class DefaultAudioSink implements AudioSink { if (outputEncoding == C.ENCODING_AC3 || outputEncoding == C.ENCODING_E_AC3) { // AC-3 allows bitrates up to 640 kbit/s. bufferSize = (int) (PASSTHROUGH_BUFFER_DURATION_US * 80 * 1024 / C.MICROS_PER_SECOND); - } else /* (outputEncoding == C.ENCODING_DTS || outputEncoding == C.ENCODING_DTS_HD */ { + } else if (outputEncoding == C.ENCODING_DTS) { // DTS allows an 'open' bitrate, but we assume the maximum listed value: 1536 kbit/s. bufferSize = (int) (PASSTHROUGH_BUFFER_DURATION_US * 192 * 1024 / C.MICROS_PER_SECOND); + } else /* outputEncoding == C.ENCODING_DTS_HD || outputEncoding == C.ENCODING_DOLBY_TRUEHD*/ { + // HD passthrough requires a larger buffer to avoid underrun. + bufferSize = (int) (PASSTHROUGH_BUFFER_DURATION_US * 192 * 6 * 1024 / C.MICROS_PER_SECOND); } } bufferSizeUs = @@ -580,6 +583,13 @@ public final class DefaultAudioSink implements AudioSink { if (!isInputPcm && framesPerEncodedSample == 0) { // If this is the first encoded sample, calculate the sample size in frames. framesPerEncodedSample = getFramesPerEncodedSample(outputEncoding, buffer); + if (framesPerEncodedSample == 0) { + // We still don't know the number of frames per sample, so drop the buffer. + // For TrueHD this can occur after some seek operations, as not every sample starts with + // a syncframe header. If we chunked samples together so the extracted samples always + // started with a syncframe header, the chunks would be too large. + return true; + } } if (drainingPlaybackParameters != null) { @@ -1225,6 +1235,9 @@ public final class DefaultAudioSink implements AudioSink { return Ac3Util.getAc3SyncframeAudioSampleCount(); } else if (encoding == C.ENCODING_E_AC3) { return Ac3Util.parseEAc3SyncframeAudioSampleCount(buffer); + } else if (encoding == C.ENCODING_DOLBY_TRUEHD) { + return Ac3Util.parseTrueHdSyncframeAudioSampleCount(buffer) + * Ac3Util.TRUEHD_RECHUNK_SAMPLE_COUNT; } else { throw new IllegalStateException("Unexpected audio encoding: " + encoding); } diff --git a/library/core/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 index 4b0bbda275..0eb7009c47 100644 --- a/library/core/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 @@ -16,11 +16,13 @@ package com.google.android.exoplayer2.extractor.mkv; import android.support.annotation.IntDef; +import android.support.annotation.Nullable; import android.util.Log; import android.util.SparseArray; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.audio.Ac3Util; import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; import com.google.android.exoplayer2.extractor.ChunkIndex; @@ -32,6 +34,7 @@ import com.google.android.exoplayer2.extractor.MpegAudioHeader; 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.util.Assertions; import com.google.android.exoplayer2.util.LongArray; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.NalUnitUtil; @@ -413,6 +416,9 @@ public final class MatroskaExtractor implements Extractor { reader.reset(); varintReader.reset(); resetSample(); + for (int i = 0; i < tracks.size(); i++) { + tracks.valueAt(i).reset(); + } } @Override @@ -431,7 +437,13 @@ public final class MatroskaExtractor implements Extractor { return Extractor.RESULT_SEEK; } } - return continueReading ? Extractor.RESULT_CONTINUE : Extractor.RESULT_END_OF_INPUT; + if (!continueReading) { + for (int i = 0; i < tracks.size(); i++) { + tracks.valueAt(i).outputPendingSampleMetadata(); + } + return Extractor.RESULT_END_OF_INPUT; + } + return Extractor.RESULT_CONTINUE; } /* package */ int getElementType(int id) { @@ -1077,14 +1089,26 @@ public final class MatroskaExtractor implements Extractor { } private void commitSampleToOutput(Track track, long timeUs) { - if (CODEC_ID_SUBRIP.equals(track.codecId)) { - commitSubtitleSample(track, SUBRIP_TIMECODE_FORMAT, SUBRIP_PREFIX_END_TIMECODE_OFFSET, - SUBRIP_TIMECODE_LAST_VALUE_SCALING_FACTOR, SUBRIP_TIMECODE_EMPTY); - } else if (CODEC_ID_ASS.equals(track.codecId)) { - commitSubtitleSample(track, SSA_TIMECODE_FORMAT, SSA_PREFIX_END_TIMECODE_OFFSET, - SSA_TIMECODE_LAST_VALUE_SCALING_FACTOR, SSA_TIMECODE_EMPTY); + if (track.trueHdSampleRechunker != null) { + track.trueHdSampleRechunker.sampleMetadata(track, timeUs); + } else { + if (CODEC_ID_SUBRIP.equals(track.codecId)) { + commitSubtitleSample( + track, + SUBRIP_TIMECODE_FORMAT, + SUBRIP_PREFIX_END_TIMECODE_OFFSET, + SUBRIP_TIMECODE_LAST_VALUE_SCALING_FACTOR, + SUBRIP_TIMECODE_EMPTY); + } else if (CODEC_ID_ASS.equals(track.codecId)) { + commitSubtitleSample( + track, + SSA_TIMECODE_FORMAT, + SSA_PREFIX_END_TIMECODE_OFFSET, + SSA_TIMECODE_LAST_VALUE_SCALING_FACTOR, + SSA_TIMECODE_EMPTY); + } + track.output.sampleMetadata(timeUs, blockFlags, sampleBytesWritten, 0, track.cryptoData); } - track.output.sampleMetadata(timeUs, blockFlags, sampleBytesWritten, 0, track.cryptoData); sampleRead = true; resetSample(); } @@ -1251,6 +1275,10 @@ public final class MatroskaExtractor implements Extractor { } } } else { + if (track.trueHdSampleRechunker != null) { + Assertions.checkState(sampleStrippedBytes.limit() == 0); + track.trueHdSampleRechunker.startSample(input, blockFlags, size); + } while (sampleBytesRead < size) { readToOutput(input, output, size - sampleBytesRead); } @@ -1510,7 +1538,70 @@ public final class MatroskaExtractor implements Extractor { throws IOException, InterruptedException { MatroskaExtractor.this.binaryElement(id, contentsSize, input); } + } + /** + * Rechunks TrueHD sample data into groups of {@link Ac3Util#TRUEHD_RECHUNK_SAMPLE_COUNT} samples. + */ + private static final class TrueHdSampleRechunker { + + private final byte[] syncframePrefix; + + private boolean foundSyncframe; + private int sampleCount; + private int chunkSize; + private long timeUs; + private @C.BufferFlags int blockFlags; + + public TrueHdSampleRechunker() { + syncframePrefix = new byte[Ac3Util.TRUEHD_SYNCFRAME_PREFIX_LENGTH]; + } + + public void reset() { + foundSyncframe = false; + } + + public void startSample(ExtractorInput input, @C.BufferFlags int blockFlags, int size) + throws IOException, InterruptedException { + if (!foundSyncframe) { + input.peekFully(syncframePrefix, 0, Ac3Util.TRUEHD_SYNCFRAME_PREFIX_LENGTH); + input.resetPeekPosition(); + if ((Ac3Util.parseTrueHdSyncframeAudioSampleCount(syncframePrefix) == C.INDEX_UNSET)) { + return; + } + foundSyncframe = true; + sampleCount = 0; + } + if (sampleCount == 0) { + // This is the first sample in the chunk, so reset the block flags and chunk size. + this.blockFlags = blockFlags; + chunkSize = 0; + } + chunkSize += size; + } + + public void sampleMetadata(Track track, long timeUs) { + if (!foundSyncframe) { + return; + } + if (sampleCount++ == 0) { + // This is the first sample in the chunk, so update the timestamp. + this.timeUs = timeUs; + } + if (sampleCount < Ac3Util.TRUEHD_RECHUNK_SAMPLE_COUNT) { + // We haven't read enough samples to output a chunk. + return; + } + track.output.sampleMetadata(this.timeUs, blockFlags, chunkSize, 0, track.cryptoData); + sampleCount = 0; + } + + public void outputPendingSampleMetadata(Track track) { + if (foundSyncframe && sampleCount > 0) { + track.output.sampleMetadata(this.timeUs, blockFlags, chunkSize, 0, track.cryptoData); + sampleCount = 0; + } + } } private static final class Track { @@ -1573,6 +1664,7 @@ public final class MatroskaExtractor implements Extractor { public int sampleRate = 8000; public long codecDelayNs = 0; public long seekPreRollNs = 0; + @Nullable public TrueHdSampleRechunker trueHdSampleRechunker; // Text elements. public boolean flagForced; @@ -1583,9 +1675,7 @@ public final class MatroskaExtractor implements Extractor { public TrackOutput output; public int nalUnitLengthFieldLength; - /** - * Initializes the track with an output. - */ + /** Initializes the track with an output. */ public void initializeOutput(ExtractorOutput output, int trackId) throws ParserException { String mimeType; int maxInputSize = Format.NO_VALUE; @@ -1669,6 +1759,7 @@ public final class MatroskaExtractor implements Extractor { break; case CODEC_ID_TRUEHD: mimeType = MimeTypes.AUDIO_TRUEHD; + trueHdSampleRechunker = new TrueHdSampleRechunker(); break; case CODEC_ID_DTS: case CODEC_ID_DTS_EXPRESS: @@ -1786,9 +1877,21 @@ public final class MatroskaExtractor implements Extractor { this.output.format(format); } - /** - * Returns the HDR Static Info as defined in CTA-861.3. - */ + /** Forces any pending sample metadata to be flushed to the output. */ + public void outputPendingSampleMetadata() { + if (trueHdSampleRechunker != null) { + trueHdSampleRechunker.outputPendingSampleMetadata(this); + } + } + + /** Resets any state stored in the track in response to a seek. */ + public void reset() { + if (trueHdSampleRechunker != null) { + trueHdSampleRechunker.reset(); + } + } + + /** 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 diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java b/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java index 8307e998a0..3e65a754e2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java @@ -264,6 +264,8 @@ public final class MimeTypes { return C.ENCODING_DTS; case MimeTypes.AUDIO_DTS_HD: return C.ENCODING_DTS_HD; + case MimeTypes.AUDIO_TRUEHD: + return C.ENCODING_DOLBY_TRUEHD; default: return C.ENCODING_INVALID; } From a314db04ad56999ab9bbcd9bfda984cf905c6eec Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 4 Jan 2018 03:13:32 -0800 Subject: [PATCH 1012/2472] Reformat UI classes ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=180777553 --- .../exoplayer2/ui/PlaybackControlView.java | 239 ++++++++---------- .../exoplayer2/ui/SimpleExoPlayerView.java | 141 +++++------ 2 files changed, 175 insertions(+), 205 deletions(-) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java index 7659dff9c6..fefbb0797a 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java @@ -42,127 +42,118 @@ import java.util.Locale; /** * A view for controlling {@link Player} instances. - *

          - * A PlaybackControlView can be customized by setting attributes (or calling corresponding methods), - * overriding the view's layout file or by specifying a custom view layout file, as outlined below. + * + *

          A PlaybackControlView can be customized by setting attributes (or calling corresponding + * methods), overriding the view's layout file or by specifying a custom view layout file, as + * outlined below. * *

          Attributes

          + * * The following attributes can be set on a PlaybackControlView when used in a layout XML file: + * *

          + * *

            *
          • {@code show_timeout} - The time between the last user interaction and the controls * being automatically hidden, in milliseconds. Use zero if the controls should not * automatically timeout. *
              - *
            • Corresponding method: {@link #setShowTimeoutMs(int)}
            • - *
            • Default: {@link #DEFAULT_SHOW_TIMEOUT_MS}
            • + *
            • Corresponding method: {@link #setShowTimeoutMs(int)} + *
            • Default: {@link #DEFAULT_SHOW_TIMEOUT_MS} *
            - *
          • *
          • {@code rewind_increment} - The duration of the rewind applied when the user taps the * rewind button, in milliseconds. Use zero to disable the rewind button. *
              - *
            • Corresponding method: {@link #setRewindIncrementMs(int)}
            • - *
            • Default: {@link #DEFAULT_REWIND_MS}
            • + *
            • Corresponding method: {@link #setRewindIncrementMs(int)} + *
            • Default: {@link #DEFAULT_REWIND_MS} *
            - *
          • *
          • {@code fastforward_increment} - Like {@code rewind_increment}, but for fast forward. *
              - *
            • Corresponding method: {@link #setFastForwardIncrementMs(int)}
            • - *
            • Default: {@link #DEFAULT_FAST_FORWARD_MS}
            • + *
            • Corresponding method: {@link #setFastForwardIncrementMs(int)} + *
            • Default: {@link #DEFAULT_FAST_FORWARD_MS} *
            - *
          • *
          • {@code repeat_toggle_modes} - A flagged enumeration value specifying which repeat - * mode toggle options are enabled. Valid values are: {@code none}, {@code one}, - * {@code all}, or {@code one|all}. + * mode toggle options are enabled. Valid values are: {@code none}, {@code one}, {@code all}, + * or {@code one|all}. *
              - *
            • Corresponding method: {@link #setRepeatToggleModes(int)}
            • - *
            • Default: {@link PlaybackControlView#DEFAULT_REPEAT_TOGGLE_MODES}
            • + *
            • Corresponding method: {@link #setRepeatToggleModes(int)} + *
            • Default: {@link PlaybackControlView#DEFAULT_REPEAT_TOGGLE_MODES} *
            - *
          • *
          • {@code show_shuffle_button} - Whether the shuffle button is shown. *
              - *
            • Corresponding method: {@link #setShowShuffleButton(boolean)}
            • - *
            • Default: false
            • + *
            • Corresponding method: {@link #setShowShuffleButton(boolean)} + *
            • Default: false *
            - *
          • *
          • {@code controller_layout_id} - Specifies the id of the layout to be inflated. See * below for more details. *
              - *
            • Corresponding method: None
            • - *
            • Default: {@code R.id.exo_playback_control_view}
            • + *
            • Corresponding method: None + *
            • Default: {@code R.id.exo_playback_control_view} *
            - *
          • *
          * *

          Overriding the layout file

          + * * To customize the layout of PlaybackControlView throughout your app, or just for certain * configurations, you can define {@code exo_playback_control_view.xml} layout files in your * application {@code res/layout*} directories. These layouts will override the one provided by the * ExoPlayer library, and will be inflated for use by PlaybackControlView. The view identifies and * binds its children by looking for the following ids: + * *

          + * *

            *
          • {@code exo_play} - The play button. *
              - *
            • Type: {@link View}
            • + *
            • Type: {@link View} *
            - *
          • *
          • {@code exo_pause} - The pause button. *
              - *
            • Type: {@link View}
            • + *
            • Type: {@link View} *
            - *
          • *
          • {@code exo_ffwd} - The fast forward button. *
              - *
            • Type: {@link View}
            • + *
            • Type: {@link View} *
            - *
          • *
          • {@code exo_rew} - The rewind button. *
              - *
            • Type: {@link View}
            • + *
            • Type: {@link View} *
            - *
          • *
          • {@code exo_prev} - The previous track button. *
              - *
            • Type: {@link View}
            • + *
            • Type: {@link View} *
            - *
          • *
          • {@code exo_next} - The next track button. *
              - *
            • Type: {@link View}
            • + *
            • Type: {@link View} *
            - *
          • *
          • {@code exo_repeat_toggle} - The repeat toggle button. *
              - *
            • Type: {@link View}
            • + *
            • Type: {@link View} *
            - *
          • *
          • {@code exo_shuffle} - The shuffle button. *
              - *
            • Type: {@link View}
            • + *
            • Type: {@link View} *
            - *
          • *
          • {@code exo_position} - Text view displaying the current playback position. *
              - *
            • Type: {@link TextView}
            • + *
            • Type: {@link TextView} *
            - *
          • *
          • {@code exo_duration} - Text view displaying the current media duration. *
              - *
            • Type: {@link TextView}
            • + *
            • Type: {@link TextView} *
            - *
          • *
          • {@code exo_progress} - Time bar that's updated during playback and allows seeking. *
              - *
            • Type: {@link TimeBar}
            • + *
            • Type: {@link TimeBar} *
            - *
          • *
          - *

          - * All child views are optional and so can be omitted if not required, however where defined they + * + *

          All child views are optional and so can be omitted if not required, however where defined they * must be of the expected type. * *

          Specifying a custom layout file

          + * * Defining your own {@code exo_playback_control_view.xml} is useful to customize the layout of * PlaybackControlView throughout your application. It's also possible to customize the layout for a * single instance in a layout file. This is achieved by setting the {@code controller_layout_id} @@ -175,15 +166,11 @@ public class PlaybackControlView extends FrameLayout { ExoPlayerLibraryInfo.registerModule("goog.exo.ui"); } - /** - * @deprecated Use {@link com.google.android.exoplayer2.ControlDispatcher}. - */ + /** @deprecated Use {@link com.google.android.exoplayer2.ControlDispatcher}. */ @Deprecated public interface ControlDispatcher extends com.google.android.exoplayer2.ControlDispatcher {} - /** - * Listener to be notified about changes of the visibility of the UI control. - */ + /** Listener to be notified about changes of the visibility of the UI control. */ public interface VisibilityListener { /** @@ -192,38 +179,25 @@ public class PlaybackControlView extends FrameLayout { * @param visibility The new visibility. Either {@link View#VISIBLE} or {@link View#GONE}. */ void onVisibilityChange(int visibility); - } private static final class DefaultControlDispatcher extends com.google.android.exoplayer2.DefaultControlDispatcher implements ControlDispatcher {} - /** - * @deprecated Use {@link com.google.android.exoplayer2.DefaultControlDispatcher}. - */ + /** @deprecated Use {@link com.google.android.exoplayer2.DefaultControlDispatcher}. */ @Deprecated public static final ControlDispatcher DEFAULT_CONTROL_DISPATCHER = new DefaultControlDispatcher(); - /** - * The default fast forward increment, in milliseconds. - */ + /** The default fast forward increment, in milliseconds. */ public static final int DEFAULT_FAST_FORWARD_MS = 15000; - /** - * The default rewind increment, in milliseconds. - */ + /** The default rewind increment, in milliseconds. */ public static final int DEFAULT_REWIND_MS = 5000; - /** - * The default show timeout, in milliseconds. - */ + /** The default show timeout, in milliseconds. */ public static final int DEFAULT_SHOW_TIMEOUT_MS = 5000; - /** - * The default repeat toggle modes. - */ + /** The default repeat toggle modes. */ public static final @RepeatModeUtil.RepeatToggleModes int DEFAULT_REPEAT_TOGGLE_MODES = RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE; - /** - * The maximum number of windows that can be shown in a multi-window time bar. - */ + /** 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; @@ -271,19 +245,21 @@ public class PlaybackControlView extends FrameLayout { private long[] extraAdGroupTimesMs; private boolean[] extraPlayedAdGroups; - private final Runnable updateProgressAction = new Runnable() { - @Override - public void run() { - updateProgress(); - } - }; + private final Runnable updateProgressAction = + new Runnable() { + @Override + public void run() { + updateProgress(); + } + }; - private final Runnable hideAction = new Runnable() { - @Override - public void run() { - hide(); - } - }; + private final Runnable hideAction = + new Runnable() { + @Override + public void run() { + hide(); + } + }; public PlaybackControlView(Context context) { this(context, null); @@ -297,8 +273,8 @@ public class PlaybackControlView extends FrameLayout { this(context, attrs, defStyleAttr, attrs); } - public PlaybackControlView(Context context, AttributeSet attrs, int defStyleAttr, - AttributeSet playbackAttrs) { + public PlaybackControlView( + Context context, AttributeSet attrs, int defStyleAttr, AttributeSet playbackAttrs) { super(context, attrs, defStyleAttr); int controllerLayoutId = R.layout.exo_playback_control_view; rewindMs = DEFAULT_REWIND_MS; @@ -307,18 +283,21 @@ public class PlaybackControlView extends FrameLayout { repeatToggleModes = DEFAULT_REPEAT_TOGGLE_MODES; showShuffleButton = false; if (playbackAttrs != null) { - TypedArray a = context.getTheme().obtainStyledAttributes(playbackAttrs, - R.styleable.PlaybackControlView, 0, 0); + TypedArray a = + context + .getTheme() + .obtainStyledAttributes(playbackAttrs, R.styleable.PlaybackControlView, 0, 0); try { rewindMs = a.getInt(R.styleable.PlaybackControlView_rewind_increment, rewindMs); - fastForwardMs = a.getInt(R.styleable.PlaybackControlView_fastforward_increment, - fastForwardMs); + fastForwardMs = + a.getInt(R.styleable.PlaybackControlView_fastforward_increment, fastForwardMs); showTimeoutMs = a.getInt(R.styleable.PlaybackControlView_show_timeout, showTimeoutMs); - controllerLayoutId = a.getResourceId(R.styleable.PlaybackControlView_controller_layout_id, - controllerLayoutId); + controllerLayoutId = + a.getResourceId( + R.styleable.PlaybackControlView_controller_layout_id, controllerLayoutId); repeatToggleModes = getRepeatToggleModes(a, repeatToggleModes); - showShuffleButton = a.getBoolean(R.styleable.PlaybackControlView_show_shuffle_button, - showShuffleButton); + showShuffleButton = + a.getBoolean(R.styleable.PlaybackControlView_show_shuffle_button, showShuffleButton); } finally { a.recycle(); } @@ -379,17 +358,17 @@ public class PlaybackControlView extends FrameLayout { repeatOffButtonDrawable = resources.getDrawable(R.drawable.exo_controls_repeat_off); repeatOneButtonDrawable = resources.getDrawable(R.drawable.exo_controls_repeat_one); repeatAllButtonDrawable = resources.getDrawable(R.drawable.exo_controls_repeat_all); - repeatOffButtonContentDescription = resources.getString( - R.string.exo_controls_repeat_off_description); - repeatOneButtonContentDescription = resources.getString( - R.string.exo_controls_repeat_one_description); - repeatAllButtonContentDescription = resources.getString( - R.string.exo_controls_repeat_all_description); + repeatOffButtonContentDescription = + resources.getString(R.string.exo_controls_repeat_off_description); + repeatOneButtonContentDescription = + resources.getString(R.string.exo_controls_repeat_one_description); + repeatAllButtonContentDescription = + resources.getString(R.string.exo_controls_repeat_all_description); } @SuppressWarnings("ResourceType") - private static @RepeatModeUtil.RepeatToggleModes int getRepeatToggleModes(TypedArray a, - @RepeatModeUtil.RepeatToggleModes int repeatToggleModes) { + private static @RepeatModeUtil.RepeatToggleModes int getRepeatToggleModes( + TypedArray a, @RepeatModeUtil.RepeatToggleModes int repeatToggleModes) { return a.getInt(R.styleable.PlaybackControlView_repeat_toggle_modes, repeatToggleModes); } @@ -422,9 +401,9 @@ public class PlaybackControlView extends FrameLayout { /** * 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. + * 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. */ @@ -443,8 +422,8 @@ public class PlaybackControlView extends FrameLayout { * @param extraPlayedAdGroups Whether each ad has been played, or {@code null} to show no extra ad * markers. */ - public void setExtraAdGroupMarkers(@Nullable long[] extraAdGroupTimesMs, - @Nullable boolean[] extraPlayedAdGroups) { + public void setExtraAdGroupMarkers( + @Nullable long[] extraAdGroupTimesMs, @Nullable boolean[] extraPlayedAdGroups) { if (extraAdGroupTimesMs == null) { this.extraAdGroupTimesMs = new long[0]; this.extraPlayedAdGroups = new boolean[0]; @@ -473,8 +452,10 @@ public class PlaybackControlView extends FrameLayout { */ public void setControlDispatcher( @Nullable com.google.android.exoplayer2.ControlDispatcher controlDispatcher) { - this.controlDispatcher = controlDispatcher == null - ? new com.google.android.exoplayer2.DefaultControlDispatcher() : controlDispatcher; + this.controlDispatcher = + controlDispatcher == null + ? new com.google.android.exoplayer2.DefaultControlDispatcher() + : controlDispatcher; } /** @@ -556,9 +537,7 @@ public class PlaybackControlView extends FrameLayout { } } - /** - * Returns whether the shuffle button is shown. - */ + /** Returns whether the shuffle button is shown. */ public boolean getShowShuffleButton() { return showShuffleButton; } @@ -590,9 +569,7 @@ public class PlaybackControlView extends FrameLayout { hideAfterTimeout(); } - /** - * Hides the controller. - */ + /** Hides the controller. */ public void hide() { if (isVisible()) { setVisibility(GONE); @@ -605,9 +582,7 @@ public class PlaybackControlView extends FrameLayout { } } - /** - * Returns whether the controller is currently visible. - */ + /** Returns whether the controller is currently visible. */ public boolean isVisible() { return getVisibility() == VISIBLE; } @@ -664,8 +639,8 @@ public class PlaybackControlView extends FrameLayout { int windowIndex = player.getCurrentWindowIndex(); timeline.getWindow(windowIndex, window); isSeekable = window.isSeekable; - enablePrevious = isSeekable || !window.isDynamic - || player.getPreviousWindowIndex() != C.INDEX_UNSET; + enablePrevious = + isSeekable || !window.isDynamic || player.getPreviousWindowIndex() != C.INDEX_UNSET; enableNext = window.isDynamic || player.getNextWindowIndex() != C.INDEX_UNSET; } setButtonEnabled(enablePrevious, previousButton); @@ -728,8 +703,8 @@ public class PlaybackControlView extends FrameLayout { if (player == null) { return; } - multiWindowTimeBar = showMultiWindowTimeBar - && canShowMultiWindowTimeBar(player.getCurrentTimeline(), window); + multiWindowTimeBar = + showMultiWindowTimeBar && canShowMultiWindowTimeBar(player.getCurrentTimeline(), window); } private void updateProgress() { @@ -836,8 +811,8 @@ public class PlaybackControlView extends FrameLayout { if (mediaTimeDelayMs < (mediaTimeUpdatePeriodMs / 5)) { mediaTimeDelayMs += mediaTimeUpdatePeriodMs; } - delayMs = playbackSpeed == 1 ? mediaTimeDelayMs - : (long) (mediaTimeDelayMs / playbackSpeed); + delayMs = + playbackSpeed == 1 ? mediaTimeDelayMs : (long) (mediaTimeDelayMs / playbackSpeed); } else { delayMs = 200; } @@ -876,7 +851,7 @@ public class PlaybackControlView extends FrameLayout { int previousWindowIndex = player.getPreviousWindowIndex(); if (previousWindowIndex != C.INDEX_UNSET && (player.getCurrentPosition() <= MAX_POSITION_FOR_SEEK_TO_PREVIOUS - || (window.isDynamic && !window.isSeekable))) { + || (window.isDynamic && !window.isSeekable))) { seekTo(previousWindowIndex, C.TIME_UNSET); } else { seekTo(0); @@ -1054,8 +1029,8 @@ public class PlaybackControlView extends FrameLayout { return true; } - private final class ComponentListener extends Player.DefaultEventListener implements - TimeBar.OnScrubListener, OnClickListener { + private final class ComponentListener extends Player.DefaultEventListener + implements TimeBar.OnScrubListener, OnClickListener { @Override public void onScrubStart(TimeBar timeBar, long position) { @@ -1104,8 +1079,8 @@ public class PlaybackControlView extends FrameLayout { } @Override - public void onTimelineChanged(Timeline timeline, Object manifest, - @Player.TimelineChangeReason int reason) { + public void onTimelineChanged( + Timeline timeline, Object manifest, @Player.TimelineChangeReason int reason) { updateNavigation(); updateTimeBarMode(); updateProgress(); @@ -1127,15 +1102,13 @@ public class PlaybackControlView extends FrameLayout { } else if (pauseButton == view) { controlDispatcher.dispatchSetPlayWhenReady(player, false); } else if (repeatToggleButton == view) { - controlDispatcher.dispatchSetRepeatMode(player, RepeatModeUtil.getNextRepeatMode( - player.getRepeatMode(), repeatToggleModes)); + controlDispatcher.dispatchSetRepeatMode( + player, RepeatModeUtil.getNextRepeatMode(player.getRepeatMode(), repeatToggleModes)); } else if (shuffleButton == view) { controlDispatcher.dispatchSetShuffleModeEnabled(player, !player.getShuffleModeEnabled()); } } hideAfterTimeout(); } - } - } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index c5a4bc8086..def8925ec3 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -268,26 +268,26 @@ public final class SimpleExoPlayerView extends FrameLayout { boolean controllerAutoShow = true; boolean controllerHideDuringAds = true; if (attrs != null) { - TypedArray a = context.getTheme().obtainStyledAttributes(attrs, - R.styleable.SimpleExoPlayerView, 0, 0); + TypedArray a = + context.getTheme().obtainStyledAttributes(attrs, R.styleable.SimpleExoPlayerView, 0, 0); try { shutterColorSet = a.hasValue(R.styleable.SimpleExoPlayerView_shutter_background_color); - shutterColor = a.getColor(R.styleable.SimpleExoPlayerView_shutter_background_color, - shutterColor); - playerLayoutId = a.getResourceId(R.styleable.SimpleExoPlayerView_player_layout_id, - playerLayoutId); + shutterColor = + a.getColor(R.styleable.SimpleExoPlayerView_shutter_background_color, shutterColor); + playerLayoutId = + a.getResourceId(R.styleable.SimpleExoPlayerView_player_layout_id, playerLayoutId); useArtwork = a.getBoolean(R.styleable.SimpleExoPlayerView_use_artwork, useArtwork); - defaultArtworkId = a.getResourceId(R.styleable.SimpleExoPlayerView_default_artwork, - defaultArtworkId); + defaultArtworkId = + a.getResourceId(R.styleable.SimpleExoPlayerView_default_artwork, defaultArtworkId); useController = a.getBoolean(R.styleable.SimpleExoPlayerView_use_controller, useController); surfaceType = a.getInt(R.styleable.SimpleExoPlayerView_surface_type, surfaceType); 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); - controllerAutoShow = a.getBoolean(R.styleable.SimpleExoPlayerView_auto_show, - controllerAutoShow); + controllerShowTimeoutMs = + a.getInt(R.styleable.SimpleExoPlayerView_show_timeout, controllerShowTimeoutMs); + controllerHideOnTouch = + a.getBoolean(R.styleable.SimpleExoPlayerView_hide_on_touch, controllerHideOnTouch); + controllerAutoShow = + a.getBoolean(R.styleable.SimpleExoPlayerView_auto_show, controllerAutoShow); controllerHideDuringAds = a.getBoolean(R.styleable.SimpleExoPlayerView_hide_during_ads, controllerHideDuringAds); } finally { @@ -313,10 +313,13 @@ public final class SimpleExoPlayerView extends FrameLayout { // Create a surface view and insert it into the content frame, if there is one. if (contentFrame != null && surfaceType != SURFACE_TYPE_NONE) { - ViewGroup.LayoutParams params = new ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); - surfaceView = surfaceType == SURFACE_TYPE_TEXTURE_VIEW ? new TextureView(context) - : new SurfaceView(context); + ViewGroup.LayoutParams params = + new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); + surfaceView = + surfaceType == SURFACE_TYPE_TEXTURE_VIEW + ? new TextureView(context) + : new SurfaceView(context); surfaceView.setLayoutParams(params); contentFrame.addView(surfaceView, 0); } else { @@ -372,8 +375,10 @@ public final class SimpleExoPlayerView extends FrameLayout { * @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) { + public static void switchTargetView( + @NonNull SimpleExoPlayer player, + @Nullable SimpleExoPlayerView oldPlayerView, + @Nullable SimpleExoPlayerView newPlayerView) { if (oldPlayerView == newPlayerView) { return; } @@ -389,21 +394,20 @@ public final class SimpleExoPlayerView extends FrameLayout { } } - /** - * Returns the player currently set on this view, or null if no player is set. - */ + /** Returns the player currently set on this view, or null if no player is set. */ public SimpleExoPlayer getPlayer() { return player; } /** * Set the {@link SimpleExoPlayer} to use. - *

          - * 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. + * + *

          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. */ @@ -467,9 +471,7 @@ public final class SimpleExoPlayerView extends FrameLayout { contentFrame.setResizeMode(resizeMode); } - /** - * Returns whether artwork is displayed if present in the media. - */ + /** Returns whether artwork is displayed if present in the media. */ public boolean getUseArtwork() { return useArtwork; } @@ -487,9 +489,7 @@ public final class SimpleExoPlayerView extends FrameLayout { } } - /** - * Returns the default artwork to display. - */ + /** Returns the default artwork to display. */ public Bitmap getDefaultArtwork() { return defaultArtwork; } @@ -507,9 +507,7 @@ public final class SimpleExoPlayerView extends FrameLayout { } } - /** - * Returns whether the playback controls can be shown. - */ + /** Returns whether the playback controls can be shown. */ public boolean getUseController() { return useController; } @@ -554,8 +552,8 @@ public final class SimpleExoPlayerView extends FrameLayout { overlayFrameLayout.requestFocus(); return super.dispatchKeyEvent(event); } - boolean isDpadWhenControlHidden = isDpadKey(event.getKeyCode()) && useController - && !controller.isVisible(); + boolean isDpadWhenControlHidden = + isDpadKey(event.getKeyCode()) && useController && !controller.isVisible(); maybeShowController(true); return isDpadWhenControlHidden || dispatchMediaKeyEvent(event) || super.dispatchKeyEvent(event); } @@ -574,17 +572,15 @@ public final class SimpleExoPlayerView extends FrameLayout { /** * Shows the playback controls. Does nothing if playback controls are disabled. * - *

          The playback controls are automatically hidden during playback after - * {{@link #getControllerShowTimeoutMs()}}. They are shown indefinitely when playback has not - * started yet, is paused, has ended or failed. + *

          The playback controls are automatically hidden during playback after {{@link + * #getControllerShowTimeoutMs()}}. They are shown indefinitely when playback has not started yet, + * is paused, has ended or failed. */ public void showController() { showController(shouldShowControllerIndefinitely()); } - /** - * Hides the playback controls. Does nothing if playback controls are disabled. - */ + /** Hides the playback controls. Does nothing if playback controls are disabled. */ public void hideController() { if (controller != null) { controller.hide(); @@ -607,8 +603,8 @@ public final class SimpleExoPlayerView extends FrameLayout { * Sets the playback controls timeout. The playback controls are automatically hidden after this * duration of time has elapsed without user input and with playback or buffering in progress. * - * @param controllerShowTimeoutMs The timeout in milliseconds. A non-positive value will cause - * the controller to remain visible indefinitely. + * @param controllerShowTimeoutMs The timeout in milliseconds. A non-positive value will cause the + * controller to remain visible indefinitely. */ public void setControllerShowTimeoutMs(int controllerShowTimeoutMs) { Assertions.checkState(controller != null); @@ -620,9 +616,7 @@ public final class SimpleExoPlayerView extends FrameLayout { } } - /** - * Returns whether the playback controls are hidden by touch events. - */ + /** Returns whether the playback controls are hidden by touch events. */ public boolean getControllerHideOnTouch() { return controllerHideOnTouch; } @@ -680,8 +674,8 @@ public final class SimpleExoPlayerView extends FrameLayout { /** * Sets the {@link ControlDispatcher}. * - * @param controlDispatcher The {@link ControlDispatcher}, or null to use - * {@link DefaultControlDispatcher}. + * @param controlDispatcher The {@link ControlDispatcher}, or null to use {@link + * DefaultControlDispatcher}. */ public void setControlDispatcher(@Nullable ControlDispatcher controlDispatcher) { Assertions.checkState(controller != null); @@ -742,11 +736,12 @@ public final class SimpleExoPlayerView extends FrameLayout { /** * Gets the view onto which video is rendered. This is a: + * *

            - *
          • {@link SurfaceView} by default, or if the {@code surface_type} attribute is set to - * {@code surface_view}.
          • - *
          • {@link TextureView} if {@code surface_type} is {@code texture_view}.
          • - *
          • {@code null} if {@code surface_type} is {@code none}.
          • + *
          • {@link SurfaceView} by default, or if the {@code surface_type} attribute is set to {@code + * surface_view}. + *
          • {@link TextureView} if {@code surface_type} is {@code texture_view}. + *
          • {@code null} if {@code surface_type} is {@code none}. *
          * * @return The {@link SurfaceView}, {@link TextureView} or {@code null}. @@ -798,9 +793,7 @@ public final class SimpleExoPlayerView extends FrameLayout { return true; } - /** - * Shows the playback controls, but only if forced or shown indefinitely. - */ + /** Shows the playback controls, but only if forced or shown indefinitely. */ private void maybeShowController(boolean isForced) { if (isPlayingAd() && controllerHideDuringAds) { return; @@ -819,8 +812,10 @@ public final class SimpleExoPlayerView extends FrameLayout { return true; } int playbackState = player.getPlaybackState(); - return controllerAutoShow && (playbackState == Player.STATE_IDLE - || playbackState == Player.STATE_ENDED || !player.getPlayWhenReady()); + return controllerAutoShow + && (playbackState == Player.STATE_IDLE + || playbackState == Player.STATE_ENDED + || !player.getPlayWhenReady()); } private void showController(boolean showIndefinitely) { @@ -927,15 +922,19 @@ public final class SimpleExoPlayerView extends FrameLayout { @SuppressLint("InlinedApi") private boolean isDpadKey(int keyCode) { - return keyCode == KeyEvent.KEYCODE_DPAD_UP || keyCode == KeyEvent.KEYCODE_DPAD_UP_RIGHT - || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT || keyCode == KeyEvent.KEYCODE_DPAD_DOWN_RIGHT - || keyCode == KeyEvent.KEYCODE_DPAD_DOWN || keyCode == KeyEvent.KEYCODE_DPAD_DOWN_LEFT - || keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_UP_LEFT + return keyCode == KeyEvent.KEYCODE_DPAD_UP + || keyCode == KeyEvent.KEYCODE_DPAD_UP_RIGHT + || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT + || keyCode == KeyEvent.KEYCODE_DPAD_DOWN_RIGHT + || keyCode == KeyEvent.KEYCODE_DPAD_DOWN + || keyCode == KeyEvent.KEYCODE_DPAD_DOWN_LEFT + || keyCode == KeyEvent.KEYCODE_DPAD_LEFT + || keyCode == KeyEvent.KEYCODE_DPAD_UP_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_CENTER; } - private final class ComponentListener extends Player.DefaultEventListener implements TextOutput, - SimpleExoPlayer.VideoListener { + private final class ComponentListener extends Player.DefaultEventListener + implements TextOutput, SimpleExoPlayer.VideoListener { // TextOutput implementation @@ -949,8 +948,8 @@ public final class SimpleExoPlayerView extends FrameLayout { // SimpleExoPlayer.VideoInfoListener implementation @Override - public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, - float pixelWidthHeightRatio) { + public void onVideoSizeChanged( + int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { if (contentFrame != null) { float aspectRatio = height == 0 ? 1 : (width * pixelWidthHeightRatio) / height; contentFrame.setAspectRatio(aspectRatio); @@ -986,7 +985,5 @@ public final class SimpleExoPlayerView extends FrameLayout { hideController(); } } - } - } From 682953c411208a673f59975620b1b55682772c70 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 4 Jan 2018 03:23:27 -0800 Subject: [PATCH 1013/2472] Fix typos ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=180778084 --- .../com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java | 2 +- .../google/android/exoplayer2/source/dash/DashMediaPeriod.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index 284d716582..15c30e8c67 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -332,7 +332,7 @@ public final class ImaAdsLoader extends Player.DefaultEventListener implements A * *

          Ads will be requested automatically when the player is prepared if this method has not been * called, so it is only necessary to call this method if you want to request ads before preparing - * the player + * the player. * * @param adUiViewGroup A {@link ViewGroup} on top of the player that will show any ad UI. */ diff --git a/library/dash/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 index 2b7b16228e..a8f9203cbf 100644 --- a/library/dash/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 @@ -104,7 +104,7 @@ import java.util.Map; /** * Updates the {@link DashManifest} and the index of this period in the manifest. - *

          + * * @param manifest The updated manifest. * @param periodIndex the new index of this period in the updated manifest. */ From c89cc81b710e7bc5e8f35c56e59cc8f8f99f1a0b Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 4 Jan 2018 04:31:44 -0800 Subject: [PATCH 1014/2472] Configure MediaCodecs for realtime priority ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=180782164 --- .../audio/MediaCodecAudioRenderer.java | 5 +++-- .../mediacodec/MediaCodecRenderer.java | 20 +++++++++++++++++++ .../video/MediaCodecVideoRenderer.java | 6 +----- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index b4459e42aa..f73d63616b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java @@ -240,14 +240,15 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media protected void configureCodec(MediaCodecInfo codecInfo, MediaCodec codec, Format format, MediaCrypto crypto) { codecNeedsDiscardChannelsWorkaround = codecNeedsDiscardChannelsWorkaround(codecInfo.name); + MediaFormat mediaFormat = getMediaFormatForPlayback(format); if (passthroughEnabled) { // Override the MIME type used to configure the codec if we are using a passthrough decoder. - passthroughMediaFormat = format.getFrameworkMediaFormatV16(); + passthroughMediaFormat = mediaFormat; passthroughMediaFormat.setString(MediaFormat.KEY_MIME, MimeTypes.AUDIO_RAW); codec.configure(passthroughMediaFormat, null, crypto, 0); passthroughMediaFormat.setString(MediaFormat.KEY_MIME, format.sampleMimeType); } else { - codec.configure(format.getFrameworkMediaFormatV16(), null, crypto, 0); + codec.configure(mediaFormat, null, crypto, 0); passthroughMediaFormat = null; } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java index ef7d691c5b..4b1af7e385 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java @@ -430,6 +430,21 @@ public abstract class MediaCodecRenderer extends BaseRenderer { return codecInfo; } + /** + * Returns the framework {@link MediaFormat} that can be used to configure a {@link MediaCodec} + * for decoding the given {@link Format} for playback. + * + * @param format The format of the media. + * @return The framework media format. + */ + protected final MediaFormat getMediaFormatForPlayback(Format format) { + MediaFormat mediaFormat = format.getFrameworkMediaFormatV16(); + if (Util.SDK_INT >= 23) { + configureMediaFormatForPlaybackV23(mediaFormat); + } + return mediaFormat; + } + @Override protected void onEnabled(boolean joining) throws ExoPlaybackException { decoderCounters = new DecoderCounters(); @@ -1108,6 +1123,11 @@ public abstract class MediaCodecRenderer extends BaseRenderer { return false; } + @TargetApi(23) + private static void configureMediaFormatForPlaybackV23(MediaFormat mediaFormat) { + mediaFormat.setInteger(MediaFormat.KEY_PRIORITY, 0 /* realtime priority */); + } + /** * Returns whether the decoder is known to fail when flushed. *

          diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index 41e3c970c4..6900823ebe 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -906,19 +906,15 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { @SuppressLint("InlinedApi") protected MediaFormat getMediaFormat(Format format, CodecMaxValues codecMaxValues, boolean deviceNeedsAutoFrcWorkaround, int tunnelingAudioSessionId) { - MediaFormat frameworkMediaFormat = format.getFrameworkMediaFormatV16(); - // Set the maximum adaptive video dimensions. + MediaFormat frameworkMediaFormat = getMediaFormatForPlayback(format); frameworkMediaFormat.setInteger(MediaFormat.KEY_MAX_WIDTH, codecMaxValues.width); frameworkMediaFormat.setInteger(MediaFormat.KEY_MAX_HEIGHT, codecMaxValues.height); - // Set the maximum input size. if (codecMaxValues.inputSize != Format.NO_VALUE) { frameworkMediaFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, codecMaxValues.inputSize); } - // Set FRC workaround. if (deviceNeedsAutoFrcWorkaround) { frameworkMediaFormat.setInteger("auto-frc", 0); } - // Configure tunneling if enabled. if (tunnelingAudioSessionId != C.AUDIO_SESSION_ID_UNSET) { configureTunnelingV21(frameworkMediaFormat, tunnelingAudioSessionId); } From a1bac99f3bae78e510e367b61dd49a37e4301476 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 4 Jan 2018 05:39:42 -0800 Subject: [PATCH 1015/2472] Fix loadDrmInitData given DASH manifest parser changes DASH manifests can now contain non-null but incomplete DRM init data. Hence using the manifest init data when non-null is not always the correct thing to do. This change merges the sample and manifest formats (which correctly merges the DRM init data) and then uses the result. Issue: #3630 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=180787784 --- .../android/exoplayer2/source/dash/DashUtil.java | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/library/dash/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 index ed2f916b87..57632225a5 100644 --- a/library/dash/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 @@ -81,14 +81,11 @@ public final class DashUtil { 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 manifestFormat = representation.format; Format sampleFormat = DashUtil.loadSampleFormat(dataSource, primaryTrackType, representation); - return sampleFormat == null ? null : sampleFormat.drmInitData; + return sampleFormat == null + ? manifestFormat.drmInitData + : sampleFormat.copyWithManifestFormatInfo(manifestFormat).drmInitData; } /** From b610e1144338a93e89604b095ac1792b72f7a5af Mon Sep 17 00:00:00 2001 From: Drew Hill Date: Fri, 5 Jan 2018 23:10:51 -0500 Subject: [PATCH 1016/2472] PGS subtitle decoding support --- .../text/SubtitleDecoderFactory.java | 6 +- .../exoplayer2/text/pgs/PgsBuilder.java | 232 ++++++++++++++++++ .../exoplayer2/text/pgs/PgsDecoder.java | 26 ++ .../exoplayer2/text/pgs/PgsSubtitle.java | 54 ++++ 4 files changed, 317 insertions(+), 1 deletion(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsBuilder.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsDecoder.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsSubtitle.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java index 6a9b83a015..4720a67bba 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java @@ -19,6 +19,7 @@ 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.pgs.PgsDecoder; import com.google.android.exoplayer2.text.ssa.SsaDecoder; import com.google.android.exoplayer2.text.subrip.SubripDecoder; import com.google.android.exoplayer2.text.ttml.TtmlDecoder; @@ -80,7 +81,8 @@ public interface SubtitleDecoderFactory { || MimeTypes.APPLICATION_CEA608.equals(mimeType) || MimeTypes.APPLICATION_MP4CEA608.equals(mimeType) || MimeTypes.APPLICATION_CEA708.equals(mimeType) - || MimeTypes.APPLICATION_DVBSUBS.equals(mimeType); + || MimeTypes.APPLICATION_DVBSUBS.equals(mimeType) + || MimeTypes.APPLICATION_PGS.equals(mimeType); } @Override @@ -105,6 +107,8 @@ public interface SubtitleDecoderFactory { return new Cea708Decoder(format.accessibilityChannel); case MimeTypes.APPLICATION_DVBSUBS: return new DvbDecoder(format.initializationData); + case MimeTypes.APPLICATION_PGS: + return new PgsDecoder(); default: throw new IllegalArgumentException("Attempted to create decoder for unsupported format"); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsBuilder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsBuilder.java new file mode 100644 index 0000000000..e67178314d --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsBuilder.java @@ -0,0 +1,232 @@ +/* +* +* Sources for this implementation PGS decoding can be founder below +* +* http://exar.ch/suprip/hddvd.php +* http://forum.doom9.org/showthread.php?t=124105 +* http://www.equasys.de/colorconversion.html + */ + +package com.google.android.exoplayer2.text.pgs; + +import android.graphics.Bitmap; + +import com.google.android.exoplayer2.text.Cue; +import com.google.android.exoplayer2.text.Subtitle; +import com.google.android.exoplayer2.util.ParsableByteArray; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +class PgsBuilder { + + private static final int SECTION_PALETTE = 0x14; + private static final int SECTION_BITMAP_PICTURE = 0x15; + private static final int SECTION_IDENTIFIER = 0x16; + private static final int SECTION_END = 0x80; + + private List list = new ArrayList<>(); + private Holder holder = new Holder(); + + boolean readNextSection(ParsableByteArray buffer) { + + if (buffer.bytesLeft() < 3) + return false; + + int sectionId = buffer.readUnsignedByte(); + int sectionLength = buffer.readUnsignedShort(); + switch(sectionId) { + case SECTION_PALETTE: + holder.parsePaletteIndexes(buffer, sectionLength); + break; + case SECTION_BITMAP_PICTURE: + holder.fetchBitmapData(buffer, sectionLength); + break; + case SECTION_IDENTIFIER: + holder.fetchIdentifierData(buffer, sectionLength); + break; + case SECTION_END: + list.add(holder); + holder = new Holder(); + break; + default: + buffer.skipBytes(Math.min(sectionLength, buffer.bytesLeft())); + break; + } + return true; + } + + public Subtitle build() { + + if (list.isEmpty()) + return new PgsSubtitle(); + + Cue[] cues = new Cue[list.size()]; + long[] cueStartTimes = new long[list.size()]; + int index = 0; + for (Holder curr : list) { + cues[index] = curr.build(); + cueStartTimes[index++] = curr.start_time; + } + return new PgsSubtitle(cues, cueStartTimes); + } + + private class Holder { + + private int[] colors = null; + private ByteBuffer rle = null; + + Bitmap bitmap = null; + int plane_width = 0; + int plane_height = 0; + int bitmap_width = 0; + int bitmap_height = 0; + public int x = 0; + public int y = 0; + long start_time = 0; + + public Cue build() { + if (rle == null || !createBitmap(new ParsableByteArray(rle.array(), rle.position()))) + return null; + float left = (float) x / plane_width; + float top = (float) y / plane_height; + return new Cue(bitmap, left, Cue.ANCHOR_TYPE_START, top, Cue.ANCHOR_TYPE_START, + (float) bitmap_width / plane_width, (float) bitmap_height / plane_height); + } + + private void parsePaletteIndexes(ParsableByteArray buffer, int dataSize) { + // must be a multi of 5 for index, y, cb, cr, alpha + if (dataSize == 0 || (dataSize - 2) % 5 != 0) + return; + // skip first two bytes + buffer.skipBytes(2); + dataSize -= 2; + colors = new int[256]; + while (dataSize > 0) { + int index = buffer.readUnsignedByte(); + int color_y = buffer.readUnsignedByte() - 16; + int color_cr = buffer.readUnsignedByte() - 128; + int color_cb = buffer.readUnsignedByte() - 128; + int color_alpha = buffer.readUnsignedByte(); + dataSize -= 5; + if (index >= colors.length) + continue; + + int color_r = (int) Math.min(Math.max(Math.round(1.1644 * color_y + 1.793 * color_cr), 0), 255); + int color_g = (int) Math.min(Math.max(Math.round(1.1644 * color_y + (-0.213 * color_cr) + (-0.533 * color_cb)), 0), 255); + int color_b = (int) Math.min(Math.max(Math.round(1.1644 * color_y + 2.112 * color_cb), 0), 255); + //ARGB_8888 + colors[index] = (color_alpha << 24) | (color_r << 16) | (color_g << 8) | color_b; + } + } + + private void fetchBitmapData(ParsableByteArray buffer, int dataSize) { + if (dataSize <= 4) { + buffer.skipBytes(dataSize); + return; + } + // skip id field (2 bytes) + // skip version field + buffer.skipBytes(3); + dataSize -= 3; + + // check to see if this section is an appended section of the base section with + // width and height values + dataSize -= 1; // decrement first + if ((0x80 & buffer.readUnsignedByte()) > 0) { + if (dataSize < 3) { + buffer.skipBytes(dataSize); + return; + } + int full_len = buffer.readUnsignedInt24(); + dataSize -= 3; + if (full_len <= 4) { + buffer.skipBytes(dataSize); + return; + } + bitmap_width = buffer.readUnsignedShort(); + dataSize -= 2; + bitmap_height = buffer.readUnsignedShort(); + dataSize -= 2; + rle = ByteBuffer.allocate(full_len - 4); // don't include width & height + buffer.readBytes(rle, Math.min(dataSize, rle.capacity())); + } else if (rle != null) { + int postSkip = dataSize > rle.capacity() ? dataSize - rle.capacity() : 0; + buffer.readBytes(rle, Math.min(dataSize, rle.capacity())); + buffer.skipBytes(postSkip); + } + } + + private void fetchIdentifierData(ParsableByteArray buffer, int dataSize) { + if (dataSize < 4) { + buffer.skipBytes(dataSize); + return; + } + plane_width = buffer.readUnsignedShort(); + plane_height = buffer.readUnsignedShort(); + dataSize -= 4; + if (dataSize < 15) { + buffer.skipBytes(dataSize); + return; + } + // skip next 11 bytes + buffer.skipBytes(11); + x = buffer.readUnsignedShort(); + y = buffer.readUnsignedShort(); + dataSize -= 15; + buffer.skipBytes(dataSize); + } + + private boolean createBitmap(ParsableByteArray rle) { + if (bitmap_width == 0 || bitmap_height == 0 + || rle == null || rle.bytesLeft() == 0 + || colors == null || colors.length == 0) + return false; + int[] argb = new int[bitmap_width * bitmap_height]; + int currPixel = 0; + int nextbits, pixel_code, switchbits; + int number_of_pixels; + int line = 0; + while (rle.bytesLeft() > 0 && line < bitmap_height) { + boolean end_of_line = false; + do { + nextbits = rle.readUnsignedByte(); + if (nextbits != 0) { + pixel_code = nextbits; + number_of_pixels = 1; + } else { + switchbits = rle.readUnsignedByte(); + if ((switchbits & 0x80) == 0) { + pixel_code = 0; + if ((switchbits & 0x40) == 0) { + if (switchbits > 0) { + number_of_pixels = switchbits; + } else { + end_of_line = true; + ++line; + continue; + } + } else { + number_of_pixels = ((switchbits & 0x3f) << 8) | rle.readUnsignedByte(); + } + } else { + if ((switchbits & 0x40) == 0) { + number_of_pixels = switchbits & 0x3f; + pixel_code = rle.readUnsignedByte(); + } else { + number_of_pixels = ((switchbits & 0x3f) << 8) | rle.readUnsignedByte(); + pixel_code = rle.readUnsignedByte(); + } + } + } + Arrays.fill(argb, currPixel, currPixel + number_of_pixels, colors[pixel_code]); + currPixel += number_of_pixels; + } while (!end_of_line); + } + bitmap = Bitmap.createBitmap(argb, 0, bitmap_width, bitmap_width, bitmap_height, Bitmap.Config.ARGB_8888); + return bitmap != null; + } + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsDecoder.java new file mode 100644 index 0000000000..04c3ecd0a3 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsDecoder.java @@ -0,0 +1,26 @@ +package com.google.android.exoplayer2.text.pgs; + +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; + +@SuppressWarnings("unused") +public class PgsDecoder extends SimpleSubtitleDecoder { + + @SuppressWarnings("unused") + public PgsDecoder() { + super("PgsDecoder"); + } + + @Override + protected Subtitle decode(byte[] data, int size, boolean reset) throws SubtitleDecoderException { + ParsableByteArray buffer = new ParsableByteArray(data, size); + PgsBuilder builder = new PgsBuilder(); + do { + if (!builder.readNextSection(buffer)) + break; + } while (buffer.bytesLeft() > 0); + return builder.build(); + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsSubtitle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsSubtitle.java new file mode 100644 index 0000000000..affb2aa15b --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsSubtitle.java @@ -0,0 +1,54 @@ +package com.google.android.exoplayer2.text.pgs; + +import com.google.android.exoplayer2.text.Cue; +import com.google.android.exoplayer2.text.Subtitle; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; + +import java.util.Collections; +import java.util.List; + +public class PgsSubtitle implements Subtitle { + + private final Cue[] cues; + private final long[] cueTimesUs; + + PgsSubtitle() { + this.cues = null; + this.cueTimesUs = new long[0]; + } + + PgsSubtitle(Cue[] cues, long[] cueTimesUs) { + this.cues = cues; + this.cueTimesUs = cueTimesUs; + } + + @Override + public int getNextEventTimeIndex(long timeUs) { + int index = Util.binarySearchCeil(cueTimesUs, timeUs, false, false); + return index < cueTimesUs.length ? index : -1; + } + + @Override + public int getEventTimeCount() { +return cueTimesUs.length; +} + + @Override + public long getEventTime(int index) { + Assertions.checkArgument(index >= 0); + Assertions.checkArgument(index < cueTimesUs.length); + return cueTimesUs[index]; + } + + @Override + public List getCues(long timeUs) { + int index = Util.binarySearchFloor(cueTimesUs, timeUs, true, false); + if (index == -1 || cues == null || cues[index] == null) { + // timeUs is earlier than the start of the first cue, or we have an empty cue. + return Collections.emptyList(); + } + else + return Collections.singletonList(cues[index]); + } +} From ca0c090c1a98aa37bfaf6d85e1ae681f6d6f5236 Mon Sep 17 00:00:00 2001 From: Drew Hill Date: Sat, 23 Dec 2017 13:25:45 -0500 Subject: [PATCH 1017/2472] add support in mediacodecaudiorenderer for 24bit pcm to float --- .../audio/FloatResamplingAudioProcessor.java | 171 ++++++++++++++++++ .../audio/MediaCodecAudioRenderer.java | 108 +++++++++-- 2 files changed, 268 insertions(+), 11 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java new file mode 100644 index 0000000000..28d2eca25f --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java @@ -0,0 +1,171 @@ +package com.google.android.exoplayer2.audio; + + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * An {@link AudioProcessor} that converts audio data to {@link C#ENCODING_PCM_16BIT}. + */ +/* package */ final class FloatResamplingAudioProcessor implements AudioProcessor { + + private int sampleRateHz; + private static final double PCM_INT32_FLOAT = 1.0 / 0x7fffffff; + + private int channelCount; + @C.PcmEncoding + private int sourceEncoding; + private ByteBuffer buffer; + private ByteBuffer outputBuffer; + private boolean inputEnded; + + /** + * Creates a new audio processor that converts audio data to {@link C#ENCODING_PCM_16BIT}. + */ + public FloatResamplingAudioProcessor() { + sampleRateHz = Format.NO_VALUE; + channelCount = Format.NO_VALUE; + sourceEncoding = C.ENCODING_INVALID; + buffer = EMPTY_BUFFER; + outputBuffer = EMPTY_BUFFER; + } + + @Override + public boolean configure(int sampleRateHz, int channelCount, @C.Encoding int encoding) + throws AudioProcessor.UnhandledFormatException { + if (encoding != C.ENCODING_PCM_24BIT) { + throw new AudioProcessor.UnhandledFormatException(sampleRateHz, channelCount, encoding); + } + if (this.sampleRateHz == sampleRateHz && this.channelCount == channelCount + && this.sourceEncoding == encoding) { + return false; + } + this.sampleRateHz = sampleRateHz; + this.channelCount = channelCount; + this.sourceEncoding = encoding; + + return true; + } + + @Override + public boolean isActive() { return sourceEncoding == C.ENCODING_PCM_24BIT; } + + @Override + public int getOutputChannelCount() { return channelCount; } + + @Override + public int getOutputEncoding() { return C.ENCODING_PCM_FLOAT; } + + @Override + public int getOutputSampleRateHz() { + return sampleRateHz; + } + + @Override + public void queueInput(ByteBuffer inputBuffer) { + int offset = inputBuffer.position(); + int limit = inputBuffer.limit(); + int size = limit - offset; + + int resampledSize; + switch (sourceEncoding) { + case C.ENCODING_PCM_24BIT: + resampledSize = (size / 3) * 4; + break; + case C.ENCODING_PCM_32BIT: + case C.ENCODING_PCM_8BIT: + case C.ENCODING_PCM_16BIT: + case C.ENCODING_PCM_FLOAT: + case C.ENCODING_INVALID: + case Format.NO_VALUE: + default: + // Never happens. + throw new IllegalStateException(); + } + + if (buffer.capacity() < resampledSize) { + buffer = ByteBuffer.allocateDirect(resampledSize).order(ByteOrder.nativeOrder()); + } else { + buffer.clear(); + } + + // Samples are little endian. + switch (sourceEncoding) { + case C.ENCODING_PCM_24BIT: + // 24->32 bit resampling. + for (int i = offset; i < limit; i += 3) { + int val = (inputBuffer.get(i) << 8) & 0x0000ff00 | (inputBuffer.get(i + 1) << 16) & 0x00ff0000 | + (inputBuffer.get(i + 2) << 24) & 0xff000000; + writePcm32bitFloat(val, buffer); + } + break; + case C.ENCODING_PCM_32BIT: + case C.ENCODING_PCM_8BIT: + case C.ENCODING_PCM_16BIT: + case C.ENCODING_PCM_FLOAT: + case C.ENCODING_INVALID: + case Format.NO_VALUE: + default: + // Never happens. + throw new IllegalStateException(); + } + + inputBuffer.position(inputBuffer.limit()); + buffer.flip(); + outputBuffer = buffer; + } + + @Override + public void queueEndOfStream() { + inputEnded = true; + } + + @Override + public ByteBuffer getOutput() { + ByteBuffer outputBuffer = this.outputBuffer; + this.outputBuffer = EMPTY_BUFFER; + return outputBuffer; + } + + @SuppressWarnings("ReferenceEquality") + @Override + public boolean isEnded() { + return inputEnded && outputBuffer == EMPTY_BUFFER; + } + + @Override + public void flush() { + outputBuffer = EMPTY_BUFFER; + inputEnded = false; + } + + @Override + public void reset() { + flush(); + buffer = EMPTY_BUFFER; + sampleRateHz = Format.NO_VALUE; + channelCount = Format.NO_VALUE; + sourceEncoding = C.ENCODING_INVALID; + } + + /** + * Converts the provided value into 32-bit float PCM and writes to buffer. + * + * @param val 32-bit int value to convert to 32-bit float [-1.0, 1.0] + * @param buffer The output buffer. + */ + private static void writePcm32bitFloat(int val, ByteBuffer buffer) { + float convVal = (float) (PCM_INT32_FLOAT * val); + int bits = Float.floatToIntBits(convVal); + if (bits == 0x7fc00000) + bits = Float.floatToIntBits((float) 0.0); + buffer.put((byte) (bits & 0xff)); + buffer.put((byte) ((bits >> 8) & 0xff)); + buffer.put((byte) ((bits >> 16) & 0xff)); + buffer.put((byte) ((bits >> 24) & 0xff)); + } + +} \ No newline at end of file diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index f73d63616b..d5e1b6ab03 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java @@ -58,6 +58,9 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media private int encoderPadding; private long currentPositionUs; private boolean allowPositionDiscontinuity; + private final boolean dontDither24bitPCM; + private ByteBuffer resampledBuffer; + private FloatResamplingAudioProcessor floatResamplingAudioProcessor; /** * @param mediaCodecSelector A decoder selector. @@ -137,7 +140,37 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media @Nullable AudioRendererEventListener eventListener, @Nullable AudioCapabilities audioCapabilities, AudioProcessor... audioProcessors) { this(mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, - eventHandler, eventListener, new DefaultAudioSink(audioCapabilities, audioProcessors)); + eventHandler, eventListener, new DefaultAudioSink(audioCapabilities, audioProcessors), + false); + } + + /** + * @param mediaCodecSelector A decoder selector. + * @param drmSessionManager For use with encrypted content. May be null if support for encrypted + * content is not required. + * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions. + * For example a media file may start with a short clear region so as to allow playback to + * begin in parallel with key acquisition. This parameter specifies whether the renderer is + * permitted to play clear regions of encrypted media files before {@code drmSessionManager} + * has obtained the keys necessary to decrypt encrypted regions of the media. + * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be + * null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + * @param audioCapabilities The audio capabilities for playback on this device. May be null if the + * default capabilities (no encoded audio passthrough support) should be assumed. + * @param dontDither24bitPCM If the input is 24bit PCM audio convert to 32bit Float PCM + * @param audioProcessors Optional {@link AudioProcessor}s that will process PCM audio before + * output. + */ + public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector, + @Nullable DrmSessionManager drmSessionManager, + boolean playClearSamplesWithoutKeys, @Nullable Handler eventHandler, + @Nullable AudioRendererEventListener eventListener, + @Nullable AudioCapabilities audioCapabilities, boolean dontDither24bitPCM, + AudioProcessor... audioProcessors) { + this(mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, + eventHandler, eventListener, new DefaultAudioSink(audioCapabilities, audioProcessors), + dontDither24bitPCM); } /** @@ -158,9 +191,34 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media @Nullable DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, @Nullable Handler eventHandler, @Nullable AudioRendererEventListener eventListener, AudioSink audioSink) { + this(mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, eventHandler, + eventListener, audioSink, false); + } + + /** + * @param mediaCodecSelector A decoder selector. + * @param drmSessionManager For use with encrypted content. May be null if support for encrypted + * content is not required. + * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions. + * For example a media file may start with a short clear region so as to allow playback to + * begin in parallel with key acquisition. This parameter specifies whether the renderer is + * permitted to play clear regions of encrypted media files before {@code drmSessionManager} + * has obtained the keys necessary to decrypt encrypted regions of the media. + * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be + * null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + * @param audioSink The sink to which audio will be output. + * @param dontDither24bitPCM If the input is 24bit PCM audio convert to 32bit Float PCM + */ + public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector, + @Nullable DrmSessionManager drmSessionManager, + boolean playClearSamplesWithoutKeys, @Nullable Handler eventHandler, + @Nullable AudioRendererEventListener eventListener, AudioSink audioSink, + boolean dontDither24bitPCM) { super(C.TRACK_TYPE_AUDIO, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys); eventDispatcher = new EventDispatcher(eventHandler, eventListener); this.audioSink = audioSink; + this.dontDither24bitPCM = dontDither24bitPCM; audioSink.setListener(new AudioSinkListener()); } @@ -268,10 +326,20 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media protected void onInputFormatChanged(Format newFormat) throws ExoPlaybackException { super.onInputFormatChanged(newFormat); eventDispatcher.inputFormatChanged(newFormat); - // If the input format is anything other than PCM then we assume that the audio decoder will - // output 16-bit PCM. - pcmEncoding = MimeTypes.AUDIO_RAW.equals(newFormat.sampleMimeType) ? newFormat.pcmEncoding - : C.ENCODING_PCM_16BIT; + + // if the input is 24bit pcm audio and we explicitly said not to dither then convert it to float + if (dontDither24bitPCM && newFormat.pcmEncoding == C.ENCODING_PCM_24BIT) { + if (floatResamplingAudioProcessor == null) + floatResamplingAudioProcessor = new FloatResamplingAudioProcessor(); + pcmEncoding = floatResamplingAudioProcessor.getOutputEncoding(); + } else { + // If the input format is anything other than PCM then we assume that the audio decoder will + // output 16-bit PCM. + pcmEncoding = MimeTypes.AUDIO_RAW.equals(newFormat.sampleMimeType) ? newFormat.pcmEncoding + : C.ENCODING_PCM_16BIT; + floatResamplingAudioProcessor = null; + } + channelCount = newFormat.channelCount; encoderDelay = newFormat.encoderDelay != Format.NO_VALUE ? newFormat.encoderDelay : 0; encoderPadding = newFormat.encoderPadding != Format.NO_VALUE ? newFormat.encoderPadding : 0; @@ -302,9 +370,11 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } try { + if (floatResamplingAudioProcessor != null) + floatResamplingAudioProcessor.configure(sampleRate, channelCount, C.ENCODING_PCM_24BIT); audioSink.configure(encoding, channelCount, sampleRate, 0, channelMap, encoderDelay, encoderPadding); - } catch (AudioSink.ConfigurationException e) { + } catch (AudioSink.ConfigurationException | AudioProcessor.UnhandledFormatException e) { throw ExoPlaybackException.createForRenderer(e, getIndex()); } } @@ -420,19 +490,35 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media codec.releaseOutputBuffer(bufferIndex, false); return true; } - if (shouldSkip) { codec.releaseOutputBuffer(bufferIndex, false); decoderCounters.skippedOutputBufferCount++; audioSink.handleDiscontinuity(); + resampledBuffer = null; return true; } try { - if (audioSink.handleBuffer(buffer, bufferPresentationTimeUs)) { - codec.releaseOutputBuffer(bufferIndex, false); - decoderCounters.renderedOutputBufferCount++; - return true; + if (floatResamplingAudioProcessor != null) { + boolean draining = resampledBuffer != null; + if (!draining) { + floatResamplingAudioProcessor.queueInput(buffer); + resampledBuffer = floatResamplingAudioProcessor.getOutput(); + } + if (audioSink.handleBuffer(resampledBuffer, bufferPresentationTimeUs)) + resampledBuffer = null; + if (!draining) { + codec.releaseOutputBuffer(bufferIndex, false); + decoderCounters.renderedOutputBufferCount++; + return true; + } + } + else { + if (audioSink.handleBuffer(buffer, bufferPresentationTimeUs)) { + codec.releaseOutputBuffer(bufferIndex, false); + decoderCounters.renderedOutputBufferCount++; + return true; + } } } catch (AudioSink.InitializationException | AudioSink.WriteException e) { throw ExoPlaybackException.createForRenderer(e, getIndex()); From 821ea0e58b94ffa2eb95a7934c173aa44752fe2a Mon Sep 17 00:00:00 2001 From: Drew Hill Date: Sat, 6 Jan 2018 00:26:18 -0500 Subject: [PATCH 1018/2472] moved floatresample into defaultaudiosink and added new constructor in defaultaudiosync to use that resample when audio input is 24/32bit pcm and the new flag is enabled --- .../exoplayer2/audio/DefaultAudioSink.java | 39 ++++++- .../audio/FloatResamplingAudioProcessor.java | 15 ++- .../audio/MediaCodecAudioRenderer.java | 108 ++---------------- 3 files changed, 60 insertions(+), 102 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java index e3bf72c541..ee5ca17bc7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java @@ -164,10 +164,12 @@ public final class DefaultAudioSink implements AudioSink { public static boolean failOnSpuriousAudioTimestamp = false; @Nullable private final AudioCapabilities audioCapabilities; + private final boolean canConvertHiResPcmToFloat; private final ChannelMappingAudioProcessor channelMappingAudioProcessor; private final TrimmingAudioProcessor trimmingAudioProcessor; private final SonicAudioProcessor sonicAudioProcessor; private final AudioProcessor[] availableAudioProcessors; + private final AudioProcessor[] hiResAvailableAudioProcessors; private final ConditionVariable releasingConditionVariable; private final long[] playheadOffsets; private final AudioTrackUtil audioTrackUtil; @@ -180,6 +182,7 @@ public final class DefaultAudioSink implements AudioSink { private AudioTrack keepSessionIdAudioTrack; private AudioTrack audioTrack; private boolean isInputPcm; + private boolean shouldUpResPCMAudio; private int inputSampleRate; private int sampleRate; private int channelConfig; @@ -233,6 +236,8 @@ public final class DefaultAudioSink implements AudioSink { private boolean hasData; private long lastFeedElapsedRealtimeMs; + + /** * @param audioCapabilities The audio capabilities for playback on this device. May be null if the * default capabilities (no encoded audio passthrough support) should be assumed. @@ -241,7 +246,23 @@ public final class DefaultAudioSink implements AudioSink { */ public DefaultAudioSink(@Nullable AudioCapabilities audioCapabilities, AudioProcessor[] audioProcessors) { + this(audioCapabilities, audioProcessors, false); + } + + /** + * @param audioCapabilities The audio capabilities for playback on this device. May be null if the + * default capabilities (no encoded audio passthrough support) should be assumed. + * @param audioProcessors An array of {@link AudioProcessor}s that will process PCM audio before + * output. May be empty. + * @param canConvertHiResPcmToFloat Flag to convert > 16bit PCM Audio to 32bit Float PCM Audio to + * avoid dithering the input audio. If enabled other audio processors that expect 16bit PCM + * are disabled + */ + public DefaultAudioSink(@Nullable AudioCapabilities audioCapabilities, + AudioProcessor[] audioProcessors, boolean canConvertHiResPcmToFloat) { + this.audioCapabilities = audioCapabilities; + this.canConvertHiResPcmToFloat = canConvertHiResPcmToFloat; releasingConditionVariable = new ConditionVariable(true); if (Util.SDK_INT >= 18) { try { @@ -265,6 +286,8 @@ public final class DefaultAudioSink implements AudioSink { availableAudioProcessors[2] = trimmingAudioProcessor; System.arraycopy(audioProcessors, 0, availableAudioProcessors, 3, audioProcessors.length); availableAudioProcessors[3 + audioProcessors.length] = sonicAudioProcessor; + hiResAvailableAudioProcessors = new AudioProcessor[1]; + hiResAvailableAudioProcessors[0] = new FloatResamplingAudioProcessor(); playheadOffsets = new long[MAX_PLAYHEAD_OFFSET_COUNT]; volume = 1.0f; startMediaTimeState = START_NOT_SET; @@ -342,15 +365,20 @@ public final class DefaultAudioSink implements AudioSink { int channelCount = inputChannelCount; int sampleRate = inputSampleRate; isInputPcm = isEncodingPcm(inputEncoding); + shouldUpResPCMAudio = canConvertHiResPcmToFloat && + (inputEncoding == C.ENCODING_PCM_24BIT || inputEncoding == C.ENCODING_PCM_32BIT); if (isInputPcm) { - pcmFrameSize = Util.getPcmFrameSize(inputEncoding, channelCount); + pcmFrameSize = Util.getPcmFrameSize(shouldUpResPCMAudio + ? C.ENCODING_PCM_FLOAT : inputEncoding, channelCount); } @C.Encoding int encoding = inputEncoding; boolean processingEnabled = isInputPcm && inputEncoding != C.ENCODING_PCM_FLOAT; if (processingEnabled) { + AudioProcessor[] activeAudioProcessors = shouldUpResPCMAudio ? + hiResAvailableAudioProcessors : availableAudioProcessors; trimmingAudioProcessor.setTrimSampleCount(trimStartSamples, trimEndSamples); channelMappingAudioProcessor.setChannelMap(outputChannels); - for (AudioProcessor audioProcessor : availableAudioProcessors) { + for (AudioProcessor audioProcessor : activeAudioProcessors) { try { flush |= audioProcessor.configure(sampleRate, channelCount, encoding); } catch (AudioProcessor.UnhandledFormatException e) { @@ -460,7 +488,9 @@ public final class DefaultAudioSink implements AudioSink { private void resetAudioProcessors() { ArrayList newAudioProcessors = new ArrayList<>(); - for (AudioProcessor audioProcessor : availableAudioProcessors) { + AudioProcessor[] activeAudioProcessors = shouldUpResPCMAudio ? + hiResAvailableAudioProcessors : availableAudioProcessors; + for (AudioProcessor audioProcessor : activeAudioProcessors) { if (audioProcessor.isActive()) { newAudioProcessors.add(audioProcessor); } else { @@ -967,6 +997,9 @@ public final class DefaultAudioSink implements AudioSink { for (AudioProcessor audioProcessor : availableAudioProcessors) { audioProcessor.reset(); } + for (AudioProcessor audioProcessor : hiResAvailableAudioProcessors) { + audioProcessor.reset(); + } audioSessionId = C.AUDIO_SESSION_ID_UNSET; playing = false; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java index 28d2eca25f..b0f48d43fa 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java @@ -36,7 +36,7 @@ import java.nio.ByteOrder; @Override public boolean configure(int sampleRateHz, int channelCount, @C.Encoding int encoding) throws AudioProcessor.UnhandledFormatException { - if (encoding != C.ENCODING_PCM_24BIT) { + if (encoding != C.ENCODING_PCM_24BIT && encoding != C.ENCODING_PCM_32BIT) { throw new AudioProcessor.UnhandledFormatException(sampleRateHz, channelCount, encoding); } if (this.sampleRateHz == sampleRateHz && this.channelCount == channelCount @@ -51,7 +51,9 @@ import java.nio.ByteOrder; } @Override - public boolean isActive() { return sourceEncoding == C.ENCODING_PCM_24BIT; } + public boolean isActive() { + return sourceEncoding == C.ENCODING_PCM_24BIT || sourceEncoding == C.ENCODING_PCM_32BIT; + } @Override public int getOutputChannelCount() { return channelCount; } @@ -76,6 +78,8 @@ import java.nio.ByteOrder; resampledSize = (size / 3) * 4; break; case C.ENCODING_PCM_32BIT: + resampledSize = size; + break; case C.ENCODING_PCM_8BIT: case C.ENCODING_PCM_16BIT: case C.ENCODING_PCM_FLOAT: @@ -103,6 +107,13 @@ import java.nio.ByteOrder; } break; case C.ENCODING_PCM_32BIT: + // 32->32 bit conversion. + for (int i = offset; i < limit; i += 4) { + int val = inputBuffer.get(i) & 0x000000ff | (inputBuffer.get(i) << 8) & 0x0000ff00 | + (inputBuffer.get(i + 1) << 16) & 0x00ff0000 | (inputBuffer.get(i + 2) << 24) & 0xff000000; + writePcm32bitFloat(val, buffer); + } + break; case C.ENCODING_PCM_8BIT: case C.ENCODING_PCM_16BIT: case C.ENCODING_PCM_FLOAT: diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index d5e1b6ab03..f73d63616b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java @@ -58,9 +58,6 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media private int encoderPadding; private long currentPositionUs; private boolean allowPositionDiscontinuity; - private final boolean dontDither24bitPCM; - private ByteBuffer resampledBuffer; - private FloatResamplingAudioProcessor floatResamplingAudioProcessor; /** * @param mediaCodecSelector A decoder selector. @@ -140,37 +137,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media @Nullable AudioRendererEventListener eventListener, @Nullable AudioCapabilities audioCapabilities, AudioProcessor... audioProcessors) { this(mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, - eventHandler, eventListener, new DefaultAudioSink(audioCapabilities, audioProcessors), - false); - } - - /** - * @param mediaCodecSelector A decoder selector. - * @param drmSessionManager For use with encrypted content. May be null if support for encrypted - * content is not required. - * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions. - * For example a media file may start with a short clear region so as to allow playback to - * begin in parallel with key acquisition. This parameter specifies whether the renderer is - * permitted to play clear regions of encrypted media files before {@code drmSessionManager} - * has obtained the keys necessary to decrypt encrypted regions of the media. - * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be - * null if delivery of events is not required. - * @param eventListener A listener of events. May be null if delivery of events is not required. - * @param audioCapabilities The audio capabilities for playback on this device. May be null if the - * default capabilities (no encoded audio passthrough support) should be assumed. - * @param dontDither24bitPCM If the input is 24bit PCM audio convert to 32bit Float PCM - * @param audioProcessors Optional {@link AudioProcessor}s that will process PCM audio before - * output. - */ - public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector, - @Nullable DrmSessionManager drmSessionManager, - boolean playClearSamplesWithoutKeys, @Nullable Handler eventHandler, - @Nullable AudioRendererEventListener eventListener, - @Nullable AudioCapabilities audioCapabilities, boolean dontDither24bitPCM, - AudioProcessor... audioProcessors) { - this(mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, - eventHandler, eventListener, new DefaultAudioSink(audioCapabilities, audioProcessors), - dontDither24bitPCM); + eventHandler, eventListener, new DefaultAudioSink(audioCapabilities, audioProcessors)); } /** @@ -191,34 +158,9 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media @Nullable DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, @Nullable Handler eventHandler, @Nullable AudioRendererEventListener eventListener, AudioSink audioSink) { - this(mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, eventHandler, - eventListener, audioSink, false); - } - - /** - * @param mediaCodecSelector A decoder selector. - * @param drmSessionManager For use with encrypted content. May be null if support for encrypted - * content is not required. - * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions. - * For example a media file may start with a short clear region so as to allow playback to - * begin in parallel with key acquisition. This parameter specifies whether the renderer is - * permitted to play clear regions of encrypted media files before {@code drmSessionManager} - * has obtained the keys necessary to decrypt encrypted regions of the media. - * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be - * null if delivery of events is not required. - * @param eventListener A listener of events. May be null if delivery of events is not required. - * @param audioSink The sink to which audio will be output. - * @param dontDither24bitPCM If the input is 24bit PCM audio convert to 32bit Float PCM - */ - public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector, - @Nullable DrmSessionManager drmSessionManager, - boolean playClearSamplesWithoutKeys, @Nullable Handler eventHandler, - @Nullable AudioRendererEventListener eventListener, AudioSink audioSink, - boolean dontDither24bitPCM) { super(C.TRACK_TYPE_AUDIO, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys); eventDispatcher = new EventDispatcher(eventHandler, eventListener); this.audioSink = audioSink; - this.dontDither24bitPCM = dontDither24bitPCM; audioSink.setListener(new AudioSinkListener()); } @@ -326,20 +268,10 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media protected void onInputFormatChanged(Format newFormat) throws ExoPlaybackException { super.onInputFormatChanged(newFormat); eventDispatcher.inputFormatChanged(newFormat); - - // if the input is 24bit pcm audio and we explicitly said not to dither then convert it to float - if (dontDither24bitPCM && newFormat.pcmEncoding == C.ENCODING_PCM_24BIT) { - if (floatResamplingAudioProcessor == null) - floatResamplingAudioProcessor = new FloatResamplingAudioProcessor(); - pcmEncoding = floatResamplingAudioProcessor.getOutputEncoding(); - } else { - // If the input format is anything other than PCM then we assume that the audio decoder will - // output 16-bit PCM. - pcmEncoding = MimeTypes.AUDIO_RAW.equals(newFormat.sampleMimeType) ? newFormat.pcmEncoding - : C.ENCODING_PCM_16BIT; - floatResamplingAudioProcessor = null; - } - + // If the input format is anything other than PCM then we assume that the audio decoder will + // output 16-bit PCM. + pcmEncoding = MimeTypes.AUDIO_RAW.equals(newFormat.sampleMimeType) ? newFormat.pcmEncoding + : C.ENCODING_PCM_16BIT; channelCount = newFormat.channelCount; encoderDelay = newFormat.encoderDelay != Format.NO_VALUE ? newFormat.encoderDelay : 0; encoderPadding = newFormat.encoderPadding != Format.NO_VALUE ? newFormat.encoderPadding : 0; @@ -370,11 +302,9 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } try { - if (floatResamplingAudioProcessor != null) - floatResamplingAudioProcessor.configure(sampleRate, channelCount, C.ENCODING_PCM_24BIT); audioSink.configure(encoding, channelCount, sampleRate, 0, channelMap, encoderDelay, encoderPadding); - } catch (AudioSink.ConfigurationException | AudioProcessor.UnhandledFormatException e) { + } catch (AudioSink.ConfigurationException e) { throw ExoPlaybackException.createForRenderer(e, getIndex()); } } @@ -490,35 +420,19 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media codec.releaseOutputBuffer(bufferIndex, false); return true; } + if (shouldSkip) { codec.releaseOutputBuffer(bufferIndex, false); decoderCounters.skippedOutputBufferCount++; audioSink.handleDiscontinuity(); - resampledBuffer = null; return true; } try { - if (floatResamplingAudioProcessor != null) { - boolean draining = resampledBuffer != null; - if (!draining) { - floatResamplingAudioProcessor.queueInput(buffer); - resampledBuffer = floatResamplingAudioProcessor.getOutput(); - } - if (audioSink.handleBuffer(resampledBuffer, bufferPresentationTimeUs)) - resampledBuffer = null; - if (!draining) { - codec.releaseOutputBuffer(bufferIndex, false); - decoderCounters.renderedOutputBufferCount++; - return true; - } - } - else { - if (audioSink.handleBuffer(buffer, bufferPresentationTimeUs)) { - codec.releaseOutputBuffer(bufferIndex, false); - decoderCounters.renderedOutputBufferCount++; - return true; - } + if (audioSink.handleBuffer(buffer, bufferPresentationTimeUs)) { + codec.releaseOutputBuffer(bufferIndex, false); + decoderCounters.renderedOutputBufferCount++; + return true; } } catch (AudioSink.InitializationException | AudioSink.WriteException e) { throw ExoPlaybackException.createForRenderer(e, getIndex()); From aaf469ce065205593e99cfbd655e6f57b7763d62 Mon Sep 17 00:00:00 2001 From: Drew Hill Date: Sun, 14 Jan 2018 10:55:54 -0500 Subject: [PATCH 1019/2472] code review changes and fix for discontinuity --- .../exoplayer2/audio/DefaultAudioSink.java | 43 ++++++++++--------- .../audio/FloatResamplingAudioProcessor.java | 20 +++++++-- 2 files changed, 38 insertions(+), 25 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java index ee5ca17bc7..892fd428ff 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java @@ -168,8 +168,8 @@ public final class DefaultAudioSink implements AudioSink { private final ChannelMappingAudioProcessor channelMappingAudioProcessor; private final TrimmingAudioProcessor trimmingAudioProcessor; private final SonicAudioProcessor sonicAudioProcessor; - private final AudioProcessor[] availableAudioProcessors; - private final AudioProcessor[] hiResAvailableAudioProcessors; + private final AudioProcessor[] toIntPcmAvailableAudioProcessors; + private final AudioProcessor[] toFloatPcmAvailableAudioProcessors; private final ConditionVariable releasingConditionVariable; private final long[] playheadOffsets; private final AudioTrackUtil audioTrackUtil; @@ -189,6 +189,7 @@ public final class DefaultAudioSink implements AudioSink { private @C.Encoding int outputEncoding; private AudioAttributes audioAttributes; private boolean processingEnabled; + private boolean canApplyPlaybackParams; private int bufferSize; private long bufferSizeUs; @@ -280,14 +281,14 @@ public final class DefaultAudioSink implements AudioSink { channelMappingAudioProcessor = new ChannelMappingAudioProcessor(); trimmingAudioProcessor = new TrimmingAudioProcessor(); sonicAudioProcessor = new SonicAudioProcessor(); - availableAudioProcessors = new AudioProcessor[4 + audioProcessors.length]; - availableAudioProcessors[0] = new ResamplingAudioProcessor(); - availableAudioProcessors[1] = channelMappingAudioProcessor; - availableAudioProcessors[2] = trimmingAudioProcessor; - System.arraycopy(audioProcessors, 0, availableAudioProcessors, 3, audioProcessors.length); - availableAudioProcessors[3 + audioProcessors.length] = sonicAudioProcessor; - hiResAvailableAudioProcessors = new AudioProcessor[1]; - hiResAvailableAudioProcessors[0] = new FloatResamplingAudioProcessor(); + toIntPcmAvailableAudioProcessors = new AudioProcessor[4 + audioProcessors.length]; + toIntPcmAvailableAudioProcessors[0] = new ResamplingAudioProcessor(); + toIntPcmAvailableAudioProcessors[1] = channelMappingAudioProcessor; + toIntPcmAvailableAudioProcessors[2] = trimmingAudioProcessor; + System.arraycopy(audioProcessors, 0, toIntPcmAvailableAudioProcessors, 3, audioProcessors.length); + toIntPcmAvailableAudioProcessors[3 + audioProcessors.length] = sonicAudioProcessor; + toFloatPcmAvailableAudioProcessors = new AudioProcessor[1]; + toFloatPcmAvailableAudioProcessors[0] = new FloatResamplingAudioProcessor(); playheadOffsets = new long[MAX_PLAYHEAD_OFFSET_COUNT]; volume = 1.0f; startMediaTimeState = START_NOT_SET; @@ -368,17 +369,17 @@ public final class DefaultAudioSink implements AudioSink { shouldUpResPCMAudio = canConvertHiResPcmToFloat && (inputEncoding == C.ENCODING_PCM_24BIT || inputEncoding == C.ENCODING_PCM_32BIT); if (isInputPcm) { - pcmFrameSize = Util.getPcmFrameSize(shouldUpResPCMAudio - ? C.ENCODING_PCM_FLOAT : inputEncoding, channelCount); + pcmFrameSize = Util.getPcmFrameSize(inputEncoding, channelCount); } @C.Encoding int encoding = inputEncoding; boolean processingEnabled = isInputPcm && inputEncoding != C.ENCODING_PCM_FLOAT; + canApplyPlaybackParams = processingEnabled && !shouldUpResPCMAudio; if (processingEnabled) { - AudioProcessor[] activeAudioProcessors = shouldUpResPCMAudio ? - hiResAvailableAudioProcessors : availableAudioProcessors; + AudioProcessor[] availableAudioProcessors = shouldUpResPCMAudio ? + toFloatPcmAvailableAudioProcessors : toIntPcmAvailableAudioProcessors; trimmingAudioProcessor.setTrimSampleCount(trimStartSamples, trimEndSamples); channelMappingAudioProcessor.setChannelMap(outputChannels); - for (AudioProcessor audioProcessor : activeAudioProcessors) { + for (AudioProcessor audioProcessor : availableAudioProcessors) { try { flush |= audioProcessor.configure(sampleRate, channelCount, encoding); } catch (AudioProcessor.UnhandledFormatException e) { @@ -488,9 +489,9 @@ public final class DefaultAudioSink implements AudioSink { private void resetAudioProcessors() { ArrayList newAudioProcessors = new ArrayList<>(); - AudioProcessor[] activeAudioProcessors = shouldUpResPCMAudio ? - hiResAvailableAudioProcessors : availableAudioProcessors; - for (AudioProcessor audioProcessor : activeAudioProcessors) { + AudioProcessor[] availableAudioProcessors = shouldUpResPCMAudio ? + toFloatPcmAvailableAudioProcessors : toIntPcmAvailableAudioProcessors; + for (AudioProcessor audioProcessor : availableAudioProcessors) { if (audioProcessor.isActive()) { newAudioProcessors.add(audioProcessor); } else { @@ -838,7 +839,7 @@ public final class DefaultAudioSink implements AudioSink { @Override public PlaybackParameters setPlaybackParameters(PlaybackParameters playbackParameters) { - if (isInitialized() && !processingEnabled) { + if (isInitialized() && !canApplyPlaybackParams) { // The playback parameters are always the default if processing is disabled. this.playbackParameters = PlaybackParameters.DEFAULT; return this.playbackParameters; @@ -994,10 +995,10 @@ public final class DefaultAudioSink implements AudioSink { public void release() { reset(); releaseKeepSessionIdAudioTrack(); - for (AudioProcessor audioProcessor : availableAudioProcessors) { + for (AudioProcessor audioProcessor : toIntPcmAvailableAudioProcessors) { audioProcessor.reset(); } - for (AudioProcessor audioProcessor : hiResAvailableAudioProcessors) { + for (AudioProcessor audioProcessor : toFloatPcmAvailableAudioProcessors) { audioProcessor.reset(); } audioSessionId = C.AUDIO_SESSION_ID_UNSET; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java index b0f48d43fa..f7073f1275 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2018 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; @@ -173,10 +188,7 @@ import java.nio.ByteOrder; int bits = Float.floatToIntBits(convVal); if (bits == 0x7fc00000) bits = Float.floatToIntBits((float) 0.0); - buffer.put((byte) (bits & 0xff)); - buffer.put((byte) ((bits >> 8) & 0xff)); - buffer.put((byte) ((bits >> 16) & 0xff)); - buffer.put((byte) ((bits >> 24) & 0xff)); + buffer.putInt(bits); } } \ No newline at end of file From 373935aeb66c5e19275fa9c42409913aa04fafc2 Mon Sep 17 00:00:00 2001 From: eguven Date: Fri, 5 Jan 2018 01:22:41 -0800 Subject: [PATCH 1020/2472] Make CacheDataSource detect cache availability change In certain conditions CacheDataSource switch to reading from upstream without writing back to cache. This change makes it detect the change of these conditions and switch to reading from or writing to cache. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=180901463 --- RELEASENOTES.md | 2 + .../upstream/cache/CacheDataSource.java | 117 +++++++++------- .../exoplayer2/upstream/cache/CacheUtil.java | 22 ++- .../upstream/cache/CacheDataSourceTest.java | 132 ++++++++++++++++-- 4 files changed, 201 insertions(+), 72 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 4679a0b376..e167fe94b6 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -41,6 +41,8 @@ * New Cast extension: Simplifies toggling between local and Cast playbacks. * Audio: Support TrueHD passthrough for rechunked samples in Matroska files ([#2147](https://github.com/google/ExoPlayer/issues/2147)). +* CacheDataSource: Check periodically if it's possible to read from/write to + cache after deciding to bypass cache. ### 2.6.1 ### diff --git a/library/core/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 index 5eea140a8b..2b151943a5 100644 --- a/library/core/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 @@ -86,6 +86,9 @@ public final class CacheDataSource implements DataSource { } + /** Minimum number of bytes to read before checking cache for availability. */ + private static final long MIN_READ_BEFORE_CHECKING_CACHE = 100 * 1024; + private final Cache cache; private final DataSource cacheReadDataSource; private final DataSource cacheWriteDataSource; @@ -97,16 +100,17 @@ public final class CacheDataSource implements DataSource { private final boolean ignoreCacheForUnsetLengthRequests; private DataSource currentDataSource; - private boolean readingUnknownLengthDataFromUpstream; + private boolean currentDataSpecLengthUnset; private Uri uri; private int flags; private String key; private long readPosition; private long bytesRemaining; - private CacheSpan lockedSpan; + private CacheSpan currentHoleSpan; private boolean seenCacheError; private boolean currentRequestIgnoresCache; private long totalCachedBytesRead; + private long checkCachePosition; /** * Constructs an instance with default {@link DataSource} and {@link DataSink} instances for @@ -219,8 +223,11 @@ public final class CacheDataSource implements DataSource { return C.RESULT_END_OF_INPUT; } try { + if (readPosition >= checkCachePosition) { + openNextSource(); + } int bytesRead = currentDataSource.read(buffer, offset, readLength); - if (bytesRead >= 0) { + if (bytesRead != C.RESULT_END_OF_INPUT) { if (currentDataSource == cacheReadDataSource) { totalCachedBytesRead += bytesRead; } @@ -228,28 +235,18 @@ public final class CacheDataSource implements DataSource { if (bytesRemaining != C.LENGTH_UNSET) { bytesRemaining -= bytesRead; } - } else { - if (readingUnknownLengthDataFromUpstream) { - setCurrentDataSourceBytesRemaining(0); - } - closeCurrentSource(); - if (bytesRemaining > 0 || bytesRemaining == C.LENGTH_UNSET) { - try { - openNextSource(); - } catch (IOException e) { - if (readingUnknownLengthDataFromUpstream && isCausedByPositionOutOfRange(e)) { - setCurrentDataSourceBytesRemaining(0); - } else { - throw e; - } - } - if (bytesRemaining != 0) { - return read(buffer, offset, readLength); - } - } + } else if (currentDataSpecLengthUnset) { + setBytesRemaining(0); + } else if (bytesRemaining > 0 || bytesRemaining == C.LENGTH_UNSET) { + openNextSource(); + return read(buffer, offset, readLength); } return bytesRead; } catch (IOException e) { + if (currentDataSpecLengthUnset && isCausedByPositionOutOfRange(e)) { + setBytesRemaining(0); + return C.RESULT_END_OF_INPUT; + } handleBeforeThrow(e); throw e; } @@ -278,62 +275,76 @@ public final class CacheDataSource implements DataSource { * opened to read from the upstream source and write into the cache. */ private void openNextSource() throws IOException { - DataSpec dataSpec; - CacheSpan span; + CacheSpan nextSpan; if (currentRequestIgnoresCache) { - span = null; + nextSpan = null; } else if (blockOnCache) { try { - span = cache.startReadWrite(key, readPosition); + nextSpan = cache.startReadWrite(key, readPosition); } catch (InterruptedException e) { throw new InterruptedIOException(); } } else { - span = cache.startReadWriteNonBlocking(key, readPosition); + nextSpan = cache.startReadWriteNonBlocking(key, readPosition); } - if (span == null) { + DataSpec nextDataSpec; + DataSource nextDataSource; + if (nextSpan == null) { // The data is locked in the cache, or we're ignoring the cache. Bypass the cache and read // from upstream. - currentDataSource = upstreamDataSource; - dataSpec = new DataSpec(uri, readPosition, bytesRemaining, key, flags); - } else if (span.isCached) { + nextDataSource = upstreamDataSource; + nextDataSpec = new DataSpec(uri, readPosition, bytesRemaining, key, flags); + } else if (nextSpan.isCached) { // Data is cached, read from cache. - Uri fileUri = Uri.fromFile(span.file); - long filePosition = readPosition - span.position; - long length = span.length - filePosition; + Uri fileUri = Uri.fromFile(nextSpan.file); + long filePosition = readPosition - nextSpan.position; + long length = nextSpan.length - filePosition; if (bytesRemaining != C.LENGTH_UNSET) { length = Math.min(length, bytesRemaining); } - dataSpec = new DataSpec(fileUri, readPosition, filePosition, length, key, flags); - currentDataSource = cacheReadDataSource; + nextDataSpec = new DataSpec(fileUri, readPosition, filePosition, length, key, flags); + nextDataSource = cacheReadDataSource; } else { // Data is not cached, and data is not locked, read from upstream with cache backing. long length; - if (span.isOpenEnded()) { + if (nextSpan.isOpenEnded()) { length = bytesRemaining; } else { - length = span.length; + length = nextSpan.length; if (bytesRemaining != C.LENGTH_UNSET) { length = Math.min(length, bytesRemaining); } } - dataSpec = new DataSpec(uri, readPosition, length, key, flags); + nextDataSpec = new DataSpec(uri, readPosition, length, key, flags); if (cacheWriteDataSource != null) { - currentDataSource = cacheWriteDataSource; - lockedSpan = span; + nextDataSource = cacheWriteDataSource; } else { - currentDataSource = upstreamDataSource; - cache.releaseHoleSpan(span); + nextDataSource = upstreamDataSource; + cache.releaseHoleSpan(nextSpan); + nextSpan = null; } } - // If the request is unbounded it must be an upstream request. - readingUnknownLengthDataFromUpstream = dataSpec.length == C.LENGTH_UNSET; + if (nextDataSource == upstreamDataSource) { + checkCachePosition = readPosition + MIN_READ_BEFORE_CHECKING_CACHE; + if (currentDataSource == upstreamDataSource) { + return; + } + } else { + checkCachePosition = Long.MAX_VALUE; + } + closeCurrentSource(); - long resolvedLength = currentDataSource.open(dataSpec); - if (readingUnknownLengthDataFromUpstream && resolvedLength != C.LENGTH_UNSET) { - setCurrentDataSourceBytesRemaining(resolvedLength); + if (nextSpan != null && nextSpan.isHoleSpan()) { + currentHoleSpan = nextSpan; + } + currentDataSource = nextDataSource; + currentDataSpecLengthUnset = nextDataSpec.length == C.LENGTH_UNSET; + + long resolvedLength = nextDataSource.open(nextDataSpec); + if (currentDataSpecLengthUnset && resolvedLength != C.LENGTH_UNSET) { + setBytesRemaining(resolvedLength); } } @@ -351,7 +362,7 @@ public final class CacheDataSource implements DataSource { return false; } - private void setCurrentDataSourceBytesRemaining(long bytesRemaining) throws IOException { + private void setBytesRemaining(long bytesRemaining) throws IOException { this.bytesRemaining = bytesRemaining; if (isWritingToCache()) { cache.setContentLength(key, readPosition + bytesRemaining); @@ -369,11 +380,11 @@ public final class CacheDataSource implements DataSource { try { currentDataSource.close(); currentDataSource = null; - readingUnknownLengthDataFromUpstream = false; + currentDataSpecLengthUnset = false; } finally { - if (lockedSpan != null) { - cache.releaseHoleSpan(lockedSpan); - lockedSpan = null; + if (currentHoleSpan != null) { + cache.releaseHoleSpan(currentHoleSpan); + currentHoleSpan = null; } } } 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 index cf2dedbe54..c612ea3739 100644 --- 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 @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.upstream.cache; import android.net.Uri; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; @@ -110,12 +111,13 @@ public final class CacheUtil { * @param dataSpec Defines the data to be cached. * @param cache A {@link Cache} to store the data. * @param upstream A {@link DataSource} for reading data not in the cache. - * @param counters Counters to update during caching. + * @param counters If not null, updated during caching. * @throws IOException If an error occurs reading from the source. * @throws InterruptedException If the thread was interrupted. */ - public static void cache(DataSpec dataSpec, Cache cache, DataSource upstream, - CachingCounters counters) throws IOException, InterruptedException { + public static void cache( + DataSpec dataSpec, Cache cache, DataSource upstream, @Nullable CachingCounters counters) + throws IOException, InterruptedException { cache(dataSpec, cache, new CacheDataSource(cache, upstream), new byte[DEFAULT_BUFFER_SIZE_BYTES], null, 0, counters, false); } @@ -131,15 +133,21 @@ public final class CacheUtil { * @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 Counters to update during caching. + * @param counters If not null, updated during caching. * @param enableEOFException Whether to throw an {@link EOFException} if end of input has been * reached unexpectedly. * @throws IOException If an error occurs reading from the source. * @throws InterruptedException If the thread was interrupted. */ - public static void cache(DataSpec dataSpec, Cache cache, CacheDataSource dataSource, - byte[] buffer, PriorityTaskManager priorityTaskManager, int priority, - CachingCounters counters, boolean enableEOFException) + public static void cache( + DataSpec dataSpec, + Cache cache, + CacheDataSource dataSource, + byte[] buffer, + PriorityTaskManager priorityTaskManager, + int priority, + @Nullable CachingCounters counters, + boolean enableEOFException) throws IOException, InterruptedException { Assertions.checkNotNull(dataSource); Assertions.checkNotNull(buffer); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java index e92f072dc2..4a2ca8c535 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.upstream.cache; -import static android.net.Uri.EMPTY; import static com.google.android.exoplayer2.C.LENGTH_UNSET; import static com.google.android.exoplayer2.upstream.cache.CacheAsserts.assertCacheEmpty; import static com.google.common.truth.Truth.assertThat; @@ -51,14 +50,16 @@ public final class CacheDataSourceTest { private static final byte[] TEST_DATA = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; private static final int MAX_CACHE_FILE_SIZE = 3; - private static final String KEY_1 = "key 1"; - private static final String KEY_2 = "key 2"; + private Uri testDataUri; + private String testDataKey; private File tempFolder; private SimpleCache cache; @Before public void setUp() throws Exception { + testDataUri = Uri.parse("test_data"); + testDataKey = CacheUtil.generateKey(testDataUri); tempFolder = Util.createTempDirectory(RuntimeEnvironment.application, "ExoPlayerTest"); cache = new SimpleCache(tempFolder, new NoOpCacheEvictor()); } @@ -116,7 +117,7 @@ public final class CacheDataSourceTest { // If the user try to access off range then it should throw an IOException try { cacheDataSource = createCacheDataSource(false, false); - cacheDataSource.open(new DataSpec(Uri.EMPTY, TEST_DATA.length, 5, KEY_1)); + cacheDataSource.open(new DataSpec(testDataUri, TEST_DATA.length, 5, testDataKey)); fail(); } catch (IOException e) { // success @@ -128,7 +129,7 @@ public final class CacheDataSourceTest { // Read partial at EOS but don't cross it so length is unknown CacheDataSource cacheDataSource = createCacheDataSource(false, true); assertReadData(cacheDataSource, true, TEST_DATA.length - 2, 2); - assertThat(cache.getContentLength(KEY_1)).isEqualTo(LENGTH_UNSET); + assertThat(cache.getContentLength(testDataKey)).isEqualTo(LENGTH_UNSET); // Now do an unbounded request for whole data. This will cause a bounded request from upstream. // End of data from upstream shouldn't be mixed up with EOS and cause length set wrong. @@ -136,12 +137,16 @@ public final class CacheDataSourceTest { assertReadDataContentLength(cacheDataSource, true, true); // Now the length set correctly do an unbounded request with offset - assertThat(cacheDataSource.open(new DataSpec(EMPTY, TEST_DATA.length - 2, - LENGTH_UNSET, KEY_1))).isEqualTo(2); + assertThat( + cacheDataSource.open( + new DataSpec(testDataUri, TEST_DATA.length - 2, LENGTH_UNSET, testDataKey))) + .isEqualTo(2); // An unbounded request with offset for not cached content - assertThat(cacheDataSource.open(new DataSpec(EMPTY, TEST_DATA.length - 2, - LENGTH_UNSET, KEY_2))).isEqualTo(LENGTH_UNSET); + assertThat( + cacheDataSource.open( + new DataSpec(Uri.parse("notCachedUri"), TEST_DATA.length - 2, LENGTH_UNSET, null))) + .isEqualTo(LENGTH_UNSET); } @Test @@ -159,6 +164,107 @@ public final class CacheDataSourceTest { assertCacheEmpty(cache); } + @Test + public void testSwitchToCacheSourceWithReadOnlyCacheDataSource() throws Exception { + // Create a fake data source with a 1 MB default data. + FakeDataSource upstream = new FakeDataSource(); + FakeData fakeData = upstream.getDataSet().newDefaultData().appendReadData(1024 * 1024 - 1); + // Insert an action just before the end of the data to fail the test if reading from upstream + // reaches end of the data. + fakeData + .appendReadAction( + new Runnable() { + @Override + public void run() { + fail("Read from upstream shouldn't reach to the end of the data."); + } + }) + .appendReadData(1); + // Create cache read-only CacheDataSource. + CacheDataSource cacheDataSource = + new CacheDataSource(cache, upstream, new FileDataSource(), null, 0, null); + + // Open source and read some data from upstream as the data hasn't cached yet. + DataSpec dataSpec = new DataSpec(testDataUri, 0, C.LENGTH_UNSET, testDataKey); + cacheDataSource.open(dataSpec); + byte[] buffer = new byte[1024]; + cacheDataSource.read(buffer, 0, buffer.length); + + // Cache the data. + // Although we use another FakeDataSource instance, it shouldn't matter. + FakeDataSource upstream2 = + new FakeDataSource( + new FakeDataSource() + .getDataSet() + .newDefaultData() + .appendReadData(1024 * 1024) + .endData()); + CacheUtil.cache(dataSpec, cache, upstream2, null); + + // Read the rest of the data. + while (true) { + if (cacheDataSource.read(buffer, 0, buffer.length) == C.RESULT_END_OF_INPUT) { + break; + } + } + cacheDataSource.close(); + } + + @Test + public void testSwitchToCacheSourceWithNonBlockingCacheDataSource() throws Exception { + // Create a fake data source with a 1 MB default data. + FakeDataSource upstream = new FakeDataSource(); + FakeData fakeData = upstream.getDataSet().newDefaultData().appendReadData(1024 * 1024 - 1); + // Insert an action just before the end of the data to fail the test if reading from upstream + // reaches end of the data. + fakeData + .appendReadAction( + new Runnable() { + @Override + public void run() { + fail("Read from upstream shouldn't reach to the end of the data."); + } + }) + .appendReadData(1); + + // Lock the content on the cache. + SimpleCacheSpan cacheSpan = cache.startReadWriteNonBlocking(testDataKey, 0); + assertThat(cacheSpan).isNotNull(); + assertThat(cacheSpan.isHoleSpan()).isTrue(); + + // Create non blocking CacheDataSource. + CacheDataSource cacheDataSource = new CacheDataSource(cache, upstream, 0); + + // Open source and read some data from upstream without writing to cache as the data is locked. + DataSpec dataSpec = new DataSpec(testDataUri, 0, C.LENGTH_UNSET, testDataKey); + cacheDataSource.open(dataSpec); + byte[] buffer = new byte[1024]; + cacheDataSource.read(buffer, 0, buffer.length); + + // Unlock the span. + cache.releaseHoleSpan(cacheSpan); + assertCacheEmpty(cache); + + // Cache the data. + // Although we use another FakeDataSource instance, it shouldn't matter. + FakeDataSource upstream2 = + new FakeDataSource( + new FakeDataSource() + .getDataSet() + .newDefaultData() + .appendReadData(1024 * 1024) + .endData()); + CacheUtil.cache(dataSpec, cache, upstream2, null); + + // Read the rest of the data. + while (true) { + if (cacheDataSource.read(buffer, 0, buffer.length) == C.RESULT_END_OF_INPUT) { + break; + } + } + cacheDataSource.close(); + } + private void assertCacheAndRead(boolean unboundedRequest, boolean simulateUnknownLength) throws IOException { // Read all data from upstream and write to cache @@ -179,8 +285,10 @@ public final class CacheDataSourceTest { boolean unboundedRequest, boolean unknownLength) throws IOException { int length = unboundedRequest ? C.LENGTH_UNSET : TEST_DATA.length; assertReadData(cacheDataSource, unknownLength, 0, length); - assertWithMessage("When the range specified, CacheDataSource doesn't reach EOS so shouldn't " - + "cache content length").that(cache.getContentLength(KEY_1)) + assertWithMessage( + "When the range specified, CacheDataSource doesn't reach EOS so shouldn't " + + "cache content length") + .that(cache.getContentLength(testDataKey)) .isEqualTo(!unboundedRequest ? C.LENGTH_UNSET : TEST_DATA.length); } @@ -190,7 +298,7 @@ public final class CacheDataSourceTest { if (length != C.LENGTH_UNSET) { testDataLength = Math.min(testDataLength, length); } - assertThat(cacheDataSource.open(new DataSpec(EMPTY, position, length, KEY_1))) + assertThat(cacheDataSource.open(new DataSpec(testDataUri, position, length, testDataKey))) .isEqualTo(unknownLength ? length : testDataLength); byte[] buffer = new byte[100]; From 5364962dca36136d0765aefb885af61015fb503e Mon Sep 17 00:00:00 2001 From: hoangtc Date: Fri, 5 Jan 2018 07:35:28 -0800 Subject: [PATCH 1021/2472] Automatically apply rotation for TextureView in SimpleExoPlayer. If SimpleExoPlayer is using TextView as output, we can handle video rotation by automatically applying a matrix transformation to the TextureView when we have this information available from the video (from video's metadata). GitHub: #91 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=180925571 --- RELEASENOTES.md | 3 + .../exoplayer2/ui/SimpleExoPlayerView.java | 74 ++++++++++++++++++- 2 files changed, 73 insertions(+), 4 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index e167fe94b6..997c3047c4 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,6 +2,9 @@ ### dev-v2 (not yet released) ### +* SimpleExoPlayerView: Automatically apply video rotation if + `SimpleExoPlayerView` is configured to use `TextureView` + ([#91](https://github.com/google/ExoPlayer/issues/91)). * Player interface: * Add optional parameter to `stop` to reset the player when stopping. * Add a reason to `EventListener.onTimelineChanged` to distinguish between diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index def8925ec3..6e69a31fd9 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -22,6 +22,8 @@ import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.BitmapFactory; +import android.graphics.Matrix; +import android.graphics.RectF; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.AttributeSet; @@ -224,6 +226,7 @@ public final class SimpleExoPlayerView extends FrameLayout { private boolean controllerAutoShow; private boolean controllerHideDuringAds; private boolean controllerHideOnTouch; + private int textureViewRotation; public SimpleExoPlayerView(Context context) { this(context, null); @@ -920,6 +923,31 @@ public final class SimpleExoPlayerView extends FrameLayout { aspectRatioFrame.setResizeMode(resizeMode); } + /** Applies a texture rotation to a {@link TextureView}. */ + private static void applyTextureViewRotation(TextureView textureView, int textureViewRotation) { + float textureViewWidth = textureView.getWidth(); + float textureViewHeight = textureView.getHeight(); + if (textureViewWidth == 0 || textureViewHeight == 0 || textureViewRotation == 0) { + textureView.setTransform(null); + } else { + Matrix transformMatrix = new Matrix(); + float pivotX = textureViewWidth / 2; + float pivotY = textureViewHeight / 2; + transformMatrix.postRotate(textureViewRotation, pivotX, pivotY); + + // After rotation, scale the rotated texture to fit the TextureView size. + RectF originalTextureRect = new RectF(0, 0, textureViewWidth, textureViewHeight); + RectF rotatedTextureRect = new RectF(); + transformMatrix.mapRect(rotatedTextureRect, originalTextureRect); + transformMatrix.postScale( + textureViewWidth / rotatedTextureRect.width(), + textureViewHeight / rotatedTextureRect.height(), + pivotX, + pivotY); + textureView.setTransform(transformMatrix); + } + } + @SuppressLint("InlinedApi") private boolean isDpadKey(int keyCode) { return keyCode == KeyEvent.KEYCODE_DPAD_UP @@ -934,7 +962,7 @@ public final class SimpleExoPlayerView extends FrameLayout { } private final class ComponentListener extends Player.DefaultEventListener - implements TextOutput, SimpleExoPlayer.VideoListener { + implements TextOutput, SimpleExoPlayer.VideoListener, OnLayoutChangeListener { // TextOutput implementation @@ -950,10 +978,32 @@ public final class SimpleExoPlayerView extends FrameLayout { @Override public void onVideoSizeChanged( int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { - if (contentFrame != null) { - float aspectRatio = height == 0 ? 1 : (width * pixelWidthHeightRatio) / height; - contentFrame.setAspectRatio(aspectRatio); + if (contentFrame == null) { + return; } + float videoAspectRatio = + (height == 0 || width == 0) ? 1 : (width * pixelWidthHeightRatio) / height; + + if (surfaceView instanceof TextureView) { + // Try to apply rotation transformation when our surface is a TextureView. + if (unappliedRotationDegrees == 90 || unappliedRotationDegrees == 270) { + // We will apply a rotation 90/270 degree to the output texture of the TextureView. + // In this case, the output video's width and height will be swapped. + videoAspectRatio = 1 / videoAspectRatio; + } + if (textureViewRotation != 0) { + surfaceView.removeOnLayoutChangeListener(this); + } + textureViewRotation = unappliedRotationDegrees; + if (textureViewRotation != 0) { + // The texture view's dimensions might be changed after layout step. + // So add an OnLayoutChangeListener to apply rotation after layout step. + surfaceView.addOnLayoutChangeListener(this); + } + applyTextureViewRotation((TextureView) surfaceView, textureViewRotation); + } + + contentFrame.setAspectRatio(videoAspectRatio); } @Override @@ -985,5 +1035,21 @@ public final class SimpleExoPlayerView extends FrameLayout { hideController(); } } + + // OnLayoutChangeListener implementation + + @Override + public void onLayoutChange( + View view, + int left, + int top, + int right, + int bottom, + int oldLeft, + int oldTop, + int oldRight, + int oldBottom) { + applyTextureViewRotation((TextureView) view, textureViewRotation); + } } } From 4b018b4d19c6dc658ba1eaaf72ab9610224a3687 Mon Sep 17 00:00:00 2001 From: eguven Date: Fri, 5 Jan 2018 08:14:43 -0800 Subject: [PATCH 1022/2472] Document how unset length request are cached ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=180929422 --- .../com/google/android/exoplayer2/upstream/DataSpec.java | 3 ++- .../android/exoplayer2/upstream/cache/CacheDataSink.java | 5 +++++ .../android/exoplayer2/upstream/cache/CacheDataSource.java | 7 ++++++- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java index cbe971bc5d..a6b89a334d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java @@ -49,7 +49,8 @@ public final class DataSpec { public static final int FLAG_ALLOW_GZIP = 1 << 0; /** - * Permits content to be cached even if its length can not be resolved. + * Permits content to be cached even if its length can not be resolved. Typically this's the case + * for progressive live streams and when {@link #FLAG_ALLOW_GZIP} is used. */ public static final int FLAG_ALLOW_CACHING_UNKNOWN_LENGTH = 1 << 1; diff --git a/library/core/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 index 33b1ca58b0..1af690e10f 100644 --- a/library/core/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 @@ -29,6 +29,11 @@ import java.io.OutputStream; /** * Writes data into a cache. + * + *

          If the {@link DataSpec} object used with {@link #open(DataSpec)} method call has the {@code + * length} field set to {@link C#LENGTH_UNSET} but {@link + * DataSpec#FLAG_ALLOW_CACHING_UNKNOWN_LENGTH} isn't set then {@link #write(byte[], int, int)} calls + * are ignored. */ public final class CacheDataSink implements DataSink { diff --git a/library/core/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 index 2b151943a5..f1d50a43e4 100644 --- a/library/core/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 @@ -35,6 +35,10 @@ import java.lang.annotation.RetentionPolicy; * A {@link DataSource} that reads and writes a {@link Cache}. Requests are fulfilled from the cache * when possible. When data is not cached it is requested from an upstream {@link DataSource} and * written into the cache. + * + *

          By default requests whose length can not be resolved are not cached. This is to prevent + * caching of progressive live streams, which should usually not be cached. Caching of this kind of + * requests can be enabled per request with {@link DataSpec#FLAG_ALLOW_CACHING_UNKNOWN_LENGTH}. */ public final class CacheDataSource implements DataSource { @@ -67,7 +71,8 @@ public final class CacheDataSource implements DataSource { public static final int FLAG_IGNORE_CACHE_ON_ERROR = 1 << 1; /** - * A flag indicating that the cache should be bypassed for requests whose lengths are unset. + * A flag indicating that the cache should be bypassed for requests whose lengths are unset. This + * flag is provided for legacy reasons only. */ public static final int FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS = 1 << 2; From 4867748c501412668572b1518e09a4c10792e244 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 5 Jan 2018 09:03:48 -0800 Subject: [PATCH 1023/2472] Add assertions to check that media sources are not prepared twice. This lets apps fail-fast when they try to reuse media source instances. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=180934445 --- .../google/android/exoplayer2/source/ClippingMediaSource.java | 3 ++- .../android/exoplayer2/source/ConcatenatingMediaSource.java | 1 + .../exoplayer2/source/DynamicConcatenatingMediaSource.java | 1 + .../android/exoplayer2/source/ExtractorMediaSource.java | 3 ++- .../google/android/exoplayer2/source/LoopingMediaSource.java | 3 +++ .../com/google/android/exoplayer2/source/MediaSource.java | 2 ++ .../google/android/exoplayer2/source/MergingMediaSource.java | 2 ++ .../android/exoplayer2/source/SingleSampleMediaSource.java | 4 ++++ .../google/android/exoplayer2/source/ads/AdsMediaSource.java | 1 + .../android/exoplayer2/source/dash/DashMediaSource.java | 1 + .../google/android/exoplayer2/source/hls/HlsMediaSource.java | 3 +-- .../exoplayer2/source/smoothstreaming/SsMediaSource.java | 2 +- 12 files changed, 21 insertions(+), 5 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java index 721950f6b9..42b1cfd1cf 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java @@ -131,7 +131,8 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste @Override public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { - this.sourceListener = listener; + Assertions.checkState(sourceListener == null, MEDIA_SOURCE_REUSED_ERROR_MESSAGE); + sourceListener = listener; mediaSource.prepareSource(player, false, this); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java index 058471f31f..2b089cfdbe 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java @@ -89,6 +89,7 @@ public final class ConcatenatingMediaSource implements MediaSource { @Override public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { + Assertions.checkState(this.listener == null, MEDIA_SOURCE_REUSED_ERROR_MESSAGE); this.listener = listener; if (mediaSources.length == 0) { listener.onSourceInfoRefreshed(this, Timeline.EMPTY, null); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java index c2e208afbe..5dd4f004fa 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DynamicConcatenatingMediaSource.java @@ -331,6 +331,7 @@ public final class DynamicConcatenatingMediaSource implements MediaSource, Playe @Override public synchronized void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { + Assertions.checkState(this.listener == null, MEDIA_SOURCE_REUSED_ERROR_MESSAGE); this.player = player; this.listener = listener; preventListenerNotification = true; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java index 14453653af..d7dff5a278 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java @@ -325,6 +325,7 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe @Override public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { + Assertions.checkState(sourceListener == null, MEDIA_SOURCE_REUSED_ERROR_MESSAGE); sourceListener = listener; notifySourceInfoRefreshed(C.TIME_UNSET, false); } @@ -356,7 +357,7 @@ public final class ExtractorMediaSource implements MediaSource, ExtractorMediaPe @Override public void releaseSource() { - sourceListener = null; + // Do nothing. } // ExtractorMediaPeriod.Listener implementation. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java index 984820cc6a..5cf110c9ac 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java @@ -36,6 +36,7 @@ public final class LoopingMediaSource implements MediaSource { private final int loopCount; private int childPeriodCount; + private boolean wasPrepareSourceCalled; /** * Loops the provided source indefinitely. Note that it is usually better to use @@ -61,6 +62,8 @@ public final class LoopingMediaSource implements MediaSource { @Override public void prepareSource(ExoPlayer player, boolean isTopLevelSource, final Listener listener) { + Assertions.checkState(!wasPrepareSourceCalled, MEDIA_SOURCE_REUSED_ERROR_MESSAGE); + wasPrepareSourceCalled = true; childSource.prepareSource(player, false, new Listener() { @Override public void onSourceInfoRefreshed(MediaSource source, Timeline timeline, Object manifest) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java index 4a0d8e196d..25da60cb74 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java @@ -149,6 +149,8 @@ public interface MediaSource { } + String MEDIA_SOURCE_REUSED_ERROR_MESSAGE = "MediaSource instances are not allowed to be reused."; + /** * Starts preparation of the source. *

          diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java index 79ed864e25..accf82a68c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java @@ -19,6 +19,7 @@ import android.support.annotation.IntDef; 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 java.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -97,6 +98,7 @@ public final class MergingMediaSource implements MediaSource { @Override public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { + Assertions.checkState(this.listener == null, MEDIA_SOURCE_REUSED_ERROR_MESSAGE); this.listener = listener; for (int i = 0; i < mediaSources.length; i++) { final int sourceIndex = i; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java index b92085d15e..ef93f74958 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java @@ -158,6 +158,8 @@ public final class SingleSampleMediaSource implements MediaSource { private final boolean treatLoadErrorsAsEndOfStream; private final Timeline timeline; + private boolean isPrepared; + /** * @param uri The {@link Uri} of the media stream. * @param dataSourceFactory The factory from which the {@link DataSource} to read the media will @@ -251,6 +253,8 @@ public final class SingleSampleMediaSource implements MediaSource { @Override public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { + Assertions.checkState(!isPrepared, MEDIA_SOURCE_REUSED_ERROR_MESSAGE); + isPrepared = true; listener.onSourceInfoRefreshed(this, timeline, null); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java index 0980e9d011..dbff387d82 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java @@ -204,6 +204,7 @@ public final class AdsMediaSource implements MediaSource { @Override public void prepareSource(final ExoPlayer player, boolean isTopLevelSource, Listener listener) { Assertions.checkArgument(isTopLevelSource); + Assertions.checkState(this.listener == null, MEDIA_SOURCE_REUSED_ERROR_MESSAGE); this.listener = listener; this.player = player; playerHandler = new Handler(); diff --git a/library/dash/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 index 9c0c58c87b..77914d6d45 100644 --- a/library/dash/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 @@ -483,6 +483,7 @@ public final class DashMediaSource implements MediaSource { @Override public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { + Assertions.checkState(sourceListener == null, MEDIA_SOURCE_REUSED_ERROR_MESSAGE); sourceListener = listener; if (sideloadedManifest) { loaderErrorThrower = new LoaderErrorThrower.Dummy(); diff --git a/library/hls/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 index b628807109..31680af8c4 100644 --- a/library/hls/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 @@ -324,7 +324,7 @@ public final class HlsMediaSource implements MediaSource, @Override public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { - Assertions.checkState(playlistTracker == null); + Assertions.checkState(sourceListener == null, MEDIA_SOURCE_REUSED_ERROR_MESSAGE); playlistTracker = new HlsPlaylistTracker(manifestUri, dataSourceFactory, eventDispatcher, minLoadableRetryCount, this, playlistParser); sourceListener = listener; @@ -361,7 +361,6 @@ public final class HlsMediaSource implements MediaSource, playlistTracker.release(); playlistTracker = null; } - sourceListener = null; } @Override diff --git a/library/smoothstreaming/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 index 9932db7869..03e8d601f5 100644 --- a/library/smoothstreaming/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 @@ -419,6 +419,7 @@ public final class SsMediaSource implements MediaSource, @Override public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) { + Assertions.checkState(sourceListener == null, MEDIA_SOURCE_REUSED_ERROR_MESSAGE); sourceListener = listener; if (manifest != null) { manifestLoaderErrorThrower = new LoaderErrorThrower.Dummy(); @@ -455,7 +456,6 @@ public final class SsMediaSource implements MediaSource, @Override public void releaseSource() { - sourceListener = null; manifest = null; manifestDataSource = null; manifestLoadStartTimestamp = 0; From c991b80c856f0d2f8cfb8afb8106a0fc05b625ae Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 8 Jan 2018 01:57:30 -0800 Subject: [PATCH 1024/2472] Rmeove unused variable in Mp4Extractor and HeifExtractor. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=181135589 --- .../android/exoplayer2/extractor/mp4/Mp4Extractor.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/library/core/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 index 50fc0aec80..2c56f9ac2f 100644 --- a/library/core/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 @@ -370,7 +370,6 @@ public final class Mp4Extractor implements Extractor, SeekMap { int firstVideoTrackIndex = C.INDEX_UNSET; long durationUs = C.TIME_UNSET; List tracks = new ArrayList<>(); - long earliestSampleOffset = Long.MAX_VALUE; Metadata metadata = null; GaplessInfoHolder gaplessInfoHolder = new GaplessInfoHolder(); @@ -423,11 +422,6 @@ public final class Mp4Extractor implements Extractor, SeekMap { firstVideoTrackIndex = tracks.size(); } tracks.add(mp4Track); - - long firstSampleOffset = trackSampleTable.offsets[0]; - if (firstSampleOffset < earliestSampleOffset) { - earliestSampleOffset = firstSampleOffset; - } } this.firstVideoTrackIndex = firstVideoTrackIndex; this.durationUs = durationUs; From 73892f21b15a90cb5ce6acda470d13c31855d3ad Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 8 Jan 2018 02:23:40 -0800 Subject: [PATCH 1025/2472] Ubernit line re-order ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=181137491 --- .../java/com/google/android/exoplayer2/DefaultLoadControl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java index 26873fcf2e..af610a8165 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLoadControl.java @@ -166,9 +166,9 @@ public class DefaultLoadControl implements LoadControl { this.allocator = allocator; minBufferUs = minBufferMs * 1000L; maxBufferUs = maxBufferMs * 1000L; - targetBufferBytesOverwrite = targetBufferBytes; bufferForPlaybackUs = bufferForPlaybackMs * 1000L; bufferForPlaybackAfterRebufferUs = bufferForPlaybackAfterRebufferMs * 1000L; + targetBufferBytesOverwrite = targetBufferBytes; this.prioritizeTimeOverSizeThresholds = prioritizeTimeOverSizeThresholds; this.priorityTaskManager = priorityTaskManager; } From d533a83ae41a19ae33b56bef83683549c52722d3 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 8 Jan 2018 02:25:16 -0800 Subject: [PATCH 1026/2472] Partial revert of DRM fixes ---------------------------------- Original change description: DRM fixes - Parse multiple kids from default_KID. It's specified as a whitespace separated list of UUIDs rather than a single UUID. - Opportunistically proceed with playback in cases where the manifest only defines a single SchemeData with the common PSSH UUID. In such cases the manifest isn't saying anything about which specific DRM schemes it supports. Issue: #3630 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=181137621 --- RELEASENOTES.md | 3 --- .../exoplayer2/drm/DefaultDrmSessionManager.java | 12 ++---------- 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 997c3047c4..e318f6a656 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -29,9 +29,6 @@ positions. * Note: `SeekParameters` are only currently effective when playing `ExtractorMediaSource`s (i.e. progressive streams). -* DRM: Optimistically attempt playback of DRM protected content that does not - declare scheme specific init data - ([#3630](https://github.com/google/ExoPlayer/issues/3630)). * DASH: Support DASH manifest EventStream elements. * HLS: Add opt-in support for chunkless preparation in HLS. This allows an HLS source to finish preparation without downloading any chunks, which can diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java index 9c134970ff..6a5185a266 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DefaultDrmSessionManager.java @@ -23,7 +23,6 @@ import android.os.Message; import android.support.annotation.IntDef; import android.support.annotation.NonNull; import android.text.TextUtils; -import android.util.Log; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.drm.DefaultDrmSession.ProvisioningManager; @@ -109,7 +108,6 @@ public class DefaultDrmSessionManager implements DrmSe /** Number of times to retry for initial provisioning and key request for reporting error. */ public static final int INITIAL_DRM_REQUEST_RETRY_COUNT = 3; - private static final String TAG = "DrmSessionManager"; private static final String CENC_SCHEME_MIME_TYPE = "cenc"; private final UUID uuid; @@ -353,14 +351,8 @@ public class DefaultDrmSessionManager implements DrmSe public boolean canAcquireSession(@NonNull DrmInitData drmInitData) { SchemeData schemeData = getSchemeData(drmInitData, uuid, true); if (schemeData == null) { - if (drmInitData.schemeDataCount == 1 && drmInitData.get(0).matches(C.COMMON_PSSH_UUID)) { - // Assume scheme specific data will be added before the session is opened. - Log.w( - TAG, "DrmInitData only contains common PSSH SchemeData. Assuming support for: " + uuid); - } else { - // No data for this manager's scheme. - return false; - } + // No data for this manager's scheme. + return false; } String schemeType = drmInitData.schemeType; if (schemeType == null || C.CENC_TYPE_cenc.equals(schemeType)) { From 67d4626701ee93f6df8f7a274af1927ecbec780d Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 8 Jan 2018 03:07:49 -0800 Subject: [PATCH 1027/2472] Add support for non-Extractor content MediaSources in IMA demo Issue: #3676 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=181140929 --- RELEASENOTES.md | 2 ++ demos/ima/build.gradle | 1 + .../exoplayer2/imademo/PlayerManager.java | 31 ++++++++++++------- 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index e318f6a656..2d14d00a49 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -43,6 +43,8 @@ ([#2147](https://github.com/google/ExoPlayer/issues/2147)). * CacheDataSource: Check periodically if it's possible to read from/write to cache after deciding to bypass cache. +* IMA extension: Add support for playing non-Extractor content MediaSources in + the IMA demo app ([#3676](https://github.com/google/ExoPlayer/issues/3676)). ### 2.6.1 ### diff --git a/demos/ima/build.gradle b/demos/ima/build.gradle index 536d8d4662..5225c260f8 100644 --- a/demos/ima/build.gradle +++ b/demos/ima/build.gradle @@ -45,5 +45,6 @@ dependencies { compile project(modulePrefix + 'library-ui') compile project(modulePrefix + 'library-dash') compile project(modulePrefix + 'library-hls') + compile project(modulePrefix + 'library-smoothstreaming') compile project(modulePrefix + 'extension-ima') } diff --git a/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java b/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java index 51959451d1..0316030ef0 100644 --- a/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java +++ b/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java @@ -32,6 +32,8 @@ import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.source.dash.DashMediaSource; import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource; import com.google.android.exoplayer2.source.hls.HlsMediaSource; +import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource; +import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelection; @@ -79,15 +81,10 @@ import com.google.android.exoplayer2.util.Util; // Bind the player to the view. simpleExoPlayerView.setPlayer(player); - // Produces DataSource instances through which media data is loaded. - DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(context, - Util.getUserAgent(context, context.getString(R.string.application_name))); - // This is the MediaSource representing the content media (i.e. not the ad). String contentUrl = context.getString(R.string.content_url); MediaSource contentMediaSource = - new ExtractorMediaSource.Factory(dataSourceFactory) - .createMediaSource(Uri.parse(contentUrl)); + buildMediaSource(Uri.parse(contentUrl), /* handler= */ null, /* listener= */ null); // Compose the content media source into a new AdsMediaSource with both ads and content. MediaSource mediaSourceWithAds = @@ -126,6 +123,19 @@ import com.google.android.exoplayer2.util.Util; @Override public MediaSource createMediaSource( Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener) { + return buildMediaSource(uri, handler, listener); + } + + @Override + public int[] getSupportedTypes() { + // IMA does not support Smooth Streaming ads. + return new int[] {C.TYPE_DASH, C.TYPE_HLS, C.TYPE_OTHER}; + } + + // Internal methods. + + private MediaSource buildMediaSource( + Uri uri, @Nullable Handler handler, @Nullable MediaSourceEventListener listener) { @ContentType int type = Util.inferContentType(uri); switch (type) { case C.TYPE_DASH: @@ -133,20 +143,19 @@ import com.google.android.exoplayer2.util.Util; new DefaultDashChunkSource.Factory(mediaDataSourceFactory), manifestDataSourceFactory) .createMediaSource(uri, handler, listener); + case C.TYPE_SS: + return new SsMediaSource.Factory( + new DefaultSsChunkSource.Factory(mediaDataSourceFactory), manifestDataSourceFactory) + .createMediaSource(uri, handler, listener); case C.TYPE_HLS: return new HlsMediaSource.Factory(mediaDataSourceFactory) .createMediaSource(uri, handler, listener); case C.TYPE_OTHER: return new ExtractorMediaSource.Factory(mediaDataSourceFactory) .createMediaSource(uri, handler, listener); - case C.TYPE_SS: default: throw new IllegalStateException("Unsupported type: " + type); } } - @Override - public int[] getSupportedTypes() { - return new int[] {C.TYPE_DASH, C.TYPE_HLS, C.TYPE_OTHER}; - } } From d427a1dd62eac30af5bb7519e0c7b437dd64e8db Mon Sep 17 00:00:00 2001 From: eguven Date: Mon, 8 Jan 2018 07:06:15 -0800 Subject: [PATCH 1028/2472] Make Cache.getCachedSpans return empty set rather than null ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=181161289 --- .../upstream/cache/CachedRegionTrackerTest.java | 7 +++++++ .../android/exoplayer2/upstream/cache/Cache.java | 15 +++++++++------ .../exoplayer2/upstream/cache/CacheUtil.java | 3 --- .../upstream/cache/CachedRegionTracker.java | 14 ++++++-------- .../exoplayer2/upstream/cache/SimpleCache.java | 3 ++- .../exoplayer2/upstream/cache/CacheAsserts.java | 3 ++- .../upstream/cache/SimpleCacheTest.java | 4 ++-- .../android/exoplayer2/testutil/CacheAsserts.java | 7 ++++--- 8 files changed, 32 insertions(+), 24 deletions(-) diff --git a/library/core/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 index fc4a9cfed6..2f54ae8972 100644 --- a/library/core/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 @@ -15,12 +15,17 @@ */ package com.google.android.exoplayer2.upstream.cache; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.when; + import android.test.InstrumentationTestCase; import com.google.android.exoplayer2.extractor.ChunkIndex; import com.google.android.exoplayer2.testutil.MockitoUtil; import com.google.android.exoplayer2.util.Util; import java.io.File; import java.io.IOException; +import java.util.TreeSet; import org.mockito.Mock; /** @@ -48,6 +53,8 @@ public final class CachedRegionTrackerTest extends InstrumentationTestCase { protected void setUp() throws Exception { super.setUp(); MockitoUtil.setUpMockito(this); + when(cache.addListener(anyString(), any(Cache.Listener.class))) + .thenReturn(new TreeSet()); tracker = new CachedRegionTracker(cache, CACHE_KEY, CHUNK_INDEX); cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest"); index = new CachedContentIndex(cacheDir); diff --git a/library/core/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 index 80ad698fa4..76481bbdf7 100644 --- a/library/core/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 @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.upstream.cache; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; import java.io.File; import java.io.IOException; @@ -80,15 +81,16 @@ public interface Cache { /** * Registers a listener to listen for changes to a given key. - *

          - * No guarantees are made about the thread or threads on which the listener is called, but it is - * guaranteed that listener methods will be called in a serial fashion (i.e. one at a time) and in - * the same order as events occurred. + * + *

          No guarantees are made about the thread or threads on which the listener is called, but it + * is guaranteed that listener methods will be called in a serial fashion (i.e. one at a time) and + * in the same order as events occurred. * * @param key The key to listen to. * @param listener The listener to add. * @return The current spans for the key. */ + @NonNull NavigableSet addListener(String key, Listener listener); /** @@ -103,9 +105,10 @@ public interface Cache { * Returns the cached spans for a given cache key. * * @param key The key for which spans should be returned. - * @return The spans for the key. May be null if there are no such spans. + * @return The spans for the key. */ - @Nullable NavigableSet getCachedSpans(String key); + @NonNull + NavigableSet getCachedSpans(String key); /** * Returns all keys in the cache. 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 index c612ea3739..2bf5cde8e0 100644 --- 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 @@ -253,9 +253,6 @@ public final class CacheUtil { /** 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); diff --git a/library/core/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 index 9559054f6d..9455aed11b 100644 --- a/library/core/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 @@ -50,14 +50,12 @@ public final class CachedRegionTracker implements Cache.Listener { synchronized (this) { NavigableSet cacheSpans = cache.addListener(cacheKey, this); - if (cacheSpans != null) { - // Merge the spans into regions. mergeSpan is more efficient when merging from high to low, - // which is why a descending iterator is used here. - Iterator spanIterator = cacheSpans.descendingIterator(); - while (spanIterator.hasNext()) { - CacheSpan span = spanIterator.next(); - mergeSpan(span); - } + // Merge the spans into regions. mergeSpan is more efficient when merging from high to low, + // which is why a descending iterator is used here. + Iterator spanIterator = cacheSpans.descendingIterator(); + while (spanIterator.hasNext()) { + CacheSpan span = spanIterator.next(); + mergeSpan(span); } } } diff --git a/library/core/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 index 599474d6c3..ffac8a35f1 100644 --- a/library/core/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 @@ -134,7 +134,8 @@ public final class SimpleCache implements Cache { @Override public synchronized NavigableSet getCachedSpans(String key) { CachedContent cachedContent = index.get(key); - return cachedContent == null || cachedContent.isEmpty() ? null + return cachedContent == null || cachedContent.isEmpty() + ? new TreeSet() : new TreeSet(cachedContent.getSpans()); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheAsserts.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheAsserts.java index aa98ad3179..65850a13e7 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheAsserts.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheAsserts.java @@ -101,7 +101,8 @@ import java.util.ArrayList; public static void assertDataNotCached(Cache cache, String... uriStrings) { for (String uriString : uriStrings) { assertWithMessage("There is cached data for '" + uriString + "'") - .that(cache.getCachedSpans(CacheUtil.generateKey(Uri.parse(uriString)))).isNull(); + .that(cache.getCachedSpans(CacheUtil.generateKey(Uri.parse(uriString)))) + .isEmpty(); } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java index d5894895b1..75a80185b9 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java @@ -77,7 +77,7 @@ public class SimpleCacheTest { assertThat(simpleCache.getKeys()).isEmpty(); NavigableSet cachedSpans = simpleCache.getCachedSpans(KEY_1); - assertThat(cachedSpans == null || cachedSpans.isEmpty()).isTrue(); + assertThat(cachedSpans.isEmpty()).isTrue(); assertThat(simpleCache.getCacheSpace()).isEqualTo(0); assertThat(cacheDir.listFiles()).hasLength(0); @@ -283,7 +283,7 @@ public class SimpleCacheTest { // Although store() has failed, it should remove the first span and add the new one. NavigableSet cachedSpans = simpleCache.getCachedSpans(KEY_1); - assertThat(cachedSpans).isNotNull(); + assertThat(cachedSpans).isNotEmpty(); assertThat(cachedSpans).hasSize(1); assertThat(cachedSpans.pollFirst().position).isEqualTo(15); } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java index 82fff0d4fe..eb53191dc8 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.testutil; import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertTrue; import android.net.Uri; import android.test.MoreAsserts; @@ -29,7 +30,6 @@ import com.google.android.exoplayer2.upstream.cache.CacheUtil; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.ArrayList; -import junit.framework.Assert; /** * Assertion methods for {@link Cache}. @@ -105,8 +105,9 @@ public final class CacheAsserts { /** Asserts that there is no cache content for the given {@code uriStrings}. */ public static void assertDataNotCached(Cache cache, String... uriStrings) { for (String uriString : uriStrings) { - Assert.assertNull("There is cached data for '" + uriString + "',", - cache.getCachedSpans(CacheUtil.generateKey(Uri.parse(uriString)))); + assertTrue( + "There is cached data for '" + uriString + "',", + cache.getCachedSpans(CacheUtil.generateKey(Uri.parse(uriString))).isEmpty()); } } From 4ee971052bb39acf1f33a52959d77c7204de8498 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 9 Jan 2018 03:34:40 -0800 Subject: [PATCH 1029/2472] Improve Extractor partial read tests. Partial reads were performed once using a partial size of 1 byte. This was not enough to detect problems which only occur in combination with IOExceptions. Partial reads are now only applied when no exception is thrown. Moreover, the tests didn't check whether the total number of sampled bytes is what it is supposed to be. Added a field to the data dumps checking the total number of bytes in the sampled data. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=181296545 --- .../src/androidTest/assets/bear.flac.0.dump | 1 + .../src/androidTest/assets/bear.flac.1.dump | 1 + .../src/androidTest/assets/bear.flac.2.dump | 1 + .../src/androidTest/assets/bear.flac.3.dump | 1 + .../androidTest/assets/flv/sample.flv.0.dump | 2 + .../androidTest/assets/mkv/sample.mkv.0.dump | 2 + .../androidTest/assets/mkv/sample.mkv.1.dump | 2 + .../androidTest/assets/mkv/sample.mkv.2.dump | 2 + .../androidTest/assets/mkv/sample.mkv.3.dump | 2 + .../subsample_encrypted_altref.webm.0.dump | 1 + .../subsample_encrypted_noaltref.webm.0.dump | 1 + .../androidTest/assets/mp3/bear.mp3.0.dump | 1 + .../androidTest/assets/mp3/bear.mp3.1.dump | 1 + .../androidTest/assets/mp3/bear.mp3.2.dump | 1 + .../androidTest/assets/mp3/bear.mp3.3.dump | 1 + .../assets/mp3/play-trimmed.mp3.0.dump | 1 + .../assets/mp3/play-trimmed.mp3.1.dump | 1 + .../assets/mp3/play-trimmed.mp3.2.dump | 1 + .../assets/mp3/play-trimmed.mp3.3.dump | 1 + .../assets/mp3/play-trimmed.mp3.unklen.dump | 1 + .../androidTest/assets/mp4/sample.mp4.0.dump | 2 + .../androidTest/assets/mp4/sample.mp4.1.dump | 2 + .../androidTest/assets/mp4/sample.mp4.2.dump | 2 + .../androidTest/assets/mp4/sample.mp4.3.dump | 2 + .../assets/mp4/sample_fragmented.mp4.0.dump | 2 + .../mp4/sample_fragmented_sei.mp4.0.dump | 3 + .../androidTest/assets/ogg/bear.opus.0.dump | 1 + .../androidTest/assets/ogg/bear.opus.1.dump | 1 + .../androidTest/assets/ogg/bear.opus.2.dump | 1 + .../androidTest/assets/ogg/bear.opus.3.dump | 1 + .../assets/ogg/bear.opus.unklen.dump | 1 + .../assets/ogg/bear_flac.ogg.0.dump | 1 + .../assets/ogg/bear_flac.ogg.1.dump | 1 + .../assets/ogg/bear_flac.ogg.2.dump | 1 + .../assets/ogg/bear_flac.ogg.3.dump | 1 + .../assets/ogg/bear_flac.ogg.unklen.dump | 1 + .../ogg/bear_flac_noseektable.ogg.0.dump | 1 + .../ogg/bear_flac_noseektable.ogg.1.dump | 1 + .../ogg/bear_flac_noseektable.ogg.2.dump | 1 + .../ogg/bear_flac_noseektable.ogg.3.dump | 1 + .../ogg/bear_flac_noseektable.ogg.unklen.dump | 1 + .../assets/ogg/bear_vorbis.ogg.0.dump | 1 + .../assets/ogg/bear_vorbis.ogg.1.dump | 1 + .../assets/ogg/bear_vorbis.ogg.2.dump | 1 + .../assets/ogg/bear_vorbis.ogg.3.dump | 1 + .../assets/ogg/bear_vorbis.ogg.unklen.dump | 1 + .../assets/rawcc/sample.rawcc.0.dump | 1 + .../androidTest/assets/ts/sample.ac3.0.dump | 1 + .../androidTest/assets/ts/sample.adts.0.dump | 2 + .../androidTest/assets/ts/sample.ps.0.dump | 2 + .../androidTest/assets/ts/sample.ts.0.dump | 2 + .../androidTest/assets/wav/sample.wav.0.dump | 1 + .../androidTest/assets/wav/sample.wav.1.dump | 1 + .../androidTest/assets/wav/sample.wav.2.dump | 1 + .../androidTest/assets/wav/sample.wav.3.dump | 1 + .../testutil/FakeExtractorInput.java | 63 +++++++++++-------- .../exoplayer2/testutil/FakeTrackOutput.java | 1 + 57 files changed, 109 insertions(+), 25 deletions(-) diff --git a/extensions/flac/src/androidTest/assets/bear.flac.0.dump b/extensions/flac/src/androidTest/assets/bear.flac.0.dump index 6908f5cc93..2a17cbdea6 100644 --- a/extensions/flac/src/androidTest/assets/bear.flac.0.dump +++ b/extensions/flac/src/androidTest/assets/bear.flac.0.dump @@ -25,6 +25,7 @@ track 0: language = null drmInitData = - initializationData: + total output bytes = 526272 sample count = 33 sample 0: time = 0 diff --git a/extensions/flac/src/androidTest/assets/bear.flac.1.dump b/extensions/flac/src/androidTest/assets/bear.flac.1.dump index 1414443187..412e4a1b8f 100644 --- a/extensions/flac/src/androidTest/assets/bear.flac.1.dump +++ b/extensions/flac/src/androidTest/assets/bear.flac.1.dump @@ -25,6 +25,7 @@ track 0: language = null drmInitData = - initializationData: + total output bytes = 362432 sample count = 23 sample 0: time = 853333 diff --git a/extensions/flac/src/androidTest/assets/bear.flac.2.dump b/extensions/flac/src/androidTest/assets/bear.flac.2.dump index e343241650..42ebb125d1 100644 --- a/extensions/flac/src/androidTest/assets/bear.flac.2.dump +++ b/extensions/flac/src/androidTest/assets/bear.flac.2.dump @@ -25,6 +25,7 @@ track 0: language = null drmInitData = - initializationData: + total output bytes = 182208 sample count = 12 sample 0: time = 1792000 diff --git a/extensions/flac/src/androidTest/assets/bear.flac.3.dump b/extensions/flac/src/androidTest/assets/bear.flac.3.dump index 95ab255bd0..958cb0d418 100644 --- a/extensions/flac/src/androidTest/assets/bear.flac.3.dump +++ b/extensions/flac/src/androidTest/assets/bear.flac.3.dump @@ -25,6 +25,7 @@ track 0: language = null drmInitData = - initializationData: + total output bytes = 18368 sample count = 2 sample 0: time = 2645333 diff --git a/library/core/src/androidTest/assets/flv/sample.flv.0.dump b/library/core/src/androidTest/assets/flv/sample.flv.0.dump index 7a4a74770c..f4502749f5 100644 --- a/library/core/src/androidTest/assets/flv/sample.flv.0.dump +++ b/library/core/src/androidTest/assets/flv/sample.flv.0.dump @@ -26,6 +26,7 @@ track 8: drmInitData = - initializationData: data = length 2, hash 5F7 + total output bytes = 9529 sample count = 45 sample 0: time = 112000 @@ -231,6 +232,7 @@ track 9: initializationData: data = length 30, hash F6F3D010 data = length 10, hash 7A0D0F2B + total output bytes = 89502 sample count = 30 sample 0: time = 67000 diff --git a/library/core/src/androidTest/assets/mkv/sample.mkv.0.dump b/library/core/src/androidTest/assets/mkv/sample.mkv.0.dump index 0f005ee5a9..009ff55c23 100644 --- a/library/core/src/androidTest/assets/mkv/sample.mkv.0.dump +++ b/library/core/src/androidTest/assets/mkv/sample.mkv.0.dump @@ -27,6 +27,7 @@ track 1: initializationData: data = length 30, hash F6F3D010 data = length 10, hash 7A0D0F2B + total output bytes = 89502 sample count = 30 sample 0: time = 67000 @@ -170,6 +171,7 @@ track 2: language = und drmInitData = - initializationData: + total output bytes = 12120 sample count = 29 sample 0: time = 129000 diff --git a/library/core/src/androidTest/assets/mkv/sample.mkv.1.dump b/library/core/src/androidTest/assets/mkv/sample.mkv.1.dump index 378f5d7f2a..91396e81b8 100644 --- a/library/core/src/androidTest/assets/mkv/sample.mkv.1.dump +++ b/library/core/src/androidTest/assets/mkv/sample.mkv.1.dump @@ -27,6 +27,7 @@ track 1: initializationData: data = length 30, hash F6F3D010 data = length 10, hash 7A0D0F2B + total output bytes = 30995 sample count = 22 sample 0: time = 334000 @@ -138,6 +139,7 @@ track 2: language = und drmInitData = - initializationData: + total output bytes = 8778 sample count = 21 sample 0: time = 408000 diff --git a/library/core/src/androidTest/assets/mkv/sample.mkv.2.dump b/library/core/src/androidTest/assets/mkv/sample.mkv.2.dump index 80caf24a93..5c56dcc8af 100644 --- a/library/core/src/androidTest/assets/mkv/sample.mkv.2.dump +++ b/library/core/src/androidTest/assets/mkv/sample.mkv.2.dump @@ -27,6 +27,7 @@ track 1: initializationData: data = length 30, hash F6F3D010 data = length 10, hash 7A0D0F2B + total output bytes = 10158 sample count = 11 sample 0: time = 700000 @@ -94,6 +95,7 @@ track 2: language = und drmInitData = - initializationData: + total output bytes = 4180 sample count = 10 sample 0: time = 791000 diff --git a/library/core/src/androidTest/assets/mkv/sample.mkv.3.dump b/library/core/src/androidTest/assets/mkv/sample.mkv.3.dump index c9672ba9c4..cf5a0199fc 100644 --- a/library/core/src/androidTest/assets/mkv/sample.mkv.3.dump +++ b/library/core/src/androidTest/assets/mkv/sample.mkv.3.dump @@ -27,6 +27,7 @@ track 1: initializationData: data = length 30, hash F6F3D010 data = length 10, hash 7A0D0F2B + total output bytes = 0 sample count = 0 track 2: format: @@ -50,6 +51,7 @@ track 2: language = und drmInitData = - initializationData: + total output bytes = 1254 sample count = 3 sample 0: time = 1035000 diff --git a/library/core/src/androidTest/assets/mkv/subsample_encrypted_altref.webm.0.dump b/library/core/src/androidTest/assets/mkv/subsample_encrypted_altref.webm.0.dump index abc07dc503..62a270eb9e 100644 --- a/library/core/src/androidTest/assets/mkv/subsample_encrypted_altref.webm.0.dump +++ b/library/core/src/androidTest/assets/mkv/subsample_encrypted_altref.webm.0.dump @@ -25,6 +25,7 @@ track 1: language = null drmInitData = 1305012705 initializationData: + total output bytes = 39 sample count = 1 sample 0: time = 0 diff --git a/library/core/src/androidTest/assets/mkv/subsample_encrypted_noaltref.webm.0.dump b/library/core/src/androidTest/assets/mkv/subsample_encrypted_noaltref.webm.0.dump index c43a43b576..43e5eed5d1 100644 --- a/library/core/src/androidTest/assets/mkv/subsample_encrypted_noaltref.webm.0.dump +++ b/library/core/src/androidTest/assets/mkv/subsample_encrypted_noaltref.webm.0.dump @@ -25,6 +25,7 @@ track 1: language = null drmInitData = 1305012705 initializationData: + total output bytes = 24 sample count = 1 sample 0: time = 0 diff --git a/library/core/src/androidTest/assets/mp3/bear.mp3.0.dump b/library/core/src/androidTest/assets/mp3/bear.mp3.0.dump index eca3a6687d..b12a68a60b 100644 --- a/library/core/src/androidTest/assets/mp3/bear.mp3.0.dump +++ b/library/core/src/androidTest/assets/mp3/bear.mp3.0.dump @@ -25,6 +25,7 @@ track 0: language = null drmInitData = - initializationData: + total output bytes = 44544 sample count = 116 sample 0: time = 0 diff --git a/library/core/src/androidTest/assets/mp3/bear.mp3.1.dump b/library/core/src/androidTest/assets/mp3/bear.mp3.1.dump index 12abf149c4..abf5b10415 100644 --- a/library/core/src/androidTest/assets/mp3/bear.mp3.1.dump +++ b/library/core/src/androidTest/assets/mp3/bear.mp3.1.dump @@ -25,6 +25,7 @@ track 0: language = null drmInitData = - initializationData: + total output bytes = 29568 sample count = 77 sample 0: time = 928568 diff --git a/library/core/src/androidTest/assets/mp3/bear.mp3.2.dump b/library/core/src/androidTest/assets/mp3/bear.mp3.2.dump index 3568616e76..813f61b7fc 100644 --- a/library/core/src/androidTest/assets/mp3/bear.mp3.2.dump +++ b/library/core/src/androidTest/assets/mp3/bear.mp3.2.dump @@ -25,6 +25,7 @@ track 0: language = null drmInitData = - initializationData: + total output bytes = 14592 sample count = 38 sample 0: time = 1871586 diff --git a/library/core/src/androidTest/assets/mp3/bear.mp3.3.dump b/library/core/src/androidTest/assets/mp3/bear.mp3.3.dump index 8a31fe5e7d..9a0207bd53 100644 --- a/library/core/src/androidTest/assets/mp3/bear.mp3.3.dump +++ b/library/core/src/androidTest/assets/mp3/bear.mp3.3.dump @@ -25,5 +25,6 @@ track 0: language = null drmInitData = - initializationData: + total output bytes = 0 sample count = 0 tracksEnded = true diff --git a/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.0.dump b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.0.dump index 88601665b0..435360dfed 100644 --- a/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.0.dump +++ b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.0.dump @@ -25,6 +25,7 @@ track 0: language = null drmInitData = - initializationData: + total output bytes = 418 sample count = 1 sample 0: time = 0 diff --git a/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.1.dump b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.1.dump index 88601665b0..435360dfed 100644 --- a/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.1.dump +++ b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.1.dump @@ -25,6 +25,7 @@ track 0: language = null drmInitData = - initializationData: + total output bytes = 418 sample count = 1 sample 0: time = 0 diff --git a/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.2.dump b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.2.dump index 88601665b0..435360dfed 100644 --- a/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.2.dump +++ b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.2.dump @@ -25,6 +25,7 @@ track 0: language = null drmInitData = - initializationData: + total output bytes = 418 sample count = 1 sample 0: time = 0 diff --git a/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.3.dump b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.3.dump index 88601665b0..435360dfed 100644 --- a/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.3.dump +++ b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.3.dump @@ -25,6 +25,7 @@ track 0: language = null drmInitData = - initializationData: + total output bytes = 418 sample count = 1 sample 0: time = 0 diff --git a/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.unklen.dump b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.unklen.dump index 2c0ac67561..6b49619b50 100644 --- a/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.unklen.dump +++ b/library/core/src/androidTest/assets/mp3/play-trimmed.mp3.unklen.dump @@ -25,6 +25,7 @@ track 0: language = null drmInitData = - initializationData: + total output bytes = 418 sample count = 1 sample 0: time = 0 diff --git a/library/core/src/androidTest/assets/mp4/sample.mp4.0.dump b/library/core/src/androidTest/assets/mp4/sample.mp4.0.dump index 7cd3486505..77708b16df 100644 --- a/library/core/src/androidTest/assets/mp4/sample.mp4.0.dump +++ b/library/core/src/androidTest/assets/mp4/sample.mp4.0.dump @@ -27,6 +27,7 @@ track 0: initializationData: data = length 29, hash 4746B5D9 data = length 10, hash 7A0D0F2B + total output bytes = 89876 sample count = 30 sample 0: time = 0 @@ -171,6 +172,7 @@ track 1: drmInitData = - initializationData: data = length 2, hash 5F7 + total output bytes = 9529 sample count = 45 sample 0: time = 44000 diff --git a/library/core/src/androidTest/assets/mp4/sample.mp4.1.dump b/library/core/src/androidTest/assets/mp4/sample.mp4.1.dump index fcf9402cba..30ed21ef98 100644 --- a/library/core/src/androidTest/assets/mp4/sample.mp4.1.dump +++ b/library/core/src/androidTest/assets/mp4/sample.mp4.1.dump @@ -27,6 +27,7 @@ track 0: initializationData: data = length 29, hash 4746B5D9 data = length 10, hash 7A0D0F2B + total output bytes = 89876 sample count = 30 sample 0: time = 0 @@ -171,6 +172,7 @@ track 1: drmInitData = - initializationData: data = length 2, hash 5F7 + total output bytes = 7464 sample count = 33 sample 0: time = 322639 diff --git a/library/core/src/androidTest/assets/mp4/sample.mp4.2.dump b/library/core/src/androidTest/assets/mp4/sample.mp4.2.dump index 5dbb6e1561..640d92722c 100644 --- a/library/core/src/androidTest/assets/mp4/sample.mp4.2.dump +++ b/library/core/src/androidTest/assets/mp4/sample.mp4.2.dump @@ -27,6 +27,7 @@ track 0: initializationData: data = length 29, hash 4746B5D9 data = length 10, hash 7A0D0F2B + total output bytes = 89876 sample count = 30 sample 0: time = 0 @@ -171,6 +172,7 @@ track 1: drmInitData = - initializationData: data = length 2, hash 5F7 + total output bytes = 4019 sample count = 18 sample 0: time = 670938 diff --git a/library/core/src/androidTest/assets/mp4/sample.mp4.3.dump b/library/core/src/androidTest/assets/mp4/sample.mp4.3.dump index bac707446d..b4fd4a0b02 100644 --- a/library/core/src/androidTest/assets/mp4/sample.mp4.3.dump +++ b/library/core/src/androidTest/assets/mp4/sample.mp4.3.dump @@ -27,6 +27,7 @@ track 0: initializationData: data = length 29, hash 4746B5D9 data = length 10, hash 7A0D0F2B + total output bytes = 89876 sample count = 30 sample 0: time = 0 @@ -171,6 +172,7 @@ track 1: drmInitData = - initializationData: data = length 2, hash 5F7 + total output bytes = 470 sample count = 3 sample 0: time = 1019238 diff --git a/library/core/src/androidTest/assets/mp4/sample_fragmented.mp4.0.dump b/library/core/src/androidTest/assets/mp4/sample_fragmented.mp4.0.dump index 736e57693c..ec2cb7b8ce 100644 --- a/library/core/src/androidTest/assets/mp4/sample_fragmented.mp4.0.dump +++ b/library/core/src/androidTest/assets/mp4/sample_fragmented.mp4.0.dump @@ -27,6 +27,7 @@ track 0: initializationData: data = length 29, hash 4746B5D9 data = length 10, hash 7A0D0F2B + total output bytes = 85933 sample count = 30 sample 0: time = 66000 @@ -171,6 +172,7 @@ track 1: drmInitData = - initializationData: data = length 5, hash 2B7623A + total output bytes = 18257 sample count = 46 sample 0: time = 0 diff --git a/library/core/src/androidTest/assets/mp4/sample_fragmented_sei.mp4.0.dump b/library/core/src/androidTest/assets/mp4/sample_fragmented_sei.mp4.0.dump index 8186a2b9ce..ae012055fe 100644 --- a/library/core/src/androidTest/assets/mp4/sample_fragmented_sei.mp4.0.dump +++ b/library/core/src/androidTest/assets/mp4/sample_fragmented_sei.mp4.0.dump @@ -27,6 +27,7 @@ track 0: initializationData: data = length 29, hash 4746B5D9 data = length 10, hash 7A0D0F2B + total output bytes = 85933 sample count = 30 sample 0: time = 66000 @@ -171,6 +172,7 @@ track 1: drmInitData = - initializationData: data = length 5, hash 2B7623A + total output bytes = 18257 sample count = 46 sample 0: time = 0 @@ -378,5 +380,6 @@ track 3: language = null drmInitData = - initializationData: + total output bytes = 0 sample count = 0 tracksEnded = true diff --git a/library/core/src/androidTest/assets/ogg/bear.opus.0.dump b/library/core/src/androidTest/assets/ogg/bear.opus.0.dump index 4d09067f3b..643972b836 100644 --- a/library/core/src/androidTest/assets/ogg/bear.opus.0.dump +++ b/library/core/src/androidTest/assets/ogg/bear.opus.0.dump @@ -28,6 +28,7 @@ track 0: data = length 19, hash BFE794DB data = length 8, hash CA22068C data = length 8, hash 79C07075 + total output bytes = 25541 sample count = 275 sample 0: time = 0 diff --git a/library/core/src/androidTest/assets/ogg/bear.opus.1.dump b/library/core/src/androidTest/assets/ogg/bear.opus.1.dump index 821351e989..8df1563d90 100644 --- a/library/core/src/androidTest/assets/ogg/bear.opus.1.dump +++ b/library/core/src/androidTest/assets/ogg/bear.opus.1.dump @@ -28,6 +28,7 @@ track 0: data = length 19, hash BFE794DB data = length 8, hash CA22068C data = length 8, hash 79C07075 + total output bytes = 17031 sample count = 184 sample 0: time = 910000 diff --git a/library/core/src/androidTest/assets/ogg/bear.opus.2.dump b/library/core/src/androidTest/assets/ogg/bear.opus.2.dump index 3aea1e8d74..bed4c46d9c 100644 --- a/library/core/src/androidTest/assets/ogg/bear.opus.2.dump +++ b/library/core/src/androidTest/assets/ogg/bear.opus.2.dump @@ -28,6 +28,7 @@ track 0: data = length 19, hash BFE794DB data = length 8, hash CA22068C data = length 8, hash 79C07075 + total output bytes = 8698 sample count = 92 sample 0: time = 1830000 diff --git a/library/core/src/androidTest/assets/ogg/bear.opus.3.dump b/library/core/src/androidTest/assets/ogg/bear.opus.3.dump index b49af29f2c..8a9c99250e 100644 --- a/library/core/src/androidTest/assets/ogg/bear.opus.3.dump +++ b/library/core/src/androidTest/assets/ogg/bear.opus.3.dump @@ -28,6 +28,7 @@ track 0: data = length 19, hash BFE794DB data = length 8, hash CA22068C data = length 8, hash 79C07075 + total output bytes = 126 sample count = 1 sample 0: time = 2741000 diff --git a/library/core/src/androidTest/assets/ogg/bear.opus.unklen.dump b/library/core/src/androidTest/assets/ogg/bear.opus.unklen.dump index b2d5a9f3d2..5d2c84b047 100644 --- a/library/core/src/androidTest/assets/ogg/bear.opus.unklen.dump +++ b/library/core/src/androidTest/assets/ogg/bear.opus.unklen.dump @@ -28,6 +28,7 @@ track 0: data = length 19, hash BFE794DB data = length 8, hash CA22068C data = length 8, hash 79C07075 + total output bytes = 25541 sample count = 275 sample 0: time = 0 diff --git a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.0.dump b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.0.dump index 572d1da891..ff22bb2d3e 100644 --- a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.0.dump +++ b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.0.dump @@ -26,6 +26,7 @@ track 0: drmInitData = - initializationData: data = length 42, hash 83F6895 + total output bytes = 164431 sample count = 33 sample 0: time = 0 diff --git a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.1.dump b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.1.dump index d53f257fd2..50110149fd 100644 --- a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.1.dump +++ b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.1.dump @@ -26,6 +26,7 @@ track 0: drmInitData = - initializationData: data = length 42, hash 83F6895 + total output bytes = 113666 sample count = 23 sample 0: time = 853333 diff --git a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.2.dump b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.2.dump index cdfd6efab8..483ae36721 100644 --- a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.2.dump +++ b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.2.dump @@ -26,6 +26,7 @@ track 0: drmInitData = - initializationData: data = length 42, hash 83F6895 + total output bytes = 55652 sample count = 12 sample 0: time = 1792000 diff --git a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.3.dump b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.3.dump index 9b029d3301..a47407e63d 100644 --- a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.3.dump +++ b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.3.dump @@ -26,6 +26,7 @@ track 0: drmInitData = - initializationData: data = length 42, hash 83F6895 + total output bytes = 445 sample count = 1 sample 0: time = 2730666 diff --git a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.unklen.dump b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.unklen.dump index 572d1da891..ff22bb2d3e 100644 --- a/library/core/src/androidTest/assets/ogg/bear_flac.ogg.unklen.dump +++ b/library/core/src/androidTest/assets/ogg/bear_flac.ogg.unklen.dump @@ -26,6 +26,7 @@ track 0: drmInitData = - initializationData: data = length 42, hash 83F6895 + total output bytes = 164431 sample count = 33 sample 0: time = 0 diff --git a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.0.dump b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.0.dump index 1c02c1bbef..32f350efcb 100644 --- a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.0.dump +++ b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.0.dump @@ -26,6 +26,7 @@ track 0: drmInitData = - initializationData: data = length 42, hash 83F6895 + total output bytes = 164431 sample count = 33 sample 0: time = 0 diff --git a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.1.dump b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.1.dump index 81d79b8674..3082e8faca 100644 --- a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.1.dump +++ b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.1.dump @@ -26,6 +26,7 @@ track 0: drmInitData = - initializationData: data = length 42, hash 83F6895 + total output bytes = 113666 sample count = 23 sample 0: time = 853333 diff --git a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.2.dump b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.2.dump index f8b00bcb3a..b574409f70 100644 --- a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.2.dump +++ b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.2.dump @@ -26,6 +26,7 @@ track 0: drmInitData = - initializationData: data = length 42, hash 83F6895 + total output bytes = 55652 sample count = 12 sample 0: time = 1792000 diff --git a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.3.dump b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.3.dump index b020618488..f411596b44 100644 --- a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.3.dump +++ b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.3.dump @@ -26,6 +26,7 @@ track 0: drmInitData = - initializationData: data = length 42, hash 83F6895 + total output bytes = 445 sample count = 1 sample 0: time = 2730666 diff --git a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.unklen.dump b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.unklen.dump index bf135434f4..bdfe90277d 100644 --- a/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.unklen.dump +++ b/library/core/src/androidTest/assets/ogg/bear_flac_noseektable.ogg.unklen.dump @@ -26,6 +26,7 @@ track 0: drmInitData = - initializationData: data = length 42, hash 83F6895 + total output bytes = 164431 sample count = 33 sample 0: time = 0 diff --git a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.0.dump b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.0.dump index 860e8a3b5b..dd129ce9dc 100644 --- a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.0.dump +++ b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.0.dump @@ -27,6 +27,7 @@ track 0: initializationData: data = length 30, hash 9A8FF207 data = length 3832, hash 8A406249 + total output bytes = 26873 sample count = 180 sample 0: time = 0 diff --git a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.1.dump b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.1.dump index 11afeb9665..4fb8a74d92 100644 --- a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.1.dump +++ b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.1.dump @@ -27,6 +27,7 @@ track 0: initializationData: data = length 30, hash 9A8FF207 data = length 3832, hash 8A406249 + total output bytes = 17598 sample count = 109 sample 0: time = 896000 diff --git a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.2.dump b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.2.dump index f2f97ebcfa..fad8f33d77 100644 --- a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.2.dump +++ b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.2.dump @@ -27,6 +27,7 @@ track 0: initializationData: data = length 30, hash 9A8FF207 data = length 3832, hash 8A406249 + total output bytes = 8658 sample count = 49 sample 0: time = 1821333 diff --git a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.3.dump b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.3.dump index 5d5f284cf2..49dca02220 100644 --- a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.3.dump +++ b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.3.dump @@ -27,5 +27,6 @@ track 0: initializationData: data = length 30, hash 9A8FF207 data = length 3832, hash 8A406249 + total output bytes = 0 sample count = 0 tracksEnded = true diff --git a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.unklen.dump b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.unklen.dump index ee1176773e..756be42854 100644 --- a/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.unklen.dump +++ b/library/core/src/androidTest/assets/ogg/bear_vorbis.ogg.unklen.dump @@ -27,6 +27,7 @@ track 0: initializationData: data = length 30, hash 9A8FF207 data = length 3832, hash 8A406249 + total output bytes = 26873 sample count = 180 sample 0: time = 0 diff --git a/library/core/src/androidTest/assets/rawcc/sample.rawcc.0.dump b/library/core/src/androidTest/assets/rawcc/sample.rawcc.0.dump index d430d1d8d4..130be06ceb 100644 --- a/library/core/src/androidTest/assets/rawcc/sample.rawcc.0.dump +++ b/library/core/src/androidTest/assets/rawcc/sample.rawcc.0.dump @@ -25,6 +25,7 @@ track 0: language = null drmInitData = - initializationData: + total output bytes = 978 sample count = 150 sample 0: time = 37657512133 diff --git a/library/core/src/androidTest/assets/ts/sample.ac3.0.dump b/library/core/src/androidTest/assets/ts/sample.ac3.0.dump index bedffcf198..46028638fe 100644 --- a/library/core/src/androidTest/assets/ts/sample.ac3.0.dump +++ b/library/core/src/androidTest/assets/ts/sample.ac3.0.dump @@ -25,6 +25,7 @@ track 0: language = null drmInitData = - initializationData: + total output bytes = 13281 sample count = 8 sample 0: time = 0 diff --git a/library/core/src/androidTest/assets/ts/sample.adts.0.dump b/library/core/src/androidTest/assets/ts/sample.adts.0.dump index a97cf860d1..132859a00e 100644 --- a/library/core/src/androidTest/assets/ts/sample.adts.0.dump +++ b/library/core/src/androidTest/assets/ts/sample.adts.0.dump @@ -26,6 +26,7 @@ track 0: drmInitData = - initializationData: data = length 2, hash 5F7 + total output bytes = 30797 sample count = 144 sample 0: time = 0 @@ -625,5 +626,6 @@ track 1: language = null drmInitData = - initializationData: + total output bytes = 0 sample count = 0 tracksEnded = true diff --git a/library/core/src/androidTest/assets/ts/sample.ps.0.dump b/library/core/src/androidTest/assets/ts/sample.ps.0.dump index 41db704d56..e833201692 100644 --- a/library/core/src/androidTest/assets/ts/sample.ps.0.dump +++ b/library/core/src/androidTest/assets/ts/sample.ps.0.dump @@ -25,6 +25,7 @@ track 192: language = null drmInitData = - initializationData: + total output bytes = 1671 sample count = 4 sample 0: time = 29088 @@ -65,6 +66,7 @@ track 224: drmInitData = - initializationData: data = length 22, hash 743CC6F8 + total output bytes = 44056 sample count = 2 sample 0: time = 40000 diff --git a/library/core/src/androidTest/assets/ts/sample.ts.0.dump b/library/core/src/androidTest/assets/ts/sample.ts.0.dump index e900b94673..39b1565289 100644 --- a/library/core/src/androidTest/assets/ts/sample.ts.0.dump +++ b/library/core/src/androidTest/assets/ts/sample.ts.0.dump @@ -26,6 +26,7 @@ track 256: drmInitData = - initializationData: data = length 22, hash CE183139 + total output bytes = 45026 sample count = 2 sample 0: time = 33366 @@ -57,6 +58,7 @@ track 257: language = und drmInitData = - initializationData: + total output bytes = 5015 sample count = 4 sample 0: time = 22455 diff --git a/library/core/src/androidTest/assets/wav/sample.wav.0.dump b/library/core/src/androidTest/assets/wav/sample.wav.0.dump index 5d0f4d77f0..32f9d495d2 100644 --- a/library/core/src/androidTest/assets/wav/sample.wav.0.dump +++ b/library/core/src/androidTest/assets/wav/sample.wav.0.dump @@ -25,6 +25,7 @@ track 0: language = null drmInitData = - initializationData: + total output bytes = 88200 sample count = 3 sample 0: time = 0 diff --git a/library/core/src/androidTest/assets/wav/sample.wav.1.dump b/library/core/src/androidTest/assets/wav/sample.wav.1.dump index e59239bff8..d4758e65b5 100644 --- a/library/core/src/androidTest/assets/wav/sample.wav.1.dump +++ b/library/core/src/androidTest/assets/wav/sample.wav.1.dump @@ -25,6 +25,7 @@ track 0: language = null drmInitData = - initializationData: + total output bytes = 58802 sample count = 2 sample 0: time = 333310 diff --git a/library/core/src/androidTest/assets/wav/sample.wav.2.dump b/library/core/src/androidTest/assets/wav/sample.wav.2.dump index c80a260385..ea33c62423 100644 --- a/library/core/src/androidTest/assets/wav/sample.wav.2.dump +++ b/library/core/src/androidTest/assets/wav/sample.wav.2.dump @@ -25,6 +25,7 @@ track 0: language = null drmInitData = - initializationData: + total output bytes = 29402 sample count = 1 sample 0: time = 666643 diff --git a/library/core/src/androidTest/assets/wav/sample.wav.3.dump b/library/core/src/androidTest/assets/wav/sample.wav.3.dump index 9f25028923..de0d8f22d0 100644 --- a/library/core/src/androidTest/assets/wav/sample.wav.3.dump +++ b/library/core/src/androidTest/assets/wav/sample.wav.3.dump @@ -25,6 +25,7 @@ track 0: language = null drmInitData = - initializationData: + total output bytes = 2 sample count = 1 sample 0: time = 999977 diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorInput.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorInput.java index 5cb11fdd81..7f6398dd5a 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorInput.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeExtractorInput.java @@ -91,23 +91,16 @@ public final class FakeExtractorInput implements ExtractorInput { @Override public int read(byte[] target, int offset, int length) throws IOException { + checkIOException(readPosition, failedReadPositions); length = getReadLength(length); - if (readFully(target, offset, length, true)) { - return length; - } - return C.RESULT_END_OF_INPUT; + return readFullyInternal(target, offset, length, true) ? length : C.RESULT_END_OF_INPUT; } @Override public boolean readFully(byte[] target, int offset, int length, boolean allowEndOfInput) throws IOException { - if (!checkXFully(allowEndOfInput, readPosition, length, failedReadPositions)) { - return false; - } - System.arraycopy(data, readPosition, target, offset, length); - readPosition += length; - peekPosition = readPosition; - return true; + checkIOException(readPosition, failedReadPositions); + return readFullyInternal(target, offset, length, allowEndOfInput); } @Override @@ -117,21 +110,15 @@ public final class FakeExtractorInput implements ExtractorInput { @Override public int skip(int length) throws IOException { + checkIOException(readPosition, failedReadPositions); length = getReadLength(length); - if (skipFully(length, true)) { - return length; - } - return C.RESULT_END_OF_INPUT; + return skipFullyInternal(length, true) ? length : C.RESULT_END_OF_INPUT; } @Override public boolean skipFully(int length, boolean allowEndOfInput) throws IOException { - if (!checkXFully(allowEndOfInput, readPosition, length, failedReadPositions)) { - return false; - } - readPosition += length; - peekPosition = readPosition; - return true; + checkIOException(readPosition, failedReadPositions); + return skipFullyInternal(length, allowEndOfInput); } @Override @@ -142,7 +129,8 @@ public final class FakeExtractorInput implements ExtractorInput { @Override public boolean peekFully(byte[] target, int offset, int length, boolean allowEndOfInput) throws IOException { - if (!checkXFully(allowEndOfInput, peekPosition, length, failedPeekPositions)) { + checkIOException(peekPosition, failedPeekPositions); + if (!checkXFully(allowEndOfInput, peekPosition, length)) { return false; } System.arraycopy(data, peekPosition, target, offset, length); @@ -157,7 +145,8 @@ public final class FakeExtractorInput implements ExtractorInput { @Override public boolean advancePeekPosition(int length, boolean allowEndOfInput) throws IOException { - if (!checkXFully(allowEndOfInput, peekPosition, length, failedPeekPositions)) { + checkIOException(peekPosition, failedPeekPositions); + if (!checkXFully(allowEndOfInput, peekPosition, length)) { return false; } peekPosition += length; @@ -196,13 +185,17 @@ public final class FakeExtractorInput implements ExtractorInput { throw e; } - private boolean checkXFully(boolean allowEndOfInput, int position, int length, - SparseBooleanArray failedPositions) throws IOException { + private void checkIOException(int position, SparseBooleanArray failedPositions) + throws SimulatedIOException { if (simulateIOErrors && !failedPositions.get(position)) { failedPositions.put(position, true); peekPosition = readPosition; throw new SimulatedIOException("Simulated IO error at position: " + position); } + } + + private boolean checkXFully(boolean allowEndOfInput, int position, int length) + throws EOFException { if (length > 0 && position == data.length) { if (allowEndOfInput) { return false; @@ -230,6 +223,26 @@ public final class FakeExtractorInput implements ExtractorInput { return Math.min(requestedLength, data.length - readPosition); } + private boolean readFullyInternal(byte[] target, int offset, int length, boolean allowEndOfInput) + throws EOFException { + if (!checkXFully(allowEndOfInput, readPosition, length)) { + return false; + } + System.arraycopy(data, readPosition, target, offset, length); + readPosition += length; + peekPosition = readPosition; + return true; + } + + private boolean skipFullyInternal(int length, boolean allowEndOfInput) throws EOFException { + if (!checkXFully(allowEndOfInput, readPosition, length)) { + return false; + } + readPosition += length; + peekPosition = readPosition; + return true; + } + /** * Builder of {@link FakeExtractorInput} instances. */ diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackOutput.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackOutput.java index b14e6f60ef..f8e5407421 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackOutput.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTrackOutput.java @@ -160,6 +160,7 @@ public final class FakeTrackOutput implements TrackOutput, Dumper.Dumpable { } dumper.endBlock().endBlock(); + dumper.add("total output bytes", sampleData.length); dumper.add("sample count", sampleTimesUs.size()); for (int i = 0; i < sampleTimesUs.size(); i++) { From ff1bb2f70287d8ddf1d9d1ddd3ccd92724db7ba1 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 9 Jan 2018 06:52:54 -0800 Subject: [PATCH 1030/2472] Apply SeekParameters to DASH + SmoothStreaming playbacks Issue: #2882 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=181314086 --- RELEASENOTES.md | 3 +- .../android/exoplayer2/SeekParameters.java | 18 +++++++++ .../source/ExtractorMediaPeriod.java | 24 +----------- .../source/chunk/ChunkSampleStream.java | 19 ++++++++- .../exoplayer2/source/chunk/ChunkSource.java | 11 ++++++ .../google/android/exoplayer2/util/Util.java | 39 +++++++++++++++++++ .../source/dash/DashChunkSource.java | 38 +++++++++++++++--- .../source/dash/DashMediaPeriod.java | 5 +++ .../source/dash/DefaultDashChunkSource.java | 19 +++++++++ .../smoothstreaming/DefaultSsChunkSource.java | 39 +++++++++++++------ .../source/smoothstreaming/SsChunkSource.java | 25 ++++++++++-- .../source/smoothstreaming/SsMediaPeriod.java | 5 +++ .../exoplayer2/testutil/FakeChunkSource.java | 14 +++++++ 13 files changed, 213 insertions(+), 46 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 2d14d00a49..80381075b8 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -27,8 +27,7 @@ performed. The `SeekParameters` class contains defaults for exact seeking and seeking to the closest sync points before, either side or after specified seek positions. - * Note: `SeekParameters` are only currently effective when playing - `ExtractorMediaSource`s (i.e. progressive streams). + * Note: `SeekParameters` are not currently supported when playing HLS streams. * DASH: Support DASH manifest EventStream elements. * HLS: Add opt-in support for chunkless preparation in HLS. This allows an HLS source to finish preparation without downloading any chunks, which can diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SeekParameters.java b/library/core/src/main/java/com/google/android/exoplayer2/SeekParameters.java index 8643b3999e..2df9840cf8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SeekParameters.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SeekParameters.java @@ -69,4 +69,22 @@ public final class SeekParameters { this.toleranceBeforeUs = toleranceBeforeUs; this.toleranceAfterUs = toleranceAfterUs; } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + SeekParameters other = (SeekParameters) obj; + return toleranceBeforeUs == other.toleranceBeforeUs + && toleranceAfterUs == other.toleranceAfterUs; + } + + @Override + public int hashCode() { + return (31 * (int) toleranceBeforeUs) + (int) toleranceAfterUs; + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java index e5d1fae7bd..76d9d22648 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -378,28 +378,8 @@ import java.util.Arrays; return 0; } SeekPoints seekPoints = seekMap.getSeekPoints(positionUs); - long minPositionUs = - Util.subtractWithOverflowDefault( - positionUs, seekParameters.toleranceBeforeUs, Long.MIN_VALUE); - long maxPositionUs = - Util.addWithOverflowDefault(positionUs, seekParameters.toleranceAfterUs, Long.MAX_VALUE); - long firstPointUs = seekPoints.first.timeUs; - boolean firstPointValid = minPositionUs <= firstPointUs && firstPointUs <= maxPositionUs; - long secondPointUs = seekPoints.second.timeUs; - boolean secondPointValid = minPositionUs <= secondPointUs && secondPointUs <= maxPositionUs; - if (firstPointValid && secondPointValid) { - if (Math.abs(firstPointUs - positionUs) <= Math.abs(secondPointUs - positionUs)) { - return firstPointUs; - } else { - return secondPointUs; - } - } else if (firstPointValid) { - return firstPointUs; - } else if (secondPointValid) { - return secondPointUs; - } else { - return minPositionUs; - } + return Util.resolveSeekPositionUs( + positionUs, seekParameters, seekPoints.first.timeUs, seekPoints.second.timeUs); } // SampleStream methods. diff --git a/library/core/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 index a96bc2dcd0..947664720b 100644 --- a/library/core/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 @@ -19,6 +19,7 @@ import android.util.Log; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; +import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SampleQueue; @@ -42,7 +43,8 @@ public class ChunkSampleStream implements SampleStream, S private static final String TAG = "ChunkSampleStream"; - private final int primaryTrackType; + public final int primaryTrackType; + private final int[] embeddedTrackTypes; private final boolean[] embeddedTracksSelected; private final T chunkSource; @@ -180,6 +182,21 @@ public class ChunkSampleStream implements SampleStream, S } } + /** + * Adjusts a seek position given the specified {@link SeekParameters}. Chunk boundaries are used + * as sync points. + * + * @param positionUs The seek position in microseconds. + * @param seekParameters Parameters that control how the seek is performed. + * @return The adjusted seek position, in microseconds. + */ + public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { + // TODO: Using this method to adjust a seek position and then passing the adjusted position to + // seekToUs does not handle small discrepancies between the chunk boundary timestamps obtained + // from the chunk source and the timestamps of the samples in the chunks. + return chunkSource.getAdjustedSeekPositionUs(positionUs, seekParameters); + } + /** * Seeks to the specified position in microseconds. * diff --git a/library/core/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 index b04dc7cbdb..568461c206 100644 --- a/library/core/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 @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source.chunk; +import com.google.android.exoplayer2.SeekParameters; import java.io.IOException; import java.util.List; @@ -23,6 +24,16 @@ import java.util.List; */ public interface ChunkSource { + /** + * Adjusts a seek position given the specified {@link SeekParameters}. Chunk boundaries are used + * as sync points. + * + * @param positionUs The seek position in microseconds. + * @param seekParameters Parameters that control how the seek is performed. + * @return The adjusted seek position, in microseconds. + */ + long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters); + /** * If the source is currently having difficulty providing chunks, then this method throws the * underlying error. Otherwise does nothing. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java index a5f5222820..b3cc282717 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -34,6 +34,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.upstream.DataSource; import java.io.ByteArrayOutputStream; import java.io.Closeable; @@ -762,6 +763,44 @@ public final class Util { return Math.round((double) mediaDuration / speed); } + /** + * Resolves a seek given the requested seek position, a {@link SeekParameters} and two candidate + * sync points. + * + * @param positionUs The requested seek position, in microseocnds. + * @param seekParameters The {@link SeekParameters}. + * @param firstSyncUs The first candidate seek point, in micrseconds. + * @param secondSyncUs The second candidate seek point, in microseconds. May equal {@code + * firstSyncUs} if there's only one candidate. + * @return The resolved seek position, in microseconds. + */ + public static long resolveSeekPositionUs( + long positionUs, SeekParameters seekParameters, long firstSyncUs, long secondSyncUs) { + if (SeekParameters.EXACT.equals(seekParameters)) { + return positionUs; + } + long minPositionUs = + subtractWithOverflowDefault(positionUs, seekParameters.toleranceBeforeUs, Long.MIN_VALUE); + long maxPositionUs = + addWithOverflowDefault(positionUs, seekParameters.toleranceAfterUs, Long.MAX_VALUE); + boolean firstSyncPositionValid = minPositionUs <= firstSyncUs && firstSyncUs <= maxPositionUs; + boolean secondSyncPositionValid = + minPositionUs <= secondSyncUs && secondSyncUs <= maxPositionUs; + if (firstSyncPositionValid && secondSyncPositionValid) { + if (Math.abs(firstSyncUs - positionUs) <= Math.abs(secondSyncUs - positionUs)) { + return firstSyncUs; + } else { + return secondSyncUs; + } + } else if (firstSyncPositionValid) { + return firstSyncUs; + } else if (secondSyncPositionValid) { + return secondSyncUs; + } else { + return minPositionUs; + } + } + /** * Converts a list of integers to a primitive array. * diff --git a/library/dash/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 index 4e25c0e333..167a8d486c 100644 --- a/library/dash/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 @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source.dash; +import android.os.SystemClock; import com.google.android.exoplayer2.source.chunk.ChunkSource; import com.google.android.exoplayer2.source.dash.manifest.DashManifest; import com.google.android.exoplayer2.trackselection.TrackSelection; @@ -25,15 +26,40 @@ import com.google.android.exoplayer2.upstream.LoaderErrorThrower; */ public interface DashChunkSource extends ChunkSource { + /** Factory for {@link DashChunkSource}s. */ interface Factory { - DashChunkSource createDashChunkSource(LoaderErrorThrower manifestLoaderErrorThrower, - DashManifest manifest, int periodIndex, int[] adaptationSetIndices, - TrackSelection trackSelection, int type, long elapsedRealtimeOffsetMs, - boolean enableEventMessageTrack, boolean enableCea608Track); - + /** + * @param manifestLoaderErrorThrower Throws errors affecting loading of manifests. + * @param manifest The initial manifest. + * @param periodIndex The index of the corresponding period in the manifest. + * @param adaptationSetIndices The indices of the corresponding adaptation sets in the period. + * @param trackSelection The track selection. + * @param elapsedRealtimeOffsetMs If known, an estimate of the instantaneous difference between + * server-side unix time and {@link SystemClock#elapsedRealtime()} in milliseconds, + * specified as the server's unix time minus the local elapsed time. If unknown, set to 0. + * @param enableEventMessageTrack Whether the chunks generated by the source may output an event + * message track. + * @param enableCea608Track Whether the chunks generated by the source may output a CEA-608 + * track. + * @return The created {@link DashChunkSource}. + */ + DashChunkSource createDashChunkSource( + LoaderErrorThrower manifestLoaderErrorThrower, + DashManifest manifest, + int periodIndex, + int[] adaptationSetIndices, + TrackSelection trackSelection, + int type, + long elapsedRealtimeOffsetMs, + boolean enableEventMessageTrack, + boolean enableCea608Track); } + /** + * Updates the manifest. + * + * @param newManifest The new manifest. + */ void updateManifest(DashManifest newManifest, int periodIndex); - } diff --git a/library/dash/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 index a8f9203cbf..8a69f98653 100644 --- a/library/dash/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 @@ -309,6 +309,11 @@ import java.util.Map; @Override public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { + for (ChunkSampleStream sampleStream : sampleStreams) { + if (sampleStream.primaryTrackType == C.TRACK_TYPE_VIDEO) { + return sampleStream.getAdjustedSeekPositionUs(positionUs, seekParameters); + } + } return positionUs; } diff --git a/library/dash/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 index b254c4f09a..1162762f7c 100644 --- a/library/dash/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 @@ -19,6 +19,7 @@ import android.net.Uri; import android.os.SystemClock; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.extractor.ChunkIndex; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.SeekMap; @@ -142,6 +143,24 @@ public class DefaultDashChunkSource implements DashChunkSource { } } + @Override + public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { + // Segments are aligned across representations, so any segment index will do. + for (RepresentationHolder representationHolder : representationHolders) { + if (representationHolder.segmentIndex != null) { + int segmentNum = representationHolder.getSegmentNum(positionUs); + long firstSyncUs = representationHolder.getSegmentStartTimeUs(segmentNum); + long secondSyncUs = + firstSyncUs < positionUs && segmentNum < representationHolder.getSegmentCount() - 1 + ? representationHolder.getSegmentStartTimeUs(segmentNum + 1) + : firstSyncUs; + return Util.resolveSeekPositionUs(positionUs, seekParameters, firstSyncUs, secondSyncUs); + } + } + // We don't have a segment index to adjust the seek position with yet. + return positionUs; + } + @Override public void updateManifest(DashManifest newManifest, int newPeriodIndex) { try { diff --git a/library/smoothstreaming/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 index 5a6493b702..79014d6f4a 100644 --- a/library/smoothstreaming/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 @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.source.smoothstreaming; import android.net.Uri; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor; import com.google.android.exoplayer2.extractor.mp4.Track; import com.google.android.exoplayer2.extractor.mp4.TrackEncryptionBox; @@ -34,6 +35,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.LoaderErrorThrower; +import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.util.List; @@ -62,7 +64,7 @@ public class DefaultSsChunkSource implements SsChunkSource { } private final LoaderErrorThrower manifestLoaderErrorThrower; - private final int elementIndex; + private final int streamElementIndex; private final TrackSelection trackSelection; private final ChunkExtractorWrapper[] extractorWrappers; private final DataSource dataSource; @@ -75,22 +77,25 @@ public class DefaultSsChunkSource implements SsChunkSource { /** * @param manifestLoaderErrorThrower Throws errors affecting loading of manifests. * @param manifest The initial manifest. - * @param elementIndex The index of the stream element in the manifest. + * @param streamElementIndex The index of the stream element in the manifest. * @param trackSelection The track selection. * @param dataSource A {@link DataSource} suitable for loading the media data. * @param trackEncryptionBoxes Track encryption boxes for the stream. */ - public DefaultSsChunkSource(LoaderErrorThrower manifestLoaderErrorThrower, SsManifest manifest, - int elementIndex, TrackSelection trackSelection, DataSource dataSource, + public DefaultSsChunkSource( + LoaderErrorThrower manifestLoaderErrorThrower, + SsManifest manifest, + int streamElementIndex, + TrackSelection trackSelection, + DataSource dataSource, TrackEncryptionBox[] trackEncryptionBoxes) { this.manifestLoaderErrorThrower = manifestLoaderErrorThrower; this.manifest = manifest; - this.elementIndex = elementIndex; + this.streamElementIndex = streamElementIndex; this.trackSelection = trackSelection; this.dataSource = dataSource; - StreamElement streamElement = manifest.streamElements[elementIndex]; - + StreamElement streamElement = manifest.streamElements[streamElementIndex]; extractorWrappers = new ChunkExtractorWrapper[trackSelection.length()]; for (int i = 0; i < extractorWrappers.length; i++) { int manifestTrackIndex = trackSelection.getIndexInTrackGroup(i); @@ -106,11 +111,23 @@ public class DefaultSsChunkSource implements SsChunkSource { } } + @Override + public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { + StreamElement streamElement = manifest.streamElements[streamElementIndex]; + int chunkIndex = streamElement.getChunkIndex(positionUs); + long firstSyncUs = streamElement.getStartTimeUs(chunkIndex); + long secondSyncUs = + firstSyncUs < positionUs && chunkIndex < streamElement.chunkCount - 1 + ? streamElement.getStartTimeUs(chunkIndex + 1) + : firstSyncUs; + return Util.resolveSeekPositionUs(positionUs, seekParameters, firstSyncUs, secondSyncUs); + } + @Override public void updateManifest(SsManifest newManifest) { - StreamElement currentElement = manifest.streamElements[elementIndex]; + StreamElement currentElement = manifest.streamElements[streamElementIndex]; int currentElementChunkCount = currentElement.chunkCount; - StreamElement newElement = newManifest.streamElements[elementIndex]; + StreamElement newElement = newManifest.streamElements[streamElementIndex]; if (currentElementChunkCount == 0 || newElement.chunkCount == 0) { // There's no overlap between the old and new elements because at least one is empty. currentManifestChunkOffset += currentElementChunkCount; @@ -155,7 +172,7 @@ public class DefaultSsChunkSource implements SsChunkSource { return; } - StreamElement streamElement = manifest.streamElements[elementIndex]; + StreamElement streamElement = manifest.streamElements[streamElementIndex]; if (streamElement.chunkCount == 0) { // There aren't any chunks for us to load. out.endOfStream = !manifest.isLive; @@ -229,7 +246,7 @@ public class DefaultSsChunkSource implements SsChunkSource { return C.TIME_UNSET; } - StreamElement currentElement = manifest.streamElements[elementIndex]; + StreamElement currentElement = manifest.streamElements[streamElementIndex]; int lastChunkIndex = currentElement.chunkCount - 1; long lastChunkEndTimeUs = currentElement.getStartTimeUs(lastChunkIndex) + currentElement.getChunkDurationUs(lastChunkIndex); diff --git a/library/smoothstreaming/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 index e8815ff424..48491cd0bd 100644 --- a/library/smoothstreaming/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 @@ -26,14 +26,31 @@ import com.google.android.exoplayer2.upstream.LoaderErrorThrower; */ public interface SsChunkSource extends ChunkSource { + /** Factory for {@link SsChunkSource}s. */ interface Factory { - SsChunkSource createChunkSource(LoaderErrorThrower manifestLoaderErrorThrower, - SsManifest manifest, int elementIndex, TrackSelection trackSelection, + /** + * Creates a new {@link SsChunkSource}. + * + * @param manifestLoaderErrorThrower Throws errors affecting loading of manifests. + * @param manifest The initial manifest. + * @param streamElementIndex The index of the corresponding stream element in the manifest. + * @param trackSelection The track selection. + * @param trackEncryptionBoxes Track encryption boxes for the stream. + * @return The created {@link SsChunkSource}. + */ + SsChunkSource createChunkSource( + LoaderErrorThrower manifestLoaderErrorThrower, + SsManifest manifest, + int streamElementIndex, + TrackSelection trackSelection, TrackEncryptionBox[] trackEncryptionBoxes); - } + /** + * Updates the manifest. + * + * @param newManifest The new manifest. + */ void updateManifest(SsManifest newManifest); - } diff --git a/library/smoothstreaming/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 index 5ee60bdeed..99804ca809 100644 --- a/library/smoothstreaming/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 @@ -185,6 +185,11 @@ import java.util.ArrayList; @Override public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { + for (ChunkSampleStream sampleStream : sampleStreams) { + if (sampleStream.primaryTrackType == C.TRACK_TYPE_VIDEO) { + return sampleStream.getAdjustedSeekPositionUs(positionUs, seekParameters); + } + } return positionUs; } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeChunkSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeChunkSource.java index 28f5926bfa..6ff18e0b3d 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeChunkSource.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeChunkSource.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.testutil; import android.net.Uri; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.source.chunk.Chunk; import com.google.android.exoplayer2.source.chunk.ChunkHolder; import com.google.android.exoplayer2.source.chunk.ChunkSource; @@ -28,6 +29,8 @@ import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.Util; + import java.io.IOException; import java.util.List; @@ -71,6 +74,17 @@ public final class FakeChunkSource implements ChunkSource { this.dataSet = dataSet; } + @Override + public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { + int chunkIndex = dataSet.getChunkIndexByPosition(positionUs); + long firstSyncUs = dataSet.getStartTime(chunkIndex); + long secondSyncUs = + firstSyncUs < positionUs && chunkIndex < dataSet.getChunkCount() - 1 + ? dataSet.getStartTime(chunkIndex + 1) + : firstSyncUs; + return Util.resolveSeekPositionUs(positionUs, seekParameters, firstSyncUs, secondSyncUs); + } + @Override public void maybeThrowError() throws IOException { // Do nothing. From 96e490d7fef3b5dee8cdcefb36cf35cf939eee78 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 9 Jan 2018 08:04:36 -0800 Subject: [PATCH 1031/2472] Make it possible to subclass LibvpxVideoRenderer Make LibvpxVideoRenderer non-final and add protected methods to match MediaCodecVideoRenderer. Reorganize methods to separate BaseRenderer, protected and internal methods. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=181320714 --- .../ext/vp9/LibvpxVideoRenderer.java | 740 ++++++++++-------- 1 file changed, 435 insertions(+), 305 deletions(-) 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 ac944a7b01..108a89f56a 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 @@ -20,7 +20,9 @@ import android.graphics.Canvas; import android.os.Handler; import android.os.Looper; import android.os.SystemClock; +import android.support.annotation.CallSuper; import android.support.annotation.IntDef; +import android.support.annotation.Nullable; import android.view.Surface; import com.google.android.exoplayer2.BaseRenderer; import com.google.android.exoplayer2.C; @@ -43,10 +45,8 @@ import com.google.android.exoplayer2.video.VideoRendererEventListener.EventDispa import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -/** - * Decodes and renders video using the native VP9 decoder. - */ -public final class LibvpxVideoRenderer extends BaseRenderer { +/** Decodes and renders video using the native VP9 decoder. */ +public class LibvpxVideoRenderer extends BaseRenderer { @Retention(RetentionPolicy.SOURCE) @IntDef({REINITIALIZATION_STATE_NONE, REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM, @@ -101,7 +101,6 @@ public final class LibvpxVideoRenderer extends BaseRenderer { private final DecoderInputBuffer flagsOnlyBuffer; private final DrmSessionManager drmSessionManager; - private DecoderCounters decoderCounters; private Format format; private VpxDecoder decoder; private VpxInputBuffer inputBuffer; @@ -132,6 +131,8 @@ public final class LibvpxVideoRenderer extends BaseRenderer { private int consecutiveDroppedFrameCount; private int buffersInCodecCount; + protected DecoderCounters decoderCounters; + /** * @param scaleToFit Whether video frames should be scaled to fit when rendering. * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer @@ -196,6 +197,8 @@ public final class LibvpxVideoRenderer extends BaseRenderer { decoderReinitializationState = REINITIALIZATION_STATE_NONE; } + // BaseRenderer implementation. + @Override public int supportsFormat(Format format) { if (!VpxLibrary.isAvailable() || !MimeTypes.VIDEO_VP9.equalsIgnoreCase(format.sampleMimeType)) { @@ -247,273 +250,6 @@ public final class LibvpxVideoRenderer extends BaseRenderer { } } - private boolean drainOutputBuffer(long positionUs) throws ExoPlaybackException, - VpxDecoderException { - // Acquire outputBuffer either from nextOutputBuffer or from the decoder. - if (outputBuffer == null) { - if (nextOutputBuffer != null) { - outputBuffer = nextOutputBuffer; - nextOutputBuffer = null; - } else { - outputBuffer = decoder.dequeueOutputBuffer(); - } - if (outputBuffer == null) { - return false; - } - decoderCounters.skippedOutputBufferCount += outputBuffer.skippedOutputBufferCount; - buffersInCodecCount -= outputBuffer.skippedOutputBufferCount; - } - - if (nextOutputBuffer == null) { - nextOutputBuffer = decoder.dequeueOutputBuffer(); - } - - if (outputBuffer.isEndOfStream()) { - if (decoderReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM) { - // We're waiting to re-initialize the decoder, and have now processed all final buffers. - releaseDecoder(); - maybeInitDecoder(); - } else { - outputBuffer.release(); - outputBuffer = null; - outputStreamEnded = true; - } - return false; - } - - 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 (isBufferLate(outputBuffer.timeUs - positionUs)) { - forceRenderFrame = false; - skipBuffer(); - buffersInCodecCount--; - return true; - } - return false; - } - - if (forceRenderFrame) { - forceRenderFrame = false; - renderBuffer(); - buffersInCodecCount--; - return true; - } - - final long nextOutputBufferTimeUs = - nextOutputBuffer != null && !nextOutputBuffer.isEndOfStream() - ? nextOutputBuffer.timeUs : C.TIME_UNSET; - - long earlyUs = outputBuffer.timeUs - positionUs; - if (shouldDropBuffersToKeyframe(earlyUs) && maybeDropBuffersToKeyframe(positionUs)) { - forceRenderFrame = true; - return false; - } else if (shouldDropOutputBuffer( - outputBuffer.timeUs, nextOutputBufferTimeUs, positionUs, joiningDeadlineMs)) { - dropBuffer(); - buffersInCodecCount--; - return true; - } - - // 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 && earlyUs <= 30000)) { - renderBuffer(); - buffersInCodecCount--; - } - 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 C#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. - */ - private boolean shouldDropOutputBuffer(long outputBufferTimeUs, long nextOutputBufferTimeUs, - long positionUs, long joiningDeadlineMs) { - return isBufferLate(outputBufferTimeUs - positionUs) - && (joiningDeadlineMs != C.TIME_UNSET || nextOutputBufferTimeUs != C.TIME_UNSET); - } - - /** - * Returns whether to drop all buffers from the buffer being processed to the keyframe at or after - * the current playback position, if possible. - * - * @param earlyUs The time until the current buffer should be presented in microseconds. A - * negative value indicates that the buffer is late. - */ - private boolean shouldDropBuffersToKeyframe(long earlyUs) { - return isBufferVeryLate(earlyUs); - } - - private void renderBuffer() { - 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 { - 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() { - updateDroppedBufferCounters(1); - outputBuffer.release(); - outputBuffer = null; - } - - private boolean maybeDropBuffersToKeyframe(long positionUs) throws ExoPlaybackException { - int droppedSourceBufferCount = skipSource(positionUs); - if (droppedSourceBufferCount == 0) { - return false; - } - decoderCounters.droppedToKeyframeCount++; - // We dropped some buffers to catch up, so update the decoder counters and flush the codec, - // which releases all pending buffers buffers including the current output buffer. - updateDroppedBufferCounters(buffersInCodecCount + droppedSourceBufferCount); - flushDecoder(); - return true; - } - - private void updateDroppedBufferCounters(int droppedBufferCount) { - decoderCounters.droppedBufferCount += droppedBufferCount; - droppedFrames += droppedBufferCount; - consecutiveDroppedFrameCount += droppedBufferCount; - decoderCounters.maxConsecutiveDroppedBufferCount = Math.max(consecutiveDroppedFrameCount, - decoderCounters.maxConsecutiveDroppedBufferCount); - if (droppedFrames >= maxDroppedFramesToNotify) { - maybeNotifyDroppedFrames(); - } - } - - private void skipBuffer() { - decoderCounters.skippedOutputBufferCount++; - outputBuffer.release(); - outputBuffer = null; - } - - private void renderRgbFrame(VpxOutputBuffer outputBuffer, boolean scale) { - if (bitmap == null || bitmap.getWidth() != outputBuffer.width - || bitmap.getHeight() != outputBuffer.height) { - bitmap = Bitmap.createBitmap(outputBuffer.width, outputBuffer.height, Bitmap.Config.RGB_565); - } - bitmap.copyPixelsFromBuffer(outputBuffer.data); - Canvas canvas = surface.lockCanvas(null); - if (scale) { - canvas.scale(((float) canvas.getWidth()) / outputBuffer.width, - ((float) canvas.getHeight()) / outputBuffer.height); - } - canvas.drawBitmap(bitmap, 0, 0, null); - surface.unlockCanvasAndPost(canvas); - } - - private boolean feedInputBuffer() throws VpxDecoderException, ExoPlaybackException { - if (decoder == null || decoderReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM - || inputStreamEnded) { - // We need to reinitialize the decoder or the input stream has ended. - return false; - } - - if (inputBuffer == null) { - inputBuffer = decoder.dequeueInputBuffer(); - if (inputBuffer == null) { - return false; - } - } - - if (decoderReinitializationState == REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM) { - inputBuffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); - decoder.queueInputBuffer(inputBuffer); - inputBuffer = null; - decoderReinitializationState = REINITIALIZATION_STATE_WAIT_END_OF_STREAM; - return false; - } - - int result; - if (waitingForKeys) { - // We've already read an encrypted sample into buffer, and are waiting for keys. - result = C.RESULT_BUFFER_READ; - } else { - result = readSource(formatHolder, inputBuffer, false); - } - - if (result == C.RESULT_NOTHING_READ) { - return false; - } - if (result == C.RESULT_FORMAT_READ) { - onInputFormatChanged(formatHolder.format); - return true; - } - if (inputBuffer.isEndOfStream()) { - inputStreamEnded = true; - decoder.queueInputBuffer(inputBuffer); - inputBuffer = null; - return false; - } - boolean bufferEncrypted = inputBuffer.isEncrypted(); - waitingForKeys = shouldWaitForKeys(bufferEncrypted); - if (waitingForKeys) { - return false; - } - inputBuffer.flip(); - inputBuffer.colorInfo = formatHolder.format.colorInfo; - decoder.queueInputBuffer(inputBuffer); - buffersInCodecCount++; - decoderReceivedBuffers = true; - decoderCounters.inputBufferCount++; - inputBuffer = null; - return true; - } - - private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException { - if (drmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) { - return false; - } - @DrmSession.State int drmSessionState = drmSession.getState(); - if (drmSessionState == DrmSession.STATE_ERROR) { - throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex()); - } - return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS; - } - - private void flushDecoder() throws ExoPlaybackException { - waitingForKeys = false; - forceRenderFrame = false; - buffersInCodecCount = 0; - if (decoderReinitializationState != REINITIALIZATION_STATE_NONE) { - releaseDecoder(); - maybeInitDecoder(); - } else { - inputBuffer = null; - if (outputBuffer != null) { - outputBuffer.release(); - outputBuffer = null; - } - if (nextOutputBuffer != null) { - nextOutputBuffer.release(); - nextOutputBuffer = null; - } - decoder.flush(); - decoderReceivedBuffers = false; - } - } @Override public boolean isEnded() { @@ -605,42 +341,53 @@ public final class LibvpxVideoRenderer extends BaseRenderer { } } - private void maybeInitDecoder() throws ExoPlaybackException { - if (decoder != null) { - return; - } + /** + * Called when a decoder has been created and configured. + * + *

          The default implementation is a no-op. + * + * @param name The name of the decoder that was initialized. + * @param initializedTimestampMs {@link SystemClock#elapsedRealtime()} when initialization + * finished. + * @param initializationDurationMs The time taken to initialize the decoder, in milliseconds. + */ + @CallSuper + protected void onDecoderInitialized( + String name, long initializedTimestampMs, long initializationDurationMs) { + eventDispatcher.decoderInitialized(name, initializedTimestampMs, initializationDurationMs); + } - drmSession = pendingDrmSession; - ExoMediaCrypto mediaCrypto = null; - if (drmSession != null) { - mediaCrypto = drmSession.getMediaCrypto(); - if (mediaCrypto == null) { - DrmSessionException drmError = drmSession.getError(); - if (drmError != null) { - throw ExoPlaybackException.createForRenderer(drmError, getIndex()); - } - // The drm session isn't open yet. - return; + /** + * Flushes the decoder. + * + * @throws ExoPlaybackException If an error occurs reinitializing a decoder. + */ + @CallSuper + protected void flushDecoder() throws ExoPlaybackException { + waitingForKeys = false; + forceRenderFrame = false; + buffersInCodecCount = 0; + if (decoderReinitializationState != REINITIALIZATION_STATE_NONE) { + releaseDecoder(); + maybeInitDecoder(); + } else { + inputBuffer = null; + if (outputBuffer != null) { + outputBuffer.release(); + outputBuffer = null; } - } - - try { - long codecInitializingTimestamp = SystemClock.elapsedRealtime(); - TraceUtil.beginSection("createVpxDecoder"); - decoder = new VpxDecoder(NUM_INPUT_BUFFERS, NUM_OUTPUT_BUFFERS, INITIAL_INPUT_BUFFER_SIZE, - mediaCrypto, disableLoopFilter); - decoder.setOutputMode(outputMode); - TraceUtil.endSection(); - long codecInitializedTimestamp = SystemClock.elapsedRealtime(); - eventDispatcher.decoderInitialized(decoder.getName(), codecInitializedTimestamp, - codecInitializedTimestamp - codecInitializingTimestamp); - decoderCounters.decoderInitCount++; - } catch (VpxDecoderException e) { - throw ExoPlaybackException.createForRenderer(e, getIndex()); + if (nextOutputBuffer != null) { + nextOutputBuffer.release(); + nextOutputBuffer = null; + } + decoder.flush(); + decoderReceivedBuffers = false; } } - private void releaseDecoder() { + /** Releases the decoder. */ + @CallSuper + protected void releaseDecoder() { if (decoder == null) { return; } @@ -657,7 +404,14 @@ public final class LibvpxVideoRenderer extends BaseRenderer { buffersInCodecCount = 0; } - private void onInputFormatChanged(Format newFormat) throws ExoPlaybackException { + /** + * Called when a new format is read from the upstream source. + * + * @param newFormat The new format. + * @throws ExoPlaybackException If an error occurs (re-)initializing the decoder. + */ + @CallSuper + protected void onInputFormatChanged(Format newFormat) throws ExoPlaybackException { Format oldFormat = format; format = newFormat; @@ -692,6 +446,147 @@ public final class LibvpxVideoRenderer extends BaseRenderer { eventDispatcher.inputFormatChanged(format); } + /** + * Called immediately before an input buffer is queued into the decoder. + * + *

          The default implementation is a no-op. + * + * @param buffer The buffer that will be queued. + */ + protected void onQueueInputBuffer(VpxInputBuffer buffer) { + // Do nothing. + } + + /** + * Called when an output buffer is successfully processed. + * + * @param presentationTimeUs The timestamp associated with the output buffer. + */ + @CallSuper + protected void onProcessedOutputBuffer(long presentationTimeUs) { + buffersInCodecCount--; + } + + /** + * 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 C#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) { + return isBufferLate(outputBufferTimeUs - positionUs) + && (joiningDeadlineMs != C.TIME_UNSET || nextOutputBufferTimeUs != C.TIME_UNSET); + } + + /** + * Returns whether to drop all buffers from the buffer being processed to the keyframe at or after + * the current playback position, if possible. + * + * @param earlyUs The time until the current buffer should be presented in microseconds. A + * negative value indicates that the buffer is late. + */ + protected boolean shouldDropBuffersToKeyframe(long earlyUs) { + return isBufferVeryLate(earlyUs); + } + + /** + * Skips the specified output buffer and releases it. + * + * @param outputBuffer The output buffer to skip. + */ + protected void skipOutputBuffer(VpxOutputBuffer outputBuffer) { + decoderCounters.skippedOutputBufferCount++; + outputBuffer.release(); + } + + /** + * Drops the specified output buffer and releases it. + * + * @param outputBuffer The output buffer to drop. + */ + protected void dropOutputBuffer(VpxOutputBuffer outputBuffer) { + updateDroppedBufferCounters(1); + outputBuffer.release(); + } + + /** + * Renders the specified output buffer. + * + *

          The implementation of this method takes ownership of the output buffer and is responsible + * for calling {@link VpxOutputBuffer#release()} either immediately or in the future. + * + * @param outputBuffer The buffer to render. + */ + protected void renderOutputBuffer(VpxOutputBuffer outputBuffer) { + 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) { + dropOutputBuffer(outputBuffer); + } else { + maybeNotifyVideoSizeChanged(outputBuffer.width, outputBuffer.height); + if (renderRgb) { + renderRgbFrame(outputBuffer, scaleToFit); + outputBuffer.release(); + } else /* renderYuv */ { + outputBufferRenderer.setOutputBuffer(outputBuffer); + // The renderer will release the buffer. + } + consecutiveDroppedFrameCount = 0; + decoderCounters.renderedOutputBufferCount++; + maybeNotifyRenderedFirstFrame(); + } + } + + /** + * Drops frames from the current output buffer to the next keyframe at or before the playback + * position. If no such keyframe exists, as the playback position is inside the same group of + * pictures as the buffer being processed, returns {@code false}. Returns {@code true} otherwise. + * + * @param positionUs The current playback position, in microseconds. + * @return Whether any buffers were dropped. + * @throws ExoPlaybackException If an error occurs flushing the decoder. + */ + protected boolean maybeDropBuffersToKeyframe(long positionUs) throws ExoPlaybackException { + int droppedSourceBufferCount = skipSource(positionUs); + if (droppedSourceBufferCount == 0) { + return false; + } + decoderCounters.droppedToKeyframeCount++; + // We dropped some buffers to catch up, so update the decoder counters and flush the decoder, + // which releases all pending buffers buffers including the current output buffer. + updateDroppedBufferCounters(buffersInCodecCount + droppedSourceBufferCount); + flushDecoder(); + return true; + } + + /** + * Updates decoder counters to reflect that {@code droppedBufferCount} additional buffers were + * dropped. + * + * @param droppedBufferCount The number of additional dropped buffers. + */ + protected void updateDroppedBufferCounters(int droppedBufferCount) { + decoderCounters.droppedBufferCount += droppedBufferCount; + droppedFrames += droppedBufferCount; + consecutiveDroppedFrameCount += droppedBufferCount; + decoderCounters.maxConsecutiveDroppedBufferCount = + Math.max(consecutiveDroppedFrameCount, decoderCounters.maxConsecutiveDroppedBufferCount); + if (droppedFrames >= maxDroppedFramesToNotify) { + maybeNotifyDroppedFrames(); + } + } + + // PlayerMessage.Target implementation. + @Override public void handleMessage(int messageType, Object message) throws ExoPlaybackException { if (messageType == C.MSG_SET_SURFACE) { @@ -703,7 +598,10 @@ public final class LibvpxVideoRenderer extends BaseRenderer { } } - private void setOutput(Surface surface, VpxOutputBufferRenderer outputBufferRenderer) { + // Internal methods. + + private void setOutput( + @Nullable Surface surface, @Nullable 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); if (this.surface != surface || this.outputBufferRenderer != outputBufferRenderer) { @@ -737,6 +635,238 @@ public final class LibvpxVideoRenderer extends BaseRenderer { } } + private void maybeInitDecoder() throws ExoPlaybackException { + if (decoder != null) { + return; + } + + drmSession = pendingDrmSession; + ExoMediaCrypto mediaCrypto = null; + if (drmSession != null) { + mediaCrypto = drmSession.getMediaCrypto(); + if (mediaCrypto == null) { + DrmSessionException drmError = drmSession.getError(); + if (drmError != null) { + throw ExoPlaybackException.createForRenderer(drmError, getIndex()); + } + // The drm session isn't open yet. + return; + } + } + + try { + long decoderInitializingTimestamp = SystemClock.elapsedRealtime(); + TraceUtil.beginSection("createVpxDecoder"); + decoder = + new VpxDecoder( + NUM_INPUT_BUFFERS, + NUM_OUTPUT_BUFFERS, + INITIAL_INPUT_BUFFER_SIZE, + mediaCrypto, + disableLoopFilter); + decoder.setOutputMode(outputMode); + TraceUtil.endSection(); + long decoderInitializedTimestamp = SystemClock.elapsedRealtime(); + onDecoderInitialized( + decoder.getName(), + decoderInitializedTimestamp, + decoderInitializedTimestamp - decoderInitializingTimestamp); + decoderCounters.decoderInitCount++; + } catch (VpxDecoderException e) { + throw ExoPlaybackException.createForRenderer(e, getIndex()); + } + } + + private boolean feedInputBuffer() throws VpxDecoderException, ExoPlaybackException { + if (decoder == null + || decoderReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM + || inputStreamEnded) { + // We need to reinitialize the decoder or the input stream has ended. + return false; + } + + if (inputBuffer == null) { + inputBuffer = decoder.dequeueInputBuffer(); + if (inputBuffer == null) { + return false; + } + } + + if (decoderReinitializationState == REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM) { + inputBuffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); + decoder.queueInputBuffer(inputBuffer); + inputBuffer = null; + decoderReinitializationState = REINITIALIZATION_STATE_WAIT_END_OF_STREAM; + return false; + } + + int result; + if (waitingForKeys) { + // We've already read an encrypted sample into buffer, and are waiting for keys. + result = C.RESULT_BUFFER_READ; + } else { + result = readSource(formatHolder, inputBuffer, false); + } + + if (result == C.RESULT_NOTHING_READ) { + return false; + } + if (result == C.RESULT_FORMAT_READ) { + onInputFormatChanged(formatHolder.format); + return true; + } + if (inputBuffer.isEndOfStream()) { + inputStreamEnded = true; + decoder.queueInputBuffer(inputBuffer); + inputBuffer = null; + return false; + } + boolean bufferEncrypted = inputBuffer.isEncrypted(); + waitingForKeys = shouldWaitForKeys(bufferEncrypted); + if (waitingForKeys) { + return false; + } + inputBuffer.flip(); + inputBuffer.colorInfo = formatHolder.format.colorInfo; + onQueueInputBuffer(inputBuffer); + decoder.queueInputBuffer(inputBuffer); + buffersInCodecCount++; + decoderReceivedBuffers = true; + decoderCounters.inputBufferCount++; + inputBuffer = null; + return true; + } + + /** + * Attempts to dequeue an output buffer from the decoder and, if successful, passes it to {@link + * #processOutputBuffer(long)}. + * + * @param positionUs The player's current position. + * @return Whether it may be possible to drain more output data. + * @throws ExoPlaybackException If an error occurs draining the output buffer. + */ + private boolean drainOutputBuffer(long positionUs) + throws ExoPlaybackException, VpxDecoderException { + // Acquire outputBuffer either from nextOutputBuffer or from the decoder. + if (outputBuffer == null) { + if (nextOutputBuffer != null) { + outputBuffer = nextOutputBuffer; + nextOutputBuffer = null; + } else { + outputBuffer = decoder.dequeueOutputBuffer(); + } + if (outputBuffer == null) { + return false; + } + decoderCounters.skippedOutputBufferCount += outputBuffer.skippedOutputBufferCount; + buffersInCodecCount -= outputBuffer.skippedOutputBufferCount; + } + + if (nextOutputBuffer == null) { + nextOutputBuffer = decoder.dequeueOutputBuffer(); + } + + if (outputBuffer.isEndOfStream()) { + if (decoderReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM) { + // We're waiting to re-initialize the decoder, and have now processed all final buffers. + releaseDecoder(); + maybeInitDecoder(); + } else { + outputBuffer.release(); + outputBuffer = null; + outputStreamEnded = true; + } + return false; + } + + return processOutputBuffer(positionUs); + } + + /** + * Processes {@link #outputBuffer} by rendering it, skipping it or doing nothing, and returns + * whether it may be possible to process another output buffer. + * + * @param positionUs The player's current position. + * @return Whether it may be possible to drain another output buffer. + * @throws ExoPlaybackException If an error occurs processing the output buffer. + */ + private boolean processOutputBuffer(long positionUs) throws ExoPlaybackException { + 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 (isBufferLate(outputBuffer.timeUs - positionUs)) { + forceRenderFrame = false; + skipOutputBuffer(outputBuffer); + onProcessedOutputBuffer(outputBuffer.timeUs); + outputBuffer = null; + return true; + } + return false; + } + + if (forceRenderFrame) { + forceRenderFrame = false; + renderOutputBuffer(outputBuffer); + onProcessedOutputBuffer(outputBuffer.timeUs); + outputBuffer = null; + return true; + } + + long nextOutputBufferTimeUs = + nextOutputBuffer != null && !nextOutputBuffer.isEndOfStream() + ? nextOutputBuffer.timeUs + : C.TIME_UNSET; + + long earlyUs = outputBuffer.timeUs - positionUs; + if (shouldDropBuffersToKeyframe(earlyUs) && maybeDropBuffersToKeyframe(positionUs)) { + forceRenderFrame = true; + return false; + } else if (shouldDropOutputBuffer( + outputBuffer.timeUs, nextOutputBufferTimeUs, positionUs, joiningDeadlineMs)) { + dropOutputBuffer(outputBuffer); + onProcessedOutputBuffer(outputBuffer.timeUs); + outputBuffer = null; + return true; + } + + // 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 && earlyUs <= 30000)) { + renderOutputBuffer(outputBuffer); + onProcessedOutputBuffer(outputBuffer.timeUs); + outputBuffer = null; + } + + return false; + } + + private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException { + if (drmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) { + return false; + } + @DrmSession.State int drmSessionState = drmSession.getState(); + if (drmSessionState == DrmSession.STATE_ERROR) { + throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex()); + } + return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS; + } + + private void renderRgbFrame(VpxOutputBuffer outputBuffer, boolean scale) { + if (bitmap == null + || bitmap.getWidth() != outputBuffer.width + || bitmap.getHeight() != outputBuffer.height) { + bitmap = Bitmap.createBitmap(outputBuffer.width, outputBuffer.height, Bitmap.Config.RGB_565); + } + bitmap.copyPixelsFromBuffer(outputBuffer.data); + Canvas canvas = surface.lockCanvas(null); + if (scale) { + canvas.scale( + ((float) canvas.getWidth()) / outputBuffer.width, + ((float) canvas.getHeight()) / outputBuffer.height); + } + canvas.drawBitmap(bitmap, 0, 0, null); + surface.unlockCanvasAndPost(canvas); + } + private void setJoiningDeadlineMs() { joiningDeadlineMs = allowedJoiningTimeMs > 0 ? (SystemClock.elapsedRealtime() + allowedJoiningTimeMs) : C.TIME_UNSET; From 13fbe7b2f23c038cadc10e264e288c7143ca7b68 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 9 Jan 2018 09:46:51 -0800 Subject: [PATCH 1032/2472] Fix cronet extension package name ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=181331715 --- extensions/cronet/src/androidTest/AndroidManifest.xml | 4 ++-- extensions/cronet/src/main/AndroidManifest.xml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/extensions/cronet/src/androidTest/AndroidManifest.xml b/extensions/cronet/src/androidTest/AndroidManifest.xml index 6c4014873d..453cc68478 100644 --- a/extensions/cronet/src/androidTest/AndroidManifest.xml +++ b/extensions/cronet/src/androidTest/AndroidManifest.xml @@ -16,7 +16,7 @@ + package="com.google.android.exoplayer2.ext.cronet"> @@ -28,6 +28,6 @@ + android:targetPackage="com.google.android.exoplayer2.ext.cronet"/> diff --git a/extensions/cronet/src/main/AndroidManifest.xml b/extensions/cronet/src/main/AndroidManifest.xml index c81d95f104..5ba54999f4 100644 --- a/extensions/cronet/src/main/AndroidManifest.xml +++ b/extensions/cronet/src/main/AndroidManifest.xml @@ -14,7 +14,7 @@ --> + package="com.google.android.exoplayer2.ext.cronet"> From 11bae0af5a2ca1cf52ffa440b4ff564d8df7c696 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 10 Jan 2018 02:48:27 -0800 Subject: [PATCH 1033/2472] Add minimal logging for SCTE-35 splice commands in the Demo App ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=181440439 --- .../java/com/google/android/exoplayer2/demo/EventLogger.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java index fa22130eea..7dc7a3567f 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java @@ -38,6 +38,7 @@ import com.google.android.exoplayer2.metadata.id3.Id3Frame; import com.google.android.exoplayer2.metadata.id3.PrivFrame; import com.google.android.exoplayer2.metadata.id3.TextInformationFrame; import com.google.android.exoplayer2.metadata.id3.UrlLinkFrame; +import com.google.android.exoplayer2.metadata.scte35.SpliceCommand; import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; @@ -429,6 +430,10 @@ import java.util.Locale; EventMessage eventMessage = (EventMessage) entry; Log.d(TAG, prefix + String.format("EMSG: scheme=%s, id=%d, value=%s", eventMessage.schemeIdUri, eventMessage.id, eventMessage.value)); + } else if (entry instanceof SpliceCommand) { + String description = + String.format("SCTE-35 splice command: type=%s.", entry.getClass().getSimpleName()); + Log.d(TAG, prefix + description); } } } From 1fc250a9f3c877bb2704096c7848f9687bc15847 Mon Sep 17 00:00:00 2001 From: eguven Date: Wed, 10 Jan 2018 02:51:40 -0800 Subject: [PATCH 1034/2472] Make CacheUtil documentation clearer Also fixed some other Cache related javadoc. Issue: #3374 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=181440687 --- .../exoplayer2/upstream/cache/Cache.java | 34 +++++++---- .../upstream/cache/CacheDataSink.java | 2 +- .../exoplayer2/upstream/cache/CacheUtil.java | 14 +++-- .../upstream/cache/CachedContent.java | 2 +- .../upstream/cache/SimpleCache.java | 6 +- .../upstream/cache/SimpleCacheSpan.java | 59 +++++++++++++++++-- .../upstream/cache/CacheAsserts.java | 22 ++++++- .../upstream/cache/CacheUtilTest.java | 2 +- .../upstream/cache/SimpleCacheTest.java | 16 ++--- .../exoplayer2/testutil/CacheAsserts.java | 22 ++++++- 10 files changed, 138 insertions(+), 41 deletions(-) diff --git a/library/core/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 index 76481bbdf7..171aa0878a 100644 --- a/library/core/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 @@ -127,23 +127,24 @@ public interface Cache { /** * A caller should invoke this method when they require data from a given position for a given * key. - *

          - * If there is a cache entry that overlaps the position, then the returned {@link CacheSpan} + * + *

          If there is a cache entry that overlaps the position, then the returned {@link CacheSpan} * defines the file in which the data is stored. {@link CacheSpan#isCached} is true. The caller * may read from the cache file, but does not acquire any locks. - *

          - * If there is no cache entry overlapping {@code offset}, then the returned {@link CacheSpan} + * + *

          If there is no cache entry overlapping {@code offset}, then the returned {@link CacheSpan} * defines a hole in the cache starting at {@code position} into which the caller may write as it * obtains the data from some other source. The returned {@link CacheSpan} serves as a lock. * Whilst the caller holds the lock it may write data into the hole. It may split data into - * multiple files. When the caller has finished writing a file it should commit it to the cache - * by calling {@link #commitFile(File)}. When the caller has finished writing, it must release - * the lock by calling {@link #releaseHoleSpan}. + * multiple files. When the caller has finished writing a file it should commit it to the cache by + * calling {@link #commitFile(File)}. When the caller has finished writing, it must release the + * lock by calling {@link #releaseHoleSpan}. * * @param key The key of the data being requested. * @param position The position of the data being requested. * @return The {@link CacheSpan}. * @throws InterruptedException If the thread was interrupted. + * @throws CacheException If an error is encountered. */ CacheSpan startReadWrite(String key, long position) throws InterruptedException, CacheException; @@ -154,8 +155,10 @@ public interface Cache { * @param key The key of the data being requested. * @param position The position of the data being requested. * @return The {@link CacheSpan}. Or null if the cache entry is locked. + * @throws CacheException If an error is encountered. */ - @Nullable CacheSpan startReadWriteNonBlocking(String key, long position) throws CacheException; + @Nullable + CacheSpan startReadWriteNonBlocking(String key, long position) throws CacheException; /** * Obtains a cache file into which data can be written. Must only be called when holding a @@ -166,14 +169,16 @@ public interface Cache { * @param maxLength The maximum length of the data to be written. Used only to ensure that there * is enough space in the cache. * @return The file into which data should be written. + * @throws CacheException If an error is encountered. */ File startFile(String key, long position, long maxLength) throws CacheException; /** - * Commits a file into the cache. Must only be called when holding a corresponding hole - * {@link CacheSpan} obtained from {@link #startReadWrite(String, long)} + * Commits a file into the cache. Must only be called when holding a corresponding hole {@link + * CacheSpan} obtained from {@link #startReadWrite(String, long)} * * @param file A newly written cache file. + * @throws CacheException If an error is encountered. */ void commitFile(File file) throws CacheException; @@ -189,6 +194,7 @@ public interface Cache { * Removes a cached {@link CacheSpan} from the cache, deleting the underlying file. * * @param span The {@link CacheSpan} to remove. + * @throws CacheException If an error is encountered. */ void removeSpan(CacheSpan span) throws CacheException; @@ -210,15 +216,16 @@ public interface Cache { * @param key The cache key for the data. * @param position The starting position of the data. * @param length The maximum length of the data to be returned. - * @return the length of the cached or not cached data block length. + * @return The length of the cached or not cached data block length. */ - long getCachedBytes(String key, long position, long length); + long getCachedLength(String key, long position, long length); /** * Sets the content length for the given key. * * @param key The cache key for the data. * @param length The length of the data. + * @throws CacheException If an error is encountered. */ void setContentLength(String key, long length) throws CacheException; @@ -227,7 +234,8 @@ public interface Cache { * com.google.android.exoplayer2.C#LENGTH_UNSET} otherwise. * * @param key The cache key for the data. + * @return The content length for the given key if one set, or {@link + * com.google.android.exoplayer2.C#LENGTH_UNSET} otherwise. */ long getContentLength(String key); - } diff --git a/library/core/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 index 1af690e10f..57f5a6ad93 100644 --- a/library/core/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 @@ -37,7 +37,7 @@ import java.io.OutputStream; */ public final class CacheDataSink implements DataSink { - /** Default buffer size. */ + /** Default buffer size in bytes. */ public static final int DEFAULT_BUFFER_SIZE = 20480; private final Cache cache; 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 index 2bf5cde8e0..22150f8e78 100644 --- 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 @@ -89,8 +89,8 @@ public final class CacheUtil { counters.alreadyCachedBytes = 0; counters.newlyCachedBytes = 0; while (left != 0) { - long blockLength = cache.getCachedBytes(key, start, - left != C.LENGTH_UNSET ? left : Long.MAX_VALUE); + long blockLength = + cache.getCachedLength(key, start, left != C.LENGTH_UNSET ? left : Long.MAX_VALUE); if (blockLength > 0) { counters.alreadyCachedBytes += blockLength; } else { @@ -126,6 +126,12 @@ public final class CacheUtil { * Caches the data defined by {@code dataSpec} while skipping already cached data. Caching stops * early if end of input is reached and {@code enableEOFException} is false. * + *

          If a {@link PriorityTaskManager} is given, it's used to pause and resume caching depending + * on {@code priority} and the priority of other tasks registered to the PriorityTaskManager. + * Please note that it's the responsibility of the calling code to call {@link + * PriorityTaskManager#add} to register with the manager before calling this method, and to call + * {@link PriorityTaskManager#remove} afterwards to unregister. + * * @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}. @@ -164,8 +170,8 @@ public final class CacheUtil { long start = dataSpec.absoluteStreamPosition; long left = dataSpec.length != C.LENGTH_UNSET ? dataSpec.length : cache.getContentLength(key); while (left != 0) { - long blockLength = cache.getCachedBytes(key, start, - left != C.LENGTH_UNSET ? left : Long.MAX_VALUE); + long blockLength = + cache.getCachedLength(key, start, left != C.LENGTH_UNSET ? left : Long.MAX_VALUE); if (blockLength > 0) { // Skip already cached data. } else { diff --git a/library/core/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 index fb59d23666..34884a457d 100644 --- a/library/core/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 @@ -125,7 +125,7 @@ import java.util.TreeSet; * @param length The maximum length of the data to be returned. * @return the length of the cached or not cached data block length. */ - public long getCachedBytes(long position, long length) { + public long getCachedBytesLength(long position, long length) { SimpleCacheSpan span = getSpan(position); if (span.isHoleSpan()) { // We don't have a span covering the start of the queried region. diff --git a/library/core/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 index ffac8a35f1..9d4a661343 100644 --- a/library/core/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 @@ -385,13 +385,13 @@ public final class SimpleCache implements Cache { @Override public synchronized boolean isCached(String key, long position, long length) { CachedContent cachedContent = index.get(key); - return cachedContent != null && cachedContent.getCachedBytes(position, length) >= length; + return cachedContent != null && cachedContent.getCachedBytesLength(position, length) >= length; } @Override - public synchronized long getCachedBytes(String key, long position, long length) { + public synchronized long getCachedLength(String key, long position, long length) { CachedContent cachedContent = index.get(key); - return cachedContent != null ? cachedContent.getCachedBytes(position, length) : -length; + return cachedContent != null ? cachedContent.getCachedBytesLength(position, length) : -length; } @Override diff --git a/library/core/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 index 8c5b7e26e7..e12d876ce1 100644 --- a/library/core/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 @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.upstream.cache; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; @@ -35,19 +36,50 @@ import java.util.regex.Pattern; private static final Pattern CACHE_FILE_PATTERN_V3 = Pattern.compile( "^(\\d+)\\.(\\d+)\\.(\\d+)\\.v3\\.exo$", Pattern.DOTALL); - public static File getCacheFile(File cacheDir, int id, long position, - long lastAccessTimestamp) { + /** + * Returns a new {@link File} instance from {@code cacheDir}, {@code id}, {@code position}, {@code + * lastAccessTimestamp}. + * + * @param cacheDir The parent abstract pathname. + * @param id The cache file id. + * @param position The position of the stored data in the original stream. + * @param lastAccessTimestamp The last access timestamp. + * @return The cache file. + */ + public static File getCacheFile(File cacheDir, int id, long position, long lastAccessTimestamp) { return new File(cacheDir, id + "." + position + "." + lastAccessTimestamp + SUFFIX); } + /** + * Creates a lookup span. + * + * @param key The cache key. + * @param position The position of the {@link CacheSpan} in the original stream. + * @return The span. + */ public static SimpleCacheSpan createLookup(String key, long position) { return new SimpleCacheSpan(key, position, C.LENGTH_UNSET, C.TIME_UNSET, null); } + /** + * Creates an open hole span. + * + * @param key The cache key. + * @param position The position of the {@link CacheSpan} in the original stream. + * @return The span. + */ public static SimpleCacheSpan createOpenHole(String key, long position) { return new SimpleCacheSpan(key, position, C.LENGTH_UNSET, C.TIME_UNSET, null); } + /** + * Creates a closed hole span. + * + * @param key The cache key. + * @param position The position of the {@link CacheSpan} in the original stream. + * @param length The length of the {@link CacheSpan}. + * @return The span. + */ public static SimpleCacheSpan createClosedHole(String key, long position, long length) { return new SimpleCacheSpan(key, position, length, C.TIME_UNSET, null); } @@ -60,6 +92,7 @@ import java.util.regex.Pattern; * @return The span, or null if the file name is not correctly formatted, or if the id is not * present in the content index. */ + @Nullable public static SimpleCacheSpan createCacheEntry(File file, CachedContentIndex index) { String name = file.getName(); if (!name.endsWith(SUFFIX)) { @@ -81,6 +114,15 @@ import java.util.regex.Pattern; Long.parseLong(matcher.group(3)), file); } + /** + * Upgrades the cache file if it is created by an earlier version of {@link SimpleCache}. + * + * @param file The cache file. + * @param index Cached content index. + * @return Upgraded cache file or {@code null} if the file name is not correctly formatted or the + * file can not be renamed. + */ + @Nullable private static File upgradeFile(File file, CachedContentIndex index) { String key; String filename = file.getName(); @@ -106,8 +148,17 @@ import java.util.regex.Pattern; return newCacheFile; } - private SimpleCacheSpan(String key, long position, long length, long lastAccessTimestamp, - File file) { + /** + * @param key The cache key. + * @param position The position of the {@link CacheSpan} in the original stream. + * @param length The length of the {@link CacheSpan}, or {@link C#LENGTH_UNSET} if this is an + * open-ended hole. + * @param lastAccessTimestamp The last access timestamp, or {@link C#TIME_UNSET} if {@link + * #isCached} is false. + * @param file The file corresponding to this {@link CacheSpan}, or null if it's a hole. + */ + private SimpleCacheSpan( + String key, long position, long length, long lastAccessTimestamp, @Nullable File file) { super(key, position, length, lastAccessTimestamp, file); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheAsserts.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheAsserts.java index 65850a13e7..c31cd0384e 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheAsserts.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheAsserts.java @@ -31,7 +31,11 @@ import java.util.ArrayList; /** Assertion methods for {@link com.google.android.exoplayer2.upstream.cache.Cache}. */ /* package */ final class CacheAsserts { - /** Asserts that the cache content is equal to the data in the {@code fakeDataSet}. */ + /** + * Asserts that the cache content is equal to the data in the {@code fakeDataSet}. + * + * @throws IOException If an error occurred reading from the Cache. + */ public static void assertCachedData(Cache cache, FakeDataSet fakeDataSet) throws IOException { ArrayList allData = fakeDataSet.getAllData(); Uri[] uris = new Uri[allData.size()]; @@ -43,6 +47,8 @@ import java.util.ArrayList; /** * Asserts that the cache content is equal to the given subset of data in the {@code fakeDataSet}. + * + * @throws IOException If an error occurred reading from the Cache. */ public static void assertCachedData(Cache cache, FakeDataSet fakeDataSet, String... uriStrings) throws IOException { @@ -55,6 +61,8 @@ import java.util.ArrayList; /** * Asserts that the cache content is equal to the given subset of data in the {@code fakeDataSet}. + * + * @throws IOException If an error occurred reading from the Cache. */ public static void assertCachedData(Cache cache, FakeDataSet fakeDataSet, Uri... uris) throws IOException { @@ -67,7 +75,11 @@ import java.util.ArrayList; assertThat(cache.getCacheSpace()).isEqualTo(totalLength); } - /** Asserts that the cache contains the given subset of data in the {@code fakeDataSet}. */ + /** + * Asserts that the cache contains the given subset of data in the {@code fakeDataSet}. + * + * @throws IOException If an error occurred reading from the Cache. + */ public static void assertDataCached(Cache cache, FakeDataSet fakeDataSet, Uri... uris) throws IOException { for (Uri uri : uris) { @@ -75,7 +87,11 @@ import java.util.ArrayList; } } - /** Asserts that the cache contains the given data for {@code uriString}. */ + /** + * Asserts that the cache contains the given data for {@code uriString} or not. + * + * @throws IOException If an error occurred reading from the Cache. + */ public static void assertDataCached(Cache cache, Uri uri, byte[] expected) throws IOException { CacheDataSource dataSource = new CacheDataSource(cache, DummyDataSource.INSTANCE, 0); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheUtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheUtilTest.java index c8231ec4ac..250e09bab4 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheUtilTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheUtilTest.java @@ -72,7 +72,7 @@ public final class CacheUtilTest { } @Override - public long getCachedBytes(String key, long position, long length) { + public long getCachedLength(String key, long position, long length) { for (int i = 0; i < spansAndGaps.length; i++) { int spanOrGap = spansAndGaps[i]; if (position < spanOrGap) { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java index 75a80185b9..e62676fc9d 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java @@ -218,36 +218,36 @@ public class SimpleCacheTest { } @Test - public void testGetCachedBytes() throws Exception { + public void testGetCachedLength() throws Exception { SimpleCache simpleCache = getSimpleCache(); CacheSpan cacheSpan = simpleCache.startReadWrite(KEY_1, 0); // No cached bytes, returns -'length' - assertThat(simpleCache.getCachedBytes(KEY_1, 0, 100)).isEqualTo(-100); + assertThat(simpleCache.getCachedLength(KEY_1, 0, 100)).isEqualTo(-100); // Position value doesn't affect the return value - assertThat(simpleCache.getCachedBytes(KEY_1, 20, 100)).isEqualTo(-100); + assertThat(simpleCache.getCachedLength(KEY_1, 20, 100)).isEqualTo(-100); addCache(simpleCache, KEY_1, 0, 15); // Returns the length of a single span - assertThat(simpleCache.getCachedBytes(KEY_1, 0, 100)).isEqualTo(15); + assertThat(simpleCache.getCachedLength(KEY_1, 0, 100)).isEqualTo(15); // Value is capped by the 'length' - assertThat(simpleCache.getCachedBytes(KEY_1, 0, 10)).isEqualTo(10); + assertThat(simpleCache.getCachedLength(KEY_1, 0, 10)).isEqualTo(10); addCache(simpleCache, KEY_1, 15, 35); // Returns the length of two adjacent spans - assertThat(simpleCache.getCachedBytes(KEY_1, 0, 100)).isEqualTo(50); + assertThat(simpleCache.getCachedLength(KEY_1, 0, 100)).isEqualTo(50); addCache(simpleCache, KEY_1, 60, 10); // Not adjacent span doesn't affect return value - assertThat(simpleCache.getCachedBytes(KEY_1, 0, 100)).isEqualTo(50); + assertThat(simpleCache.getCachedLength(KEY_1, 0, 100)).isEqualTo(50); // Returns length of hole up to the next cached span - assertThat(simpleCache.getCachedBytes(KEY_1, 55, 100)).isEqualTo(-5); + assertThat(simpleCache.getCachedLength(KEY_1, 55, 100)).isEqualTo(-5); simpleCache.releaseHoleSpan(cacheSpan); } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java index eb53191dc8..2174de1fd5 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java @@ -36,7 +36,11 @@ import java.util.ArrayList; */ public final class CacheAsserts { - /** Asserts that the cache content is equal to the data in the {@code fakeDataSet}. */ + /** + * Asserts that the cache content is equal to the data in the {@code fakeDataSet}. + * + * @throws IOException If an error occurred reading from the Cache. + */ public static void assertCachedData(Cache cache, FakeDataSet fakeDataSet) throws IOException { ArrayList allData = fakeDataSet.getAllData(); Uri[] uris = new Uri[allData.size()]; @@ -48,6 +52,8 @@ public final class CacheAsserts { /** * Asserts that the cache content is equal to the given subset of data in the {@code fakeDataSet}. + * + * @throws IOException If an error occurred reading from the Cache. */ public static void assertCachedData(Cache cache, FakeDataSet fakeDataSet, String... uriStrings) throws IOException { @@ -60,6 +66,8 @@ public final class CacheAsserts { /** * Asserts that the cache content is equal to the given subset of data in the {@code fakeDataSet}. + * + * @throws IOException If an error occurred reading from the Cache. */ public static void assertCachedData(Cache cache, FakeDataSet fakeDataSet, Uri... uris) throws IOException { @@ -72,7 +80,11 @@ public final class CacheAsserts { assertEquals(totalLength, cache.getCacheSpace()); } - /** Asserts that the cache contains the given subset of data in the {@code fakeDataSet}. */ + /** + * Asserts that the cache contains the given subset of data in the {@code fakeDataSet}. + * + * @throws IOException If an error occurred reading from the Cache. + */ public static void assertDataCached(Cache cache, FakeDataSet fakeDataSet, Uri... uris) throws IOException { for (Uri uri : uris) { @@ -80,7 +92,11 @@ public final class CacheAsserts { } } - /** Asserts that the cache contains the given data for {@code uriString}. */ + /** + * Asserts that the cache contains the given data for {@code uriString}. + * + * @throws IOException If an error occurred reading from the Cache. + */ public static void assertDataCached(Cache cache, Uri uri, byte[] expected) throws IOException { CacheDataSource dataSource = new CacheDataSource(cache, DummyDataSource.INSTANCE, 0); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); From 214d46d95781401eaad48eb2510200b63651a1e9 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 10 Jan 2018 06:13:25 -0800 Subject: [PATCH 1035/2472] Set selection flags on image sample formats. Issue: #3008 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=181455340 --- .../com/google/android/exoplayer2/Format.java | 40 ++++++++++++++++--- .../extractor/mkv/MatroskaExtractor.java | 12 +++++- .../extractor/ts/DvbSubtitleReader.java | 13 ++++-- 3 files changed, 55 insertions(+), 10 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Format.java b/library/core/src/main/java/com/google/android/exoplayer2/Format.java index 4bd23e2cb6..7799f411a9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Format.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Format.java @@ -324,11 +324,41 @@ public final class Format implements Parcelable { // Image. - 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, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, - NO_VALUE, 0, language, NO_VALUE, OFFSET_SAMPLE_RELATIVE, initializationData, drmInitData, + public static Format createImageSampleFormat( + String id, + String sampleMimeType, + String codecs, + int bitrate, + @C.SelectionFlags int selectionFlags, + 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, + null, + NO_VALUE, + NO_VALUE, + NO_VALUE, + NO_VALUE, + NO_VALUE, + selectionFlags, + language, + NO_VALUE, + OFFSET_SAMPLE_RELATIVE, + initializationData, + drmInitData, null); } diff --git a/library/core/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 index 0eb7009c47..57128f45f0 100644 --- a/library/core/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 @@ -1867,8 +1867,16 @@ public final class MatroskaExtractor implements Extractor { || 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); + format = + Format.createImageSampleFormat( + Integer.toString(trackId), + mimeType, + null, + Format.NO_VALUE, + selectionFlags, + initializationData, + language, + drmInitData); } else { throw new ParserException("Unexpected MIME type."); } 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 index e00c63a354..0944d1810e 100644 --- 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 @@ -58,9 +58,16 @@ public final class DvbSubtitleReader implements ElementaryStreamReader { 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)); + output.format( + Format.createImageSampleFormat( + idGenerator.getFormatId(), + MimeTypes.APPLICATION_DVBSUBS, + null, + Format.NO_VALUE, + 0, + Collections.singletonList(subtitleInfo.initializationData), + subtitleInfo.language, + null)); outputs[i] = output; } } From f977ba256fdef5fd808d0ead6fdfea0a10719149 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 11 Jan 2018 07:30:25 -0800 Subject: [PATCH 1036/2472] Add ad insertion discontinuity reason. This it to distinguish between actual period transitions and the transitions occuring to and from ads within one timeline period. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=181606023 --- RELEASENOTES.md | 4 +++ .../android/exoplayer2/demo/EventLogger.java | 2 ++ .../exoplayer2/ExoPlayerImplInternal.java | 6 ++++- .../com/google/android/exoplayer2/Player.java | 25 ++++++++++--------- 4 files changed, 24 insertions(+), 13 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 80381075b8..b0907f0a42 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -9,6 +9,10 @@ * Add optional parameter to `stop` to reset the player when stopping. * Add a reason to `EventListener.onTimelineChanged` to distinguish between initial preparation, reset and dynamic updates. + * Add `Player.DISCONTINUITY_REASON_AD_INSERTION` to the possible reasons + reported in `Eventlistener.onPositionDiscontinuity` to distinguish + transitions to and from ads within one period from transitions between + periods. * Replaced `ExoPlayer.sendMessages` with `ExoPlayer.createMessage` to allow more customization of the message. Now supports setting a message delivery playback position and/or a delivery handler. diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java index 7dc7a3567f..9d28aa47f0 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java @@ -528,6 +528,8 @@ import java.util.Locale; return "SEEK"; case Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT: return "SEEK_ADJUSTMENT"; + case Player.DISCONTINUITY_REASON_AD_INSERTION: + return "AD_INSERTION"; case Player.DISCONTINUITY_REASON_INTERNAL: return "INTERNAL"; default: diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 8fd508a2f0..2647a44dee 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -1448,11 +1448,15 @@ import java.util.Collections; // If we advance more than one period at a time, notify listeners after each update. maybeNotifyPlaybackInfoChanged(); } + int discontinuityReason = + playingPeriodHolder.info.isLastInTimelinePeriod + ? Player.DISCONTINUITY_REASON_PERIOD_TRANSITION + : Player.DISCONTINUITY_REASON_AD_INSERTION; playingPeriodHolder.release(); setPlayingPeriodHolder(playingPeriodHolder.next); playbackInfo = playbackInfo.fromNewPosition(playingPeriodHolder.info.id, playingPeriodHolder.info.startPositionUs, playingPeriodHolder.info.contentPositionUs); - playbackInfoUpdate.setPositionDiscontinuity(Player.DISCONTINUITY_REASON_PERIOD_TRANSITION); + playbackInfoUpdate.setPositionDiscontinuity(discontinuityReason); updatePlaybackPositions(); advancedPlayingPeriod = true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Player.java b/library/core/src/main/java/com/google/android/exoplayer2/Player.java index b3ae4c28c6..97cd9449d3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Player.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Player.java @@ -251,31 +251,32 @@ public interface Player { */ int REPEAT_MODE_ALL = 2; - /** - * Reasons for position discontinuities. - */ + /** Reasons for position discontinuities. */ @Retention(RetentionPolicy.SOURCE) - @IntDef({DISCONTINUITY_REASON_PERIOD_TRANSITION, DISCONTINUITY_REASON_SEEK, - DISCONTINUITY_REASON_SEEK_ADJUSTMENT, DISCONTINUITY_REASON_INTERNAL}) + @IntDef({ + DISCONTINUITY_REASON_PERIOD_TRANSITION, + DISCONTINUITY_REASON_SEEK, + DISCONTINUITY_REASON_SEEK_ADJUSTMENT, + DISCONTINUITY_REASON_AD_INSERTION, + DISCONTINUITY_REASON_INTERNAL + }) public @interface DiscontinuityReason {} /** * Automatic playback transition from one period in the timeline to the next. The period index may * be the same as it was before the discontinuity in case the current period is repeated. */ int DISCONTINUITY_REASON_PERIOD_TRANSITION = 0; - /** - * Seek within the current period or to another period. - */ + /** Seek within the current period or to another period. */ int DISCONTINUITY_REASON_SEEK = 1; /** * Seek adjustment due to being unable to seek to the requested position or because the seek was * permitted to be inexact. */ int DISCONTINUITY_REASON_SEEK_ADJUSTMENT = 2; - /** - * Discontinuity introduced internally by the source. - */ - int DISCONTINUITY_REASON_INTERNAL = 3; + /** Discontinuity to or from an ad within one period in the timeline. */ + int DISCONTINUITY_REASON_AD_INSERTION = 3; + /** Discontinuity introduced internally by the source. */ + int DISCONTINUITY_REASON_INTERNAL = 4; /** * Reasons for timeline and/or manifest changes. From ad3e6ef4cdac12d1983f4998e594a70fbe28eee8 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 12 Jan 2018 07:52:49 -0800 Subject: [PATCH 1037/2472] Add missing onLoadStarted callback to Extractor and SingleSample media period. We added the other callbacks some time ago, but didn't include onLoadStarted. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=181743764 --- .../source/ExtractorMediaPeriod.java | 19 +++++++++++++++---- .../source/MediaSourceEventListener.java | 2 +- .../source/SingleSampleMediaPeriod.java | 19 +++++++++++++++---- 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java index 76d9d22648..bc84546c83 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -168,6 +168,7 @@ import java.util.Arrays; sampleQueues = new SampleQueue[0]; pendingResetPositionUs = C.TIME_UNSET; length = C.LENGTH_UNSET; + durationUs = C.TIME_UNSET; // Assume on-demand for MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA, until prepared. actualMinLoadableRetryCount = minLoadableRetryCount == ExtractorMediaSource.MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA @@ -436,7 +437,7 @@ import java.util.Arrays; /* trackFormat= */ null, C.SELECTION_REASON_UNKNOWN, /* trackSelectionData= */ null, - /* mediaStartTimeUs= */ 0, + /* mediaStartTimeUs= */ loadable.seekTimeUs, durationUs, elapsedRealtimeMs, loadDurationMs, @@ -456,7 +457,7 @@ import java.util.Arrays; /* trackFormat= */ null, C.SELECTION_REASON_UNKNOWN, /* trackSelectionData= */ null, - /* mediaStartTimeUs= */ 0, + /* mediaStartTimeUs= */ loadable.seekTimeUs, durationUs, elapsedRealtimeMs, loadDurationMs, @@ -483,7 +484,7 @@ import java.util.Arrays; /* trackFormat= */ null, C.SELECTION_REASON_UNKNOWN, /* trackSelectionData= */ null, - /* mediaStartTimeUs= */ 0, + /* mediaStartTimeUs= */ loadable.seekTimeUs, durationUs, elapsedRealtimeMs, loadDurationMs, @@ -595,7 +596,17 @@ import java.util.Arrays; pendingResetPositionUs = C.TIME_UNSET; } extractedSamplesCountAtStartOfLoad = getExtractedSamplesCount(); - loader.startLoading(loadable, this, actualMinLoadableRetryCount); + long elapsedRealtimeMs = loader.startLoading(loadable, this, actualMinLoadableRetryCount); + eventDispatcher.loadStarted( + loadable.dataSpec, + C.DATA_TYPE_MEDIA, + C.TRACK_TYPE_UNKNOWN, + /* trackFormat= */ null, + C.SELECTION_REASON_UNKNOWN, + /* trackSelectionData= */ null, + /* mediaStartTimeUs= */ loadable.seekTimeUs, + durationUs, + elapsedRealtimeMs); } private void configureRetry(ExtractingLoadable loadable) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java index 4d500f94bd..9fc2572b55 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceEventListener.java @@ -44,7 +44,7 @@ public interface MediaSourceEventListener { * @param mediaStartTimeMs The start time of the media being loaded, or {@link C#TIME_UNSET} if * the load is not for media data. * @param mediaEndTimeMs The end time of the media being loaded, or {@link C#TIME_UNSET} if the - * load is not for media data. + * load is not for media data or the end time is unknown. * @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} when the load began. */ void onLoadStarted( diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java index e76de60b86..cc7179ae18 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java @@ -133,10 +133,21 @@ import java.util.Arrays; if (loadingFinished || loader.isLoading()) { return false; } - loader.startLoading( - new SourceLoadable(dataSpec, dataSourceFactory.createDataSource()), - this, - minLoadableRetryCount); + long elapsedRealtimeMs = + loader.startLoading( + new SourceLoadable(dataSpec, dataSourceFactory.createDataSource()), + this, + minLoadableRetryCount); + eventDispatcher.loadStarted( + dataSpec, + C.DATA_TYPE_MEDIA, + C.TRACK_TYPE_UNKNOWN, + format, + C.SELECTION_REASON_UNKNOWN, + /* trackSelectionData= */ null, + /* mediaStartTimeUs= */ 0, + durationUs, + elapsedRealtimeMs); return true; } From 7a5640304601f5518060689785ad5fa27b82f25f Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 12 Jan 2018 09:26:04 -0800 Subject: [PATCH 1038/2472] Add missing downstreamFormatChanged to Extractor and SingleSample media source. These haven't been included in the recent changes but can be reported as soon as the first sample of each stream is read. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=181753141 --- .../source/ExtractorMediaPeriod.java | 37 ++++++++++++++++--- .../source/SingleSampleMediaPeriod.java | 15 ++++++++ 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java index bc84546c83..c995884515 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -105,6 +105,7 @@ import java.util.Arrays; private long durationUs; private boolean[] trackEnabledStates; private boolean[] trackIsAudioVideoFlags; + private boolean[] trackFormatNotificationSent; private boolean haveAudioVideoTracks; private long length; @@ -398,8 +399,13 @@ import java.util.Arrays; if (suppressRead()) { return C.RESULT_NOTHING_READ; } - return sampleQueues[track].read(formatHolder, buffer, formatRequired, loadingFinished, - lastSeekPositionUs); + int result = + sampleQueues[track].read( + formatHolder, buffer, formatRequired, loadingFinished, lastSeekPositionUs); + if (result == C.RESULT_BUFFER_READ) { + maybeNotifyTrackFormat(track); + } + return result; } /* package */ int skipData(int track, long positionUs) { @@ -407,11 +413,31 @@ import java.util.Arrays; return 0; } SampleQueue sampleQueue = sampleQueues[track]; + int skipCount; if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) { - return sampleQueue.advanceToEnd(); + skipCount = sampleQueue.advanceToEnd(); } else { - int skipCount = sampleQueue.advanceTo(positionUs, true, true); - return skipCount == SampleQueue.ADVANCE_FAILED ? 0 : skipCount; + skipCount = sampleQueue.advanceTo(positionUs, true, true); + if (skipCount == SampleQueue.ADVANCE_FAILED) { + skipCount = 0; + } + } + if (skipCount > 0) { + maybeNotifyTrackFormat(track); + } + return skipCount; + } + + private void maybeNotifyTrackFormat(int track) { + if (!trackFormatNotificationSent[track]) { + Format trackFormat = tracks.get(track).getFormat(0); + eventDispatcher.downstreamFormatChanged( + MimeTypes.getTrackType(trackFormat.sampleMimeType), + trackFormat, + C.SELECTION_REASON_UNKNOWN, + /* trackSelectionData= */ null, + lastSeekPositionUs); + trackFormatNotificationSent[track] = true; } } @@ -556,6 +582,7 @@ import java.util.Arrays; TrackGroup[] trackArray = new TrackGroup[trackCount]; trackIsAudioVideoFlags = new boolean[trackCount]; trackEnabledStates = new boolean[trackCount]; + trackFormatNotificationSent = new boolean[trackCount]; durationUs = seekMap.getDurationUs(); for (int i = 0; i < trackCount; i++) { Format trackFormat = sampleQueues[i].getUpstreamFormat(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java index cc7179ae18..36e5d910c4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaPeriod.java @@ -26,6 +26,7 @@ import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.Loader; import com.google.android.exoplayer2.upstream.Loader.Loadable; +import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.util.ArrayList; @@ -252,6 +253,7 @@ import java.util.Arrays; private static final int STREAM_STATE_END_OF_STREAM = 2; private int streamState; + private boolean formatSent; public void reset() { if (streamState == STREAM_STATE_END_OF_STREAM) { @@ -287,6 +289,7 @@ import java.util.Arrays; buffer.addFlag(C.BUFFER_FLAG_KEY_FRAME); buffer.ensureSpaceForWrite(sampleSize); buffer.data.put(sampleData, 0, sampleSize); + sendFormat(); } else { buffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM); } @@ -300,11 +303,23 @@ import java.util.Arrays; public int skipData(long positionUs) { if (positionUs > 0 && streamState != STREAM_STATE_END_OF_STREAM) { streamState = STREAM_STATE_END_OF_STREAM; + sendFormat(); return 1; } return 0; } + private void sendFormat() { + if (!formatSent) { + eventDispatcher.downstreamFormatChanged( + MimeTypes.getTrackType(format.sampleMimeType), + format, + C.SELECTION_REASON_UNKNOWN, + /* trackSelectionData= */ null, + /* mediaTimeUs= */ 0); + formatSent = true; + } + } } /* package */ static final class SourceLoadable implements Loadable { From f20c158a38ec8f4aa4775f0f6ee9a34eeda62f61 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 15 Jan 2018 03:10:17 -0800 Subject: [PATCH 1039/2472] Fix IMA sample ad tag URL Issue: #3703 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=181947101 --- demos/main/src/main/assets/media.exolist.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demos/main/src/main/assets/media.exolist.json b/demos/main/src/main/assets/media.exolist.json index 38a0c577ae..15183a4a8b 100644 --- a/demos/main/src/main/assets/media.exolist.json +++ b/demos/main/src/main/assets/media.exolist.json @@ -540,7 +540,7 @@ { "name": "VMAP pre-, mid- and post-rolls, single ads", "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", - "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dlinear&correlator=" + "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpost&cmsid=496&vid=short_onecue&correlator=" }, { "name": "VMAP pre-roll single ad, mid-roll standard pod with 3 ads, post-roll single ad", From 141f3aa836daf22e9daf514ca02dfe0a9474bfb8 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 15 Jan 2018 14:13:30 +0000 Subject: [PATCH 1040/2472] Simplify PGS captions + sync with internal tree --- RELEASENOTES.md | 9 +- demos/main/src/main/assets/media.exolist.json | 2 +- extensions/vp9/README.md | 1 + .../jni/generate_libvpx_android_configs.sh | 5 +- .../text/SubtitleDecoderFactory.java | 118 ++++----- .../exoplayer2/text/dvb/DvbDecoder.java | 4 +- .../exoplayer2/text/pgs/PgsBuilder.java | 232 ------------------ .../exoplayer2/text/pgs/PgsDecoder.java | 229 ++++++++++++++++- .../exoplayer2/text/pgs/PgsSubtitle.java | 53 ++-- 9 files changed, 317 insertions(+), 336 deletions(-) delete mode 100644 library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsBuilder.java diff --git a/RELEASENOTES.md b/RELEASENOTES.md index b0907f0a42..6d4347490e 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -37,13 +37,14 @@ HLS source to finish preparation without downloading any chunks, which can significantly reduce initial buffering time ([#3149](https://github.com/google/ExoPlayer/issues/3149)). -* DefaultTrackSelector: Replace `DefaultTrackSelector.Parameters` copy methods - with a builder. -* DefaultTrackSelector: Support disabling of individual text track selection - flags. +* DefaultTrackSelector: + * Replace `DefaultTrackSelector.Parameters` copy methods with a builder. + * Support disabling of individual text track selection flags. * New Cast extension: Simplifies toggling between local and Cast playbacks. * Audio: Support TrueHD passthrough for rechunked samples in Matroska files ([#2147](https://github.com/google/ExoPlayer/issues/2147)). +* Captions: Initial support for PGS subtitles + ([#3008](https://github.com/google/ExoPlayer/issues/3008)). * CacheDataSource: Check periodically if it's possible to read from/write to cache after deciding to bypass cache. * IMA extension: Add support for playing non-Extractor content MediaSources in diff --git a/demos/main/src/main/assets/media.exolist.json b/demos/main/src/main/assets/media.exolist.json index 15183a4a8b..38a0c577ae 100644 --- a/demos/main/src/main/assets/media.exolist.json +++ b/demos/main/src/main/assets/media.exolist.json @@ -540,7 +540,7 @@ { "name": "VMAP pre-, mid- and post-rolls, single ads", "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", - "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpost&cmsid=496&vid=short_onecue&correlator=" + "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dlinear&correlator=" }, { "name": "VMAP pre-roll single ad, mid-roll standard pod with 3 ads, post-roll single ad", diff --git a/extensions/vp9/README.md b/extensions/vp9/README.md index 8dc4974430..9601829c91 100644 --- a/extensions/vp9/README.md +++ b/extensions/vp9/README.md @@ -29,6 +29,7 @@ VP9_EXT_PATH="${EXOPLAYER_ROOT}/extensions/vp9/src/main" ``` * Download the [Android NDK][] and set its location in an environment variable. + ``` NDK_PATH="" ``` 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 4aabf2379e..eab6862555 100755 --- a/extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh +++ b/extensions/vp9/src/main/jni/generate_libvpx_android_configs.sh @@ -102,7 +102,10 @@ for i in $(seq 0 ${limit}); do # configure and make echo "build_android_configs: " echo "configure ${config[${i}]} ${common_params}" - ../../libvpx/configure ${config[${i}]} ${common_params} --extra-cflags="-isystem $ndk/sysroot/usr/include/arm-linux-androideabi -isystem $ndk/sysroot/usr/include" + ../../libvpx/configure ${config[${i}]} ${common_params} --extra-cflags=" \ + -isystem $ndk/sysroot/usr/include/arm-linux-androideabi \ + -isystem $ndk/sysroot/usr/include \ + " rm -f libvpx_srcs.txt for f in ${allowed_files}; do # the build system supports multiple different configurations. avoid diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java index 4720a67bba..139e403844 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java @@ -53,67 +53,69 @@ public interface SubtitleDecoderFactory { /** * Default {@link SubtitleDecoderFactory} implementation. - *

          - * The formats supported by this factory are: + * + *

          The formats supported by this factory are: + * *

            - *
          • WebVTT ({@link WebvttDecoder})
          • - *
          • WebVTT (MP4) ({@link Mp4WebvttDecoder})
          • - *
          • TTML ({@link TtmlDecoder})
          • - *
          • SubRip ({@link SubripDecoder})
          • - *
          • SSA/ASS ({@link SsaDecoder})
          • - *
          • TX3G ({@link Tx3gDecoder})
          • - *
          • Cea608 ({@link Cea608Decoder})
          • - *
          • Cea708 ({@link Cea708Decoder})
          • - *
          • DVB ({@link DvbDecoder})
          • + *
          • WebVTT ({@link WebvttDecoder}) + *
          • WebVTT (MP4) ({@link Mp4WebvttDecoder}) + *
          • TTML ({@link TtmlDecoder}) + *
          • SubRip ({@link SubripDecoder}) + *
          • SSA/ASS ({@link SsaDecoder}) + *
          • TX3G ({@link Tx3gDecoder}) + *
          • Cea608 ({@link Cea608Decoder}) + *
          • Cea708 ({@link Cea708Decoder}) + *
          • DVB ({@link DvbDecoder}) + *
          • PGS ({@link PgsDecoder}) *
          */ - SubtitleDecoderFactory DEFAULT = new SubtitleDecoderFactory() { + SubtitleDecoderFactory DEFAULT = + new SubtitleDecoderFactory() { - @Override - public boolean supportsFormat(Format format) { - String mimeType = format.sampleMimeType; - return MimeTypes.TEXT_VTT.equals(mimeType) - || MimeTypes.TEXT_SSA.equals(mimeType) - || MimeTypes.APPLICATION_TTML.equals(mimeType) - || MimeTypes.APPLICATION_MP4VTT.equals(mimeType) - || MimeTypes.APPLICATION_SUBRIP.equals(mimeType) - || MimeTypes.APPLICATION_TX3G.equals(mimeType) - || MimeTypes.APPLICATION_CEA608.equals(mimeType) - || MimeTypes.APPLICATION_MP4CEA608.equals(mimeType) - || MimeTypes.APPLICATION_CEA708.equals(mimeType) - || MimeTypes.APPLICATION_DVBSUBS.equals(mimeType) - || MimeTypes.APPLICATION_PGS.equals(mimeType); - } - - @Override - public SubtitleDecoder createDecoder(Format format) { - switch (format.sampleMimeType) { - case MimeTypes.TEXT_VTT: - return new WebvttDecoder(); - case MimeTypes.TEXT_SSA: - return new SsaDecoder(format.initializationData); - case MimeTypes.APPLICATION_MP4VTT: - return new Mp4WebvttDecoder(); - case MimeTypes.APPLICATION_TTML: - return new TtmlDecoder(); - case MimeTypes.APPLICATION_SUBRIP: - return new SubripDecoder(); - case MimeTypes.APPLICATION_TX3G: - return new Tx3gDecoder(format.initializationData); - case MimeTypes.APPLICATION_CEA608: - case MimeTypes.APPLICATION_MP4CEA608: - return new Cea608Decoder(format.sampleMimeType, format.accessibilityChannel); - case MimeTypes.APPLICATION_CEA708: - return new Cea708Decoder(format.accessibilityChannel); - case MimeTypes.APPLICATION_DVBSUBS: - return new DvbDecoder(format.initializationData); - case MimeTypes.APPLICATION_PGS: - return new PgsDecoder(); - default: - throw new IllegalArgumentException("Attempted to create decoder for unsupported format"); - } - } - - }; + @Override + public boolean supportsFormat(Format format) { + String mimeType = format.sampleMimeType; + return MimeTypes.TEXT_VTT.equals(mimeType) + || MimeTypes.TEXT_SSA.equals(mimeType) + || MimeTypes.APPLICATION_TTML.equals(mimeType) + || MimeTypes.APPLICATION_MP4VTT.equals(mimeType) + || MimeTypes.APPLICATION_SUBRIP.equals(mimeType) + || MimeTypes.APPLICATION_TX3G.equals(mimeType) + || MimeTypes.APPLICATION_CEA608.equals(mimeType) + || MimeTypes.APPLICATION_MP4CEA608.equals(mimeType) + || MimeTypes.APPLICATION_CEA708.equals(mimeType) + || MimeTypes.APPLICATION_DVBSUBS.equals(mimeType) + || MimeTypes.APPLICATION_PGS.equals(mimeType); + } + @Override + public SubtitleDecoder createDecoder(Format format) { + switch (format.sampleMimeType) { + case MimeTypes.TEXT_VTT: + return new WebvttDecoder(); + case MimeTypes.TEXT_SSA: + return new SsaDecoder(format.initializationData); + case MimeTypes.APPLICATION_MP4VTT: + return new Mp4WebvttDecoder(); + case MimeTypes.APPLICATION_TTML: + return new TtmlDecoder(); + case MimeTypes.APPLICATION_SUBRIP: + return new SubripDecoder(); + case MimeTypes.APPLICATION_TX3G: + return new Tx3gDecoder(format.initializationData); + case MimeTypes.APPLICATION_CEA608: + case MimeTypes.APPLICATION_MP4CEA608: + return new Cea608Decoder(format.sampleMimeType, format.accessibilityChannel); + case MimeTypes.APPLICATION_CEA708: + return new Cea708Decoder(format.accessibilityChannel); + case MimeTypes.APPLICATION_DVBSUBS: + return new DvbDecoder(format.initializationData); + case MimeTypes.APPLICATION_PGS: + return new PgsDecoder(); + default: + throw new IllegalArgumentException( + "Attempted to create decoder for unsupported format"); + } + } + }; } 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 index dbdc0434a1..df5b19c052 100644 --- 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 @@ -19,9 +19,7 @@ import com.google.android.exoplayer2.text.SimpleSubtitleDecoder; import com.google.android.exoplayer2.util.ParsableByteArray; import java.util.List; -/** - * A {@link SimpleSubtitleDecoder} for DVB Subtitles. - */ +/** A {@link SimpleSubtitleDecoder} for DVB subtitles. */ public final class DvbDecoder extends SimpleSubtitleDecoder { private final DvbParser parser; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsBuilder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsBuilder.java deleted file mode 100644 index e67178314d..0000000000 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsBuilder.java +++ /dev/null @@ -1,232 +0,0 @@ -/* -* -* Sources for this implementation PGS decoding can be founder below -* -* http://exar.ch/suprip/hddvd.php -* http://forum.doom9.org/showthread.php?t=124105 -* http://www.equasys.de/colorconversion.html - */ - -package com.google.android.exoplayer2.text.pgs; - -import android.graphics.Bitmap; - -import com.google.android.exoplayer2.text.Cue; -import com.google.android.exoplayer2.text.Subtitle; -import com.google.android.exoplayer2.util.ParsableByteArray; - -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -class PgsBuilder { - - private static final int SECTION_PALETTE = 0x14; - private static final int SECTION_BITMAP_PICTURE = 0x15; - private static final int SECTION_IDENTIFIER = 0x16; - private static final int SECTION_END = 0x80; - - private List list = new ArrayList<>(); - private Holder holder = new Holder(); - - boolean readNextSection(ParsableByteArray buffer) { - - if (buffer.bytesLeft() < 3) - return false; - - int sectionId = buffer.readUnsignedByte(); - int sectionLength = buffer.readUnsignedShort(); - switch(sectionId) { - case SECTION_PALETTE: - holder.parsePaletteIndexes(buffer, sectionLength); - break; - case SECTION_BITMAP_PICTURE: - holder.fetchBitmapData(buffer, sectionLength); - break; - case SECTION_IDENTIFIER: - holder.fetchIdentifierData(buffer, sectionLength); - break; - case SECTION_END: - list.add(holder); - holder = new Holder(); - break; - default: - buffer.skipBytes(Math.min(sectionLength, buffer.bytesLeft())); - break; - } - return true; - } - - public Subtitle build() { - - if (list.isEmpty()) - return new PgsSubtitle(); - - Cue[] cues = new Cue[list.size()]; - long[] cueStartTimes = new long[list.size()]; - int index = 0; - for (Holder curr : list) { - cues[index] = curr.build(); - cueStartTimes[index++] = curr.start_time; - } - return new PgsSubtitle(cues, cueStartTimes); - } - - private class Holder { - - private int[] colors = null; - private ByteBuffer rle = null; - - Bitmap bitmap = null; - int plane_width = 0; - int plane_height = 0; - int bitmap_width = 0; - int bitmap_height = 0; - public int x = 0; - public int y = 0; - long start_time = 0; - - public Cue build() { - if (rle == null || !createBitmap(new ParsableByteArray(rle.array(), rle.position()))) - return null; - float left = (float) x / plane_width; - float top = (float) y / plane_height; - return new Cue(bitmap, left, Cue.ANCHOR_TYPE_START, top, Cue.ANCHOR_TYPE_START, - (float) bitmap_width / plane_width, (float) bitmap_height / plane_height); - } - - private void parsePaletteIndexes(ParsableByteArray buffer, int dataSize) { - // must be a multi of 5 for index, y, cb, cr, alpha - if (dataSize == 0 || (dataSize - 2) % 5 != 0) - return; - // skip first two bytes - buffer.skipBytes(2); - dataSize -= 2; - colors = new int[256]; - while (dataSize > 0) { - int index = buffer.readUnsignedByte(); - int color_y = buffer.readUnsignedByte() - 16; - int color_cr = buffer.readUnsignedByte() - 128; - int color_cb = buffer.readUnsignedByte() - 128; - int color_alpha = buffer.readUnsignedByte(); - dataSize -= 5; - if (index >= colors.length) - continue; - - int color_r = (int) Math.min(Math.max(Math.round(1.1644 * color_y + 1.793 * color_cr), 0), 255); - int color_g = (int) Math.min(Math.max(Math.round(1.1644 * color_y + (-0.213 * color_cr) + (-0.533 * color_cb)), 0), 255); - int color_b = (int) Math.min(Math.max(Math.round(1.1644 * color_y + 2.112 * color_cb), 0), 255); - //ARGB_8888 - colors[index] = (color_alpha << 24) | (color_r << 16) | (color_g << 8) | color_b; - } - } - - private void fetchBitmapData(ParsableByteArray buffer, int dataSize) { - if (dataSize <= 4) { - buffer.skipBytes(dataSize); - return; - } - // skip id field (2 bytes) - // skip version field - buffer.skipBytes(3); - dataSize -= 3; - - // check to see if this section is an appended section of the base section with - // width and height values - dataSize -= 1; // decrement first - if ((0x80 & buffer.readUnsignedByte()) > 0) { - if (dataSize < 3) { - buffer.skipBytes(dataSize); - return; - } - int full_len = buffer.readUnsignedInt24(); - dataSize -= 3; - if (full_len <= 4) { - buffer.skipBytes(dataSize); - return; - } - bitmap_width = buffer.readUnsignedShort(); - dataSize -= 2; - bitmap_height = buffer.readUnsignedShort(); - dataSize -= 2; - rle = ByteBuffer.allocate(full_len - 4); // don't include width & height - buffer.readBytes(rle, Math.min(dataSize, rle.capacity())); - } else if (rle != null) { - int postSkip = dataSize > rle.capacity() ? dataSize - rle.capacity() : 0; - buffer.readBytes(rle, Math.min(dataSize, rle.capacity())); - buffer.skipBytes(postSkip); - } - } - - private void fetchIdentifierData(ParsableByteArray buffer, int dataSize) { - if (dataSize < 4) { - buffer.skipBytes(dataSize); - return; - } - plane_width = buffer.readUnsignedShort(); - plane_height = buffer.readUnsignedShort(); - dataSize -= 4; - if (dataSize < 15) { - buffer.skipBytes(dataSize); - return; - } - // skip next 11 bytes - buffer.skipBytes(11); - x = buffer.readUnsignedShort(); - y = buffer.readUnsignedShort(); - dataSize -= 15; - buffer.skipBytes(dataSize); - } - - private boolean createBitmap(ParsableByteArray rle) { - if (bitmap_width == 0 || bitmap_height == 0 - || rle == null || rle.bytesLeft() == 0 - || colors == null || colors.length == 0) - return false; - int[] argb = new int[bitmap_width * bitmap_height]; - int currPixel = 0; - int nextbits, pixel_code, switchbits; - int number_of_pixels; - int line = 0; - while (rle.bytesLeft() > 0 && line < bitmap_height) { - boolean end_of_line = false; - do { - nextbits = rle.readUnsignedByte(); - if (nextbits != 0) { - pixel_code = nextbits; - number_of_pixels = 1; - } else { - switchbits = rle.readUnsignedByte(); - if ((switchbits & 0x80) == 0) { - pixel_code = 0; - if ((switchbits & 0x40) == 0) { - if (switchbits > 0) { - number_of_pixels = switchbits; - } else { - end_of_line = true; - ++line; - continue; - } - } else { - number_of_pixels = ((switchbits & 0x3f) << 8) | rle.readUnsignedByte(); - } - } else { - if ((switchbits & 0x40) == 0) { - number_of_pixels = switchbits & 0x3f; - pixel_code = rle.readUnsignedByte(); - } else { - number_of_pixels = ((switchbits & 0x3f) << 8) | rle.readUnsignedByte(); - pixel_code = rle.readUnsignedByte(); - } - } - } - Arrays.fill(argb, currPixel, currPixel + number_of_pixels, colors[pixel_code]); - currPixel += number_of_pixels; - } while (!end_of_line); - } - bitmap = Bitmap.createBitmap(argb, 0, bitmap_width, bitmap_width, bitmap_height, Bitmap.Config.ARGB_8888); - return bitmap != null; - } - } -} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsDecoder.java index 04c3ecd0a3..7ad70397a0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsDecoder.java @@ -1,26 +1,237 @@ +/* + * Copyright (C) 2018 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.pgs; +import android.graphics.Bitmap; +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.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; -@SuppressWarnings("unused") -public class PgsDecoder extends SimpleSubtitleDecoder { +/** A {@link SimpleSubtitleDecoder} for PGS subtitles. */ +public final class PgsDecoder extends SimpleSubtitleDecoder { + + private static final int SECTION_TYPE_PALETTE = 0x14; + private static final int SECTION_TYPE_BITMAP_PICTURE = 0x15; + private static final int SECTION_TYPE_IDENTIFIER = 0x16; + private static final int SECTION_TYPE_END = 0x80; + + private final ParsableByteArray buffer; + private final CueBuilder cueBuilder; - @SuppressWarnings("unused") public PgsDecoder() { super("PgsDecoder"); + buffer = new ParsableByteArray(); + cueBuilder = new CueBuilder(); } @Override protected Subtitle decode(byte[] data, int size, boolean reset) throws SubtitleDecoderException { - ParsableByteArray buffer = new ParsableByteArray(data, size); - PgsBuilder builder = new PgsBuilder(); - do { - if (!builder.readNextSection(buffer)) + buffer.reset(data, size); + cueBuilder.reset(); + ArrayList cues = new ArrayList<>(); + while (buffer.bytesLeft() >= 3) { + Cue cue = readNextSection(buffer, cueBuilder); + if (cue != null) { + cues.add(cue); + } + } + return new PgsSubtitle(Collections.unmodifiableList(cues)); + } + + private static Cue readNextSection(ParsableByteArray buffer, CueBuilder cueBuilder) { + int limit = buffer.limit(); + int sectionType = buffer.readUnsignedByte(); + int sectionLength = buffer.readUnsignedShort(); + + int nextSectionPosition = buffer.getPosition() + sectionLength; + if (nextSectionPosition > limit) { + buffer.setPosition(limit); + return null; + } + + Cue cue = null; + switch (sectionType) { + case SECTION_TYPE_PALETTE: + cueBuilder.parsePaletteSection(buffer, sectionLength); break; - } while (buffer.bytesLeft() > 0); - return builder.build(); + case SECTION_TYPE_BITMAP_PICTURE: + cueBuilder.parseBitmapSection(buffer, sectionLength); + break; + case SECTION_TYPE_IDENTIFIER: + cueBuilder.parseIdentifierSection(buffer, sectionLength); + break; + case SECTION_TYPE_END: + cue = cueBuilder.build(); + cueBuilder.reset(); + break; + default: + break; + } + + buffer.setPosition(nextSectionPosition); + return cue; + } + + private static final class CueBuilder { + + private final ParsableByteArray bitmapData; + private final int[] colors; + + private boolean colorsSet; + private int planeWidth; + private int planeHeight; + private int bitmapX; + private int bitmapY; + private int bitmapWidth; + private int bitmapHeight; + + public CueBuilder() { + bitmapData = new ParsableByteArray(); + colors = new int[256]; + } + + private void parsePaletteSection(ParsableByteArray buffer, int sectionLength) { + if ((sectionLength % 5) != 2) { + // Section must be two bytes followed by a whole number of (index, y, cb, cr, a) entries. + return; + } + buffer.skipBytes(2); + + Arrays.fill(colors, 0); + int entryCount = sectionLength / 5; + for (int i = 0; i < entryCount; i++) { + int index = buffer.readUnsignedByte(); + int y = buffer.readUnsignedByte(); + int cr = buffer.readUnsignedByte(); + int cb = buffer.readUnsignedByte(); + int a = buffer.readUnsignedByte(); + 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))); + colors[index] = + (a << 24) + | (Util.constrainValue(r, 0, 255) << 16) + | (Util.constrainValue(g, 0, 255) << 8) + | Util.constrainValue(b, 0, 255); + } + colorsSet = true; + } + + private void parseBitmapSection(ParsableByteArray buffer, int sectionLength) { + if (sectionLength < 4) { + return; + } + buffer.skipBytes(3); // Id (2 bytes), version (1 byte). + boolean isBaseSection = (0x80 & buffer.readUnsignedByte()) != 0; + sectionLength -= 4; + + if (isBaseSection) { + if (sectionLength < 7) { + return; + } + int totalLength = buffer.readUnsignedInt24() - 4; + if (totalLength < 4) { + return; + } + bitmapWidth = buffer.readUnsignedShort(); + bitmapHeight = buffer.readUnsignedShort(); + bitmapData.reset(totalLength - 4); + sectionLength -= 7; + } + + int position = bitmapData.getPosition(); + int limit = bitmapData.limit(); + if (position < limit && sectionLength > 0) { + int bytesToRead = Math.min(sectionLength, limit - position); + buffer.readBytes(bitmapData.data, position, bytesToRead); + bitmapData.setPosition(position + bytesToRead); + } + } + + private void parseIdentifierSection(ParsableByteArray buffer, int sectionLength) { + if (sectionLength < 19) { + return; + } + planeWidth = buffer.readUnsignedShort(); + planeHeight = buffer.readUnsignedShort(); + buffer.skipBytes(11); + bitmapX = buffer.readUnsignedShort(); + bitmapY = buffer.readUnsignedShort(); + } + + public Cue build() { + if (planeWidth == 0 + || planeHeight == 0 + || bitmapWidth == 0 + || bitmapHeight == 0 + || bitmapData.limit() == 0 + || bitmapData.getPosition() != bitmapData.limit() + || !colorsSet) { + return null; + } + // Build the bitmapData. + bitmapData.setPosition(0); + int[] argbBitmapData = new int[bitmapWidth * bitmapHeight]; + int argbBitmapDataIndex = 0; + while (argbBitmapDataIndex < argbBitmapData.length) { + int colorIndex = bitmapData.readUnsignedByte(); + if (colorIndex != 0) { + argbBitmapData[argbBitmapDataIndex++] = colors[colorIndex]; + } else { + int switchBits = bitmapData.readUnsignedByte(); + if (switchBits != 0) { + int runLength = + (switchBits & 0x40) == 0 + ? (switchBits & 0x3F) + : (((switchBits & 0x3F) << 8) | bitmapData.readUnsignedByte()); + int color = (switchBits & 0x80) == 0 ? 0 : colors[bitmapData.readUnsignedByte()]; + Arrays.fill( + argbBitmapData, argbBitmapDataIndex, argbBitmapDataIndex + runLength, color); + argbBitmapDataIndex += runLength; + } + } + } + Bitmap bitmap = + Bitmap.createBitmap(argbBitmapData, bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888); + // Build the cue. + return new Cue( + bitmap, + (float) bitmapX / planeWidth, + Cue.ANCHOR_TYPE_START, + (float) bitmapY / planeHeight, + Cue.ANCHOR_TYPE_START, + (float) bitmapWidth / planeWidth, + (float) bitmapHeight / planeHeight); + } + + public void reset() { + planeWidth = 0; + planeHeight = 0; + bitmapX = 0; + bitmapY = 0; + bitmapWidth = 0; + bitmapHeight = 0; + bitmapData.reset(0); + colorsSet = false; + } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsSubtitle.java b/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsSubtitle.java index affb2aa15b..9f9af6b6a4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsSubtitle.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsSubtitle.java @@ -1,54 +1,51 @@ +/* + * Copyright (C) 2018 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.pgs; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.Subtitle; -import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.Util; - -import java.util.Collections; import java.util.List; -public class PgsSubtitle implements Subtitle { +/** A representation of a PGS subtitle. */ +/* package */ final class PgsSubtitle implements Subtitle { - private final Cue[] cues; - private final long[] cueTimesUs; + private final List cues; - PgsSubtitle() { - this.cues = null; - this.cueTimesUs = new long[0]; - } - - PgsSubtitle(Cue[] cues, long[] cueTimesUs) { + public PgsSubtitle(List cues) { this.cues = cues; - this.cueTimesUs = cueTimesUs; } @Override public int getNextEventTimeIndex(long timeUs) { - int index = Util.binarySearchCeil(cueTimesUs, timeUs, false, false); - return index < cueTimesUs.length ? index : -1; + return C.INDEX_UNSET; } @Override public int getEventTimeCount() { -return cueTimesUs.length; -} + return 1; + } @Override public long getEventTime(int index) { - Assertions.checkArgument(index >= 0); - Assertions.checkArgument(index < cueTimesUs.length); - return cueTimesUs[index]; + return 0; } @Override public List getCues(long timeUs) { - int index = Util.binarySearchFloor(cueTimesUs, timeUs, true, false); - if (index == -1 || cues == null || cues[index] == null) { - // timeUs is earlier than the start of the first cue, or we have an empty cue. - return Collections.emptyList(); - } - else - return Collections.singletonList(cues[index]); + return cues; } } From 408bc08682328c50ba323ac74406983682464d4b Mon Sep 17 00:00:00 2001 From: eneim Date: Thu, 18 Jan 2018 09:32:46 +0900 Subject: [PATCH 1041/2472] Make Raw Resource Scheme to be public, accessible from outside. --- .../android/exoplayer2/upstream/RawResourceDataSource.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/RawResourceDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/RawResourceDataSource.java index 0b7b85b8c3..941fa90e8f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/RawResourceDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/RawResourceDataSource.java @@ -58,7 +58,7 @@ public final class RawResourceDataSource implements DataSource { return Uri.parse(RAW_RESOURCE_SCHEME + ":///" + rawResourceId); } - private static final String RAW_RESOURCE_SCHEME = "rawresource"; + public static final String RAW_RESOURCE_SCHEME = "rawresource"; private final Resources resources; private final TransferListener listener; From 88f8c768b0aca93da652d3955dda846dbc03b97f Mon Sep 17 00:00:00 2001 From: eneim Date: Thu, 18 Jan 2018 09:36:48 +0900 Subject: [PATCH 1042/2472] Also check if the Uri is built for raw resource data, and then create the RawResourceDataSource for it. --- .../exoplayer2/upstream/DefaultDataSource.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java index 853b40f73f..6d22f8b6c5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java @@ -31,6 +31,9 @@ import java.lang.reflect.InvocationTargetException; * /path/to/media/media.mp4 because the implementation assumes that a URI without a scheme is a * local file URI). *
        • asset: For fetching data from an asset in the application's apk (e.g. asset:///media.mp4). + *
        • rawresource: For fetching data from a raw resource in the applications' apk + * (e.g. rawresource:///resourceId, where rawResourceId is the integer identifier of the raw + * resource).
        • *
        • content: For fetching data from a content URI (e.g. content://authority/path/123). *
        • rtmp: For fetching data over RTMP. Only supported if the project using ExoPlayer has an * explicit dependency on ExoPlayer's RTMP extension.
        • @@ -48,6 +51,7 @@ public final class DefaultDataSource implements DataSource { private static final String SCHEME_ASSET = "asset"; private static final String SCHEME_CONTENT = "content"; private static final String SCHEME_RTMP = "rtmp"; + private static final String SCHEME_RAW = RawResourceDataSource.RAW_RESOURCE_SCHEME; private final Context context; private final TransferListener listener; @@ -60,6 +64,7 @@ public final class DefaultDataSource implements DataSource { private DataSource contentDataSource; private DataSource rtmpDataSource; private DataSource dataSchemeDataSource; + private DataSource rawResourceDataSource; private DataSource dataSource; @@ -134,6 +139,8 @@ public final class DefaultDataSource implements DataSource { dataSource = getRtmpDataSource(); } else if (DataSchemeDataSource.SCHEME_DATA.equals(scheme)) { dataSource = getDataSchemeDataSource(); + } else if (SCHEME_RAW.equals(scheme)) { + dataSource = getRawResourceDataSource(); } else { dataSource = baseDataSource; } @@ -213,4 +220,10 @@ public final class DefaultDataSource implements DataSource { return dataSchemeDataSource; } + private DataSource getRawResourceDataSource() { + if (rawResourceDataSource == null) { + rawResourceDataSource = new RawResourceDataSource(context, listener); + } + return rawResourceDataSource; + } } From 7b534cd9fda70290aac6f270291e5c28abb0fad4 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 15 Jan 2018 06:22:08 -0800 Subject: [PATCH 1043/2472] Update moe equivalence ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=181962471 --- demos/main/src/main/assets/media.exolist.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demos/main/src/main/assets/media.exolist.json b/demos/main/src/main/assets/media.exolist.json index 38a0c577ae..15183a4a8b 100644 --- a/demos/main/src/main/assets/media.exolist.json +++ b/demos/main/src/main/assets/media.exolist.json @@ -540,7 +540,7 @@ { "name": "VMAP pre-, mid- and post-rolls, single ads", "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mkv/android-screens-lavf-56.36.100-aac-avc-main-1280x720.mkv", - "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dlinear&correlator=" + "ad_tag_uri": "https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpost&cmsid=496&vid=short_onecue&correlator=" }, { "name": "VMAP pre-roll single ad, mid-roll standard pod with 3 ads, post-roll single ad", From ebfd5a7fe039d53108e8afcd6772f3371eef9aa7 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 15 Jan 2018 07:04:15 -0800 Subject: [PATCH 1044/2472] Don't discard embedded queues beyond primary queue. ChunkSampleStream.seekToUs assumes that if we can seek within the primary sample queue, we can also seek within the embedded queues. This assumption can be violated fairly easily if discardBuffer is called with toKeyframe=true, since this can cause samples to be discarded from the embedded queues within the period for which a seek in the primary sample queue will succeed. This change fixes the issue. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=181965902 --- .../exoplayer2/source/SampleMetadataQueue.java | 8 ++++++-- .../android/exoplayer2/source/SampleQueue.java | 5 +++++ .../exoplayer2/source/chunk/ChunkSampleStream.java | 13 +++++++++---- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java index 65c443d425..54db9d7880 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java @@ -186,6 +186,11 @@ import com.google.android.exoplayer2.util.Util; return largestQueuedTimestampUs; } + /** Returns the timestamp of the first sample, or {@link Long#MIN_VALUE} if the queue is empty. */ + public synchronized long getFirstTimestampUs() { + return length == 0 ? Long.MIN_VALUE : timesUs[relativeFirstIndex]; + } + /** * Rewinds the read position to the first sample retained in the queue. */ @@ -487,8 +492,7 @@ import com.google.android.exoplayer2.util.Util; * Discards the specified number of samples. * * @param discardCount The number of samples to discard. - * @return The corresponding offset up to which data should be discarded, or - * {@link C#POSITION_UNSET} if no discarding of data is necessary. + * @return The corresponding offset up to which data should be discarded. */ private long discardSamples(int discardCount) { largestDiscardedTimestampUs = Math.max(largestDiscardedTimestampUs, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java index 78b16bf377..a4feb924b8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java @@ -226,6 +226,11 @@ public final class SampleQueue implements TrackOutput { return metadataQueue.getLargestQueuedTimestampUs(); } + /** Returns the timestamp of the first sample, or {@link Long#MIN_VALUE} if the queue is empty. */ + public long getFirstTimestampUs() { + return metadataQueue.getFirstTimestampUs(); + } + /** * Rewinds the read position to the first sample in the queue. */ diff --git a/library/core/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 index 947664720b..b0a2686ef6 100644 --- a/library/core/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 @@ -120,11 +120,16 @@ public class ChunkSampleStream implements SampleStream, S * the specified position, rather than any sample before or at that position. */ public void discardBuffer(long positionUs, boolean toKeyframe) { + int oldFirstIndex = primarySampleQueue.getFirstIndex(); primarySampleQueue.discardTo(positionUs, toKeyframe, true); - for (int i = 0; i < embeddedSampleQueues.length; i++) { - embeddedSampleQueues[i].discardTo(positionUs, toKeyframe, embeddedTracksSelected[i]); + int newFirstIndex = primarySampleQueue.getFirstIndex(); + if (newFirstIndex > oldFirstIndex) { + long discardToUs = primarySampleQueue.getFirstTimestampUs(); + for (int i = 0; i < embeddedSampleQueues.length; i++) { + embeddedSampleQueues[i].discardTo(discardToUs, toKeyframe, embeddedTracksSelected[i]); + } + discardDownstreamMediaChunks(newFirstIndex); } - discardDownstreamMediaChunks(primarySampleQueue.getFirstIndex()); } /** @@ -209,7 +214,7 @@ public class ChunkSampleStream implements SampleStream, S boolean seekInsideBuffer = !isPendingReset() && (primarySampleQueue.advanceTo(positionUs, true, positionUs < getNextLoadPositionUs()) != SampleQueue.ADVANCE_FAILED); if (seekInsideBuffer) { - // We succeeded. Discard samples and corresponding chunks prior to the seek position. + // We succeeded. Advance the embedded sample queues to the seek position. for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) { embeddedSampleQueue.rewind(); embeddedSampleQueue.advanceTo(positionUs, true, false); From cfed8791b0fb5698cf8d0b268e7746d1153cfb3a Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 15 Jan 2018 07:55:53 -0800 Subject: [PATCH 1045/2472] Send downStreamFormatChanged notification for embedded streams. This allows listeners to get notified of any change to the embedded tracks. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=181969023 --- .../source/chunk/ChunkSampleStream.java | 55 ++++++-- .../source/dash/DashMediaPeriod.java | 127 +++++++++++++----- .../source/smoothstreaming/SsMediaPeriod.java | 12 +- .../testutil/FakeAdaptiveMediaPeriod.java | 11 +- 4 files changed, 154 insertions(+), 51 deletions(-) diff --git a/library/core/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 index b0a2686ef6..e740e6607e 100644 --- a/library/core/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 @@ -46,6 +46,7 @@ public class ChunkSampleStream implements SampleStream, S public final int primaryTrackType; private final int[] embeddedTrackTypes; + private final Format[] embeddedTrackFormats; private final boolean[] embeddedTracksSelected; private final T chunkSource; private final SequenceableLoader.Callback> callback; @@ -65,9 +66,10 @@ public class ChunkSampleStream implements SampleStream, S /* package */ boolean loadingFinished; /** - * @param primaryTrackType The type of the primary track. One of the {@link C} - * {@code TRACK_TYPE_*} constants. + * @param primaryTrackType The type of the primary track. One of the {@link C} {@code + * TRACK_TYPE_*} constants. * @param embeddedTrackTypes The types of any embedded tracks, or null. + * @param embeddedTrackFormats The formats of the embedded tracks, or null. * @param chunkSource A {@link ChunkSource} from which chunks to load are obtained. * @param callback An {@link Callback} for the stream. * @param allocator An {@link Allocator} from which allocations can be obtained. @@ -76,11 +78,19 @@ public class ChunkSampleStream implements SampleStream, S * before propagating an error. * @param eventDispatcher A dispatcher to notify of events. */ - public ChunkSampleStream(int primaryTrackType, int[] embeddedTrackTypes, T chunkSource, - Callback> callback, Allocator allocator, long positionUs, - int minLoadableRetryCount, EventDispatcher eventDispatcher) { + public ChunkSampleStream( + int primaryTrackType, + int[] embeddedTrackTypes, + Format[] embeddedTrackFormats, + T chunkSource, + Callback> callback, + Allocator allocator, + long positionUs, + int minLoadableRetryCount, + EventDispatcher eventDispatcher) { this.primaryTrackType = primaryTrackType; this.embeddedTrackTypes = embeddedTrackTypes; + this.embeddedTrackFormats = embeddedTrackFormats; this.chunkSource = chunkSource; this.callback = callback; this.eventDispatcher = eventDispatcher; @@ -555,6 +565,8 @@ public class ChunkSampleStream implements SampleStream, S private final SampleQueue sampleQueue; private final int index; + private boolean formatNotificationSent; + public EmbeddedSampleStream(ChunkSampleStream parent, SampleQueue sampleQueue, int index) { this.parent = parent; this.sampleQueue = sampleQueue; @@ -568,12 +580,19 @@ public class ChunkSampleStream implements SampleStream, S @Override public int skipData(long positionUs) { + int skipCount; if (loadingFinished && positionUs > sampleQueue.getLargestQueuedTimestampUs()) { - return sampleQueue.advanceToEnd(); + skipCount = sampleQueue.advanceToEnd(); } else { - int skipCount = sampleQueue.advanceTo(positionUs, true, true); - return skipCount == SampleQueue.ADVANCE_FAILED ? 0 : skipCount; + skipCount = sampleQueue.advanceTo(positionUs, true, true); + if (skipCount == SampleQueue.ADVANCE_FAILED) { + skipCount = 0; + } } + if (skipCount > 0) { + maybeNotifyTrackFormatChanged(); + } + return skipCount; } @Override @@ -587,8 +606,13 @@ public class ChunkSampleStream implements SampleStream, S if (isPendingReset()) { return C.RESULT_NOTHING_READ; } - return sampleQueue.read(formatHolder, buffer, formatRequired, loadingFinished, - lastSeekPositionUs); + int result = + sampleQueue.read( + formatHolder, buffer, formatRequired, loadingFinished, lastSeekPositionUs); + if (result == C.RESULT_BUFFER_READ) { + maybeNotifyTrackFormatChanged(); + } + return result; } public void release() { @@ -596,6 +620,17 @@ public class ChunkSampleStream implements SampleStream, S embeddedTracksSelected[index] = false; } + private void maybeNotifyTrackFormatChanged() { + if (!formatNotificationSent) { + eventDispatcher.downstreamFormatChanged( + embeddedTrackTypes[index], + embeddedTrackFormats[index], + C.SELECTION_REASON_UNKNOWN, + /* trackSelectionData= */ null, + lastSeekPositionUs); + formatNotificationSent = true; + } + } } } diff --git a/library/dash/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 index 8a69f98653..569328c101 100644 --- a/library/dash/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 @@ -436,26 +436,33 @@ import java.util.Map; } AdaptationSet firstAdaptationSet = adaptationSets.get(adaptationSetIndices[0]); - int primaryTrackGroupIndex = trackGroupCount; - boolean hasEventMessageTrack = primaryGroupHasEventMessageTrackFlags[i]; - boolean hasCea608Track = primaryGroupHasCea608TrackFlags[i]; + int primaryTrackGroupIndex = trackGroupCount++; + int eventMessageTrackGroupIndex = + primaryGroupHasEventMessageTrackFlags[i] ? trackGroupCount++ : C.INDEX_UNSET; + int cea608TrackGroupIndex = + primaryGroupHasCea608TrackFlags[i] ? trackGroupCount++ : C.INDEX_UNSET; - trackGroups[trackGroupCount] = new TrackGroup(formats); - trackGroupInfos[trackGroupCount++] = TrackGroupInfo.primaryTrack(firstAdaptationSet.type, - adaptationSetIndices, primaryTrackGroupIndex, hasEventMessageTrack, hasCea608Track); - if (hasEventMessageTrack) { + trackGroups[primaryTrackGroupIndex] = new TrackGroup(formats); + trackGroupInfos[primaryTrackGroupIndex] = + TrackGroupInfo.primaryTrack( + firstAdaptationSet.type, + adaptationSetIndices, + primaryTrackGroupIndex, + eventMessageTrackGroupIndex, + cea608TrackGroupIndex); + if (eventMessageTrackGroupIndex != C.INDEX_UNSET) { Format format = Format.createSampleFormat(firstAdaptationSet.id + ":emsg", MimeTypes.APPLICATION_EMSG, null, Format.NO_VALUE, null); - trackGroups[trackGroupCount] = new TrackGroup(format); - trackGroupInfos[trackGroupCount++] = TrackGroupInfo.embeddedEmsgTrack(adaptationSetIndices, - primaryTrackGroupIndex); + trackGroups[eventMessageTrackGroupIndex] = new TrackGroup(format); + trackGroupInfos[eventMessageTrackGroupIndex] = + TrackGroupInfo.embeddedEmsgTrack(adaptationSetIndices, primaryTrackGroupIndex); } - if (hasCea608Track) { + if (cea608TrackGroupIndex != C.INDEX_UNSET) { Format format = Format.createTextSampleFormat(firstAdaptationSet.id + ":cea608", MimeTypes.APPLICATION_CEA608, 0, null); - trackGroups[trackGroupCount] = new TrackGroup(format); - trackGroupInfos[trackGroupCount++] = TrackGroupInfo.embeddedCea608Track( - adaptationSetIndices, primaryTrackGroupIndex); + trackGroups[cea608TrackGroupIndex] = new TrackGroup(format); + trackGroupInfos[cea608TrackGroupIndex] = + TrackGroupInfo.embeddedCea608Track(adaptationSetIndices, primaryTrackGroupIndex); } } return trackGroupCount; @@ -476,24 +483,39 @@ import java.util.Map; TrackSelection selection, long positionUs) { int embeddedTrackCount = 0; int[] embeddedTrackTypes = new int[2]; - boolean enableEventMessageTrack = trackGroupInfo.hasEmbeddedEventMessageTrack; + Format[] embeddedTrackFormats = new Format[2]; + boolean enableEventMessageTrack = + trackGroupInfo.embeddedEventMessageTrackGroupIndex != C.INDEX_UNSET; if (enableEventMessageTrack) { + embeddedTrackFormats[embeddedTrackCount] = + trackGroups.get(trackGroupInfo.embeddedEventMessageTrackGroupIndex).getFormat(0); embeddedTrackTypes[embeddedTrackCount++] = C.TRACK_TYPE_METADATA; } - boolean enableCea608Track = trackGroupInfo.hasEmbeddedCea608Track; + boolean enableCea608Track = trackGroupInfo.embeddedCea608TrackGroupIndex != C.INDEX_UNSET; if (enableCea608Track) { + embeddedTrackFormats[embeddedTrackCount] = + trackGroups.get(trackGroupInfo.embeddedCea608TrackGroupIndex).getFormat(0); embeddedTrackTypes[embeddedTrackCount++] = C.TRACK_TYPE_TEXT; } if (embeddedTrackCount < embeddedTrackTypes.length) { + embeddedTrackFormats = Arrays.copyOf(embeddedTrackFormats, embeddedTrackCount); embeddedTrackTypes = Arrays.copyOf(embeddedTrackTypes, embeddedTrackCount); } DashChunkSource chunkSource = chunkSourceFactory.createDashChunkSource( manifestLoaderErrorThrower, manifest, periodIndex, trackGroupInfo.adaptationSetIndices, selection, trackGroupInfo.trackType, elapsedRealtimeOffset, enableEventMessageTrack, enableCea608Track); - ChunkSampleStream stream = new ChunkSampleStream<>(trackGroupInfo.trackType, - embeddedTrackTypes, chunkSource, this, allocator, positionUs, minLoadableRetryCount, - eventDispatcher); + ChunkSampleStream stream = + new ChunkSampleStream<>( + trackGroupInfo.trackType, + embeddedTrackTypes, + embeddedTrackFormats, + chunkSource, + this, + allocator, + positionUs, + minLoadableRetryCount, + eventDispatcher); return stream; } @@ -578,43 +600,74 @@ import java.util.Map; public final int eventStreamGroupIndex; public final int primaryTrackGroupIndex; - public final boolean hasEmbeddedEventMessageTrack; - public final boolean hasEmbeddedCea608Track; + public final int embeddedEventMessageTrackGroupIndex; + public final int embeddedCea608TrackGroupIndex; - public static TrackGroupInfo primaryTrack(int trackType, int[] adaptationSetIndices, - int primaryTrackGroupIndex, boolean hasEmbeddedEventMessageTrack, - boolean hasEmbeddedCea608Track) { - return new TrackGroupInfo(trackType, CATEGORY_PRIMARY, adaptationSetIndices, - primaryTrackGroupIndex, hasEmbeddedEventMessageTrack, hasEmbeddedCea608Track, -1); + public static TrackGroupInfo primaryTrack( + int trackType, + int[] adaptationSetIndices, + int primaryTrackGroupIndex, + int embeddedEventMessageTrackGroupIndex, + int embeddedCea608TrackGroupIndex) { + return new TrackGroupInfo( + trackType, + CATEGORY_PRIMARY, + adaptationSetIndices, + primaryTrackGroupIndex, + embeddedEventMessageTrackGroupIndex, + embeddedCea608TrackGroupIndex, + -1); } public static TrackGroupInfo embeddedEmsgTrack(int[] adaptationSetIndices, int primaryTrackGroupIndex) { - return new TrackGroupInfo(C.TRACK_TYPE_METADATA, CATEGORY_EMBEDDED, - adaptationSetIndices, primaryTrackGroupIndex, false, false, -1); + return new TrackGroupInfo( + C.TRACK_TYPE_METADATA, + CATEGORY_EMBEDDED, + adaptationSetIndices, + primaryTrackGroupIndex, + C.INDEX_UNSET, + C.INDEX_UNSET, + -1); } public static TrackGroupInfo embeddedCea608Track(int[] adaptationSetIndices, int primaryTrackGroupIndex) { - return new TrackGroupInfo(C.TRACK_TYPE_TEXT, CATEGORY_EMBEDDED, - adaptationSetIndices, primaryTrackGroupIndex, false, false, -1); + return new TrackGroupInfo( + C.TRACK_TYPE_TEXT, + CATEGORY_EMBEDDED, + adaptationSetIndices, + primaryTrackGroupIndex, + C.INDEX_UNSET, + C.INDEX_UNSET, + -1); } public static TrackGroupInfo mpdEventTrack(int eventStreamIndex) { - return new TrackGroupInfo(C.TRACK_TYPE_METADATA, CATEGORY_MANIFEST_EVENTS, - null, -1, false, false, eventStreamIndex); + return new TrackGroupInfo( + C.TRACK_TYPE_METADATA, + CATEGORY_MANIFEST_EVENTS, + null, + -1, + C.INDEX_UNSET, + C.INDEX_UNSET, + eventStreamIndex); } - private TrackGroupInfo(int trackType, @TrackGroupCategory int trackGroupCategory, - int[] adaptationSetIndices, int primaryTrackGroupIndex, - boolean hasEmbeddedEventMessageTrack, boolean hasEmbeddedCea608Track, + private TrackGroupInfo( + int trackType, + @TrackGroupCategory int trackGroupCategory, + int[] adaptationSetIndices, + int primaryTrackGroupIndex, + int embeddedEventMessageTrackGroupIndex, + int embeddedCea608TrackGroupIndex, int eventStreamGroupIndex) { this.trackType = trackType; this.adaptationSetIndices = adaptationSetIndices; this.trackGroupCategory = trackGroupCategory; this.primaryTrackGroupIndex = primaryTrackGroupIndex; - this.hasEmbeddedEventMessageTrack = hasEmbeddedEventMessageTrack; - this.hasEmbeddedCea608Track = hasEmbeddedCea608Track; + this.embeddedEventMessageTrackGroupIndex = embeddedEventMessageTrackGroupIndex; + this.embeddedCea608TrackGroupIndex = embeddedCea608TrackGroupIndex; this.eventStreamGroupIndex = eventStreamGroupIndex; } } diff --git a/library/smoothstreaming/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 index 99804ca809..a600741362 100644 --- a/library/smoothstreaming/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 @@ -207,8 +207,16 @@ import java.util.ArrayList; int streamElementIndex = trackGroups.indexOf(selection.getTrackGroup()); SsChunkSource chunkSource = chunkSourceFactory.createChunkSource(manifestLoaderErrorThrower, manifest, streamElementIndex, selection, trackEncryptionBoxes); - return new ChunkSampleStream<>(manifest.streamElements[streamElementIndex].type, null, - chunkSource, this, allocator, positionUs, minLoadableRetryCount, eventDispatcher); + return new ChunkSampleStream<>( + manifest.streamElements[streamElementIndex].type, + null, + null, + chunkSource, + this, + allocator, + positionUs, + minLoadableRetryCount, + eventDispatcher); } private static TrackGroupArray buildTrackGroups(SsManifest manifest) { diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java index 7b9fe3db07..d32dda65f4 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAdaptiveMediaPeriod.java @@ -132,8 +132,15 @@ public class FakeAdaptiveMediaPeriod extends FakeMediaPeriod protected SampleStream createSampleStream(TrackSelection trackSelection) { FakeChunkSource chunkSource = chunkSourceFactory.createChunkSource(trackSelection, durationUs); return new ChunkSampleStream<>( - MimeTypes.getTrackType(trackSelection.getSelectedFormat().sampleMimeType), null, - chunkSource, this, allocator, 0, 3, eventDispatcher); + MimeTypes.getTrackType(trackSelection.getSelectedFormat().sampleMimeType), + null, + null, + chunkSource, + this, + allocator, + 0, + 3, + eventDispatcher); } @Override From 515fdf3bfd9aed9826bd84a3f2194ffc8481f000 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Mon, 15 Jan 2018 08:21:33 -0800 Subject: [PATCH 1046/2472] Allow extension of DebugTextViewHelper ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=181970955 --- .../exoplayer2/ui/DebugTextViewHelper.java | 53 ++++++++++--------- 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java index fda74db28d..6066445e9d 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java @@ -27,7 +27,7 @@ import java.util.Locale; * A helper class for periodically updating a {@link TextView} with debug information obtained from * a {@link SimpleExoPlayer}. */ -public final class DebugTextViewHelper extends Player.DefaultEventListener implements Runnable { +public class DebugTextViewHelper extends Player.DefaultEventListener implements Runnable { private static final int REFRESH_INTERVAL_MS = 1000; @@ -49,7 +49,7 @@ public final class DebugTextViewHelper extends Player.DefaultEventListener imple * Starts periodic updates of the {@link TextView}. Must be called from the application's main * thread. */ - public void start() { + public final void start() { if (started) { return; } @@ -62,7 +62,7 @@ public final class DebugTextViewHelper extends Player.DefaultEventListener imple * Stops periodic updates of the {@link TextView}. Must be called from the application's main * thread. */ - public void stop() { + public final void stop() { if (!started) { return; } @@ -74,59 +74,63 @@ public final class DebugTextViewHelper extends Player.DefaultEventListener imple // Player.EventListener implementation. @Override - public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + public final void onPlayerStateChanged(boolean playWhenReady, int playbackState) { updateAndPost(); } @Override - public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { + public final void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { updateAndPost(); } // Runnable implementation. @Override - public void run() { + public final void run() { updateAndPost(); } - // Private methods. + // Protected methods. @SuppressLint("SetTextI18n") - private void updateAndPost() { - textView.setText(getPlayerStateString() + getPlayerWindowIndexString() + getVideoString() - + getAudioString()); + protected final void updateAndPost() { + textView.setText(getDebugString()); textView.removeCallbacks(this); textView.postDelayed(this, REFRESH_INTERVAL_MS); } - private String getPlayerStateString() { - String text = "playWhenReady:" + player.getPlayWhenReady() + " playbackState:"; + /** Returns the debugging information string to be shown by the target {@link TextView}. */ + protected String getDebugString() { + return getPlayerStateString() + getVideoString() + getAudioString(); + } + + /** Returns a string containing player state debugging information. */ + protected String getPlayerStateString() { + String playbackStateString; switch (player.getPlaybackState()) { case Player.STATE_BUFFERING: - text += "buffering"; + playbackStateString = "buffering"; break; case Player.STATE_ENDED: - text += "ended"; + playbackStateString = "ended"; break; case Player.STATE_IDLE: - text += "idle"; + playbackStateString = "idle"; break; case Player.STATE_READY: - text += "ready"; + playbackStateString = "ready"; break; default: - text += "unknown"; + playbackStateString = "unknown"; break; } - return text; + return String.format( + "playWhenReady:%s playbackState:%s window:%s", + player.getPlayWhenReady(), playbackStateString, player.getCurrentWindowIndex()); } - private String getPlayerWindowIndexString() { - return " window:" + player.getCurrentWindowIndex(); - } - - private String getVideoString() { + /** Returns a string containing video debugging information. */ + protected String getVideoString() { Format format = player.getVideoFormat(); if (format == null) { return ""; @@ -136,7 +140,8 @@ public final class DebugTextViewHelper extends Player.DefaultEventListener imple + getDecoderCountersBufferCountString(player.getVideoDecoderCounters()) + ")"; } - private String getAudioString() { + /** Returns a string containing audio debugging information. */ + protected String getAudioString() { Format format = player.getAudioFormat(); if (format == null) { return ""; From 6bed2ffc047130faf3beb32aacc75aa251d355bc Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 16 Jan 2018 00:23:47 -0800 Subject: [PATCH 1047/2472] Remove ndk-build from [] flac build rules Android NDK r9 in [] is deprecated (see [] Update the ExoPlayer flac extensions to use android_jni_library. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=182017669 --- extensions/flac/src/main/jni/include/data_source.h | 1 + 1 file changed, 1 insertion(+) diff --git a/extensions/flac/src/main/jni/include/data_source.h b/extensions/flac/src/main/jni/include/data_source.h index 175431dd7a..88af3e1277 100644 --- a/extensions/flac/src/main/jni/include/data_source.h +++ b/extensions/flac/src/main/jni/include/data_source.h @@ -22,6 +22,7 @@ class DataSource { public: + virtual ~DataSource() {} // Returns the number of bytes read, or -1 on failure. It's not an error if // this returns zero; it just means the given offset is equal to, or // beyond, the end of the source. From 6749623cd127f15c014dcf21ade1349fe436ef60 Mon Sep 17 00:00:00 2001 From: hoangtc Date: Tue, 16 Jan 2018 04:09:09 -0800 Subject: [PATCH 1048/2472] Handle DASH `emsg' events targeting player. For live streaming, there are several types of DASH `emsg' events that directly target the player. These events can signal whether the manifest is expired, or the live streaming has ended, and should be handle directly within the player. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=182034591 --- RELEASENOTES.md | 8 +- .../source/chunk/ChunkSampleStream.java | 36 +- .../source/dash/DashChunkSource.java | 5 +- .../dash/DashManifestExpiredException.java | 21 + .../source/dash/DashMediaPeriod.java | 82 +++- .../source/dash/DashMediaSource.java | 115 ++++- .../source/dash/DefaultDashChunkSource.java | 110 ++++- .../source/dash/PlayerEmsgHandler.java | 454 ++++++++++++++++++ 8 files changed, 775 insertions(+), 56 deletions(-) create mode 100644 library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashManifestExpiredException.java create mode 100644 library/dash/src/main/java/com/google/android/exoplayer2/source/dash/PlayerEmsgHandler.java diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 6d4347490e..ba82f46525 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -32,7 +32,13 @@ seeking to the closest sync points before, either side or after specified seek positions. * Note: `SeekParameters` are not currently supported when playing HLS streams. -* DASH: Support DASH manifest EventStream elements. +* DRM: Optimistically attempt playback of DRM protected content that does not + declare scheme specific init data + ([#3630](https://github.com/google/ExoPlayer/issues/3630)). +* DASH: + * Support in-band Emsg events targeting player with scheme id + "urn:mpeg:dash:event:2012" and scheme value of either "1", "2" or "3". + * Support DASH manifest EventStream elements. * HLS: Add opt-in support for chunkless preparation in HLS. This allows an HLS source to finish preparation without downloading any chunks, which can significantly reduce initial buffering time diff --git a/library/core/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 index e740e6607e..29a6ce29fb 100644 --- a/library/core/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 @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.source.chunk; +import android.support.annotation.Nullable; import android.util.Log; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; @@ -41,6 +42,17 @@ import java.util.List; public class ChunkSampleStream implements SampleStream, SequenceableLoader, Loader.Callback, Loader.ReleaseCallback { + /** A callback to be notified when a sample stream has finished being released. */ + public interface ReleaseCallback { + + /** + * Called when the {@link ChunkSampleStream} has finished being released. + * + * @param chunkSampleStream The released sample stream. + */ + void onSampleStreamReleased(ChunkSampleStream chunkSampleStream); + } + private static final String TAG = "ChunkSampleStream"; public final int primaryTrackType; @@ -61,6 +73,7 @@ public class ChunkSampleStream implements SampleStream, S private final BaseMediaChunkOutput mediaChunkOutput; private Format primaryDownstreamTrackFormat; + private ReleaseCallback releaseCallback; private long pendingResetPositionUs; /* package */ long lastSeekPositionUs; /* package */ boolean loadingFinished; @@ -247,10 +260,26 @@ public class ChunkSampleStream implements SampleStream, S /** * Releases the stream. - *

          - * This method should be called when the stream is no longer required. + * + *

          This method should be called when the stream is no longer required. Either this method or + * {@link #release(ReleaseCallback)} can be used to release this stream. */ public void release() { + release(null); + } + + /** + * Releases the stream. + * + *

          This method should be called when the stream is no longer required. Either this method or + * {@link #release()} can be used to release this stream. + * + * @param callback A callback to be called when the release ends. Will be called synchronously + * from this method if no load is in progress, or asynchronously once the load has been + * canceled otherwise. + */ + public void release(@Nullable ReleaseCallback callback) { + this.releaseCallback = callback; boolean releasedSynchronously = loader.release(this); if (!releasedSynchronously) { // Discard as much as we can synchronously. @@ -267,6 +296,9 @@ public class ChunkSampleStream implements SampleStream, S for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) { embeddedSampleQueue.reset(); } + if (releaseCallback != null) { + releaseCallback.onSampleStreamReleased(this); + } } // SampleStream implementation. diff --git a/library/dash/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 index 167a8d486c..31c32e6100 100644 --- a/library/dash/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 @@ -16,7 +16,9 @@ package com.google.android.exoplayer2.source.dash; import android.os.SystemClock; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.source.chunk.ChunkSource; +import com.google.android.exoplayer2.source.dash.PlayerEmsgHandler.PlayerTrackEmsgHandler; import com.google.android.exoplayer2.source.dash.manifest.DashManifest; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.LoaderErrorThrower; @@ -53,7 +55,8 @@ public interface DashChunkSource extends ChunkSource { int type, long elapsedRealtimeOffsetMs, boolean enableEventMessageTrack, - boolean enableCea608Track); + boolean enableCea608Track, + @Nullable PlayerTrackEmsgHandler playerEmsgHandler); } /** diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashManifestExpiredException.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashManifestExpiredException.java new file mode 100644 index 0000000000..2af847467c --- /dev/null +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashManifestExpiredException.java @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2018 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 java.io.IOException; + +/** Thrown when a live playback's manifest is expired and a new manifest could not be loaded. */ +public final class DashManifestExpiredException extends IOException {} diff --git a/library/dash/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 index 569328c101..4dab4e2279 100644 --- a/library/dash/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 @@ -31,6 +31,8 @@ import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.chunk.ChunkSampleStream; import com.google.android.exoplayer2.source.chunk.ChunkSampleStream.EmbeddedSampleStream; +import com.google.android.exoplayer2.source.dash.PlayerEmsgHandler.PlayerEmsgCallback; +import com.google.android.exoplayer2.source.dash.PlayerEmsgHandler.PlayerTrackEmsgHandler; 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.Descriptor; @@ -47,14 +49,15 @@ import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; +import java.util.IdentityHashMap; import java.util.List; import java.util.Map; -/** - * A DASH {@link MediaPeriod}. - */ -/* package */ final class DashMediaPeriod implements MediaPeriod, - SequenceableLoader.Callback> { +/** A DASH {@link MediaPeriod}. */ +/* package */ final class DashMediaPeriod + implements MediaPeriod, + SequenceableLoader.Callback>, + ChunkSampleStream.ReleaseCallback { /* package */ final int id; private final DashChunkSource.Factory chunkSourceFactory; @@ -66,6 +69,9 @@ import java.util.Map; private final TrackGroupArray trackGroups; private final TrackGroupInfo[] trackGroupInfos; private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; + private final PlayerEmsgHandler playerEmsgHandler; + private final IdentityHashMap, PlayerTrackEmsgHandler> + trackEmsgHandlerBySampleStream; private Callback callback; private ChunkSampleStream[] sampleStreams; @@ -75,11 +81,18 @@ import java.util.Map; private int periodIndex; private List eventStreams; - public DashMediaPeriod(int id, DashManifest manifest, int periodIndex, - DashChunkSource.Factory chunkSourceFactory, int minLoadableRetryCount, - EventDispatcher eventDispatcher, long elapsedRealtimeOffset, - LoaderErrorThrower manifestLoaderErrorThrower, Allocator allocator, - CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory) { + public DashMediaPeriod( + int id, + DashManifest manifest, + int periodIndex, + DashChunkSource.Factory chunkSourceFactory, + int minLoadableRetryCount, + EventDispatcher eventDispatcher, + long elapsedRealtimeOffset, + LoaderErrorThrower manifestLoaderErrorThrower, + Allocator allocator, + CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, + PlayerEmsgCallback playerEmsgCallback) { this.id = id; this.manifest = manifest; this.periodIndex = periodIndex; @@ -90,8 +103,10 @@ import java.util.Map; this.manifestLoaderErrorThrower = manifestLoaderErrorThrower; this.allocator = allocator; this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; + playerEmsgHandler = new PlayerEmsgHandler(manifest, playerEmsgCallback, allocator); sampleStreams = newSampleStreamArray(0); eventSampleStreams = new EventSampleStream[0]; + trackEmsgHandlerBySampleStream = new IdentityHashMap<>(); compositeSequenceableLoader = compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(sampleStreams); Period period = manifest.getPeriod(periodIndex); @@ -111,14 +126,14 @@ import java.util.Map; public void updateManifest(DashManifest manifest, int periodIndex) { this.manifest = manifest; this.periodIndex = periodIndex; - Period period = manifest.getPeriod(periodIndex); + playerEmsgHandler.updateManifest(manifest); if (sampleStreams != null) { for (ChunkSampleStream sampleStream : sampleStreams) { sampleStream.getChunkSource().updateManifest(manifest, periodIndex); } callback.onContinueLoadingRequested(this); } - eventStreams = period.eventStreams; + eventStreams = manifest.getPeriod(periodIndex).eventStreams; for (EventSampleStream eventSampleStream : eventSampleStreams) { for (EventStream eventStream : eventStreams) { if (eventStream.id().equals(eventSampleStream.eventStreamId())) { @@ -130,11 +145,24 @@ import java.util.Map; } public void release() { + playerEmsgHandler.release(); for (ChunkSampleStream sampleStream : sampleStreams) { - sampleStream.release(); + sampleStream.release(this); } } + // ChunkSampleStream.ReleaseCallback implementation. + + @Override + public void onSampleStreamReleased(ChunkSampleStream stream) { + PlayerTrackEmsgHandler trackEmsgHandler = trackEmsgHandlerBySampleStream.remove(stream); + if (trackEmsgHandler != null) { + trackEmsgHandler.release(); + } + } + + // MediaPeriod implementation. + @Override public void prepare(Callback callback, long positionUs) { this.callback = callback; @@ -181,7 +209,7 @@ import java.util.Map; @SuppressWarnings("unchecked") ChunkSampleStream stream = (ChunkSampleStream) streams[i]; if (selections[i] == null || !mayRetainStreamFlags[i]) { - stream.release(); + stream.release(this); streams[i] = null; } else { int trackGroupIndex = trackGroups.indexOf(selections[i].getTrackGroup()); @@ -501,10 +529,22 @@ import java.util.Map; embeddedTrackFormats = Arrays.copyOf(embeddedTrackFormats, embeddedTrackCount); embeddedTrackTypes = Arrays.copyOf(embeddedTrackTypes, embeddedTrackCount); } - DashChunkSource chunkSource = chunkSourceFactory.createDashChunkSource( - manifestLoaderErrorThrower, manifest, periodIndex, trackGroupInfo.adaptationSetIndices, - selection, trackGroupInfo.trackType, elapsedRealtimeOffset, enableEventMessageTrack, - enableCea608Track); + PlayerTrackEmsgHandler trackPlayerEmsgHandler = + manifest.dynamic && enableEventMessageTrack + ? playerEmsgHandler.newPlayerTrackEmsgHandler() + : null; + DashChunkSource chunkSource = + chunkSourceFactory.createDashChunkSource( + manifestLoaderErrorThrower, + manifest, + periodIndex, + trackGroupInfo.adaptationSetIndices, + selection, + trackGroupInfo.trackType, + elapsedRealtimeOffset, + enableEventMessageTrack, + enableCea608Track, + trackPlayerEmsgHandler); ChunkSampleStream stream = new ChunkSampleStream<>( trackGroupInfo.trackType, @@ -516,6 +556,7 @@ import java.util.Map; positionUs, minLoadableRetryCount, eventDispatcher); + trackEmsgHandlerBySampleStream.put(stream, trackPlayerEmsgHandler); return stream; } @@ -581,9 +622,8 @@ import java.util.Map; private static final int CATEGORY_PRIMARY = 0; /** - * A track group whose samples are embedded within one of the primary streams. - * For example: an EMSG track has its sample embedded in `emsg' atoms in one of the primary - * streams. + * A track group whose samples are embedded within one of the primary streams. For example: an + * EMSG track has its sample embedded in emsg atoms in one of the primary streams. */ private static final int CATEGORY_EMBEDDED = 1; diff --git a/library/dash/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 index 77914d6d45..08e25f216a 100644 --- a/library/dash/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 @@ -35,6 +35,7 @@ import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.SequenceableLoader; import com.google.android.exoplayer2.source.ads.AdsMediaSource; +import com.google.android.exoplayer2.source.dash.PlayerEmsgHandler.PlayerEmsgCallback; 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.UtcTimingElement; @@ -56,9 +57,7 @@ import java.util.TimeZone; import java.util.regex.Matcher; import java.util.regex.Pattern; -/** - * A DASH {@link MediaSource}. - */ +/** A DASH {@link MediaSource}. */ public final class DashMediaSource implements MediaSource { static { @@ -280,6 +279,7 @@ public final class DashMediaSource implements MediaSource { private final SparseArray periodsById; private final Runnable refreshManifestRunnable; private final Runnable simulateManifestRefreshRunnable; + private final PlayerEmsgCallback playerEmsgCallback; private Listener sourceListener; private DataSource dataSource; @@ -291,7 +291,11 @@ public final class DashMediaSource implements MediaSource { private long manifestLoadEndTimestamp; private DashManifest manifest; private Handler handler; + private boolean pendingManifestLoading; private long elapsedRealtimeOffsetMs; + private long expiredManifestPublishTimeUs; + private boolean dynamicMediaPresentationEnded; + private int staleManifestReloadAttempt; private int firstPeriodId; @@ -446,6 +450,8 @@ public final class DashMediaSource implements MediaSource { eventDispatcher = new EventDispatcher(eventHandler, eventListener); manifestUriLock = new Object(); periodsById = new SparseArray<>(); + playerEmsgCallback = new DefaultPlayerEmsgCallback(); + expiredManifestPublishTimeUs = C.TIME_UNSET; if (sideloadedManifest) { Assertions.checkState(!manifest.dynamic); manifestCallback = null; @@ -507,9 +513,19 @@ public final class DashMediaSource implements MediaSource { int periodIndex = periodId.periodIndex; EventDispatcher periodEventDispatcher = eventDispatcher.copyWithMediaTimeOffsetMs( manifest.getPeriod(periodIndex).startMs); - DashMediaPeriod mediaPeriod = new DashMediaPeriod(firstPeriodId + periodIndex, manifest, - periodIndex, chunkSourceFactory, minLoadableRetryCount, periodEventDispatcher, - elapsedRealtimeOffsetMs, loaderErrorThrower, allocator, compositeSequenceableLoaderFactory); + DashMediaPeriod mediaPeriod = + new DashMediaPeriod( + firstPeriodId + periodIndex, + manifest, + periodIndex, + chunkSourceFactory, + minLoadableRetryCount, + periodEventDispatcher, + elapsedRealtimeOffsetMs, + loaderErrorThrower, + allocator, + compositeSequenceableLoaderFactory, + playerEmsgCallback); periodsById.put(mediaPeriod.id, mediaPeriod); return mediaPeriod; } @@ -523,6 +539,7 @@ public final class DashMediaSource implements MediaSource { @Override public void releaseSource() { + pendingManifestLoading = false; dataSource = null; loaderErrorThrower = null; if (loader != null) { @@ -540,6 +557,24 @@ public final class DashMediaSource implements MediaSource { periodsById.clear(); } + // PlayerEmsgCallback callbacks. + + /* package */ void onDashManifestRefreshRequested() { + handler.removeCallbacks(simulateManifestRefreshRunnable); + startLoadingManifest(); + } + + /* package */ void onDashLiveMediaPresentationEndSignalEncountered() { + this.dynamicMediaPresentationEnded = true; + } + + /* package */ void onDashManifestPublishTimeExpired(long expiredManifestPublishTimeUs) { + if (this.expiredManifestPublishTimeUs == C.TIME_UNSET + || this.expiredManifestPublishTimeUs < expiredManifestPublishTimeUs) { + this.expiredManifestPublishTimeUs = expiredManifestPublishTimeUs; + } + } + // Loadable callbacks. /* package */ void onManifestLoadCompleted(ParsingLoadable loadable, @@ -566,9 +601,16 @@ public final class DashMediaSource implements MediaSource { return; } + if (maybeReloadStaleDynamicManifest(newManifest)) { + return; + } manifest = newManifest; manifestLoadStartTimestamp = elapsedRealtimeMs - loadDurationMs; manifestLoadEndTimestamp = elapsedRealtimeMs; + staleManifestReloadAttempt = 0; + if (!manifest.dynamic) { + pendingManifestLoading = false; + } if (manifest.location != null) { synchronized (manifestUriLock) { // This condition checks that replaceManifestUri wasn't called between the start and end of @@ -622,11 +664,41 @@ public final class DashMediaSource implements MediaSource { // Internal methods. + /** + * Reloads a stale dynamic manifest to get a more recent version if possible. + * + * @return True if the reload is scheduled. False if we have already retried too many times. + */ + private boolean maybeReloadStaleDynamicManifest(DashManifest manifest) { + if (!isManifestStale(manifest)) { + return false; + } + String warning = + "Loaded a stale dynamic manifest " + + manifest.publishTimeMs + + " " + + dynamicMediaPresentationEnded + + " " + + expiredManifestPublishTimeUs; + Log.w(TAG, warning); + if (staleManifestReloadAttempt++ < minLoadableRetryCount) { + startLoadingManifest(); + return true; + } + return false; + } + private void startLoadingManifest() { + handler.removeCallbacks(refreshManifestRunnable); + if (loader.isLoading()) { + pendingManifestLoading = true; + return; + } Uri manifestUri; synchronized (manifestUriLock) { manifestUri = this.manifestUri; } + pendingManifestLoading = false; startLoading(new ParsingLoadable<>(dataSource, manifestUri, C.DATA_TYPE_MANIFEST, manifestParser), manifestCallback, minLoadableRetryCount); } @@ -753,13 +825,21 @@ public final class DashMediaSource implements MediaSource { if (windowChangingImplicitly) { handler.postDelayed(simulateManifestRefreshRunnable, NOTIFY_MANIFEST_INTERVAL_MS); } - // Schedule an explicit refresh if needed. - if (scheduleRefresh) { + if (pendingManifestLoading) { + startLoadingManifest(); + } else if (scheduleRefresh) { + // Schedule an explicit refresh if needed. scheduleManifestRefresh(); } } } + private boolean isManifestStale(DashManifest manifest) { + return manifest.dynamic + && (dynamicMediaPresentationEnded + || manifest.publishTimeMs <= expiredManifestPublishTimeUs); + } + private void scheduleManifestRefresh() { if (!manifest.dynamic) { return; @@ -948,6 +1028,24 @@ public final class DashMediaSource implements MediaSource { } + private final class DefaultPlayerEmsgCallback implements PlayerEmsgCallback { + + @Override + public void onDashManifestRefreshRequested() { + DashMediaSource.this.onDashManifestRefreshRequested(); + } + + @Override + public void onDashManifestPublishTimeExpired(long expiredManifestPublishTimeUs) { + DashMediaSource.this.onDashManifestPublishTimeExpired(expiredManifestPublishTimeUs); + } + + @Override + public void onDashLiveMediaPresentationEndSignalEncountered() { + DashMediaSource.this.onDashLiveMediaPresentationEndSignalEncountered(); + } + } + private final class ManifestCallback implements Loader.Callback> { @Override @@ -1039,5 +1137,4 @@ public final class DashMediaSource implements MediaSource { } } - } diff --git a/library/dash/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 index 1162762f7c..4635a08a3c 100644 --- a/library/dash/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 @@ -17,12 +17,14 @@ package com.google.android.exoplayer2.source.dash; import android.net.Uri; import android.os.SystemClock; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.extractor.ChunkIndex; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.SeekMap; +import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor; import com.google.android.exoplayer2.extractor.rawcc.RawCcExtractor; @@ -35,6 +37,7 @@ import com.google.android.exoplayer2.source.chunk.ContainerMediaChunk; import com.google.android.exoplayer2.source.chunk.InitializationChunk; import com.google.android.exoplayer2.source.chunk.MediaChunk; import com.google.android.exoplayer2.source.chunk.SingleSampleMediaChunk; +import com.google.android.exoplayer2.source.dash.PlayerEmsgHandler.PlayerTrackEmsgHandler; 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.RangedUri; @@ -71,14 +74,31 @@ public class DefaultDashChunkSource implements DashChunkSource { } @Override - public DashChunkSource createDashChunkSource(LoaderErrorThrower manifestLoaderErrorThrower, - DashManifest manifest, int periodIndex, int[] adaptationSetIndices, - TrackSelection trackSelection, int trackType, long elapsedRealtimeOffsetMs, - boolean enableEventMessageTrack, boolean enableCea608Track) { + public DashChunkSource createDashChunkSource( + LoaderErrorThrower manifestLoaderErrorThrower, + DashManifest manifest, + int periodIndex, + int[] adaptationSetIndices, + TrackSelection trackSelection, + int trackType, + long elapsedRealtimeOffsetMs, + boolean enableEventMessageTrack, + boolean enableCea608Track, + @Nullable PlayerTrackEmsgHandler playerEmsgHandler) { DataSource dataSource = dataSourceFactory.createDataSource(); - return new DefaultDashChunkSource(manifestLoaderErrorThrower, manifest, periodIndex, - adaptationSetIndices, trackSelection, trackType, dataSource, elapsedRealtimeOffsetMs, - maxSegmentsPerLoad, enableEventMessageTrack, enableCea608Track); + return new DefaultDashChunkSource( + manifestLoaderErrorThrower, + manifest, + periodIndex, + adaptationSetIndices, + trackSelection, + trackType, + dataSource, + elapsedRealtimeOffsetMs, + maxSegmentsPerLoad, + enableEventMessageTrack, + enableCea608Track, + playerEmsgHandler); } } @@ -90,6 +110,7 @@ public class DefaultDashChunkSource implements DashChunkSource { private final DataSource dataSource; private final long elapsedRealtimeOffsetMs; private final int maxSegmentsPerLoad; + @Nullable private final PlayerTrackEmsgHandler playerTrackEmsgHandler; protected final RepresentationHolder[] representationHolders; @@ -110,18 +131,28 @@ public class DefaultDashChunkSource implements DashChunkSource { * @param elapsedRealtimeOffsetMs If known, an estimate of the instantaneous difference between * server-side unix time and {@link SystemClock#elapsedRealtime()} in milliseconds, specified * as the server's unix time minus the local elapsed time. If unknown, set to 0. - * @param maxSegmentsPerLoad The maximum number of segments to combine into a single request. - * Note that segments will only be combined if their {@link Uri}s are the same and if their - * data ranges are adjacent. + * @param maxSegmentsPerLoad The maximum number of segments to combine into a single request. Note + * that segments will only be combined if their {@link Uri}s are the same and if their data + * ranges are adjacent. * @param enableEventMessageTrack Whether the chunks generated by the source may output an event * message track. * @param enableCea608Track Whether the chunks generated by the source may output a CEA-608 track. + * @param playerTrackEmsgHandler The {@link PlayerTrackEmsgHandler} instance to handle emsg + * messages targeting the player. Maybe null if this is not necessary. */ - public DefaultDashChunkSource(LoaderErrorThrower manifestLoaderErrorThrower, - DashManifest manifest, int periodIndex, int[] adaptationSetIndices, - TrackSelection trackSelection, int trackType, DataSource dataSource, - long elapsedRealtimeOffsetMs, int maxSegmentsPerLoad, boolean enableEventMessageTrack, - boolean enableCea608Track) { + public DefaultDashChunkSource( + LoaderErrorThrower manifestLoaderErrorThrower, + DashManifest manifest, + int periodIndex, + int[] adaptationSetIndices, + TrackSelection trackSelection, + int trackType, + DataSource dataSource, + long elapsedRealtimeOffsetMs, + int maxSegmentsPerLoad, + boolean enableEventMessageTrack, + boolean enableCea608Track, + @Nullable PlayerTrackEmsgHandler playerTrackEmsgHandler) { this.manifestLoaderErrorThrower = manifestLoaderErrorThrower; this.manifest = manifest; this.adaptationSetIndices = adaptationSetIndices; @@ -131,15 +162,23 @@ public class DefaultDashChunkSource implements DashChunkSource { this.periodIndex = periodIndex; this.elapsedRealtimeOffsetMs = elapsedRealtimeOffsetMs; this.maxSegmentsPerLoad = maxSegmentsPerLoad; + this.playerTrackEmsgHandler = playerTrackEmsgHandler; long periodDurationUs = manifest.getPeriodDurationUs(periodIndex); liveEdgeTimeUs = C.TIME_UNSET; + List representations = getRepresentations(); representationHolders = new RepresentationHolder[trackSelection.length()]; for (int i = 0; i < representationHolders.length; i++) { Representation representation = representations.get(trackSelection.getIndexInTrackGroup(i)); - representationHolders[i] = new RepresentationHolder(periodDurationUs, trackType, - representation, enableEventMessageTrack, enableCea608Track); + representationHolders[i] = + new RepresentationHolder( + periodDurationUs, + trackType, + representation, + enableEventMessageTrack, + enableCea608Track, + playerTrackEmsgHandler); } } @@ -203,6 +242,20 @@ public class DefaultDashChunkSource implements DashChunkSource { long bufferedDurationUs = loadPositionUs - playbackPositionUs; long timeToLiveEdgeUs = resolveTimeToLiveEdgeUs(playbackPositionUs); + long presentationPositionUs = + C.msToUs(manifest.availabilityStartTimeMs) + + C.msToUs(manifest.getPeriod(periodIndex).startMs) + + loadPositionUs; + try { + if (playerTrackEmsgHandler != null + && playerTrackEmsgHandler.maybeRefreshManifestBeforeLoadingNextChunk( + presentationPositionUs)) { + return; + } + } catch (DashManifestExpiredException e) { + fatalError = e; + return; + } trackSelection.updateSelectedTrack(playbackPositionUs, bufferedDurationUs, timeToLiveEdgeUs); RepresentationHolder representationHolder = @@ -298,6 +351,9 @@ public class DefaultDashChunkSource implements DashChunkSource { } } } + if (playerTrackEmsgHandler != null) { + playerTrackEmsgHandler.onChunkLoadCompleted(chunk); + } } @Override @@ -305,6 +361,10 @@ public class DefaultDashChunkSource implements DashChunkSource { if (!cancelable) { return false; } + if (playerTrackEmsgHandler != null + && playerTrackEmsgHandler.maybeRefreshManifestOnLoadingError(chunk)) { + return true; + } // Workaround for missing segment at the end of the period if (!manifest.dynamic && chunk instanceof MediaChunk && e instanceof InvalidResponseCodeException @@ -426,8 +486,13 @@ public class DefaultDashChunkSource implements DashChunkSource { private long periodDurationUs; private int segmentNumShift; - /* package */ RepresentationHolder(long periodDurationUs, int trackType, - Representation representation, boolean enableEventMessageTrack, boolean enableCea608Track) { + /* package */ RepresentationHolder( + long periodDurationUs, + int trackType, + Representation representation, + boolean enableEventMessageTrack, + boolean enableCea608Track, + TrackOutput playerEmsgTrackOutput) { this.periodDurationUs = periodDurationUs; this.representation = representation; String containerMimeType = representation.format.containerMimeType; @@ -449,7 +514,10 @@ public class DefaultDashChunkSource implements DashChunkSource { ? Collections.singletonList( Format.createTextSampleFormat(null, MimeTypes.APPLICATION_CEA608, 0, null)) : Collections.emptyList(); - extractor = new FragmentedMp4Extractor(flags, null, null, null, closedCaptionFormats); + + extractor = + new FragmentedMp4Extractor( + flags, null, null, null, closedCaptionFormats, playerEmsgTrackOutput); } // Prefer drmInitData obtained from the manifest over drmInitData obtained from the stream, // as per DASH IF Interoperability Recommendations V3.0, 7.5.3. @@ -534,7 +602,5 @@ public class DefaultDashChunkSource implements DashChunkSource { private static boolean mimeTypeIsRawText(String mimeType) { return MimeTypes.isText(mimeType) || MimeTypes.APPLICATION_TTML.equals(mimeType); } - } - } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/PlayerEmsgHandler.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/PlayerEmsgHandler.java new file mode 100644 index 0000000000..bdcfef24c1 --- /dev/null +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/PlayerEmsgHandler.java @@ -0,0 +1,454 @@ +/* + * 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 static com.google.android.exoplayer2.util.Util.parseXsDateTime; + +import android.os.Handler; +import android.os.Message; +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.FormatHolder; +import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.extractor.ExtractorInput; +import com.google.android.exoplayer2.extractor.TrackOutput; +import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.metadata.MetadataInputBuffer; +import com.google.android.exoplayer2.metadata.emsg.EventMessage; +import com.google.android.exoplayer2.metadata.emsg.EventMessageDecoder; +import com.google.android.exoplayer2.source.SampleQueue; +import com.google.android.exoplayer2.source.chunk.Chunk; +import com.google.android.exoplayer2.source.dash.manifest.DashManifest; +import com.google.android.exoplayer2.upstream.Allocator; +import com.google.android.exoplayer2.util.ParsableByteArray; +import java.io.IOException; +import java.util.Map; +import java.util.TreeMap; + +/** + * Handles all emsg messages from all media tracks for the player. + * + *

          This class will only respond to emsg messages which have schemeIdUri + * "urn:mpeg:dash:event:2012", and value "1"/"2"/"3". When it encounters one of these messages, it + * will handle the message according to Section 4.5.2.1 DASH -IF IOP Version 4.1: + * + *

            + *
          • If both presentation time delta and event duration are zero, it means the media + * presentation has ended. + *
          • Else, it will parse the message data from the emsg message to find the publishTime of the + * expired manifest, and mark manifest with publishTime smaller than that values to be + * expired. + *
          + * + * In both cases, the DASH media source will be notified, and a manifest reload should be triggered. + */ +public final class PlayerEmsgHandler implements Handler.Callback { + + private static final int EMSG_MEDIA_PRESENTATION_ENDED = 1; + private static final int EMSG_MANIFEST_EXPIRED = 2; + + /** Callbacks for player emsg events encountered during DASH live stream. */ + public interface PlayerEmsgCallback { + + /** Called when the current manifest should be refreshed. */ + void onDashManifestRefreshRequested(); + + /** + * Called when the manifest with the publish time has been expired. + * + * @param expiredManifestPublishTimeUs The manifest publish time that has been expired. + */ + void onDashManifestPublishTimeExpired(long expiredManifestPublishTimeUs); + + /** Called when a media presentation end signal is encountered during live stream. * */ + void onDashLiveMediaPresentationEndSignalEncountered(); + } + + private final Allocator allocator; + private final PlayerEmsgCallback playerEmsgCallback; + private final EventMessageDecoder decoder; + private final Handler handler; + private final TreeMap manifestPublishTimeToExpiryTimeUs; + + private DashManifest manifest; + + private boolean dynamicMediaPresentationEnded; + private long expiredManifestPublishTimeUs; + private long lastLoadedChunkEndTimeUs; + private long lastLoadedChunkEndTimeBeforeRefreshUs; + private boolean isWaitingForManifestRefresh; + private boolean released; + private DashManifestExpiredException fatalError; + + /** + * @param manifest The initial manifest. + * @param playerEmsgCallback The callback that this event handler can invoke when handling emsg + * messages that generate DASH media source events. + * @param allocator An {@link Allocator} from which allocations can be obtained. + */ + public PlayerEmsgHandler( + DashManifest manifest, PlayerEmsgCallback playerEmsgCallback, Allocator allocator) { + this.manifest = manifest; + this.playerEmsgCallback = playerEmsgCallback; + this.allocator = allocator; + + manifestPublishTimeToExpiryTimeUs = new TreeMap<>(); + handler = new Handler(this); + decoder = new EventMessageDecoder(); + lastLoadedChunkEndTimeUs = C.TIME_UNSET; + lastLoadedChunkEndTimeBeforeRefreshUs = C.TIME_UNSET; + } + + /** + * Updates the {@link DashManifest} that this handler works on. + * + * @param newManifest The updated manifest. + */ + public void updateManifest(DashManifest newManifest) { + if (isManifestStale(newManifest)) { + fatalError = new DashManifestExpiredException(); + } + + isWaitingForManifestRefresh = false; + expiredManifestPublishTimeUs = C.TIME_UNSET; + this.manifest = newManifest; + } + + private boolean isManifestStale(DashManifest manifest) { + return manifest.dynamic + && (dynamicMediaPresentationEnded + || manifest.publishTimeMs <= expiredManifestPublishTimeUs); + } + + /* package*/ boolean maybeRefreshManifestBeforeLoadingNextChunk(long presentationPositionUs) + throws DashManifestExpiredException { + if (fatalError != null) { + throw fatalError; + } + if (!manifest.dynamic) { + return false; + } + if (isWaitingForManifestRefresh) { + return true; + } + boolean manifestRefreshNeeded = false; + if (dynamicMediaPresentationEnded) { + // The manifest we have is dynamic, but we know a non-dynamic one representing the final state + // should be available. + manifestRefreshNeeded = true; + } else { + // Find the smallest publishTime (greater than or equal to the current manifest's publish + // time) that has a corresponding expiry time. + Map.Entry expiredEntry = ceilingExpiryEntryForPublishTime(manifest.publishTimeMs); + if (expiredEntry != null) { + long expiredPointUs = expiredEntry.getValue(); + if (expiredPointUs < presentationPositionUs) { + expiredManifestPublishTimeUs = expiredEntry.getKey(); + notifyManifestPublishTimeExpired(); + manifestRefreshNeeded = true; + } + } + } + if (manifestRefreshNeeded) { + maybeNotifyDashManifestRefreshNeeded(); + } + return manifestRefreshNeeded; + } + + /** + * For live streaming with emsg event stream, forward seeking can seek pass the emsg messages that + * signals end-of-stream or Manifest expiry, which results in load error. In this case, we should + * notify the Dash media source to refresh its manifest. + * + * @param chunk The chunk whose load encountered the error. + * @return True if manifest refresh has been requested, false otherwise. + */ + /* package */ boolean maybeRefreshManifestOnLoadingError(Chunk chunk) { + if (!manifest.dynamic) { + return false; + } + if (isWaitingForManifestRefresh) { + return true; + } + boolean isAfterForwardSeek = + lastLoadedChunkEndTimeUs != C.TIME_UNSET && lastLoadedChunkEndTimeUs < chunk.startTimeUs; + if (isAfterForwardSeek) { + // if we are after a forward seek, and the playback is dynamic with embedded emsg stream, + // there's a chance that we have seek over the emsg messages, in which case we should ask + // media source for a refresh. + maybeNotifyDashManifestRefreshNeeded(); + return true; + } + return false; + } + + /** + * Called when the a new chunk in the current media stream has been loaded. + * + * @param chunk The chunk whose load has been completed. + */ + /* package */ void onChunkLoadCompleted(Chunk chunk) { + if (lastLoadedChunkEndTimeUs != C.TIME_UNSET || chunk.endTimeUs > lastLoadedChunkEndTimeUs) { + lastLoadedChunkEndTimeUs = chunk.endTimeUs; + } + } + + /** + * Returns whether an event with given schemeIdUri and value is a DASH emsg event targeting the + * player. + */ + public static boolean isPlayerEmsgEvent(String schemeIdUri, String value) { + return "urn:mpeg:dash:event:2012".equals(schemeIdUri) + && ("1".equals(value) || "2".equals(value) || "3".equals(value)); + } + + /** Returns a {@link TrackOutput} that emsg messages could be written to. */ + public PlayerTrackEmsgHandler newPlayerTrackEmsgHandler() { + return new PlayerTrackEmsgHandler(new SampleQueue(allocator)); + } + + /** Release this emsg handler. It should not be reused after this call. */ + public void release() { + released = true; + handler.removeCallbacksAndMessages(null); + } + + @Override + public boolean handleMessage(Message message) { + if (released) { + return true; + } + switch (message.what) { + case (EMSG_MEDIA_PRESENTATION_ENDED): + handleMediaPresentationEndedMessageEncountered(); + return true; + case (EMSG_MANIFEST_EXPIRED): + ManifestExpiryEventInfo messageObj = (ManifestExpiryEventInfo) message.obj; + handleManifestExpiredMessage( + messageObj.eventTimeUs, messageObj.manifestPublishTimeMsInEmsg); + return true; + default: + // Do nothing. + } + return false; + } + + // Internal methods. + + private void handleManifestExpiredMessage(long eventTimeUs, long manifestPublishTimeMsInEmsg) { + if (!manifestPublishTimeToExpiryTimeUs.containsKey(manifestPublishTimeMsInEmsg)) { + manifestPublishTimeToExpiryTimeUs.put(manifestPublishTimeMsInEmsg, eventTimeUs); + } else { + long previousExpiryTimeUs = + manifestPublishTimeToExpiryTimeUs.get(manifestPublishTimeMsInEmsg); + if (previousExpiryTimeUs > eventTimeUs) { + manifestPublishTimeToExpiryTimeUs.put(manifestPublishTimeMsInEmsg, eventTimeUs); + } + } + } + + private void handleMediaPresentationEndedMessageEncountered() { + dynamicMediaPresentationEnded = true; + notifySourceMediaPresentationEnded(); + } + + private Map.Entry ceilingExpiryEntryForPublishTime(long publishTimeMs) { + if (manifestPublishTimeToExpiryTimeUs.isEmpty()) { + return null; + } + return manifestPublishTimeToExpiryTimeUs.ceilingEntry(publishTimeMs); + } + + private void notifyManifestPublishTimeExpired() { + playerEmsgCallback.onDashManifestPublishTimeExpired(expiredManifestPublishTimeUs); + } + + private void notifySourceMediaPresentationEnded() { + playerEmsgCallback.onDashLiveMediaPresentationEndSignalEncountered(); + } + + /** Requests DASH media manifest to be refreshed if necessary. */ + private void maybeNotifyDashManifestRefreshNeeded() { + if (lastLoadedChunkEndTimeBeforeRefreshUs != C.TIME_UNSET + && lastLoadedChunkEndTimeBeforeRefreshUs == lastLoadedChunkEndTimeUs) { + // Already requested manifest refresh. + return; + } + isWaitingForManifestRefresh = true; + lastLoadedChunkEndTimeBeforeRefreshUs = lastLoadedChunkEndTimeUs; + playerEmsgCallback.onDashManifestRefreshRequested(); + } + + private static long getManifestPublishTimeMsInEmsg(EventMessage eventMessage) { + try { + return parseXsDateTime(new String(eventMessage.messageData)); + } catch (ParserException ignored) { + // if we can't parse this event, ignore + return C.TIME_UNSET; + } + } + + private static boolean isMessageSignalingMediaPresentationEnded(EventMessage eventMessage) { + // According to section 4.5.2.1 DASH-IF IOP, if both presentation time delta and event duration + // are zero, the media presentation is ended. + return eventMessage.presentationTimeUs == 0 && eventMessage.durationMs == 0; + } + + /** Handles emsg messages for a specific track for the player. */ + public final class PlayerTrackEmsgHandler implements TrackOutput { + + private final SampleQueue sampleQueue; + private final FormatHolder formatHolder; + private final MetadataInputBuffer buffer; + + /* package */ PlayerTrackEmsgHandler(SampleQueue sampleQueue) { + this.sampleQueue = sampleQueue; + + formatHolder = new FormatHolder(); + buffer = new MetadataInputBuffer(); + } + + @Override + public void format(Format format) { + sampleQueue.format(format); + } + + @Override + public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput) + throws IOException, InterruptedException { + return sampleQueue.sampleData(input, length, allowEndOfInput); + } + + @Override + public void sampleData(ParsableByteArray data, int length) { + sampleQueue.sampleData(data, length); + } + + @Override + public void sampleMetadata( + long timeUs, int flags, int size, int offset, CryptoData encryptionData) { + sampleQueue.sampleMetadata(timeUs, flags, size, offset, encryptionData); + parseAndDiscardSamples(); + } + + /** + * For live streaming, check if the DASH manifest is expired before the next segment start time. + * If it is, the DASH media source will be notified to refresh the manifest. + * + * @param presentationPositionUs The next load position in presentation time. + * @return True if manifest refresh has been requested, false otherwise. + * @throws DashManifestExpiredException If the current DASH manifest is expired, but a new + * manifest could not be loaded. + */ + public boolean maybeRefreshManifestBeforeLoadingNextChunk(long presentationPositionUs) + throws DashManifestExpiredException { + return PlayerEmsgHandler.this.maybeRefreshManifestBeforeLoadingNextChunk( + presentationPositionUs); + } + + /** + * Called when the a new chunk in the current media stream has been loaded. + * + * @param chunk The chunk whose load has been completed. + */ + public void onChunkLoadCompleted(Chunk chunk) { + PlayerEmsgHandler.this.onChunkLoadCompleted(chunk); + } + + /** + * For live streaming with emsg event stream, forward seeking can seek pass the emsg messages + * that signals end-of-stream or Manifest expiry, which results in load error. In this case, we + * should notify the Dash media source to refresh its manifest. + * + * @param chunk The chunk whose load encountered the error. + * @return True if manifest refresh has been requested, false otherwise. + */ + public boolean maybeRefreshManifestOnLoadingError(Chunk chunk) { + return PlayerEmsgHandler.this.maybeRefreshManifestOnLoadingError(chunk); + } + + /** Release this track emsg handler. It should not be reused after this call. */ + public void release() { + sampleQueue.reset(); + } + + // Internal methods. + + private void parseAndDiscardSamples() { + while (sampleQueue.hasNextSample()) { + MetadataInputBuffer inputBuffer = dequeueSample(); + if (inputBuffer == null) { + continue; + } + long eventTimeUs = inputBuffer.timeUs; + Metadata metadata = decoder.decode(inputBuffer); + EventMessage eventMessage = (EventMessage) metadata.get(0); + if (isPlayerEmsgEvent(eventMessage.schemeIdUri, eventMessage.value)) { + parsePlayerEmsgEvent(eventTimeUs, eventMessage); + } + } + sampleQueue.discardToRead(); + } + + @Nullable + private MetadataInputBuffer dequeueSample() { + buffer.clear(); + int result = sampleQueue.read(formatHolder, buffer, false, false, 0); + if (result == C.RESULT_BUFFER_READ) { + buffer.flip(); + return buffer; + } + return null; + } + + private void parsePlayerEmsgEvent(long eventTimeUs, EventMessage eventMessage) { + long manifestPublishTimeMsInEmsg = getManifestPublishTimeMsInEmsg(eventMessage); + if (manifestPublishTimeMsInEmsg == C.TIME_UNSET) { + return; + } + + if (isMessageSignalingMediaPresentationEnded(eventMessage)) { + onMediaPresentationEndedMessageEncountered(); + } else { + onManifestExpiredMessageEncountered(eventTimeUs, manifestPublishTimeMsInEmsg); + } + } + + private void onMediaPresentationEndedMessageEncountered() { + handler.sendMessage(handler.obtainMessage(EMSG_MEDIA_PRESENTATION_ENDED)); + } + + private void onManifestExpiredMessageEncountered( + long eventTimeUs, long manifestPublishTimeMsInEmsg) { + ManifestExpiryEventInfo manifestExpiryEventInfo = + new ManifestExpiryEventInfo(eventTimeUs, manifestPublishTimeMsInEmsg); + handler.sendMessage(handler.obtainMessage(EMSG_MANIFEST_EXPIRED, manifestExpiryEventInfo)); + } + } + + /** Holds information related to a manifest expiry event. */ + private static final class ManifestExpiryEventInfo { + + public final long eventTimeUs; + public final long manifestPublishTimeMsInEmsg; + + public ManifestExpiryEventInfo(long eventTimeUs, long manifestPublishTimeMsInEmsg) { + this.eventTimeUs = eventTimeUs; + this.manifestPublishTimeMsInEmsg = manifestPublishTimeMsInEmsg; + } + } +} From 965bc4f6771c1fc9d848188b9e11807bf90f23ba Mon Sep 17 00:00:00 2001 From: eguven Date: Tue, 16 Jan 2018 07:23:59 -0800 Subject: [PATCH 1049/2472] Add JobDispatcherScheduler This is a Scheduler implementation which uses Firebase JobDispatcher. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=182051350 --- extensions/leanback/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/leanback/build.gradle b/extensions/leanback/build.gradle index 715e2e56d7..d8952ca2b8 100644 --- a/extensions/leanback/build.gradle +++ b/extensions/leanback/build.gradle @@ -30,7 +30,7 @@ dependencies { } ext { - javadocTitle = 'Leanback extension for Exoplayer library' + javadocTitle = 'Leanback extension' } apply from: '../../javadoc_library.gradle' From a4114f59b6897a28dd10cff2f11311dc04fc9363 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 16 Jan 2018 07:59:51 -0800 Subject: [PATCH 1050/2472] Seek at chunk level when seeking to chunk start positions This avoids issues that can arise due to slight discrepancies between chunk start times (obtained from the manifest of segment index) and the timestamps of the samples contained within those chunks. Issue: #2882 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=182054959 --- .../source/SampleMetadataQueue.java | 16 +++++ .../exoplayer2/source/SampleQueue.java | 12 ++++ .../source/chunk/ChunkSampleStream.java | 63 +++++++++++++++---- 3 files changed, 79 insertions(+), 12 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java index 54db9d7880..e5b950cf2e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java @@ -296,6 +296,22 @@ import com.google.android.exoplayer2.util.Util; return skipCount; } + /** + * Attempts to set the read position to the specified sample index. + * + * @param sampleIndex The sample index. + * @return Whether the read position was set successfully. False is returned if the specified + * index is smaller than the index of the first sample in the queue, or larger than the index + * of the next sample that will be written. + */ + public synchronized boolean setReadPosition(int sampleIndex) { + if (absoluteFirstIndex <= sampleIndex && sampleIndex <= absoluteFirstIndex + length) { + readPosition = sampleIndex - absoluteFirstIndex; + return true; + } + return false; + } + /** * Discards up to but not including the sample immediately before or at the specified time. * diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java index a4feb924b8..d9090baf3b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java @@ -293,6 +293,18 @@ public final class SampleQueue implements TrackOutput { return metadataQueue.advanceTo(timeUs, toKeyframe, allowTimeBeyondBuffer); } + /** + * Attempts to set the read position to the specified sample index. + * + * @param sampleIndex The sample index. + * @return Whether the read position was set successfully. False is returned if the specified + * index is smaller than the index of the first sample in the queue, or larger than the index + * of the next sample that will be written. + */ + public boolean setReadPosition(int sampleIndex) { + return metadataQueue.setReadPosition(sampleIndex); + } + /** * Attempts to read from the queue. * diff --git a/library/core/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 index 29a6ce29fb..e0c5d35996 100644 --- a/library/core/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 @@ -75,7 +75,8 @@ public class ChunkSampleStream implements SampleStream, S private Format primaryDownstreamTrackFormat; private ReleaseCallback releaseCallback; private long pendingResetPositionUs; - /* package */ long lastSeekPositionUs; + private long lastSeekPositionUs; + /* package */ long decodeOnlyUntilPositionUs; /* package */ boolean loadingFinished; /** @@ -219,9 +220,6 @@ public class ChunkSampleStream implements SampleStream, S * @return The adjusted seek position, in microseconds. */ public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { - // TODO: Using this method to adjust a seek position and then passing the adjusted position to - // seekToUs does not handle small discrepancies between the chunk boundary timestamps obtained - // from the chunk source and the timestamps of the samples in the chunks. return chunkSource.getAdjustedSeekPositionUs(positionUs, seekParameters); } @@ -233,9 +231,43 @@ public class ChunkSampleStream implements SampleStream, S public void seekToUs(long positionUs) { lastSeekPositionUs = positionUs; primarySampleQueue.rewind(); - // If we're not pending a reset, see if we can seek within the primary sample queue. - boolean seekInsideBuffer = !isPendingReset() && (primarySampleQueue.advanceTo(positionUs, true, - positionUs < getNextLoadPositionUs()) != SampleQueue.ADVANCE_FAILED); + + // See if we can seek within the primary sample queue. + boolean seekInsideBuffer; + if (isPendingReset()) { + seekInsideBuffer = false; + } else { + // Detect whether the seek is to the start of a chunk that's at least partially buffered. + BaseMediaChunk seekToMediaChunk = null; + for (int i = 0; i < mediaChunks.size(); i++) { + BaseMediaChunk mediaChunk = mediaChunks.get(i); + long mediaChunkStartTimeUs = mediaChunk.startTimeUs; + if (mediaChunkStartTimeUs == positionUs) { + seekToMediaChunk = mediaChunk; + break; + } else if (mediaChunkStartTimeUs > positionUs) { + // We're not going to find a chunk with a matching start time. + break; + } + } + if (seekToMediaChunk != null) { + // When seeking to the start of a chunk we use the index of the first sample in the chunk + // rather than the seek position. This ensures we seek to the keyframe at the start of the + // chunk even if the sample timestamps are slightly offset from the chunk start times. + seekInsideBuffer = + primarySampleQueue.setReadPosition(seekToMediaChunk.getFirstSampleIndex(0)); + decodeOnlyUntilPositionUs = Long.MIN_VALUE; + } else { + seekInsideBuffer = + primarySampleQueue.advanceTo( + positionUs, + /* toKeyframe= */ true, + /* allowTimeBeyondBuffer= */ positionUs < getNextLoadPositionUs()) + != SampleQueue.ADVANCE_FAILED; + decodeOnlyUntilPositionUs = lastSeekPositionUs; + } + } + if (seekInsideBuffer) { // We succeeded. Advance the embedded sample queues to the seek position. for (SampleQueue embeddedSampleQueue : embeddedSampleQueues) { @@ -322,8 +354,9 @@ public class ChunkSampleStream implements SampleStream, S if (isPendingReset()) { return C.RESULT_NOTHING_READ; } - int result = primarySampleQueue.read(formatHolder, buffer, formatRequired, loadingFinished, - lastSeekPositionUs); + int result = + primarySampleQueue.read( + formatHolder, buffer, formatRequired, loadingFinished, decodeOnlyUntilPositionUs); if (result == C.RESULT_BUFFER_READ) { maybeNotifyPrimaryTrackFormatChanged(primarySampleQueue.getReadIndex(), 1); } @@ -421,9 +454,10 @@ public class ChunkSampleStream implements SampleStream, S return false; } + boolean pendingReset = isPendingReset(); MediaChunk previousChunk; long loadPositionUs; - if (isPendingReset()) { + if (pendingReset) { previousChunk = null; loadPositionUs = pendingResetPositionUs; } else { @@ -446,8 +480,13 @@ public class ChunkSampleStream implements SampleStream, S } if (isMediaChunk(loadable)) { - pendingResetPositionUs = C.TIME_UNSET; BaseMediaChunk mediaChunk = (BaseMediaChunk) loadable; + if (pendingReset) { + boolean resetToMediaChunk = mediaChunk.startTimeUs == pendingResetPositionUs; + // Only enable setting of the decode only flag if we're not resetting to a chunk boundary. + decodeOnlyUntilPositionUs = resetToMediaChunk ? Long.MIN_VALUE : pendingResetPositionUs; + pendingResetPositionUs = C.TIME_UNSET; + } mediaChunk.init(mediaChunkOutput); mediaChunks.add(mediaChunk); } @@ -640,7 +679,7 @@ public class ChunkSampleStream implements SampleStream, S } int result = sampleQueue.read( - formatHolder, buffer, formatRequired, loadingFinished, lastSeekPositionUs); + formatHolder, buffer, formatRequired, loadingFinished, decodeOnlyUntilPositionUs); if (result == C.RESULT_BUFFER_READ) { maybeNotifyTrackFormatChanged(); } From be304486e043616a36ee13eba75dab6431ed6930 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 16 Jan 2018 09:11:07 -0800 Subject: [PATCH 1051/2472] Fix HLS' mime type propagation Issue:#3653 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=182064250 --- RELEASENOTES.md | 2 ++ .../java/com/google/android/exoplayer2/Format.java | 11 +++++++++-- .../exoplayer2/source/hls/HlsSampleStreamWrapper.java | 2 ++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index ba82f46525..f086bbf515 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,6 +2,8 @@ ### dev-v2 (not yet released) ### +* HLS: Fix mime type propagation + ([#3653](https://github.com/google/ExoPlayer/issues/3653)). * SimpleExoPlayerView: Automatically apply video rotation if `SimpleExoPlayerView` is configured to use `TextureView` ([#91](https://github.com/google/ExoPlayer/issues/91)). diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Format.java b/library/core/src/main/java/com/google/android/exoplayer2/Format.java index 7799f411a9..6ef57537f1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Format.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Format.java @@ -474,8 +474,15 @@ public final class Format implements Parcelable { drmInitData, metadata); } - public Format copyWithContainerInfo(String id, String codecs, int bitrate, int width, int height, - @C.SelectionFlags int selectionFlags, String language) { + public Format copyWithContainerInfo( + String id, + String sampleMimeType, + 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, colorInfo, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, diff --git a/library/hls/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 index eba4596b7f..0dbadcd8e0 100644 --- a/library/hls/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 @@ -882,8 +882,10 @@ import java.util.Arrays; int bitrate = propagateBitrate ? playlistFormat.bitrate : Format.NO_VALUE; int sampleTrackType = MimeTypes.getTrackType(sampleFormat.sampleMimeType); String codecs = Util.getCodecsOfType(playlistFormat.codecs, sampleTrackType); + String mimeType = MimeTypes.getMediaMimeType(codecs); return sampleFormat.copyWithContainerInfo( playlistFormat.id, + mimeType, codecs, bitrate, playlistFormat.width, From 3919843db27c78dc04ec498087182058393cced0 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 16 Jan 2018 09:56:12 -0800 Subject: [PATCH 1052/2472] Force single audio and video sample queues This solves the problem of having dense tracks' ids change. For example, if the available variants offer both HEVC and AVC video tracks, all video samples will map to the same sample queue even if IDs don't match. Issue:#3653 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=182070486 --- .../exoplayer2/source/hls/HlsMediaChunk.java | 2 +- .../source/hls/HlsSampleStreamWrapper.java | 69 ++++++++++++++++--- 2 files changed, 62 insertions(+), 9 deletions(-) diff --git a/library/hls/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 index c4e54d4bd3..50c1200fae 100644 --- a/library/hls/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 @@ -156,7 +156,7 @@ import java.util.concurrent.atomic.AtomicInteger; */ public void init(HlsSampleStreamWrapper output) { this.output = output; - output.init(uid, shouldSpliceIn); + output.init(uid, shouldSpliceIn, reusingExtractor); if (!reusingExtractor) { extractor.init(output); } diff --git a/library/hls/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 index 0dbadcd8e0..e4c71e43c5 100644 --- a/library/hls/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 @@ -93,6 +93,10 @@ import java.util.Arrays; private SampleQueue[] sampleQueues; private int[] sampleQueueTrackIds; + private boolean audioSampleQueueMappingDone; + private int audioSampleQueueIndex; + private boolean videoSampleQueueMappingDone; + private int videoSampleQueueIndex; private boolean sampleQueuesBuilt; private boolean prepared; private int enabledTrackGroupCount; @@ -143,6 +147,8 @@ import java.util.Arrays; loader = new Loader("Loader:HlsSampleStreamWrapper"); nextChunkHolder = new HlsChunkSource.HlsChunkHolder(); sampleQueueTrackIds = new int[0]; + audioSampleQueueIndex = C.INDEX_UNSET; + videoSampleQueueIndex = C.INDEX_UNSET; sampleQueues = new SampleQueue[0]; sampleQueueIsAudioVideoFlags = new boolean[0]; sampleQueuesEnabledStates = new boolean[0]; @@ -616,8 +622,14 @@ import java.util.Arrays; * @param chunkUid The chunk's uid. * @param shouldSpliceIn Whether the samples parsed from the chunk should be spliced into any * samples already queued to the wrapper. + * @param reusingExtractor Whether the extractor for the chunk has already been used for preceding + * chunks. */ - public void init(int chunkUid, boolean shouldSpliceIn) { + public void init(int chunkUid, boolean shouldSpliceIn, boolean reusingExtractor) { + if (!reusingExtractor) { + audioSampleQueueMappingDone = false; + videoSampleQueueMappingDone = false; + } for (SampleQueue sampleQueue : sampleQueues) { sampleQueue.sourceId(chunkUid); } @@ -633,14 +645,43 @@ import java.util.Arrays; @Override public TrackOutput track(int id, int type) { int trackCount = sampleQueues.length; - for (int i = 0; i < trackCount; i++) { - if (sampleQueueTrackIds[i] == id) { - return sampleQueues[i]; + + // Audio and video tracks are handled manually to ignore ids. + if (type == C.TRACK_TYPE_AUDIO) { + if (audioSampleQueueIndex != C.INDEX_UNSET) { + if (audioSampleQueueMappingDone) { + return sampleQueueTrackIds[audioSampleQueueIndex] == id + ? sampleQueues[audioSampleQueueIndex] + : createDummyTrackOutput(id, type); + } + audioSampleQueueMappingDone = true; + sampleQueueTrackIds[audioSampleQueueIndex] = id; + return sampleQueues[audioSampleQueueIndex]; + } else if (tracksEnded) { + return createDummyTrackOutput(id, type); + } + } else if (type == C.TRACK_TYPE_VIDEO) { + if (videoSampleQueueIndex != C.INDEX_UNSET) { + if (videoSampleQueueMappingDone) { + return sampleQueueTrackIds[videoSampleQueueIndex] == id + ? sampleQueues[videoSampleQueueIndex] + : createDummyTrackOutput(id, type); + } + videoSampleQueueMappingDone = true; + sampleQueueTrackIds[videoSampleQueueIndex] = id; + return sampleQueues[videoSampleQueueIndex]; + } else if (tracksEnded) { + return createDummyTrackOutput(id, type); + } + } else /* sparse track */ { + for (int i = 0; i < trackCount; i++) { + if (sampleQueueTrackIds[i] == id) { + return sampleQueues[i]; + } + } + if (tracksEnded) { + return createDummyTrackOutput(id, type); } - } - if (tracksEnded) { - Log.w(TAG, "Unmapped track with id " + id + " of type " + type); - return new DummyTrackOutput(); } SampleQueue trackOutput = new SampleQueue(allocator); trackOutput.setSampleOffsetUs(sampleOffsetUs); @@ -653,6 +694,13 @@ import java.util.Arrays; sampleQueueIsAudioVideoFlags[trackCount] = type == C.TRACK_TYPE_AUDIO || type == C.TRACK_TYPE_VIDEO; haveAudioVideoSampleQueues |= sampleQueueIsAudioVideoFlags[trackCount]; + if (type == C.TRACK_TYPE_AUDIO) { + audioSampleQueueMappingDone = true; + audioSampleQueueIndex = trackCount; + } else if (type == C.TRACK_TYPE_VIDEO) { + videoSampleQueueMappingDone = true; + videoSampleQueueIndex = trackCount; + } sampleQueuesEnabledStates = Arrays.copyOf(sampleQueuesEnabledStates, trackCount + 1); return trackOutput; } @@ -913,4 +961,9 @@ import java.util.Arrays; } return true; } + + private static DummyTrackOutput createDummyTrackOutput(int id, int type) { + Log.w(TAG, "Unmapped track with id " + id + " of type " + type); + return new DummyTrackOutput(); + } } From 0708aa87ba87a54fb5c1e85a6d2c5359439b1f66 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 17 Jan 2018 02:11:55 -0800 Subject: [PATCH 1053/2472] Fix stray calculation in PGS decoder ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=182183184 --- .../java/com/google/android/exoplayer2/text/pgs/PgsDecoder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsDecoder.java index 7ad70397a0..6d60da7d81 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/pgs/PgsDecoder.java @@ -148,7 +148,7 @@ public final class PgsDecoder extends SimpleSubtitleDecoder { if (sectionLength < 7) { return; } - int totalLength = buffer.readUnsignedInt24() - 4; + int totalLength = buffer.readUnsignedInt24(); if (totalLength < 4) { return; } From 0697fb3955d8a30edb935cc428d515e2fd9d48cb Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 17 Jan 2018 05:59:50 -0800 Subject: [PATCH 1054/2472] Fail on HLS+TS loss of sync Issue:#3632 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=182202289 --- RELEASENOTES.md | 10 ++++++---- .../exoplayer2/extractor/ts/TsExtractor.java | 13 +++++++++++-- .../source/hls/HlsSampleStreamWrapper.java | 3 ++- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index f086bbf515..715e09c977 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -41,13 +41,15 @@ * Support in-band Emsg events targeting player with scheme id "urn:mpeg:dash:event:2012" and scheme value of either "1", "2" or "3". * Support DASH manifest EventStream elements. -* HLS: Add opt-in support for chunkless preparation in HLS. This allows an - HLS source to finish preparation without downloading any chunks, which can - significantly reduce initial buffering time - ([#3149](https://github.com/google/ExoPlayer/issues/3149)). * DefaultTrackSelector: * Replace `DefaultTrackSelector.Parameters` copy methods with a builder. * Support disabling of individual text track selection flags. +* HLS: + * Add opt-in support for chunkless preparation in HLS. This allows an + HLS source to finish preparation without downloading any chunks, which can + significantly reduce initial buffering time + ([#3149](https://github.com/google/ExoPlayer/issues/3149)). + * Fail on loss of sync with Transport Stream. * New Cast extension: Simplifies toggling between local and Cast playbacks. * Audio: Support TrueHD passthrough for rechunked samples in Matroska files ([#2147](https://github.com/google/ExoPlayer/issues/2147)). diff --git a/library/core/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 index 13e669da23..50931e2d90 100644 --- a/library/core/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 @@ -20,6 +20,7 @@ import android.util.SparseArray; import android.util.SparseBooleanArray; import android.util.SparseIntArray; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorOutput; @@ -122,6 +123,7 @@ public final class TsExtractor implements Extractor { private int remainingPmts; private boolean tracksEnded; private TsPayloadReader id3Reader; + private int bytesSinceLastSync; public TsExtractor() { this(0); @@ -163,7 +165,7 @@ public final class TsExtractor implements Extractor { timestampAdjusters = new ArrayList<>(); timestampAdjusters.add(timestampAdjuster); } - tsPacketBuffer = new ParsableByteArray(BUFFER_SIZE); + tsPacketBuffer = new ParsableByteArray(new byte[BUFFER_SIZE], 0); trackIds = new SparseBooleanArray(); tsPayloadReaders = new SparseArray<>(); continuityCounters = new SparseIntArray(); @@ -206,6 +208,7 @@ public final class TsExtractor implements Extractor { continuityCounters.clear(); // Elementary stream readers' state should be cleared to get consistent behaviours when seeking. resetPayloadReaders(); + bytesSinceLastSync = 0; } @Override @@ -238,8 +241,9 @@ public final class TsExtractor implements Extractor { } // Note: See ISO/IEC 13818-1, section 2.4.3.2 for details of the header format. - final int limit = tsPacketBuffer.limit(); + int limit = tsPacketBuffer.limit(); int position = tsPacketBuffer.getPosition(); + int searchStart = position; while (position < limit && data[position] != TS_SYNC_BYTE) { position++; } @@ -247,8 +251,13 @@ public final class TsExtractor implements Extractor { int endOfPacket = position + TS_PACKET_SIZE; if (endOfPacket > limit) { + bytesSinceLastSync += position - searchStart; + if (mode == MODE_HLS && bytesSinceLastSync > TS_PACKET_SIZE * 2) { + throw new ParserException("Cannot find sync byte. Most likely not a Transport Stream."); + } return RESULT_CONTINUE; } + bytesSinceLastSync = 0; int tsPacketHeader = tsPacketBuffer.readInt(); if ((tsPacketHeader & 0x800000) != 0) { // transport_error_indicator diff --git a/library/hls/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 index e4c71e43c5..4a529aef18 100644 --- a/library/hls/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 @@ -20,6 +20,7 @@ import android.util.Log; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; +import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.extractor.DummyTrackOutput; import com.google.android.exoplayer2.extractor.ExtractorOutput; @@ -610,7 +611,7 @@ import java.util.Arrays; } return Loader.DONT_RETRY; } else { - return Loader.RETRY; + return error instanceof ParserException ? Loader.DONT_RETRY_FATAL : Loader.RETRY; } } From 78c6b39ae82843ad8dfdbfe75bab91d254f431f2 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 17 Jan 2018 06:40:23 -0800 Subject: [PATCH 1055/2472] Fix HLS media playlist only playback This was broken by [] ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=182206548 --- .../android/exoplayer2/source/hls/HlsSampleStreamWrapper.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/library/hls/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 index 4a529aef18..508f2f0f2f 100644 --- a/library/hls/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 @@ -932,6 +932,9 @@ import java.util.Arrays; int sampleTrackType = MimeTypes.getTrackType(sampleFormat.sampleMimeType); String codecs = Util.getCodecsOfType(playlistFormat.codecs, sampleTrackType); String mimeType = MimeTypes.getMediaMimeType(codecs); + if (mimeType == null) { + mimeType = sampleFormat.sampleMimeType; + } return sampleFormat.copyWithContainerInfo( playlistFormat.id, mimeType, From 65597e0db8b1e525d077a61170f5adc5d4c47cc4 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 17 Jan 2018 13:33:39 -0800 Subject: [PATCH 1056/2472] DashMediaSource variable name cleanup ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=182261649 --- .../source/dash/DashMediaSource.java | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/library/dash/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 index 08e25f216a..28b3de357d 100644 --- a/library/dash/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 @@ -285,17 +285,18 @@ public final class DashMediaSource implements MediaSource { private DataSource dataSource; private Loader loader; private LoaderErrorThrower loaderErrorThrower; + private Handler handler; private Uri manifestUri; - private long manifestLoadStartTimestamp; - private long manifestLoadEndTimestamp; private DashManifest manifest; - private Handler handler; - private boolean pendingManifestLoading; + private boolean manifestLoadPending; + private long manifestLoadStartTimestampMs; + private long manifestLoadEndTimestampMs; private long elapsedRealtimeOffsetMs; + + private int staleManifestReloadAttempt; private long expiredManifestPublishTimeUs; private boolean dynamicMediaPresentationEnded; - private int staleManifestReloadAttempt; private int firstPeriodId; @@ -539,15 +540,15 @@ public final class DashMediaSource implements MediaSource { @Override public void releaseSource() { - pendingManifestLoading = false; + manifestLoadPending = false; dataSource = null; loaderErrorThrower = null; if (loader != null) { loader.release(); loader = null; } - manifestLoadStartTimestamp = 0; - manifestLoadEndTimestamp = 0; + manifestLoadStartTimestampMs = 0; + manifestLoadEndTimestampMs = 0; manifest = null; if (handler != null) { handler.removeCallbacksAndMessages(null); @@ -605,11 +606,11 @@ public final class DashMediaSource implements MediaSource { return; } manifest = newManifest; - manifestLoadStartTimestamp = elapsedRealtimeMs - loadDurationMs; - manifestLoadEndTimestamp = elapsedRealtimeMs; + manifestLoadStartTimestampMs = elapsedRealtimeMs - loadDurationMs; + manifestLoadEndTimestampMs = elapsedRealtimeMs; staleManifestReloadAttempt = 0; if (!manifest.dynamic) { - pendingManifestLoading = false; + manifestLoadPending = false; } if (manifest.location != null) { synchronized (manifestUriLock) { @@ -691,14 +692,14 @@ public final class DashMediaSource implements MediaSource { private void startLoadingManifest() { handler.removeCallbacks(refreshManifestRunnable); if (loader.isLoading()) { - pendingManifestLoading = true; + manifestLoadPending = true; return; } Uri manifestUri; synchronized (manifestUriLock) { manifestUri = this.manifestUri; } - pendingManifestLoading = false; + manifestLoadPending = false; startLoading(new ParsingLoadable<>(dataSource, manifestUri, C.DATA_TYPE_MANIFEST, manifestParser), manifestCallback, minLoadableRetryCount); } @@ -722,8 +723,8 @@ public final class DashMediaSource implements MediaSource { private void resolveUtcTimingElementDirect(UtcTimingElement timingElement) { try { - long utcTimestamp = Util.parseXsDateTime(timingElement.value); - onUtcTimestampResolved(utcTimestamp - manifestLoadEndTimestamp); + long utcTimestampMs = Util.parseXsDateTime(timingElement.value); + onUtcTimestampResolved(utcTimestampMs - manifestLoadEndTimestampMs); } catch (ParserException e) { onUtcTimestampResolutionError(e); } @@ -825,7 +826,7 @@ public final class DashMediaSource implements MediaSource { if (windowChangingImplicitly) { handler.postDelayed(simulateManifestRefreshRunnable, NOTIFY_MANIFEST_INTERVAL_MS); } - if (pendingManifestLoading) { + if (manifestLoadPending) { startLoadingManifest(); } else if (scheduleRefresh) { // Schedule an explicit refresh if needed. @@ -852,7 +853,7 @@ public final class DashMediaSource implements MediaSource { // http://azure.microsoft.com/blog/2014/09/13/dash-live-streaming-with-azure-media-service/ minUpdatePeriodMs = 5000; } - long nextLoadTimestamp = manifestLoadStartTimestamp + minUpdatePeriodMs; + long nextLoadTimestamp = manifestLoadStartTimestampMs + minUpdatePeriodMs; long delayUntilNextLoad = Math.max(0, nextLoadTimestamp - SystemClock.elapsedRealtime()); handler.postDelayed(refreshManifestRunnable, delayUntilNextLoad); } From 605aeb3a42c66c01069071c6d1fe6bba3ee39084 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 18 Jan 2018 06:46:31 -0800 Subject: [PATCH 1057/2472] Make id3 context usage robust against container format changes Issue:#3622 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=182363243 --- RELEASENOTES.md | 6 ++++-- .../android/exoplayer2/source/hls/HlsMediaChunk.java | 10 +++++++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 715e09c977..ed6dedd0c3 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,8 +2,6 @@ ### dev-v2 (not yet released) ### -* HLS: Fix mime type propagation - ([#3653](https://github.com/google/ExoPlayer/issues/3653)). * SimpleExoPlayerView: Automatically apply video rotation if `SimpleExoPlayerView` is configured to use `TextureView` ([#91](https://github.com/google/ExoPlayer/issues/91)). @@ -50,6 +48,10 @@ significantly reduce initial buffering time ([#3149](https://github.com/google/ExoPlayer/issues/3149)). * Fail on loss of sync with Transport Stream. + * Fix mime type propagation + ([#3653](https://github.com/google/ExoPlayer/issues/3653)). + * Fix ID3 context reuse across segment format changes + ([#3622](https://github.com/google/ExoPlayer/issues/3622)). * New Cast extension: Simplifies toggling between local and Cast playbacks. * Audio: Support TrueHD passthrough for rechunked samples in Matroska files ([#2147](https://github.com/google/ExoPlayer/issues/2147)). diff --git a/library/hls/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 index 50c1200fae..5457f33867 100644 --- a/library/hls/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 @@ -137,9 +137,13 @@ import java.util.concurrent.atomic.AtomicInteger; reusingExtractor = extractor == previousExtractor; initLoadCompleted = reusingExtractor && initDataSpec != null; if (isPackedAudioExtractor) { - id3Decoder = previousChunk != null ? previousChunk.id3Decoder : new Id3Decoder(); - id3Data = previousChunk != null ? previousChunk.id3Data - : new ParsableByteArray(Id3Decoder.ID3_HEADER_LENGTH); + if (previousChunk != null && previousChunk.id3Data != null) { + id3Decoder = previousChunk.id3Decoder; + id3Data = previousChunk.id3Data; + } else { + id3Decoder = new Id3Decoder(); + id3Data = new ParsableByteArray(Id3Decoder.ID3_HEADER_LENGTH); + } } else { id3Decoder = null; id3Data = null; From c577d9d35191b11b46ec215be4812b96accd06a0 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 18 Jan 2018 06:59:30 -0800 Subject: [PATCH 1058/2472] Let SimpleExoPlayerView/LeanbackPlayerAdapter bind with any Player Also sanitize naming (PlayerView/PlayerControlView). ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=182364487 --- RELEASENOTES.md | 8 + .../exoplayer2/castdemo/MainActivity.java | 21 +- .../exoplayer2/castdemo/PlayerManager.java | 52 +- .../src/main/res/layout/main_activity.xml | 4 +- .../exoplayer2/imademo/MainActivity.java | 4 +- .../exoplayer2/imademo/PlayerManager.java | 8 +- .../ima/src/main/res/layout/main_activity.xml | 2 +- .../exoplayer2/demo/PlayerActivity.java | 28 +- .../src/main/res/layout/player_activity.xml | 2 +- extensions/cast/README.md | 5 +- .../exoplayer2/ext/cast/CastPlayer.java | 10 + .../ext/leanback/LeanbackPlayerAdapter.java | 39 +- .../android/exoplayer2/ExoPlayerImpl.java | 10 + .../com/google/android/exoplayer2/Player.java | 138 +++ .../android/exoplayer2/SimpleExoPlayer.java | 177 +-- .../exoplayer2/video/VideoListener.java | 45 + .../exoplayer2/ui/PlaybackControlView.java | 1078 +--------------- .../exoplayer2/ui/PlayerControlView.java | 1101 +++++++++++++++++ .../android/exoplayer2/ui/PlayerView.java | 1057 ++++++++++++++++ .../exoplayer2/ui/SimpleExoPlayerView.java | 1011 +-------------- .../res/layout/exo_player_control_view.xml | 18 + .../src/main/res/layout/exo_player_view.xml | 18 + library/ui/src/main/res/values/attrs.xml | 6 +- .../exoplayer2/testutil/StubExoPlayer.java | 10 + 24 files changed, 2575 insertions(+), 2277 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/video/VideoListener.java create mode 100644 library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java create mode 100644 library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java create mode 100644 library/ui/src/main/res/layout/exo_player_control_view.xml create mode 100644 library/ui/src/main/res/layout/exo_player_view.xml diff --git a/RELEASENOTES.md b/RELEASENOTES.md index ed6dedd0c3..1a54a44058 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -6,6 +6,10 @@ `SimpleExoPlayerView` is configured to use `TextureView` ([#91](https://github.com/google/ExoPlayer/issues/91)). * Player interface: + * Add `Player.VideoComponent`, `Player.TextComponent` and + `Player.MetadataComponent` interfaces that define optional video, text and + metadata output functionality. New `getVideoComponent`, `getTextComponent` + and `getMetadataComponent` methods provide access to this functionality. * Add optional parameter to `stop` to reset the player when stopping. * Add a reason to `EventListener.onTimelineChanged` to distinguish between initial preparation, reset and dynamic updates. @@ -17,6 +21,10 @@ more customization of the message. Now supports setting a message delivery playback position and/or a delivery handler. ([#2189](https://github.com/google/ExoPlayer/issues/2189)). +* UI components: + * Generalized player and control views to allow them to bind with any + `Player`, and renamed them to `PlayerView` and `PlayerControlView` + respectively. * Buffering: * Allow a back-buffer of media to be retained behind the current playback position, for fast backward seeking. The back-buffer can be configured by diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java index d34888352f..07781c091e 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java @@ -39,8 +39,8 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.castdemo.DemoUtil.Sample; import com.google.android.exoplayer2.ext.cast.CastPlayer; -import com.google.android.exoplayer2.ui.PlaybackControlView; -import com.google.android.exoplayer2.ui.SimpleExoPlayerView; +import com.google.android.exoplayer2.ui.PlayerControlView; +import com.google.android.exoplayer2.ui.PlayerView; import com.google.android.gms.cast.framework.CastButtonFactory; import com.google.android.gms.cast.framework.CastContext; @@ -50,8 +50,8 @@ import com.google.android.gms.cast.framework.CastContext; public class MainActivity extends AppCompatActivity implements OnClickListener, PlayerManager.QueuePositionListener { - private SimpleExoPlayerView simpleExoPlayerView; - private PlaybackControlView castControlView; + private PlayerView localPlayerView; + private PlayerControlView castControlView; private PlayerManager playerManager; private MediaQueueAdapter listAdapter; private CastContext castContext; @@ -66,8 +66,8 @@ public class MainActivity extends AppCompatActivity implements OnClickListener, setContentView(R.layout.main_activity); - simpleExoPlayerView = findViewById(R.id.player_view); - simpleExoPlayerView.requestFocus(); + localPlayerView = findViewById(R.id.local_player_view); + localPlayerView.requestFocus(); castControlView = findViewById(R.id.cast_control_view); @@ -93,8 +93,13 @@ public class MainActivity extends AppCompatActivity implements OnClickListener, @Override public void onResume() { super.onResume(); - playerManager = PlayerManager.createPlayerManager(this, simpleExoPlayerView, castControlView, - this, castContext); + playerManager = + PlayerManager.createPlayerManager( + /* queuePositionListener= */ this, + localPlayerView, + castControlView, + /* context= */ this, + castContext); } @Override diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java index 548482f61f..ac488ff3fd 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java @@ -40,8 +40,8 @@ import com.google.android.exoplayer2.source.hls.HlsMediaSource; import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource; import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; -import com.google.android.exoplayer2.ui.PlaybackControlView; -import com.google.android.exoplayer2.ui.SimpleExoPlayerView; +import com.google.android.exoplayer2.ui.PlayerControlView; +import com.google.android.exoplayer2.ui.PlayerView; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; import com.google.android.gms.cast.MediaInfo; @@ -73,12 +73,12 @@ import java.util.ArrayList; private static final DefaultHttpDataSourceFactory DATA_SOURCE_FACTORY = new DefaultHttpDataSourceFactory(USER_AGENT, BANDWIDTH_METER); - private final SimpleExoPlayerView exoPlayerView; - private final PlaybackControlView castControlView; + private final PlayerView localPlayerView; + private final PlayerControlView castControlView; private final SimpleExoPlayer exoPlayer; private final CastPlayer castPlayer; private final ArrayList mediaQueue; - private final QueuePositionListener listener; + private final QueuePositionListener queuePositionListener; private DynamicConcatenatingMediaSource dynamicConcatenatingMediaSource; private boolean castMediaQueueCreationPending; @@ -86,25 +86,33 @@ import java.util.ArrayList; private Player currentPlayer; /** - * @param listener A {@link QueuePositionListener} for queue position changes. - * @param exoPlayerView The {@link SimpleExoPlayerView} for local playback. - * @param castControlView The {@link PlaybackControlView} to control remote playback. + * @param queuePositionListener A {@link QueuePositionListener} for queue position changes. + * @param localPlayerView The {@link PlayerView} for local playback. + * @param castControlView The {@link PlayerControlView} to control remote playback. * @param context A {@link Context}. * @param castContext The {@link CastContext}. */ - public static PlayerManager createPlayerManager(QueuePositionListener listener, - SimpleExoPlayerView exoPlayerView, PlaybackControlView castControlView, Context context, + public static PlayerManager createPlayerManager( + QueuePositionListener queuePositionListener, + PlayerView localPlayerView, + PlayerControlView castControlView, + Context context, CastContext castContext) { - PlayerManager playerManager = new PlayerManager(listener, exoPlayerView, castControlView, - context, castContext); + PlayerManager playerManager = + new PlayerManager( + queuePositionListener, localPlayerView, castControlView, context, castContext); playerManager.init(); return playerManager; } - private PlayerManager(QueuePositionListener listener, SimpleExoPlayerView exoPlayerView, - PlaybackControlView castControlView, Context context, CastContext castContext) { - this.listener = listener; - this.exoPlayerView = exoPlayerView; + private PlayerManager( + QueuePositionListener queuePositionListener, + PlayerView localPlayerView, + PlayerControlView castControlView, + Context context, + CastContext castContext) { + this.queuePositionListener = queuePositionListener; + this.localPlayerView = localPlayerView; this.castControlView = castControlView; mediaQueue = new ArrayList<>(); currentItemIndex = C.INDEX_UNSET; @@ -113,7 +121,7 @@ import java.util.ArrayList; RenderersFactory renderersFactory = new DefaultRenderersFactory(context, null); exoPlayer = ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector); exoPlayer.addListener(this); - exoPlayerView.setPlayer(exoPlayer); + localPlayerView.setPlayer(exoPlayer); castPlayer = new CastPlayer(castContext); castPlayer.addListener(this); @@ -242,7 +250,7 @@ import java.util.ArrayList; */ public boolean dispatchKeyEvent(KeyEvent event) { if (currentPlayer == exoPlayer) { - return exoPlayerView.dispatchKeyEvent(event); + return localPlayerView.dispatchKeyEvent(event); } else /* currentPlayer == castPlayer */ { return castControlView.dispatchKeyEvent(event); } @@ -256,7 +264,7 @@ import java.util.ArrayList; mediaQueue.clear(); castPlayer.setSessionAvailabilityListener(null); castPlayer.release(); - exoPlayerView.setPlayer(null); + localPlayerView.setPlayer(null); exoPlayer.release(); } @@ -309,10 +317,10 @@ import java.util.ArrayList; // View management. if (currentPlayer == exoPlayer) { - exoPlayerView.setVisibility(View.VISIBLE); + localPlayerView.setVisibility(View.VISIBLE); castControlView.hide(); } else /* currentPlayer == castPlayer */ { - exoPlayerView.setVisibility(View.GONE); + localPlayerView.setVisibility(View.GONE); castControlView.show(); } @@ -380,7 +388,7 @@ import java.util.ArrayList; if (this.currentItemIndex != currentItemIndex) { int oldIndex = this.currentItemIndex; this.currentItemIndex = currentItemIndex; - listener.onQueuePositionChanged(oldIndex, currentItemIndex); + queuePositionListener.onQueuePositionChanged(oldIndex, currentItemIndex); } } diff --git a/demos/cast/src/main/res/layout/main_activity.xml b/demos/cast/src/main/res/layout/main_activity.xml index 1cce287b28..01e48cdea7 100644 --- a/demos/cast/src/main/res/layout/main_activity.xml +++ b/demos/cast/src/main/res/layout/main_activity.xml @@ -19,7 +19,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:keepScreenOn="true"> - - - - diff --git a/extensions/cast/README.md b/extensions/cast/README.md index 73f7041729..8666690661 100644 --- a/extensions/cast/README.md +++ b/extensions/cast/README.md @@ -27,7 +27,4 @@ locally. Instructions for doing this can be found in ExoPlayer's ## Using the extension ## Create a `CastPlayer` and use it to integrate Cast into your app using -ExoPlayer's common Player interface. You can try the Cast Extension to see how a -[PlaybackControlView][] can be used to control playback in a remote receiver app. - -[PlaybackControlView]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/ui/PlaybackControlView.html +ExoPlayer's common `Player` interface. diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java index 1f39fe0023..e545dfd352 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java @@ -280,6 +280,16 @@ public final class CastPlayer implements Player { // Player implementation. + @Override + public VideoComponent getVideoComponent() { + return null; + } + + @Override + public TextComponent getTextComponent() { + return null; + } + @Override public void addListener(EventListener listener) { listeners.add(listener); diff --git a/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java b/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java index c9ed54398e..cbb950093c 100644 --- a/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java +++ b/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java @@ -33,13 +33,11 @@ import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player.DiscontinuityReason; import com.google.android.exoplayer2.Player.TimelineChangeReason; -import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.util.ErrorMessageProvider; +import com.google.android.exoplayer2.video.VideoListener; -/** - * Leanback {@code PlayerAdapter} implementation for {@link SimpleExoPlayer}. - */ +/** Leanback {@code PlayerAdapter} implementation for {@link Player}. */ public final class LeanbackPlayerAdapter extends PlayerAdapter { static { @@ -47,7 +45,7 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { } private final Context context; - private final SimpleExoPlayer player; + private final Player player; private final Handler handler; private final ComponentListener componentListener; private final Runnable updateProgressRunnable; @@ -60,14 +58,14 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { /** * Builds an instance. Note that the {@code PlayerAdapter} does not manage the lifecycle of the - * {@link SimpleExoPlayer} instance. The caller remains responsible for releasing the player when - * it's no longer required. + * {@link Player} instance. The caller remains responsible for releasing the player when it's no + * longer required. * * @param context The current context (activity). * @param player Instance of your exoplayer that needs to be configured. * @param updatePeriodMs The delay between player control updates, in milliseconds. */ - public LeanbackPlayerAdapter(Context context, SimpleExoPlayer player, final int updatePeriodMs) { + public LeanbackPlayerAdapter(Context context, Player player, final int updatePeriodMs) { this.context = context; this.player = player; handler = new Handler(); @@ -115,13 +113,19 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { } notifyStateChanged(); player.addListener(componentListener); - player.addVideoListener(componentListener); + Player.VideoComponent videoComponent = player.getVideoComponent(); + if (videoComponent != null) { + videoComponent.addVideoListener(componentListener); + } } @Override public void onDetachedFromHost() { player.removeListener(componentListener); - player.removeVideoListener(componentListener); + Player.VideoComponent videoComponent = player.getVideoComponent(); + if (videoComponent != null) { + videoComponent.removeVideoListener(componentListener); + } if (surfaceHolderGlueHost != null) { surfaceHolderGlueHost.setSurfaceHolderCallback(null); surfaceHolderGlueHost = null; @@ -196,7 +200,10 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { /* package */ void setVideoSurface(Surface surface) { hasSurface = surface != null; - player.setVideoSurface(surface); + Player.VideoComponent videoComponent = player.getVideoComponent(); + if (videoComponent != null) { + videoComponent.setVideoSurface(surface); + } maybeNotifyPreparedStateChanged(getCallback()); } @@ -219,8 +226,8 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { } } - private final class ComponentListener extends Player.DefaultEventListener implements - SimpleExoPlayer.VideoListener, SurfaceHolder.Callback { + private final class ComponentListener extends Player.DefaultEventListener + implements SurfaceHolder.Callback, VideoListener { // SurfaceHolder.Callback implementation. @@ -274,11 +281,11 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { callback.onBufferedPositionChanged(LeanbackPlayerAdapter.this); } - // SimpleExoplayerView.Callback implementation. + // VideoListener implementation. @Override - public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, - float pixelWidthHeightRatio) { + public void onVideoSizeChanged( + int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { getCallback().onVideoSizeChanged(LeanbackPlayerAdapter.this, width, height); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index b5f6e623eb..83bbdd1157 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -124,6 +124,16 @@ import java.util.concurrent.CopyOnWriteArraySet; internalPlayerHandler = new Handler(internalPlayer.getPlaybackLooper()); } + @Override + public VideoComponent getVideoComponent() { + return null; + } + + @Override + public TextComponent getTextComponent() { + return null; + } + @Override public Looper getPlaybackLooper() { return internalPlayer.getPlaybackLooper(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Player.java b/library/core/src/main/java/com/google/android/exoplayer2/Player.java index 97cd9449d3..443ff8a2ea 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Player.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Player.java @@ -18,8 +18,14 @@ package com.google.android.exoplayer2; import android.os.Looper; import android.support.annotation.IntDef; import android.support.annotation.Nullable; +import android.view.Surface; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.TextureView; import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.text.TextOutput; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import com.google.android.exoplayer2.video.VideoListener; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -44,6 +50,130 @@ import java.lang.annotation.RetentionPolicy; */ public interface Player { + /** The video component of a {@link Player}. */ + interface VideoComponent { + + /** + * Sets the video scaling mode. + * + * @param videoScalingMode The video scaling mode. + */ + void setVideoScalingMode(@C.VideoScalingMode int videoScalingMode); + + /** Returns the video scaling mode. */ + @C.VideoScalingMode + int getVideoScalingMode(); + + /** + * Adds a listener to receive video events. + * + * @param listener The listener to register. + */ + void addVideoListener(VideoListener listener); + + /** + * Removes a listener of video events. + * + * @param listener The listener to unregister. + */ + void removeVideoListener(VideoListener listener); + + /** + * Clears any {@link Surface}, {@link SurfaceHolder}, {@link SurfaceView} or {@link TextureView} + * currently set on the player. + */ + void clearVideoSurface(); + + /** + * Sets the {@link Surface} onto which video will be rendered. The caller is responsible for + * tracking the lifecycle of the surface, and must clear the surface by calling {@code + * setVideoSurface(null)} if the surface is destroyed. + * + *

          If the surface is held by a {@link SurfaceView}, {@link TextureView} or {@link + * SurfaceHolder} then it's recommended to use {@link #setVideoSurfaceView(SurfaceView)}, {@link + * #setVideoTextureView(TextureView)} or {@link #setVideoSurfaceHolder(SurfaceHolder)} rather + * than this method, since passing the holder allows the player to track the lifecycle of the + * surface automatically. + * + * @param surface The {@link Surface}. + */ + void setVideoSurface(Surface surface); + + /** + * 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. + */ + void clearVideoSurface(Surface surface); + + /** + * 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. + * + * @param surfaceHolder The surface holder. + */ + void setVideoSurfaceHolder(SurfaceHolder surfaceHolder); + + /** + * 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. + */ + void clearVideoSurfaceHolder(SurfaceHolder surfaceHolder); + + /** + * Sets the {@link SurfaceView} onto which video will be rendered. The player will track the + * lifecycle of the surface automatically. + * + * @param surfaceView The surface view. + */ + void setVideoSurfaceView(SurfaceView surfaceView); + + /** + * 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. + */ + void clearVideoSurfaceView(SurfaceView surfaceView); + + /** + * Sets the {@link TextureView} onto which video will be rendered. The player will track the + * lifecycle of the surface automatically. + * + * @param textureView The texture view. + */ + void setVideoTextureView(TextureView textureView); + + /** + * 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. + */ + void clearVideoTextureView(TextureView textureView); + } + + /** The text component of a {@link Player}. */ + interface TextComponent { + + /** + * Registers an output to receive text events. + * + * @param listener The output to register. + */ + void addTextOutput(TextOutput listener); + + /** + * Removes a text output. + * + * @param listener The output to remove. + */ + void removeTextOutput(TextOutput listener); + } + /** * Listener of changes in player state. */ @@ -298,6 +428,14 @@ public interface Player { */ int TIMELINE_CHANGE_REASON_DYNAMIC = 2; + /** Returns the component of this player for video output, or null if video is not supported. */ + @Nullable + VideoComponent getVideoComponent(); + + /** Returns the component of this player for text output, or null if text is not supported. */ + @Nullable + TextComponent getTextComponent(); + /** * Register a listener to receive events from the player. The listener's methods will be called on * the thread that was used to construct the player. However, if the thread used to construct the diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index ec53e5a964..98ef35d62c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -50,39 +50,11 @@ import java.util.concurrent.CopyOnWriteArraySet; * be obtained from {@link ExoPlayerFactory}. */ @TargetApi(16) -public class SimpleExoPlayer implements ExoPlayer { +public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player.TextComponent { - /** - * A listener for video rendering information from a {@link SimpleExoPlayer}. - */ - public interface VideoListener { - - /** - * Called each time there's a change in the size of the video being rendered. - * - * @param width The video width in pixels. - * @param height The video height in pixels. - * @param unappliedRotationDegrees For videos that require a rotation, this is the clockwise - * rotation in degrees that the application should apply for the video for it to be rendered - * in the correct orientation. This value will always be zero on API levels 21 and above, - * since the renderer will apply all necessary rotations internally. On earlier API levels - * this is not possible. Applications that use {@link android.view.TextureView} can apply - * the rotation by calling {@link android.view.TextureView#setTransform}. Applications that - * do not expect to encounter rotated videos can safely ignore this parameter. - * @param pixelWidthHeightRatio The width to height ratio of each pixel. For the normal case - * of square pixels this will be equal to 1.0. Different values are indicative of anamorphic - * content. - */ - void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, - float pixelWidthHeightRatio); - - /** - * Called when a frame is rendered for the first time since setting the surface, and when a - * frame is rendered for the first time since a video track was selected. - */ - void onRenderedFirstFrame(); - - } + /** @deprecated Use {@link com.google.android.exoplayer2.video.VideoListener}. */ + @Deprecated + public interface VideoListener extends com.google.android.exoplayer2.video.VideoListener {} private static final String TAG = "SimpleExoPlayer"; @@ -90,7 +62,8 @@ public class SimpleExoPlayer implements ExoPlayer { private final ExoPlayer player; private final ComponentListener componentListener; - private final CopyOnWriteArraySet videoListeners; + private final CopyOnWriteArraySet + videoListeners; private final CopyOnWriteArraySet textOutputs; private final CopyOnWriteArraySet metadataOutputs; private final CopyOnWriteArraySet videoDebugListeners; @@ -154,14 +127,25 @@ public class SimpleExoPlayer implements ExoPlayer { player = createExoPlayerImpl(renderers, trackSelector, loadControl, clock); } + @Override + public VideoComponent getVideoComponent() { + return this; + } + + @Override + public TextComponent getTextComponent() { + return this; + } + /** * Sets the video scaling mode. - *

          - * Note that the scaling mode only applies if a {@link MediaCodec}-based video {@link Renderer} is - * enabled and if the output surface is owned by a {@link android.view.SurfaceView}. + * + *

          Note that the scaling mode only applies if a {@link MediaCodec}-based video {@link Renderer} + * is enabled and if the output surface is owned by a {@link android.view.SurfaceView}. * * @param videoScalingMode The video scaling mode. */ + @Override public void setVideoScalingMode(@C.VideoScalingMode int videoScalingMode) { this.videoScalingMode = videoScalingMode; for (Renderer renderer : renderers) { @@ -175,57 +159,30 @@ public class SimpleExoPlayer implements ExoPlayer { } } - /** - * Returns the video scaling mode. - */ + @Override public @C.VideoScalingMode int getVideoScalingMode() { return videoScalingMode; } - /** - * Clears any {@link Surface}, {@link SurfaceHolder}, {@link SurfaceView} or {@link TextureView} - * currently set on the player. - */ + @Override public void clearVideoSurface() { setVideoSurface(null); } - /** - * Sets the {@link Surface} onto which video will be rendered. The caller is responsible for - * tracking the lifecycle of the surface, and must clear the surface by calling - * {@code setVideoSurface(null)} if the surface is destroyed. - *

          - * If the surface is held by a {@link SurfaceView}, {@link TextureView} or {@link SurfaceHolder} - * then it's recommended to use {@link #setVideoSurfaceView(SurfaceView)}, - * {@link #setVideoTextureView(TextureView)} or {@link #setVideoSurfaceHolder(SurfaceHolder)} - * rather than this method, since passing the holder allows the player to track the lifecycle of - * the surface automatically. - * - * @param surface The {@link Surface}. - */ + @Override public void setVideoSurface(Surface surface) { removeSurfaceCallbacks(); 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. - */ + @Override 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. - * - * @param surfaceHolder The surface holder. - */ + @Override public void setVideoSurfaceHolder(SurfaceHolder surfaceHolder) { removeSurfaceCallbacks(); this.surfaceHolder = surfaceHolder; @@ -238,44 +195,24 @@ 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. - */ + @Override 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. - * - * @param surfaceView The surface view. - */ + @Override public void setVideoSurfaceView(SurfaceView surfaceView) { 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. - */ + @Override public void clearVideoSurfaceView(SurfaceView surfaceView) { clearVideoSurfaceHolder(surfaceView == null ? null : surfaceView.getHolder()); } - /** - * Sets the {@link TextureView} onto which video will be rendered. The player will track the - * lifecycle of the surface automatically. - * - * @param textureView The texture view. - */ + @Override public void setVideoTextureView(TextureView textureView) { removeSurfaceCallbacks(); this.textureView = textureView; @@ -292,12 +229,7 @@ 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. - */ + @Override public void clearVideoTextureView(TextureView textureView) { if (textureView != null && textureView == this.textureView) { setVideoTextureView(null); @@ -446,21 +378,13 @@ public class SimpleExoPlayer implements ExoPlayer { return audioDecoderCounters; } - /** - * Adds a listener to receive video events. - * - * @param listener The listener to register. - */ - public void addVideoListener(VideoListener listener) { + @Override + public void addVideoListener(com.google.android.exoplayer2.video.VideoListener listener) { videoListeners.add(listener); } - /** - * Removes a listener of video events. - * - * @param listener The listener to unregister. - */ - public void removeVideoListener(VideoListener listener) { + @Override + public void removeVideoListener(com.google.android.exoplayer2.video.VideoListener listener) { videoListeners.remove(listener); } @@ -468,7 +392,7 @@ public class SimpleExoPlayer implements ExoPlayer { * Sets a listener to receive video events, removing all existing listeners. * * @param listener The listener. - * @deprecated Use {@link #addVideoListener(VideoListener)}. + * @deprecated Use {@link #addVideoListener(com.google.android.exoplayer2.video.VideoListener)}. */ @Deprecated public void setVideoListener(VideoListener listener) { @@ -479,30 +403,23 @@ public class SimpleExoPlayer implements ExoPlayer { } /** - * Equivalent to {@link #removeVideoListener(VideoListener)}. + * Equivalent to {@link #removeVideoListener(com.google.android.exoplayer2.video.VideoListener)}. * * @param listener The listener to clear. - * @deprecated Use {@link #removeVideoListener(VideoListener)}. + * @deprecated Use {@link + * #removeVideoListener(com.google.android.exoplayer2.video.VideoListener)}. */ @Deprecated public void clearVideoListener(VideoListener listener) { removeVideoListener(listener); } - /** - * Registers an output to receive text events. - * - * @param listener The output to register. - */ + @Override public void addTextOutput(TextOutput listener) { textOutputs.add(listener); } - /** - * Removes a text output. - * - * @param listener The output to remove. - */ + @Override public void removeTextOutput(TextOutput listener) { textOutputs.remove(listener); } @@ -532,20 +449,10 @@ public class SimpleExoPlayer implements ExoPlayer { removeTextOutput(output); } - /** - * Registers an output to receive metadata events. - * - * @param listener The output to register. - */ public void addMetadataOutput(MetadataOutput listener) { metadataOutputs.add(listener); } - /** - * Removes a metadata output. - * - * @param listener The output to remove. - */ public void removeMetadataOutput(MetadataOutput listener) { metadataOutputs.remove(listener); } @@ -978,7 +885,7 @@ public class SimpleExoPlayer implements ExoPlayer { @Override public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { - for (VideoListener videoListener : videoListeners) { + for (com.google.android.exoplayer2.video.VideoListener videoListener : videoListeners) { videoListener.onVideoSizeChanged(width, height, unappliedRotationDegrees, pixelWidthHeightRatio); } @@ -991,7 +898,7 @@ public class SimpleExoPlayer implements ExoPlayer { @Override public void onRenderedFirstFrame(Surface surface) { if (SimpleExoPlayer.this.surface == surface) { - for (VideoListener videoListener : videoListeners) { + for (com.google.android.exoplayer2.video.VideoListener videoListener : videoListeners) { videoListener.onRenderedFirstFrame(); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoListener.java b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoListener.java new file mode 100644 index 0000000000..ab09e0bbc2 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoListener.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2018 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; + +/** A listener for metadata corresponding to video being rendered. */ +public interface VideoListener { + + /** + * Called each time there's a change in the size of the video being rendered. + * + * @param width The video width in pixels. + * @param height The video height in pixels. + * @param unappliedRotationDegrees For videos that require a rotation, this is the clockwise + * rotation in degrees that the application should apply for the video for it to be rendered + * in the correct orientation. This value will always be zero on API levels 21 and above, + * since the renderer will apply all necessary rotations internally. On earlier API levels + * this is not possible. Applications that use {@link android.view.TextureView} can apply the + * rotation by calling {@link android.view.TextureView#setTransform}. Applications that do not + * expect to encounter rotated videos can safely ignore this parameter. + * @param pixelWidthHeightRatio The width to height ratio of each pixel. For the normal case of + * square pixels this will be equal to 1.0. Different values are indicative of anamorphic + * content. + */ + void onVideoSizeChanged( + int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio); + + /** + * Called when a frame is rendered for the first time since setting the surface, and when a frame + * is rendered for the first time since a video track was selected. + */ + void onRenderedFirstFrame(); +} diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java index fefbb0797a..da03d28cba 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java @@ -15,171 +15,24 @@ */ package com.google.android.exoplayer2.ui; -import android.annotation.SuppressLint; import android.content.Context; -import android.content.res.Resources; -import android.content.res.TypedArray; -import android.graphics.drawable.Drawable; -import android.os.SystemClock; -import android.support.annotation.Nullable; import android.util.AttributeSet; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.FrameLayout; -import android.widget.ImageView; -import android.widget.TextView; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlayerLibraryInfo; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.RepeatModeUtil; -import com.google.android.exoplayer2.util.Util; -import java.util.Arrays; -import java.util.Formatter; -import java.util.Locale; -/** - * A view for controlling {@link Player} instances. - * - *

          A PlaybackControlView can be customized by setting attributes (or calling corresponding - * methods), overriding the view's layout file or by specifying a custom view layout file, as - * outlined below. - * - *

          Attributes

          - * - * The following attributes can be set on a PlaybackControlView when used in a layout XML file: - * - *

          - * - *

            - *
          • {@code show_timeout} - The time between the last user interaction and the controls - * being automatically hidden, in milliseconds. Use zero if the controls should not - * automatically timeout. - *
              - *
            • Corresponding method: {@link #setShowTimeoutMs(int)} - *
            • Default: {@link #DEFAULT_SHOW_TIMEOUT_MS} - *
            - *
          • {@code rewind_increment} - The duration of the rewind applied when the user taps the - * rewind button, in milliseconds. Use zero to disable the rewind button. - *
              - *
            • Corresponding method: {@link #setRewindIncrementMs(int)} - *
            • Default: {@link #DEFAULT_REWIND_MS} - *
            - *
          • {@code fastforward_increment} - Like {@code rewind_increment}, but for fast forward. - *
              - *
            • Corresponding method: {@link #setFastForwardIncrementMs(int)} - *
            • Default: {@link #DEFAULT_FAST_FORWARD_MS} - *
            - *
          • {@code repeat_toggle_modes} - A flagged enumeration value specifying which repeat - * mode toggle options are enabled. Valid values are: {@code none}, {@code one}, {@code all}, - * or {@code one|all}. - *
              - *
            • Corresponding method: {@link #setRepeatToggleModes(int)} - *
            • Default: {@link PlaybackControlView#DEFAULT_REPEAT_TOGGLE_MODES} - *
            - *
          • {@code show_shuffle_button} - Whether the shuffle button is shown. - *
              - *
            • Corresponding method: {@link #setShowShuffleButton(boolean)} - *
            • Default: false - *
            - *
          • {@code controller_layout_id} - Specifies the id of the layout to be inflated. See - * below for more details. - *
              - *
            • Corresponding method: None - *
            • Default: {@code R.id.exo_playback_control_view} - *
            - *
          - * - *

          Overriding the layout file

          - * - * To customize the layout of PlaybackControlView throughout your app, or just for certain - * configurations, you can define {@code exo_playback_control_view.xml} layout files in your - * application {@code res/layout*} directories. These layouts will override the one provided by the - * ExoPlayer library, and will be inflated for use by PlaybackControlView. The view identifies and - * binds its children by looking for the following ids: - * - *

          - * - *

            - *
          • {@code exo_play} - The play button. - *
              - *
            • Type: {@link View} - *
            - *
          • {@code exo_pause} - The pause button. - *
              - *
            • Type: {@link View} - *
            - *
          • {@code exo_ffwd} - The fast forward button. - *
              - *
            • Type: {@link View} - *
            - *
          • {@code exo_rew} - The rewind button. - *
              - *
            • Type: {@link View} - *
            - *
          • {@code exo_prev} - The previous track button. - *
              - *
            • Type: {@link View} - *
            - *
          • {@code exo_next} - The next track button. - *
              - *
            • Type: {@link View} - *
            - *
          • {@code exo_repeat_toggle} - The repeat toggle button. - *
              - *
            • Type: {@link View} - *
            - *
          • {@code exo_shuffle} - The shuffle button. - *
              - *
            • Type: {@link View} - *
            - *
          • {@code exo_position} - Text view displaying the current playback position. - *
              - *
            • Type: {@link TextView} - *
            - *
          • {@code exo_duration} - Text view displaying the current media duration. - *
              - *
            • Type: {@link TextView} - *
            - *
          • {@code exo_progress} - Time bar that's updated during playback and allows seeking. - *
              - *
            • Type: {@link TimeBar} - *
            - *
          - * - *

          All child views are optional and so can be omitted if not required, however where defined they - * must be of the expected type. - * - *

          Specifying a custom layout file

          - * - * Defining your own {@code exo_playback_control_view.xml} is useful to customize the layout of - * PlaybackControlView throughout your application. It's also possible to customize the layout for a - * single instance in a layout file. This is achieved by setting the {@code controller_layout_id} - * attribute on a PlaybackControlView. This will cause the specified layout to be inflated instead - * of {@code exo_playback_control_view.xml} for only the instance on which the attribute is set. - */ -public class PlaybackControlView extends FrameLayout { - - static { - ExoPlayerLibraryInfo.registerModule("goog.exo.ui"); - } +/** @deprecated Use {@link PlayerControlView}. */ +@Deprecated +public class PlaybackControlView extends PlayerControlView { /** @deprecated Use {@link com.google.android.exoplayer2.ControlDispatcher}. */ @Deprecated public interface ControlDispatcher extends com.google.android.exoplayer2.ControlDispatcher {} - /** Listener to be notified about changes of the visibility of the UI control. */ - public interface VisibilityListener { - - /** - * Called when the visibility changes. - * - * @param visibility The new visibility. Either {@link View#VISIBLE} or {@link View#GONE}. - */ - void onVisibilityChange(int visibility); - } + /** + * @deprecated Use {@link com.google.android.exoplayer2.ui.PlayerControlView.VisibilityListener}. + */ + @Deprecated + public interface VisibilityListener + extends com.google.android.exoplayer2.ui.PlayerControlView.VisibilityListener {} private static final class DefaultControlDispatcher extends com.google.android.exoplayer2.DefaultControlDispatcher implements ControlDispatcher {} @@ -188,927 +41,34 @@ public class PlaybackControlView extends FrameLayout { public static final ControlDispatcher DEFAULT_CONTROL_DISPATCHER = new DefaultControlDispatcher(); /** The default fast forward increment, in milliseconds. */ - public static final int DEFAULT_FAST_FORWARD_MS = 15000; + public static final int DEFAULT_FAST_FORWARD_MS = PlayerControlView.DEFAULT_FAST_FORWARD_MS; /** The default rewind increment, in milliseconds. */ - public static final int DEFAULT_REWIND_MS = 5000; + public static final int DEFAULT_REWIND_MS = PlayerControlView.DEFAULT_REWIND_MS; /** The default show timeout, in milliseconds. */ - public static final int DEFAULT_SHOW_TIMEOUT_MS = 5000; + public static final int DEFAULT_SHOW_TIMEOUT_MS = PlayerControlView.DEFAULT_SHOW_TIMEOUT_MS; /** The default repeat toggle modes. */ public static final @RepeatModeUtil.RepeatToggleModes int DEFAULT_REPEAT_TOGGLE_MODES = - RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE; + PlayerControlView.DEFAULT_REPEAT_TOGGLE_MODES; /** 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; - private final View previousButton; - private final View nextButton; - private final View playButton; - private final View pauseButton; - private final View fastForwardButton; - private final View rewindButton; - private final ImageView repeatToggleButton; - private final View shuffleButton; - private final TextView durationView; - private final TextView positionView; - private final TimeBar timeBar; - private final StringBuilder formatBuilder; - private final Formatter formatter; - private final Timeline.Period period; - private final Timeline.Window window; - - private final Drawable repeatOffButtonDrawable; - private final Drawable repeatOneButtonDrawable; - private final Drawable repeatAllButtonDrawable; - private final String repeatOffButtonContentDescription; - private final String repeatOneButtonContentDescription; - private final String repeatAllButtonContentDescription; - - private Player player; - private com.google.android.exoplayer2.ControlDispatcher controlDispatcher; - private VisibilityListener visibilityListener; - - private boolean isAttachedToWindow; - private boolean showMultiWindowTimeBar; - private boolean multiWindowTimeBar; - private boolean scrubbing; - private int rewindMs; - private int fastForwardMs; - private int showTimeoutMs; - private @RepeatModeUtil.RepeatToggleModes int repeatToggleModes; - private boolean showShuffleButton; - private long hideAtMs; - private long[] adGroupTimesMs; - private boolean[] playedAdGroups; - private long[] extraAdGroupTimesMs; - private boolean[] extraPlayedAdGroups; - - private final Runnable updateProgressAction = - new Runnable() { - @Override - public void run() { - updateProgress(); - } - }; - - private final Runnable hideAction = - new Runnable() { - @Override - public void run() { - hide(); - } - }; + public static final int MAX_WINDOWS_FOR_MULTI_WINDOW_TIME_BAR = + PlayerControlView.MAX_WINDOWS_FOR_MULTI_WINDOW_TIME_BAR; public PlaybackControlView(Context context) { - this(context, null); + super(context); } public PlaybackControlView(Context context, AttributeSet attrs) { - this(context, attrs, 0); + super(context, attrs); } public PlaybackControlView(Context context, AttributeSet attrs, int defStyleAttr) { - this(context, attrs, defStyleAttr, attrs); + super(context, attrs, defStyleAttr); } public PlaybackControlView( Context context, AttributeSet attrs, int defStyleAttr, AttributeSet playbackAttrs) { - super(context, attrs, defStyleAttr); - int controllerLayoutId = R.layout.exo_playback_control_view; - rewindMs = DEFAULT_REWIND_MS; - fastForwardMs = DEFAULT_FAST_FORWARD_MS; - showTimeoutMs = DEFAULT_SHOW_TIMEOUT_MS; - repeatToggleModes = DEFAULT_REPEAT_TOGGLE_MODES; - showShuffleButton = false; - if (playbackAttrs != null) { - TypedArray a = - context - .getTheme() - .obtainStyledAttributes(playbackAttrs, R.styleable.PlaybackControlView, 0, 0); - try { - rewindMs = a.getInt(R.styleable.PlaybackControlView_rewind_increment, rewindMs); - fastForwardMs = - a.getInt(R.styleable.PlaybackControlView_fastforward_increment, fastForwardMs); - showTimeoutMs = a.getInt(R.styleable.PlaybackControlView_show_timeout, showTimeoutMs); - controllerLayoutId = - a.getResourceId( - R.styleable.PlaybackControlView_controller_layout_id, controllerLayoutId); - repeatToggleModes = getRepeatToggleModes(a, repeatToggleModes); - showShuffleButton = - a.getBoolean(R.styleable.PlaybackControlView_show_shuffle_button, showShuffleButton); - } finally { - a.recycle(); - } - } - period = new Timeline.Period(); - window = new Timeline.Window(); - formatBuilder = new StringBuilder(); - formatter = new Formatter(formatBuilder, Locale.getDefault()); - adGroupTimesMs = new long[0]; - playedAdGroups = new boolean[0]; - extraAdGroupTimesMs = new long[0]; - extraPlayedAdGroups = new boolean[0]; - componentListener = new ComponentListener(); - controlDispatcher = new com.google.android.exoplayer2.DefaultControlDispatcher(); - - LayoutInflater.from(context).inflate(controllerLayoutId, this); - setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); - - durationView = findViewById(R.id.exo_duration); - positionView = findViewById(R.id.exo_position); - timeBar = findViewById(R.id.exo_progress); - if (timeBar != null) { - timeBar.addListener(componentListener); - } - playButton = findViewById(R.id.exo_play); - if (playButton != null) { - playButton.setOnClickListener(componentListener); - } - pauseButton = findViewById(R.id.exo_pause); - if (pauseButton != null) { - pauseButton.setOnClickListener(componentListener); - } - previousButton = findViewById(R.id.exo_prev); - if (previousButton != null) { - previousButton.setOnClickListener(componentListener); - } - nextButton = findViewById(R.id.exo_next); - if (nextButton != null) { - nextButton.setOnClickListener(componentListener); - } - rewindButton = findViewById(R.id.exo_rew); - if (rewindButton != null) { - rewindButton.setOnClickListener(componentListener); - } - fastForwardButton = findViewById(R.id.exo_ffwd); - if (fastForwardButton != null) { - fastForwardButton.setOnClickListener(componentListener); - } - repeatToggleButton = findViewById(R.id.exo_repeat_toggle); - if (repeatToggleButton != null) { - repeatToggleButton.setOnClickListener(componentListener); - } - shuffleButton = findViewById(R.id.exo_shuffle); - if (shuffleButton != null) { - shuffleButton.setOnClickListener(componentListener); - } - Resources resources = context.getResources(); - repeatOffButtonDrawable = resources.getDrawable(R.drawable.exo_controls_repeat_off); - repeatOneButtonDrawable = resources.getDrawable(R.drawable.exo_controls_repeat_one); - repeatAllButtonDrawable = resources.getDrawable(R.drawable.exo_controls_repeat_all); - repeatOffButtonContentDescription = - resources.getString(R.string.exo_controls_repeat_off_description); - repeatOneButtonContentDescription = - resources.getString(R.string.exo_controls_repeat_one_description); - repeatAllButtonContentDescription = - resources.getString(R.string.exo_controls_repeat_all_description); + super(context, attrs, defStyleAttr, playbackAttrs); } - @SuppressWarnings("ResourceType") - private static @RepeatModeUtil.RepeatToggleModes int getRepeatToggleModes( - TypedArray a, @RepeatModeUtil.RepeatToggleModes int repeatToggleModes) { - return a.getInt(R.styleable.PlaybackControlView_repeat_toggle_modes, repeatToggleModes); - } - - /** - * Returns the {@link Player} currently being controlled by this view, or null if no player is - * set. - */ - public Player getPlayer() { - return player; - } - - /** - * Sets the {@link Player} to control. - * - * @param player The {@link Player} to control. - */ - public void setPlayer(Player player) { - if (this.player == player) { - return; - } - if (this.player != null) { - this.player.removeListener(componentListener); - } - this.player = player; - if (player != null) { - player.addListener(componentListener); - } - 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 millisecond positions of extra ad markers relative to the start of the window (or - * timeline, if in multi-window mode) and whether each extra ad has been played or not. The - * markers are shown in addition to any ad markers for ads in the player's timeline. - * - * @param extraAdGroupTimesMs The millisecond timestamps of the extra ad markers to show, or - * {@code null} to show no extra ad markers. - * @param extraPlayedAdGroups Whether each ad has been played, or {@code null} to show no extra ad - * markers. - */ - public void setExtraAdGroupMarkers( - @Nullable long[] extraAdGroupTimesMs, @Nullable boolean[] extraPlayedAdGroups) { - if (extraAdGroupTimesMs == null) { - this.extraAdGroupTimesMs = new long[0]; - this.extraPlayedAdGroups = new boolean[0]; - } else { - Assertions.checkArgument(extraAdGroupTimesMs.length == extraPlayedAdGroups.length); - this.extraAdGroupTimesMs = extraAdGroupTimesMs; - this.extraPlayedAdGroups = extraPlayedAdGroups; - } - updateProgress(); - } - - /** - * Sets the {@link VisibilityListener}. - * - * @param listener The listener to be notified about visibility changes. - */ - public void setVisibilityListener(VisibilityListener listener) { - this.visibilityListener = listener; - } - - /** - * Sets the {@link com.google.android.exoplayer2.ControlDispatcher}. - * - * @param controlDispatcher The {@link com.google.android.exoplayer2.ControlDispatcher}, or null - * to use {@link com.google.android.exoplayer2.DefaultControlDispatcher}. - */ - public void setControlDispatcher( - @Nullable com.google.android.exoplayer2.ControlDispatcher controlDispatcher) { - this.controlDispatcher = - controlDispatcher == null - ? new com.google.android.exoplayer2.DefaultControlDispatcher() - : controlDispatcher; - } - - /** - * Sets the rewind increment in milliseconds. - * - * @param rewindMs The rewind increment in milliseconds. A non-positive value will cause the - * rewind button to be disabled. - */ - public void setRewindIncrementMs(int rewindMs) { - this.rewindMs = rewindMs; - updateNavigation(); - } - - /** - * Sets the fast forward increment in milliseconds. - * - * @param fastForwardMs The fast forward increment in milliseconds. A non-positive value will - * cause the fast forward button to be disabled. - */ - public void setFastForwardIncrementMs(int fastForwardMs) { - this.fastForwardMs = fastForwardMs; - updateNavigation(); - } - - /** - * Returns the playback controls timeout. The playback controls are automatically hidden after - * this duration of time has elapsed without user input. - * - * @return The duration in milliseconds. A non-positive value indicates that the controls will - * remain visible indefinitely. - */ - public int getShowTimeoutMs() { - return showTimeoutMs; - } - - /** - * Sets the playback controls timeout. The playback controls are automatically hidden after this - * duration of time has elapsed without user input. - * - * @param showTimeoutMs The duration in milliseconds. A non-positive value will cause the controls - * to remain visible indefinitely. - */ - public void setShowTimeoutMs(int showTimeoutMs) { - this.showTimeoutMs = showTimeoutMs; - // showTimeoutMs is changed, so call hideAfterTimeout to reset the timeout. - if (isVisible()) { - hideAfterTimeout(); - } - } - - /** - * Returns which repeat toggle modes are enabled. - * - * @return The currently enabled {@link RepeatModeUtil.RepeatToggleModes}. - */ - public @RepeatModeUtil.RepeatToggleModes int getRepeatToggleModes() { - return repeatToggleModes; - } - - /** - * Sets which repeat toggle modes are enabled. - * - * @param repeatToggleModes A set of {@link RepeatModeUtil.RepeatToggleModes}. - */ - public void setRepeatToggleModes(@RepeatModeUtil.RepeatToggleModes int repeatToggleModes) { - this.repeatToggleModes = repeatToggleModes; - if (player != null) { - @Player.RepeatMode int currentMode = player.getRepeatMode(); - if (repeatToggleModes == RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE - && currentMode != Player.REPEAT_MODE_OFF) { - controlDispatcher.dispatchSetRepeatMode(player, Player.REPEAT_MODE_OFF); - } else if (repeatToggleModes == RepeatModeUtil.REPEAT_TOGGLE_MODE_ONE - && currentMode == Player.REPEAT_MODE_ALL) { - controlDispatcher.dispatchSetRepeatMode(player, Player.REPEAT_MODE_ONE); - } else if (repeatToggleModes == RepeatModeUtil.REPEAT_TOGGLE_MODE_ALL - && currentMode == Player.REPEAT_MODE_ONE) { - controlDispatcher.dispatchSetRepeatMode(player, Player.REPEAT_MODE_ALL); - } - } - } - - /** Returns whether the shuffle button is shown. */ - public boolean getShowShuffleButton() { - return showShuffleButton; - } - - /** - * Sets whether the shuffle button is shown. - * - * @param showShuffleButton Whether the shuffle button is shown. - */ - public void setShowShuffleButton(boolean showShuffleButton) { - this.showShuffleButton = showShuffleButton; - updateShuffleButton(); - } - - /** - * Shows the playback controls. If {@link #getShowTimeoutMs()} is positive then the controls will - * be automatically hidden after this duration of time has elapsed without user input. - */ - public void show() { - if (!isVisible()) { - setVisibility(VISIBLE); - if (visibilityListener != null) { - visibilityListener.onVisibilityChange(getVisibility()); - } - updateAll(); - requestPlayPauseFocus(); - } - // Call hideAfterTimeout even if already visible to reset the timeout. - hideAfterTimeout(); - } - - /** Hides the controller. */ - public void hide() { - if (isVisible()) { - setVisibility(GONE); - if (visibilityListener != null) { - visibilityListener.onVisibilityChange(getVisibility()); - } - removeCallbacks(updateProgressAction); - removeCallbacks(hideAction); - hideAtMs = C.TIME_UNSET; - } - } - - /** Returns whether the controller is currently visible. */ - public boolean isVisible() { - return getVisibility() == VISIBLE; - } - - private void hideAfterTimeout() { - removeCallbacks(hideAction); - if (showTimeoutMs > 0) { - hideAtMs = SystemClock.uptimeMillis() + showTimeoutMs; - if (isAttachedToWindow) { - postDelayed(hideAction, showTimeoutMs); - } - } else { - hideAtMs = C.TIME_UNSET; - } - } - - private void updateAll() { - updatePlayPauseButton(); - updateNavigation(); - updateRepeatModeButton(); - updateShuffleButton(); - updateProgress(); - } - - private void updatePlayPauseButton() { - if (!isVisible() || !isAttachedToWindow) { - return; - } - boolean requestPlayPauseFocus = false; - boolean playing = player != null && player.getPlayWhenReady(); - if (playButton != null) { - requestPlayPauseFocus |= playing && playButton.isFocused(); - playButton.setVisibility(playing ? View.GONE : View.VISIBLE); - } - if (pauseButton != null) { - requestPlayPauseFocus |= !playing && pauseButton.isFocused(); - pauseButton.setVisibility(!playing ? View.GONE : View.VISIBLE); - } - if (requestPlayPauseFocus) { - requestPlayPauseFocus(); - } - } - - private void updateNavigation() { - if (!isVisible() || !isAttachedToWindow) { - return; - } - Timeline timeline = player != null ? player.getCurrentTimeline() : null; - boolean haveNonEmptyTimeline = timeline != null && !timeline.isEmpty(); - boolean isSeekable = false; - boolean enablePrevious = false; - boolean enableNext = false; - if (haveNonEmptyTimeline && !player.isPlayingAd()) { - int windowIndex = player.getCurrentWindowIndex(); - timeline.getWindow(windowIndex, window); - isSeekable = window.isSeekable; - enablePrevious = - isSeekable || !window.isDynamic || player.getPreviousWindowIndex() != C.INDEX_UNSET; - enableNext = window.isDynamic || player.getNextWindowIndex() != C.INDEX_UNSET; - } - setButtonEnabled(enablePrevious, previousButton); - setButtonEnabled(enableNext, nextButton); - setButtonEnabled(fastForwardMs > 0 && isSeekable, fastForwardButton); - setButtonEnabled(rewindMs > 0 && isSeekable, rewindButton); - if (timeBar != null) { - timeBar.setEnabled(isSeekable); - } - } - - private void updateRepeatModeButton() { - if (!isVisible() || !isAttachedToWindow || repeatToggleButton == null) { - return; - } - if (repeatToggleModes == RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE) { - repeatToggleButton.setVisibility(View.GONE); - return; - } - if (player == null) { - setButtonEnabled(false, repeatToggleButton); - return; - } - setButtonEnabled(true, repeatToggleButton); - switch (player.getRepeatMode()) { - case Player.REPEAT_MODE_OFF: - repeatToggleButton.setImageDrawable(repeatOffButtonDrawable); - repeatToggleButton.setContentDescription(repeatOffButtonContentDescription); - break; - case Player.REPEAT_MODE_ONE: - repeatToggleButton.setImageDrawable(repeatOneButtonDrawable); - repeatToggleButton.setContentDescription(repeatOneButtonContentDescription); - break; - case Player.REPEAT_MODE_ALL: - repeatToggleButton.setImageDrawable(repeatAllButtonDrawable); - repeatToggleButton.setContentDescription(repeatAllButtonContentDescription); - break; - default: - // Never happens. - } - repeatToggleButton.setVisibility(View.VISIBLE); - } - - private void updateShuffleButton() { - if (!isVisible() || !isAttachedToWindow || shuffleButton == null) { - return; - } - if (!showShuffleButton) { - shuffleButton.setVisibility(View.GONE); - } else if (player == null) { - setButtonEnabled(false, shuffleButton); - } else { - shuffleButton.setAlpha(player.getShuffleModeEnabled() ? 1f : 0.3f); - shuffleButton.setEnabled(true); - shuffleButton.setVisibility(View.VISIBLE); - } - } - - private void updateTimeBarMode() { - if (player == null) { - return; - } - multiWindowTimeBar = - showMultiWindowTimeBar && canShowMultiWindowTimeBar(player.getCurrentTimeline(), window); - } - - private void updateProgress() { - if (!isVisible() || !isAttachedToWindow) { - return; - } - - long position = 0; - long bufferedPosition = 0; - long duration = 0; - if (player != null) { - long currentWindowTimeBarOffsetUs = 0; - long durationUs = 0; - int adGroupCount = 0; - Timeline timeline = player.getCurrentTimeline(); - if (!timeline.isEmpty()) { - int currentWindowIndex = player.getCurrentWindowIndex(); - int firstWindowIndex = multiWindowTimeBar ? 0 : currentWindowIndex; - int lastWindowIndex = - multiWindowTimeBar ? timeline.getWindowCount() - 1 : currentWindowIndex; - for (int i = firstWindowIndex; i <= lastWindowIndex; i++) { - if (i == currentWindowIndex) { - currentWindowTimeBarOffsetUs = durationUs; - } - timeline.getWindow(i, window); - if (window.durationUs == C.TIME_UNSET) { - Assertions.checkState(!multiWindowTimeBar); - break; - } - for (int j = window.firstPeriodIndex; j <= window.lastPeriodIndex; j++) { - timeline.getPeriod(j, period); - int periodAdGroupCount = period.getAdGroupCount(); - for (int adGroupIndex = 0; adGroupIndex < periodAdGroupCount; adGroupIndex++) { - long adGroupTimeInPeriodUs = period.getAdGroupTimeUs(adGroupIndex); - if (adGroupTimeInPeriodUs == C.TIME_END_OF_SOURCE) { - if (period.durationUs == C.TIME_UNSET) { - // Don't show ad markers for postrolls in periods with unknown duration. - continue; - } - adGroupTimeInPeriodUs = period.durationUs; - } - long adGroupTimeInWindowUs = adGroupTimeInPeriodUs + period.getPositionInWindowUs(); - if (adGroupTimeInWindowUs >= 0 && adGroupTimeInWindowUs <= window.durationUs) { - if (adGroupCount == adGroupTimesMs.length) { - int newLength = adGroupTimesMs.length == 0 ? 1 : adGroupTimesMs.length * 2; - adGroupTimesMs = Arrays.copyOf(adGroupTimesMs, newLength); - playedAdGroups = Arrays.copyOf(playedAdGroups, newLength); - } - adGroupTimesMs[adGroupCount] = C.usToMs(durationUs + adGroupTimeInWindowUs); - playedAdGroups[adGroupCount] = period.hasPlayedAdGroup(adGroupIndex); - adGroupCount++; - } - } - } - durationUs += window.durationUs; - } - } - duration = C.usToMs(durationUs); - position = C.usToMs(currentWindowTimeBarOffsetUs); - bufferedPosition = position; - if (player.isPlayingAd()) { - position += player.getContentPosition(); - bufferedPosition = position; - } else { - position += player.getCurrentPosition(); - bufferedPosition += player.getBufferedPosition(); - } - if (timeBar != null) { - int extraAdGroupCount = extraAdGroupTimesMs.length; - int totalAdGroupCount = adGroupCount + extraAdGroupCount; - if (totalAdGroupCount > adGroupTimesMs.length) { - adGroupTimesMs = Arrays.copyOf(adGroupTimesMs, totalAdGroupCount); - playedAdGroups = Arrays.copyOf(playedAdGroups, totalAdGroupCount); - } - System.arraycopy(extraAdGroupTimesMs, 0, adGroupTimesMs, adGroupCount, extraAdGroupCount); - System.arraycopy(extraPlayedAdGroups, 0, playedAdGroups, adGroupCount, extraAdGroupCount); - timeBar.setAdGroupTimesMs(adGroupTimesMs, playedAdGroups, totalAdGroupCount); - } - } - 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); - } - - // Cancel any pending updates and schedule a new one if necessary. - removeCallbacks(updateProgressAction); - int playbackState = player == null ? Player.STATE_IDLE : player.getPlaybackState(); - if (playbackState != Player.STATE_IDLE && playbackState != Player.STATE_ENDED) { - long delayMs; - if (player.getPlayWhenReady() && playbackState == Player.STATE_READY) { - float playbackSpeed = player.getPlaybackParameters().speed; - if (playbackSpeed <= 0.1f) { - delayMs = 1000; - } else if (playbackSpeed <= 5f) { - long mediaTimeUpdatePeriodMs = 1000 / Math.max(1, Math.round(1 / playbackSpeed)); - long mediaTimeDelayMs = mediaTimeUpdatePeriodMs - (position % mediaTimeUpdatePeriodMs); - if (mediaTimeDelayMs < (mediaTimeUpdatePeriodMs / 5)) { - mediaTimeDelayMs += mediaTimeUpdatePeriodMs; - } - delayMs = - playbackSpeed == 1 ? mediaTimeDelayMs : (long) (mediaTimeDelayMs / playbackSpeed); - } else { - delayMs = 200; - } - } else { - delayMs = 1000; - } - postDelayed(updateProgressAction, delayMs); - } - } - - private void requestPlayPauseFocus() { - boolean playing = player != null && player.getPlayWhenReady(); - if (!playing && playButton != null) { - playButton.requestFocus(); - } else if (playing && pauseButton != null) { - pauseButton.requestFocus(); - } - } - - private void setButtonEnabled(boolean enabled, View view) { - if (view == null) { - return; - } - view.setEnabled(enabled); - view.setAlpha(enabled ? 1f : 0.3f); - view.setVisibility(VISIBLE); - } - - private void previous() { - Timeline timeline = player.getCurrentTimeline(); - if (timeline.isEmpty()) { - return; - } - int windowIndex = player.getCurrentWindowIndex(); - timeline.getWindow(windowIndex, window); - int previousWindowIndex = player.getPreviousWindowIndex(); - if (previousWindowIndex != C.INDEX_UNSET - && (player.getCurrentPosition() <= MAX_POSITION_FOR_SEEK_TO_PREVIOUS - || (window.isDynamic && !window.isSeekable))) { - seekTo(previousWindowIndex, C.TIME_UNSET); - } else { - seekTo(0); - } - } - - private void next() { - Timeline timeline = player.getCurrentTimeline(); - if (timeline.isEmpty()) { - return; - } - int windowIndex = player.getCurrentWindowIndex(); - int nextWindowIndex = player.getNextWindowIndex(); - if (nextWindowIndex != C.INDEX_UNSET) { - seekTo(nextWindowIndex, C.TIME_UNSET); - } else if (timeline.getWindow(windowIndex, window, false).isDynamic) { - seekTo(windowIndex, C.TIME_UNSET); - } - } - - private void rewind() { - if (rewindMs <= 0) { - return; - } - seekTo(Math.max(player.getCurrentPosition() - rewindMs, 0)); - } - - private void fastForward() { - if (fastForwardMs <= 0) { - return; - } - long durationMs = player.getDuration(); - long seekPositionMs = player.getCurrentPosition() + fastForwardMs; - if (durationMs != C.TIME_UNSET) { - seekPositionMs = Math.min(seekPositionMs, durationMs); - } - seekTo(seekPositionMs); - } - - private void seekTo(long positionMs) { - seekTo(player.getCurrentWindowIndex(), positionMs); - } - - private void seekTo(int windowIndex, long 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. - updateProgress(); - } - } - - private void seekToTimeBarPosition(long positionMs) { - int windowIndex; - Timeline timeline = player.getCurrentTimeline(); - if (multiWindowTimeBar && !timeline.isEmpty()) { - int windowCount = timeline.getWindowCount(); - windowIndex = 0; - while (true) { - long windowDurationMs = timeline.getWindow(windowIndex, window).getDurationMs(); - if (positionMs < windowDurationMs) { - break; - } else if (windowIndex == windowCount - 1) { - // Seeking past the end of the last window should seek to the end of the timeline. - positionMs = windowDurationMs; - break; - } - positionMs -= windowDurationMs; - windowIndex++; - } - } else { - windowIndex = player.getCurrentWindowIndex(); - } - seekTo(windowIndex, positionMs); - } - - @Override - public void onAttachedToWindow() { - super.onAttachedToWindow(); - isAttachedToWindow = true; - if (hideAtMs != C.TIME_UNSET) { - long delayMs = hideAtMs - SystemClock.uptimeMillis(); - if (delayMs <= 0) { - hide(); - } else { - postDelayed(hideAction, delayMs); - } - } - updateAll(); - } - - @Override - public void onDetachedFromWindow() { - super.onDetachedFromWindow(); - isAttachedToWindow = false; - removeCallbacks(updateProgressAction); - removeCallbacks(hideAction); - } - - @Override - public boolean dispatchKeyEvent(KeyEvent event) { - return dispatchMediaKeyEvent(event) || super.dispatchKeyEvent(event); - } - - /** - * Called to process media key events. Any {@link KeyEvent} can be passed but only media key - * events will be handled. - * - * @param event A key event. - * @return Whether the key event was handled. - */ - public boolean dispatchMediaKeyEvent(KeyEvent event) { - int keyCode = event.getKeyCode(); - if (player == null || !isHandledMediaKey(keyCode)) { - return false; - } - if (event.getAction() == KeyEvent.ACTION_DOWN) { - if (keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD) { - fastForward(); - } else if (keyCode == KeyEvent.KEYCODE_MEDIA_REWIND) { - rewind(); - } else if (event.getRepeatCount() == 0) { - switch (keyCode) { - case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: - controlDispatcher.dispatchSetPlayWhenReady(player, !player.getPlayWhenReady()); - break; - case KeyEvent.KEYCODE_MEDIA_PLAY: - controlDispatcher.dispatchSetPlayWhenReady(player, true); - break; - case KeyEvent.KEYCODE_MEDIA_PAUSE: - controlDispatcher.dispatchSetPlayWhenReady(player, false); - break; - case KeyEvent.KEYCODE_MEDIA_NEXT: - next(); - break; - case KeyEvent.KEYCODE_MEDIA_PREVIOUS: - previous(); - break; - default: - break; - } - } - } - return true; - } - - @SuppressLint("InlinedApi") - private static boolean isHandledMediaKey(int keyCode) { - return keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD - || keyCode == KeyEvent.KEYCODE_MEDIA_REWIND - || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE - || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY - || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE - || keyCode == KeyEvent.KEYCODE_MEDIA_NEXT - || keyCode == KeyEvent.KEYCODE_MEDIA_PREVIOUS; - } - - /** - * Returns whether the specified {@code timeline} can be shown on a multi-window time bar. - * - * @param timeline The {@link Timeline} to check. - * @param window A scratch {@link Timeline.Window} instance. - * @return Whether the specified timeline can be shown on a multi-window time bar. - */ - private static boolean canShowMultiWindowTimeBar(Timeline timeline, Timeline.Window window) { - if (timeline.getWindowCount() > MAX_WINDOWS_FOR_MULTI_WINDOW_TIME_BAR) { - return false; - } - int windowCount = timeline.getWindowCount(); - for (int i = 0; i < windowCount; i++) { - if (timeline.getWindow(i, window).durationUs == C.TIME_UNSET) { - return false; - } - } - return true; - } - - private final class ComponentListener extends Player.DefaultEventListener - implements TimeBar.OnScrubListener, OnClickListener { - - @Override - public void onScrubStart(TimeBar timeBar, long position) { - removeCallbacks(hideAction); - scrubbing = true; - } - - @Override - public void onScrubMove(TimeBar timeBar, long position) { - if (positionView != null) { - positionView.setText(Util.getStringForTime(formatBuilder, formatter, position)); - } - } - - @Override - public void onScrubStop(TimeBar timeBar, long position, boolean canceled) { - scrubbing = false; - if (!canceled && player != null) { - seekToTimeBarPosition(position); - } - hideAfterTimeout(); - } - - @Override - public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { - updatePlayPauseButton(); - updateProgress(); - } - - @Override - public void onRepeatModeChanged(int repeatMode) { - updateRepeatModeButton(); - updateNavigation(); - } - - @Override - public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { - updateShuffleButton(); - updateNavigation(); - } - - @Override - public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { - updateNavigation(); - updateProgress(); - } - - @Override - public void onTimelineChanged( - Timeline timeline, Object manifest, @Player.TimelineChangeReason int reason) { - updateNavigation(); - updateTimeBarMode(); - updateProgress(); - } - - @Override - public void onClick(View view) { - if (player != null) { - if (nextButton == view) { - next(); - } else if (previousButton == view) { - previous(); - } else if (fastForwardButton == view) { - fastForward(); - } else if (rewindButton == view) { - rewind(); - } else if (playButton == view) { - controlDispatcher.dispatchSetPlayWhenReady(player, true); - } else if (pauseButton == view) { - controlDispatcher.dispatchSetPlayWhenReady(player, false); - } else if (repeatToggleButton == view) { - controlDispatcher.dispatchSetRepeatMode( - player, RepeatModeUtil.getNextRepeatMode(player.getRepeatMode(), repeatToggleModes)); - } else if (shuffleButton == view) { - controlDispatcher.dispatchSetShuffleModeEnabled(player, !player.getShuffleModeEnabled()); - } - } - hideAfterTimeout(); - } - } } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java new file mode 100644 index 0000000000..20c3ef02dc --- /dev/null +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java @@ -0,0 +1,1101 @@ +/* + * 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.ui; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.os.SystemClock; +import android.support.annotation.Nullable; +import android.util.AttributeSet; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.TextView; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlayerLibraryInfo; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.RepeatModeUtil; +import com.google.android.exoplayer2.util.Util; +import java.util.Arrays; +import java.util.Formatter; +import java.util.Locale; + +/** + * A view for controlling {@link Player} instances. + * + *

          A PlayerControlView can be customized by setting attributes (or calling corresponding + * methods), overriding the view's layout file or by specifying a custom view layout file, as + * outlined below. + * + *

          Attributes

          + * + * The following attributes can be set on a PlayerControlView when used in a layout XML file: + * + *
            + *
          • {@code show_timeout} - The time between the last user interaction and the controls + * being automatically hidden, in milliseconds. Use zero if the controls should not + * automatically timeout. + *
              + *
            • Corresponding method: {@link #setShowTimeoutMs(int)} + *
            • Default: {@link #DEFAULT_SHOW_TIMEOUT_MS} + *
            + *
          • {@code rewind_increment} - The duration of the rewind applied when the user taps the + * rewind button, in milliseconds. Use zero to disable the rewind button. + *
              + *
            • Corresponding method: {@link #setRewindIncrementMs(int)} + *
            • Default: {@link #DEFAULT_REWIND_MS} + *
            + *
          • {@code fastforward_increment} - Like {@code rewind_increment}, but for fast forward. + *
              + *
            • Corresponding method: {@link #setFastForwardIncrementMs(int)} + *
            • Default: {@link #DEFAULT_FAST_FORWARD_MS} + *
            + *
          • {@code repeat_toggle_modes} - A flagged enumeration value specifying which repeat + * mode toggle options are enabled. Valid values are: {@code none}, {@code one}, {@code all}, + * or {@code one|all}. + *
              + *
            • Corresponding method: {@link #setRepeatToggleModes(int)} + *
            • Default: {@link PlayerControlView#DEFAULT_REPEAT_TOGGLE_MODES} + *
            + *
          • {@code show_shuffle_button} - Whether the shuffle button is shown. + *
              + *
            • Corresponding method: {@link #setShowShuffleButton(boolean)} + *
            • Default: false + *
            + *
          • {@code controller_layout_id} - Specifies the id of the layout to be inflated. See + * below for more details. + *
              + *
            • Corresponding method: None + *
            • Default: {@code R.id.exo_player_control_view} + *
            + *
          + * + *

          Overriding the layout file

          + * + * To customize the layout of PlayerControlView throughout your app, or just for certain + * configurations, you can define {@code exo_player_control_view.xml} layout files in your + * application {@code res/layout*} directories. These layouts will override the one provided by the + * ExoPlayer library, and will be inflated for use by PlayerControlView. The view identifies and + * binds its children by looking for the following ids: + * + *

          + * + *

            + *
          • {@code exo_play} - The play button. + *
              + *
            • Type: {@link View} + *
            + *
          • {@code exo_pause} - The pause button. + *
              + *
            • Type: {@link View} + *
            + *
          • {@code exo_ffwd} - The fast forward button. + *
              + *
            • Type: {@link View} + *
            + *
          • {@code exo_rew} - The rewind button. + *
              + *
            • Type: {@link View} + *
            + *
          • {@code exo_prev} - The previous track button. + *
              + *
            • Type: {@link View} + *
            + *
          • {@code exo_next} - The next track button. + *
              + *
            • Type: {@link View} + *
            + *
          • {@code exo_repeat_toggle} - The repeat toggle button. + *
              + *
            • Type: {@link View} + *
            + *
          • {@code exo_shuffle} - The shuffle button. + *
              + *
            • Type: {@link View} + *
            + *
          • {@code exo_position} - Text view displaying the current playback position. + *
              + *
            • Type: {@link TextView} + *
            + *
          • {@code exo_duration} - Text view displaying the current media duration. + *
              + *
            • Type: {@link TextView} + *
            + *
          • {@code exo_progress} - Time bar that's updated during playback and allows seeking. + *
              + *
            • Type: {@link TimeBar} + *
            + *
          + * + *

          All child views are optional and so can be omitted if not required, however where defined they + * must be of the expected type. + * + *

          Specifying a custom layout file

          + * + * Defining your own {@code exo_player_control_view.xml} is useful to customize the layout of + * PlayerControlView throughout your application. It's also possible to customize the layout for a + * single instance in a layout file. This is achieved by setting the {@code controller_layout_id} + * attribute on a PlayerControlView. This will cause the specified layout to be inflated instead of + * {@code exo_player_control_view.xml} for only the instance on which the attribute is set. + */ +public class PlayerControlView extends FrameLayout { + + static { + ExoPlayerLibraryInfo.registerModule("goog.exo.ui"); + } + + /** Listener to be notified about changes of the visibility of the UI control. */ + public interface VisibilityListener { + + /** + * Called when the visibility changes. + * + * @param visibility The new visibility. Either {@link View#VISIBLE} or {@link View#GONE}. + */ + void onVisibilityChange(int visibility); + } + + /** The default fast forward increment, in milliseconds. */ + public static final int DEFAULT_FAST_FORWARD_MS = 15000; + /** The default rewind increment, in milliseconds. */ + public static final int DEFAULT_REWIND_MS = 5000; + /** The default show timeout, in milliseconds. */ + public static final int DEFAULT_SHOW_TIMEOUT_MS = 5000; + /** The default repeat toggle modes. */ + public static final @RepeatModeUtil.RepeatToggleModes int DEFAULT_REPEAT_TOGGLE_MODES = + RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE; + + /** 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; + private final View previousButton; + private final View nextButton; + private final View playButton; + private final View pauseButton; + private final View fastForwardButton; + private final View rewindButton; + private final ImageView repeatToggleButton; + private final View shuffleButton; + private final TextView durationView; + private final TextView positionView; + private final TimeBar timeBar; + private final StringBuilder formatBuilder; + private final Formatter formatter; + private final Timeline.Period period; + private final Timeline.Window window; + + private final Drawable repeatOffButtonDrawable; + private final Drawable repeatOneButtonDrawable; + private final Drawable repeatAllButtonDrawable; + private final String repeatOffButtonContentDescription; + private final String repeatOneButtonContentDescription; + private final String repeatAllButtonContentDescription; + + private Player player; + private com.google.android.exoplayer2.ControlDispatcher controlDispatcher; + private VisibilityListener visibilityListener; + + private boolean isAttachedToWindow; + private boolean showMultiWindowTimeBar; + private boolean multiWindowTimeBar; + private boolean scrubbing; + private int rewindMs; + private int fastForwardMs; + private int showTimeoutMs; + private @RepeatModeUtil.RepeatToggleModes int repeatToggleModes; + private boolean showShuffleButton; + private long hideAtMs; + private long[] adGroupTimesMs; + private boolean[] playedAdGroups; + private long[] extraAdGroupTimesMs; + private boolean[] extraPlayedAdGroups; + + private final Runnable updateProgressAction = + new Runnable() { + @Override + public void run() { + updateProgress(); + } + }; + + private final Runnable hideAction = + new Runnable() { + @Override + public void run() { + hide(); + } + }; + + public PlayerControlView(Context context) { + this(context, null); + } + + public PlayerControlView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public PlayerControlView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, attrs); + } + + public PlayerControlView( + Context context, AttributeSet attrs, int defStyleAttr, AttributeSet playbackAttrs) { + super(context, attrs, defStyleAttr); + int controllerLayoutId = R.layout.exo_player_control_view; + rewindMs = DEFAULT_REWIND_MS; + fastForwardMs = DEFAULT_FAST_FORWARD_MS; + showTimeoutMs = DEFAULT_SHOW_TIMEOUT_MS; + repeatToggleModes = DEFAULT_REPEAT_TOGGLE_MODES; + showShuffleButton = false; + if (playbackAttrs != null) { + TypedArray a = + context + .getTheme() + .obtainStyledAttributes(playbackAttrs, R.styleable.PlayerControlView, 0, 0); + try { + rewindMs = a.getInt(R.styleable.PlayerControlView_rewind_increment, rewindMs); + fastForwardMs = + a.getInt(R.styleable.PlayerControlView_fastforward_increment, fastForwardMs); + showTimeoutMs = a.getInt(R.styleable.PlayerControlView_show_timeout, showTimeoutMs); + controllerLayoutId = + a.getResourceId(R.styleable.PlayerControlView_controller_layout_id, controllerLayoutId); + repeatToggleModes = getRepeatToggleModes(a, repeatToggleModes); + showShuffleButton = + a.getBoolean(R.styleable.PlayerControlView_show_shuffle_button, showShuffleButton); + } finally { + a.recycle(); + } + } + period = new Timeline.Period(); + window = new Timeline.Window(); + formatBuilder = new StringBuilder(); + formatter = new Formatter(formatBuilder, Locale.getDefault()); + adGroupTimesMs = new long[0]; + playedAdGroups = new boolean[0]; + extraAdGroupTimesMs = new long[0]; + extraPlayedAdGroups = new boolean[0]; + componentListener = new ComponentListener(); + controlDispatcher = new com.google.android.exoplayer2.DefaultControlDispatcher(); + + LayoutInflater.from(context).inflate(controllerLayoutId, this); + setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); + + durationView = findViewById(R.id.exo_duration); + positionView = findViewById(R.id.exo_position); + timeBar = findViewById(R.id.exo_progress); + if (timeBar != null) { + timeBar.addListener(componentListener); + } + playButton = findViewById(R.id.exo_play); + if (playButton != null) { + playButton.setOnClickListener(componentListener); + } + pauseButton = findViewById(R.id.exo_pause); + if (pauseButton != null) { + pauseButton.setOnClickListener(componentListener); + } + previousButton = findViewById(R.id.exo_prev); + if (previousButton != null) { + previousButton.setOnClickListener(componentListener); + } + nextButton = findViewById(R.id.exo_next); + if (nextButton != null) { + nextButton.setOnClickListener(componentListener); + } + rewindButton = findViewById(R.id.exo_rew); + if (rewindButton != null) { + rewindButton.setOnClickListener(componentListener); + } + fastForwardButton = findViewById(R.id.exo_ffwd); + if (fastForwardButton != null) { + fastForwardButton.setOnClickListener(componentListener); + } + repeatToggleButton = findViewById(R.id.exo_repeat_toggle); + if (repeatToggleButton != null) { + repeatToggleButton.setOnClickListener(componentListener); + } + shuffleButton = findViewById(R.id.exo_shuffle); + if (shuffleButton != null) { + shuffleButton.setOnClickListener(componentListener); + } + Resources resources = context.getResources(); + repeatOffButtonDrawable = resources.getDrawable(R.drawable.exo_controls_repeat_off); + repeatOneButtonDrawable = resources.getDrawable(R.drawable.exo_controls_repeat_one); + repeatAllButtonDrawable = resources.getDrawable(R.drawable.exo_controls_repeat_all); + repeatOffButtonContentDescription = + resources.getString(R.string.exo_controls_repeat_off_description); + repeatOneButtonContentDescription = + resources.getString(R.string.exo_controls_repeat_one_description); + repeatAllButtonContentDescription = + resources.getString(R.string.exo_controls_repeat_all_description); + } + + @SuppressWarnings("ResourceType") + private static @RepeatModeUtil.RepeatToggleModes int getRepeatToggleModes( + TypedArray a, @RepeatModeUtil.RepeatToggleModes int repeatToggleModes) { + return a.getInt(R.styleable.PlayerControlView_repeat_toggle_modes, repeatToggleModes); + } + + /** + * Returns the {@link Player} currently being controlled by this view, or null if no player is + * set. + */ + public Player getPlayer() { + return player; + } + + /** + * Sets the {@link Player} to control. + * + * @param player The {@link Player} to control. + */ + public void setPlayer(Player player) { + if (this.player == player) { + return; + } + if (this.player != null) { + this.player.removeListener(componentListener); + } + this.player = player; + if (player != null) { + player.addListener(componentListener); + } + 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 millisecond positions of extra ad markers relative to the start of the window (or + * timeline, if in multi-window mode) and whether each extra ad has been played or not. The + * markers are shown in addition to any ad markers for ads in the player's timeline. + * + * @param extraAdGroupTimesMs The millisecond timestamps of the extra ad markers to show, or + * {@code null} to show no extra ad markers. + * @param extraPlayedAdGroups Whether each ad has been played, or {@code null} to show no extra ad + * markers. + */ + public void setExtraAdGroupMarkers( + @Nullable long[] extraAdGroupTimesMs, @Nullable boolean[] extraPlayedAdGroups) { + if (extraAdGroupTimesMs == null) { + this.extraAdGroupTimesMs = new long[0]; + this.extraPlayedAdGroups = new boolean[0]; + } else { + Assertions.checkArgument(extraAdGroupTimesMs.length == extraPlayedAdGroups.length); + this.extraAdGroupTimesMs = extraAdGroupTimesMs; + this.extraPlayedAdGroups = extraPlayedAdGroups; + } + updateProgress(); + } + + /** + * Sets the {@link VisibilityListener}. + * + * @param listener The listener to be notified about visibility changes. + */ + public void setVisibilityListener(VisibilityListener listener) { + this.visibilityListener = listener; + } + + /** + * Sets the {@link com.google.android.exoplayer2.ControlDispatcher}. + * + * @param controlDispatcher The {@link com.google.android.exoplayer2.ControlDispatcher}, or null + * to use {@link com.google.android.exoplayer2.DefaultControlDispatcher}. + */ + public void setControlDispatcher( + @Nullable com.google.android.exoplayer2.ControlDispatcher controlDispatcher) { + this.controlDispatcher = + controlDispatcher == null + ? new com.google.android.exoplayer2.DefaultControlDispatcher() + : controlDispatcher; + } + + /** + * Sets the rewind increment in milliseconds. + * + * @param rewindMs The rewind increment in milliseconds. A non-positive value will cause the + * rewind button to be disabled. + */ + public void setRewindIncrementMs(int rewindMs) { + this.rewindMs = rewindMs; + updateNavigation(); + } + + /** + * Sets the fast forward increment in milliseconds. + * + * @param fastForwardMs The fast forward increment in milliseconds. A non-positive value will + * cause the fast forward button to be disabled. + */ + public void setFastForwardIncrementMs(int fastForwardMs) { + this.fastForwardMs = fastForwardMs; + updateNavigation(); + } + + /** + * Returns the playback controls timeout. The playback controls are automatically hidden after + * this duration of time has elapsed without user input. + * + * @return The duration in milliseconds. A non-positive value indicates that the controls will + * remain visible indefinitely. + */ + public int getShowTimeoutMs() { + return showTimeoutMs; + } + + /** + * Sets the playback controls timeout. The playback controls are automatically hidden after this + * duration of time has elapsed without user input. + * + * @param showTimeoutMs The duration in milliseconds. A non-positive value will cause the controls + * to remain visible indefinitely. + */ + public void setShowTimeoutMs(int showTimeoutMs) { + this.showTimeoutMs = showTimeoutMs; + if (isVisible()) { + // Reset the timeout. + hideAfterTimeout(); + } + } + + /** + * Returns which repeat toggle modes are enabled. + * + * @return The currently enabled {@link RepeatModeUtil.RepeatToggleModes}. + */ + public @RepeatModeUtil.RepeatToggleModes int getRepeatToggleModes() { + return repeatToggleModes; + } + + /** + * Sets which repeat toggle modes are enabled. + * + * @param repeatToggleModes A set of {@link RepeatModeUtil.RepeatToggleModes}. + */ + public void setRepeatToggleModes(@RepeatModeUtil.RepeatToggleModes int repeatToggleModes) { + this.repeatToggleModes = repeatToggleModes; + if (player != null) { + @Player.RepeatMode int currentMode = player.getRepeatMode(); + if (repeatToggleModes == RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE + && currentMode != Player.REPEAT_MODE_OFF) { + controlDispatcher.dispatchSetRepeatMode(player, Player.REPEAT_MODE_OFF); + } else if (repeatToggleModes == RepeatModeUtil.REPEAT_TOGGLE_MODE_ONE + && currentMode == Player.REPEAT_MODE_ALL) { + controlDispatcher.dispatchSetRepeatMode(player, Player.REPEAT_MODE_ONE); + } else if (repeatToggleModes == RepeatModeUtil.REPEAT_TOGGLE_MODE_ALL + && currentMode == Player.REPEAT_MODE_ONE) { + controlDispatcher.dispatchSetRepeatMode(player, Player.REPEAT_MODE_ALL); + } + } + } + + /** Returns whether the shuffle button is shown. */ + public boolean getShowShuffleButton() { + return showShuffleButton; + } + + /** + * Sets whether the shuffle button is shown. + * + * @param showShuffleButton Whether the shuffle button is shown. + */ + public void setShowShuffleButton(boolean showShuffleButton) { + this.showShuffleButton = showShuffleButton; + updateShuffleButton(); + } + + /** + * Shows the playback controls. If {@link #getShowTimeoutMs()} is positive then the controls will + * be automatically hidden after this duration of time has elapsed without user input. + */ + public void show() { + if (!isVisible()) { + setVisibility(VISIBLE); + if (visibilityListener != null) { + visibilityListener.onVisibilityChange(getVisibility()); + } + updateAll(); + requestPlayPauseFocus(); + } + // Call hideAfterTimeout even if already visible to reset the timeout. + hideAfterTimeout(); + } + + /** Hides the controller. */ + public void hide() { + if (isVisible()) { + setVisibility(GONE); + if (visibilityListener != null) { + visibilityListener.onVisibilityChange(getVisibility()); + } + removeCallbacks(updateProgressAction); + removeCallbacks(hideAction); + hideAtMs = C.TIME_UNSET; + } + } + + /** Returns whether the controller is currently visible. */ + public boolean isVisible() { + return getVisibility() == VISIBLE; + } + + private void hideAfterTimeout() { + removeCallbacks(hideAction); + if (showTimeoutMs > 0) { + hideAtMs = SystemClock.uptimeMillis() + showTimeoutMs; + if (isAttachedToWindow) { + postDelayed(hideAction, showTimeoutMs); + } + } else { + hideAtMs = C.TIME_UNSET; + } + } + + private void updateAll() { + updatePlayPauseButton(); + updateNavigation(); + updateRepeatModeButton(); + updateShuffleButton(); + updateProgress(); + } + + private void updatePlayPauseButton() { + if (!isVisible() || !isAttachedToWindow) { + return; + } + boolean requestPlayPauseFocus = false; + boolean playing = player != null && player.getPlayWhenReady(); + if (playButton != null) { + requestPlayPauseFocus |= playing && playButton.isFocused(); + playButton.setVisibility(playing ? View.GONE : View.VISIBLE); + } + if (pauseButton != null) { + requestPlayPauseFocus |= !playing && pauseButton.isFocused(); + pauseButton.setVisibility(!playing ? View.GONE : View.VISIBLE); + } + if (requestPlayPauseFocus) { + requestPlayPauseFocus(); + } + } + + private void updateNavigation() { + if (!isVisible() || !isAttachedToWindow) { + return; + } + Timeline timeline = player != null ? player.getCurrentTimeline() : null; + boolean haveNonEmptyTimeline = timeline != null && !timeline.isEmpty(); + boolean isSeekable = false; + boolean enablePrevious = false; + boolean enableNext = false; + if (haveNonEmptyTimeline && !player.isPlayingAd()) { + int windowIndex = player.getCurrentWindowIndex(); + timeline.getWindow(windowIndex, window); + isSeekable = window.isSeekable; + enablePrevious = + isSeekable || !window.isDynamic || player.getPreviousWindowIndex() != C.INDEX_UNSET; + enableNext = window.isDynamic || player.getNextWindowIndex() != C.INDEX_UNSET; + } + setButtonEnabled(enablePrevious, previousButton); + setButtonEnabled(enableNext, nextButton); + setButtonEnabled(fastForwardMs > 0 && isSeekable, fastForwardButton); + setButtonEnabled(rewindMs > 0 && isSeekable, rewindButton); + if (timeBar != null) { + timeBar.setEnabled(isSeekable); + } + } + + private void updateRepeatModeButton() { + if (!isVisible() || !isAttachedToWindow || repeatToggleButton == null) { + return; + } + if (repeatToggleModes == RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE) { + repeatToggleButton.setVisibility(View.GONE); + return; + } + if (player == null) { + setButtonEnabled(false, repeatToggleButton); + return; + } + setButtonEnabled(true, repeatToggleButton); + switch (player.getRepeatMode()) { + case Player.REPEAT_MODE_OFF: + repeatToggleButton.setImageDrawable(repeatOffButtonDrawable); + repeatToggleButton.setContentDescription(repeatOffButtonContentDescription); + break; + case Player.REPEAT_MODE_ONE: + repeatToggleButton.setImageDrawable(repeatOneButtonDrawable); + repeatToggleButton.setContentDescription(repeatOneButtonContentDescription); + break; + case Player.REPEAT_MODE_ALL: + repeatToggleButton.setImageDrawable(repeatAllButtonDrawable); + repeatToggleButton.setContentDescription(repeatAllButtonContentDescription); + break; + default: + // Never happens. + } + repeatToggleButton.setVisibility(View.VISIBLE); + } + + private void updateShuffleButton() { + if (!isVisible() || !isAttachedToWindow || shuffleButton == null) { + return; + } + if (!showShuffleButton) { + shuffleButton.setVisibility(View.GONE); + } else if (player == null) { + setButtonEnabled(false, shuffleButton); + } else { + shuffleButton.setAlpha(player.getShuffleModeEnabled() ? 1f : 0.3f); + shuffleButton.setEnabled(true); + shuffleButton.setVisibility(View.VISIBLE); + } + } + + private void updateTimeBarMode() { + if (player == null) { + return; + } + multiWindowTimeBar = + showMultiWindowTimeBar && canShowMultiWindowTimeBar(player.getCurrentTimeline(), window); + } + + private void updateProgress() { + if (!isVisible() || !isAttachedToWindow) { + return; + } + + long position = 0; + long bufferedPosition = 0; + long duration = 0; + if (player != null) { + long currentWindowTimeBarOffsetUs = 0; + long durationUs = 0; + int adGroupCount = 0; + Timeline timeline = player.getCurrentTimeline(); + if (!timeline.isEmpty()) { + int currentWindowIndex = player.getCurrentWindowIndex(); + int firstWindowIndex = multiWindowTimeBar ? 0 : currentWindowIndex; + int lastWindowIndex = + multiWindowTimeBar ? timeline.getWindowCount() - 1 : currentWindowIndex; + for (int i = firstWindowIndex; i <= lastWindowIndex; i++) { + if (i == currentWindowIndex) { + currentWindowTimeBarOffsetUs = durationUs; + } + timeline.getWindow(i, window); + if (window.durationUs == C.TIME_UNSET) { + Assertions.checkState(!multiWindowTimeBar); + break; + } + for (int j = window.firstPeriodIndex; j <= window.lastPeriodIndex; j++) { + timeline.getPeriod(j, period); + int periodAdGroupCount = period.getAdGroupCount(); + for (int adGroupIndex = 0; adGroupIndex < periodAdGroupCount; adGroupIndex++) { + long adGroupTimeInPeriodUs = period.getAdGroupTimeUs(adGroupIndex); + if (adGroupTimeInPeriodUs == C.TIME_END_OF_SOURCE) { + if (period.durationUs == C.TIME_UNSET) { + // Don't show ad markers for postrolls in periods with unknown duration. + continue; + } + adGroupTimeInPeriodUs = period.durationUs; + } + long adGroupTimeInWindowUs = adGroupTimeInPeriodUs + period.getPositionInWindowUs(); + if (adGroupTimeInWindowUs >= 0 && adGroupTimeInWindowUs <= window.durationUs) { + if (adGroupCount == adGroupTimesMs.length) { + int newLength = adGroupTimesMs.length == 0 ? 1 : adGroupTimesMs.length * 2; + adGroupTimesMs = Arrays.copyOf(adGroupTimesMs, newLength); + playedAdGroups = Arrays.copyOf(playedAdGroups, newLength); + } + adGroupTimesMs[adGroupCount] = C.usToMs(durationUs + adGroupTimeInWindowUs); + playedAdGroups[adGroupCount] = period.hasPlayedAdGroup(adGroupIndex); + adGroupCount++; + } + } + } + durationUs += window.durationUs; + } + } + duration = C.usToMs(durationUs); + position = C.usToMs(currentWindowTimeBarOffsetUs); + bufferedPosition = position; + if (player.isPlayingAd()) { + position += player.getContentPosition(); + bufferedPosition = position; + } else { + position += player.getCurrentPosition(); + bufferedPosition += player.getBufferedPosition(); + } + if (timeBar != null) { + int extraAdGroupCount = extraAdGroupTimesMs.length; + int totalAdGroupCount = adGroupCount + extraAdGroupCount; + if (totalAdGroupCount > adGroupTimesMs.length) { + adGroupTimesMs = Arrays.copyOf(adGroupTimesMs, totalAdGroupCount); + playedAdGroups = Arrays.copyOf(playedAdGroups, totalAdGroupCount); + } + System.arraycopy(extraAdGroupTimesMs, 0, adGroupTimesMs, adGroupCount, extraAdGroupCount); + System.arraycopy(extraPlayedAdGroups, 0, playedAdGroups, adGroupCount, extraAdGroupCount); + timeBar.setAdGroupTimesMs(adGroupTimesMs, playedAdGroups, totalAdGroupCount); + } + } + 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); + } + + // Cancel any pending updates and schedule a new one if necessary. + removeCallbacks(updateProgressAction); + int playbackState = player == null ? Player.STATE_IDLE : player.getPlaybackState(); + if (playbackState != Player.STATE_IDLE && playbackState != Player.STATE_ENDED) { + long delayMs; + if (player.getPlayWhenReady() && playbackState == Player.STATE_READY) { + float playbackSpeed = player.getPlaybackParameters().speed; + if (playbackSpeed <= 0.1f) { + delayMs = 1000; + } else if (playbackSpeed <= 5f) { + long mediaTimeUpdatePeriodMs = 1000 / Math.max(1, Math.round(1 / playbackSpeed)); + long mediaTimeDelayMs = mediaTimeUpdatePeriodMs - (position % mediaTimeUpdatePeriodMs); + if (mediaTimeDelayMs < (mediaTimeUpdatePeriodMs / 5)) { + mediaTimeDelayMs += mediaTimeUpdatePeriodMs; + } + delayMs = + playbackSpeed == 1 ? mediaTimeDelayMs : (long) (mediaTimeDelayMs / playbackSpeed); + } else { + delayMs = 200; + } + } else { + delayMs = 1000; + } + postDelayed(updateProgressAction, delayMs); + } + } + + private void requestPlayPauseFocus() { + boolean playing = player != null && player.getPlayWhenReady(); + if (!playing && playButton != null) { + playButton.requestFocus(); + } else if (playing && pauseButton != null) { + pauseButton.requestFocus(); + } + } + + private void setButtonEnabled(boolean enabled, View view) { + if (view == null) { + return; + } + view.setEnabled(enabled); + view.setAlpha(enabled ? 1f : 0.3f); + view.setVisibility(VISIBLE); + } + + private void previous() { + Timeline timeline = player.getCurrentTimeline(); + if (timeline.isEmpty()) { + return; + } + int windowIndex = player.getCurrentWindowIndex(); + timeline.getWindow(windowIndex, window); + int previousWindowIndex = player.getPreviousWindowIndex(); + if (previousWindowIndex != C.INDEX_UNSET + && (player.getCurrentPosition() <= MAX_POSITION_FOR_SEEK_TO_PREVIOUS + || (window.isDynamic && !window.isSeekable))) { + seekTo(previousWindowIndex, C.TIME_UNSET); + } else { + seekTo(0); + } + } + + private void next() { + Timeline timeline = player.getCurrentTimeline(); + if (timeline.isEmpty()) { + return; + } + int windowIndex = player.getCurrentWindowIndex(); + int nextWindowIndex = player.getNextWindowIndex(); + if (nextWindowIndex != C.INDEX_UNSET) { + seekTo(nextWindowIndex, C.TIME_UNSET); + } else if (timeline.getWindow(windowIndex, window, false).isDynamic) { + seekTo(windowIndex, C.TIME_UNSET); + } + } + + private void rewind() { + if (rewindMs <= 0) { + return; + } + seekTo(Math.max(player.getCurrentPosition() - rewindMs, 0)); + } + + private void fastForward() { + if (fastForwardMs <= 0) { + return; + } + long durationMs = player.getDuration(); + long seekPositionMs = player.getCurrentPosition() + fastForwardMs; + if (durationMs != C.TIME_UNSET) { + seekPositionMs = Math.min(seekPositionMs, durationMs); + } + seekTo(seekPositionMs); + } + + private void seekTo(long positionMs) { + seekTo(player.getCurrentWindowIndex(), positionMs); + } + + private void seekTo(int windowIndex, long 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. + updateProgress(); + } + } + + private void seekToTimeBarPosition(long positionMs) { + int windowIndex; + Timeline timeline = player.getCurrentTimeline(); + if (multiWindowTimeBar && !timeline.isEmpty()) { + int windowCount = timeline.getWindowCount(); + windowIndex = 0; + while (true) { + long windowDurationMs = timeline.getWindow(windowIndex, window).getDurationMs(); + if (positionMs < windowDurationMs) { + break; + } else if (windowIndex == windowCount - 1) { + // Seeking past the end of the last window should seek to the end of the timeline. + positionMs = windowDurationMs; + break; + } + positionMs -= windowDurationMs; + windowIndex++; + } + } else { + windowIndex = player.getCurrentWindowIndex(); + } + seekTo(windowIndex, positionMs); + } + + @Override + public void onAttachedToWindow() { + super.onAttachedToWindow(); + isAttachedToWindow = true; + if (hideAtMs != C.TIME_UNSET) { + long delayMs = hideAtMs - SystemClock.uptimeMillis(); + if (delayMs <= 0) { + hide(); + } else { + postDelayed(hideAction, delayMs); + } + } + updateAll(); + } + + @Override + public void onDetachedFromWindow() { + super.onDetachedFromWindow(); + isAttachedToWindow = false; + removeCallbacks(updateProgressAction); + removeCallbacks(hideAction); + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + return dispatchMediaKeyEvent(event) || super.dispatchKeyEvent(event); + } + + /** + * Called to process media key events. Any {@link KeyEvent} can be passed but only media key + * events will be handled. + * + * @param event A key event. + * @return Whether the key event was handled. + */ + public boolean dispatchMediaKeyEvent(KeyEvent event) { + int keyCode = event.getKeyCode(); + if (player == null || !isHandledMediaKey(keyCode)) { + return false; + } + if (event.getAction() == KeyEvent.ACTION_DOWN) { + if (keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD) { + fastForward(); + } else if (keyCode == KeyEvent.KEYCODE_MEDIA_REWIND) { + rewind(); + } else if (event.getRepeatCount() == 0) { + switch (keyCode) { + case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: + controlDispatcher.dispatchSetPlayWhenReady(player, !player.getPlayWhenReady()); + break; + case KeyEvent.KEYCODE_MEDIA_PLAY: + controlDispatcher.dispatchSetPlayWhenReady(player, true); + break; + case KeyEvent.KEYCODE_MEDIA_PAUSE: + controlDispatcher.dispatchSetPlayWhenReady(player, false); + break; + case KeyEvent.KEYCODE_MEDIA_NEXT: + next(); + break; + case KeyEvent.KEYCODE_MEDIA_PREVIOUS: + previous(); + break; + default: + break; + } + } + } + return true; + } + + @SuppressLint("InlinedApi") + private static boolean isHandledMediaKey(int keyCode) { + return keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD + || keyCode == KeyEvent.KEYCODE_MEDIA_REWIND + || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE + || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY + || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE + || keyCode == KeyEvent.KEYCODE_MEDIA_NEXT + || keyCode == KeyEvent.KEYCODE_MEDIA_PREVIOUS; + } + + /** + * Returns whether the specified {@code timeline} can be shown on a multi-window time bar. + * + * @param timeline The {@link Timeline} to check. + * @param window A scratch {@link Timeline.Window} instance. + * @return Whether the specified timeline can be shown on a multi-window time bar. + */ + private static boolean canShowMultiWindowTimeBar(Timeline timeline, Timeline.Window window) { + if (timeline.getWindowCount() > MAX_WINDOWS_FOR_MULTI_WINDOW_TIME_BAR) { + return false; + } + int windowCount = timeline.getWindowCount(); + for (int i = 0; i < windowCount; i++) { + if (timeline.getWindow(i, window).durationUs == C.TIME_UNSET) { + return false; + } + } + return true; + } + + private final class ComponentListener extends Player.DefaultEventListener + implements TimeBar.OnScrubListener, OnClickListener { + + @Override + public void onScrubStart(TimeBar timeBar, long position) { + removeCallbacks(hideAction); + scrubbing = true; + } + + @Override + public void onScrubMove(TimeBar timeBar, long position) { + if (positionView != null) { + positionView.setText(Util.getStringForTime(formatBuilder, formatter, position)); + } + } + + @Override + public void onScrubStop(TimeBar timeBar, long position, boolean canceled) { + scrubbing = false; + if (!canceled && player != null) { + seekToTimeBarPosition(position); + } + hideAfterTimeout(); + } + + @Override + public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + updatePlayPauseButton(); + updateProgress(); + } + + @Override + public void onRepeatModeChanged(int repeatMode) { + updateRepeatModeButton(); + updateNavigation(); + } + + @Override + public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { + updateShuffleButton(); + updateNavigation(); + } + + @Override + public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { + updateNavigation(); + updateProgress(); + } + + @Override + public void onTimelineChanged( + Timeline timeline, Object manifest, @Player.TimelineChangeReason int reason) { + updateNavigation(); + updateTimeBarMode(); + updateProgress(); + } + + @Override + public void onClick(View view) { + if (player != null) { + if (nextButton == view) { + next(); + } else if (previousButton == view) { + previous(); + } else if (fastForwardButton == view) { + fastForward(); + } else if (rewindButton == view) { + rewind(); + } else if (playButton == view) { + controlDispatcher.dispatchSetPlayWhenReady(player, true); + } else if (pauseButton == view) { + controlDispatcher.dispatchSetPlayWhenReady(player, false); + } else if (repeatToggleButton == view) { + controlDispatcher.dispatchSetRepeatMode( + player, RepeatModeUtil.getNextRepeatMode(player.getRepeatMode(), repeatToggleModes)); + } else if (shuffleButton == view) { + controlDispatcher.dispatchSetShuffleModeEnabled(player, !player.getShuffleModeEnabled()); + } + } + hideAfterTimeout(); + } + } +} diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java new file mode 100644 index 0000000000..66c197ecb7 --- /dev/null +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java @@ -0,0 +1,1057 @@ +/* + * 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.ui; + +import android.annotation.SuppressLint; +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.graphics.Matrix; +import android.graphics.RectF; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.util.AttributeSet; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.SurfaceView; +import android.view.TextureView; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.ImageView; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ControlDispatcher; +import com.google.android.exoplayer2.DefaultControlDispatcher; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Player.DiscontinuityReason; +import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.metadata.id3.ApicFrame; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.text.Cue; +import com.google.android.exoplayer2.text.TextOutput; +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.util.Assertions; +import com.google.android.exoplayer2.util.RepeatModeUtil; +import com.google.android.exoplayer2.util.Util; +import com.google.android.exoplayer2.video.VideoListener; +import java.util.List; + +/** + * A high level view for {@link Player} media playbacks. It displays video, subtitles and album art + * during playback, and displays playback controls using a {@link PlayerControlView}. + * + *

          A PlayerView can be customized by setting attributes (or calling corresponding methods), + * overriding the view's layout file or by specifying a custom view layout file, as outlined below. + * + *

          Attributes

          + * + * The following attributes can be set on a PlayerView when used in a layout XML file: + * + *
            + *
          • {@code use_artwork} - Whether artwork is used if available in audio streams. + *
              + *
            • Corresponding method: {@link #setUseArtwork(boolean)} + *
            • Default: {@code true} + *
            + *
          • {@code default_artwork} - Default artwork to use if no artwork available in audio + * streams. + *
              + *
            • Corresponding method: {@link #setDefaultArtwork(Bitmap)} + *
            • Default: {@code null} + *
            + *
          • {@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 auto_show} - Whether the playback controls are automatically shown when + * playback starts, pauses, ends, or fails. If set to false, the playback controls can be + * manually operated with {@link #showController()} and {@link #hideController()}. + *
              + *
            • Corresponding method: {@link #setControllerAutoShow(boolean)} + *
            • Default: {@code true} + *
            + *
          • {@code hide_during_ads} - Whether the playback controls are hidden during ads. + * Controls are always shown during ads if they are enabled and the player is paused. + *
              + *
            • Corresponding method: {@link #setControllerHideDuringAds(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}. + *
              + *
            • Corresponding method: {@link #setResizeMode(int)} + *
            • Default: {@code fit} + *
            + *
          • {@code surface_type} - The type of surface view used for video playbacks. Valid + * values are {@code surface_view}, {@code texture_view} and {@code none}. Using {@code none} + * is recommended for audio only applications, since creating the surface can be expensive. + * Using {@code surface_view} is recommended for video applications. + *
              + *
            • Corresponding method: None + *
            • Default: {@code surface_view} + *
            + *
          • {@code shutter_background_color} - The background color of the {@code exo_shutter} + * view. + *
              + *
            • Corresponding method: {@link #setShutterBackgroundColor(int)} + *
            • Default: {@code unset} + *
            + *
          • {@code player_layout_id} - Specifies the id of the layout to be inflated. See below + * for more details. + *
              + *
            • Corresponding method: None + *
            • Default: {@code R.id.exo_player_view} + *
            + *
          • {@code controller_layout_id} - Specifies the id of the layout resource to be + * inflated by the child {@link PlayerControlView}. See below for more details. + *
              + *
            • Corresponding method: None + *
            • Default: {@code R.id.exo_player_control_view} + *
            + *
          • All attributes that can be set on a {@link PlayerControlView} can also be set on a + * PlayerView, and will be propagated to the inflated {@link PlayerControlView} unless the + * layout is overridden to specify a custom {@code exo_controller} (see below). + *
          + * + *

          Overriding the layout file

          + * + * To customize the layout of PlayerView throughout your app, or just for certain configurations, + * you can define {@code exo_player_view.xml} layout files in your application {@code res/layout*} + * directories. These layouts will override the one provided by the ExoPlayer library, and will be + * inflated for use by PlayerView. The view identifies and binds its children by looking for the + * following ids: + * + *

          + * + *

            + *
          • {@code exo_content_frame} - A frame whose aspect ratio is resized based on the video + * or album art of the media being played, and the configured {@code resize_mode}. The video + * surface view is inflated into this frame as its first child. + *
              + *
            • Type: {@link AspectRatioFrameLayout} + *
            + *
          • {@code exo_shutter} - A view that's made visible when video should be hidden. This + * view is typically an opaque view that covers the video surface view, thereby obscuring it + * when visible. + *
              + *
            • Type: {@link View} + *
            + *
          • {@code exo_subtitles} - Displays subtitles. + *
              + *
            • Type: {@link SubtitleView} + *
            + *
          • {@code exo_artwork} - Displays album art. + *
              + *
            • Type: {@link ImageView} + *
            + *
          • {@code exo_controller_placeholder} - A placeholder that's replaced with the inflated + * {@link PlayerControlView}. Ignored if an {@code exo_controller} view exists. + *
              + *
            • Type: {@link View} + *
            + *
          • {@code exo_controller} - An already inflated {@link PlayerControlView}. Allows use + * of a custom extension of {@link PlayerControlView}. Note that attributes such as {@code + * rewind_increment} will not be automatically propagated through to this instance. If a view + * exists with this id, any {@code exo_controller_placeholder} view will be ignored. + *
              + *
            • Type: {@link PlayerControlView} + *
            + *
          • {@code exo_overlay} - A {@link FrameLayout} positioned on top of the player which + * the app can access via {@link #getOverlayFrameLayout()}, provided for convenience. + *
              + *
            • Type: {@link FrameLayout} + *
            + *
          + * + *

          All child views are optional and so can be omitted if not required, however where defined they + * must be of the expected type. + * + *

          Specifying a custom layout file

          + * + * Defining your own {@code exo_player_view.xml} is useful to customize the layout of PlayerView + * throughout your application. It's also possible to customize the layout for a single instance in + * a layout file. This is achieved by setting the {@code player_layout_id} attribute on a + * PlayerView. This will cause the specified layout to be inflated instead of {@code + * exo_player_view.xml} for only the instance on which the attribute is set. + */ +public class PlayerView extends FrameLayout { + + private static final int SURFACE_TYPE_NONE = 0; + private static final int SURFACE_TYPE_SURFACE_VIEW = 1; + private static final int SURFACE_TYPE_TEXTURE_VIEW = 2; + + private final AspectRatioFrameLayout contentFrame; + private final View shutterView; + private final View surfaceView; + private final ImageView artworkView; + private final SubtitleView subtitleView; + private final PlayerControlView controller; + private final ComponentListener componentListener; + private final FrameLayout overlayFrameLayout; + + private Player player; + private boolean useController; + private boolean useArtwork; + private Bitmap defaultArtwork; + private int controllerShowTimeoutMs; + private boolean controllerAutoShow; + private boolean controllerHideDuringAds; + private boolean controllerHideOnTouch; + private int textureViewRotation; + + public PlayerView(Context context) { + this(context, null); + } + + public PlayerView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public PlayerView(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); + if (Util.SDK_INT >= 23) { + configureEditModeLogoV23(getResources(), logo); + } else { + configureEditModeLogo(getResources(), logo); + } + addView(logo); + return; + } + + boolean shutterColorSet = false; + int shutterColor = 0; + int playerLayoutId = R.layout.exo_player_view; + boolean useArtwork = true; + int defaultArtworkId = 0; + boolean useController = true; + int surfaceType = SURFACE_TYPE_SURFACE_VIEW; + int resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT; + int controllerShowTimeoutMs = PlayerControlView.DEFAULT_SHOW_TIMEOUT_MS; + boolean controllerHideOnTouch = true; + boolean controllerAutoShow = true; + boolean controllerHideDuringAds = true; + if (attrs != null) { + TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.PlayerView, 0, 0); + try { + shutterColorSet = a.hasValue(R.styleable.PlayerView_shutter_background_color); + shutterColor = a.getColor(R.styleable.PlayerView_shutter_background_color, shutterColor); + playerLayoutId = a.getResourceId(R.styleable.PlayerView_player_layout_id, playerLayoutId); + useArtwork = a.getBoolean(R.styleable.PlayerView_use_artwork, useArtwork); + defaultArtworkId = + a.getResourceId(R.styleable.PlayerView_default_artwork, defaultArtworkId); + useController = a.getBoolean(R.styleable.PlayerView_use_controller, useController); + surfaceType = a.getInt(R.styleable.PlayerView_surface_type, surfaceType); + resizeMode = a.getInt(R.styleable.PlayerView_resize_mode, resizeMode); + controllerShowTimeoutMs = + a.getInt(R.styleable.PlayerView_show_timeout, controllerShowTimeoutMs); + controllerHideOnTouch = + a.getBoolean(R.styleable.PlayerView_hide_on_touch, controllerHideOnTouch); + controllerAutoShow = a.getBoolean(R.styleable.PlayerView_auto_show, controllerAutoShow); + controllerHideDuringAds = + a.getBoolean(R.styleable.PlayerView_hide_during_ads, controllerHideDuringAds); + } finally { + a.recycle(); + } + } + + LayoutInflater.from(context).inflate(playerLayoutId, this); + componentListener = new ComponentListener(); + setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); + + // Content frame. + contentFrame = findViewById(R.id.exo_content_frame); + if (contentFrame != null) { + setResizeModeRaw(contentFrame, resizeMode); + } + + // Shutter view. + shutterView = findViewById(R.id.exo_shutter); + if (shutterView != null && shutterColorSet) { + shutterView.setBackgroundColor(shutterColor); + } + + // Create a surface view and insert it into the content frame, if there is one. + if (contentFrame != null && surfaceType != SURFACE_TYPE_NONE) { + ViewGroup.LayoutParams params = + new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); + surfaceView = + surfaceType == SURFACE_TYPE_TEXTURE_VIEW + ? new TextureView(context) + : new SurfaceView(context); + surfaceView.setLayoutParams(params); + contentFrame.addView(surfaceView, 0); + } else { + surfaceView = null; + } + + // Overlay frame layout. + overlayFrameLayout = findViewById(R.id.exo_overlay); + + // Artwork view. + artworkView = findViewById(R.id.exo_artwork); + this.useArtwork = useArtwork && artworkView != null; + if (defaultArtworkId != 0) { + defaultArtwork = BitmapFactory.decodeResource(context.getResources(), defaultArtworkId); + } + + // Subtitle view. + subtitleView = findViewById(R.id.exo_subtitles); + if (subtitleView != null) { + subtitleView.setUserDefaultStyle(); + subtitleView.setUserDefaultTextSize(); + } + + // Playback control view. + PlayerControlView customController = findViewById(R.id.exo_controller); + View controllerPlaceholder = findViewById(R.id.exo_controller_placeholder); + if (customController != null) { + this.controller = customController; + } else if (controllerPlaceholder != null) { + // Propagate attrs as playbackAttrs so that PlayerControlView's custom attributes are + // transferred, but standard FrameLayout attributes (e.g. background) are not. + this.controller = new PlayerControlView(context, null, 0, attrs); + controller.setLayoutParams(controllerPlaceholder.getLayoutParams()); + ViewGroup parent = ((ViewGroup) controllerPlaceholder.getParent()); + int controllerIndex = parent.indexOfChild(controllerPlaceholder); + parent.removeView(controllerPlaceholder); + parent.addView(controller, controllerIndex); + } else { + this.controller = null; + } + this.controllerShowTimeoutMs = controller != null ? controllerShowTimeoutMs : 0; + this.controllerHideOnTouch = controllerHideOnTouch; + this.controllerAutoShow = controllerAutoShow; + this.controllerHideDuringAds = controllerHideDuringAds; + this.useController = useController && controller != null; + hideController(); + } + + /** + * Switches the view targeted by a given {@link Player}. + * + * @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 Player player, + @Nullable PlayerView oldPlayerView, + @Nullable PlayerView 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. */ + public Player getPlayer() { + return player; + } + + /** + * Set the {@link Player} to use. + * + *

          To transition a {@link Player} from targeting one view to another, it's recommended to use + * {@link #switchTargetView(Player, PlayerView, PlayerView)} 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 Player} to use. + */ + public void setPlayer(Player player) { + if (this.player == player) { + return; + } + if (this.player != null) { + this.player.removeListener(componentListener); + Player.VideoComponent oldVideoComponent = this.player.getVideoComponent(); + if (oldVideoComponent != null) { + oldVideoComponent.removeVideoListener(componentListener); + if (surfaceView instanceof TextureView) { + oldVideoComponent.clearVideoTextureView((TextureView) surfaceView); + } else if (surfaceView instanceof SurfaceView) { + oldVideoComponent.clearVideoSurfaceView((SurfaceView) surfaceView); + } + } + Player.TextComponent oldTextComponent = this.player.getTextComponent(); + if (oldTextComponent != null) { + oldTextComponent.removeTextOutput(componentListener); + } + } + this.player = player; + if (useController) { + controller.setPlayer(player); + } + if (shutterView != null) { + shutterView.setVisibility(VISIBLE); + } + if (subtitleView != null) { + subtitleView.setCues(null); + } + if (player != null) { + Player.VideoComponent newVideoComponent = player.getVideoComponent(); + if (newVideoComponent != null) { + if (surfaceView instanceof TextureView) { + newVideoComponent.setVideoTextureView((TextureView) surfaceView); + } else if (surfaceView instanceof SurfaceView) { + newVideoComponent.setVideoSurfaceView((SurfaceView) surfaceView); + } + newVideoComponent.addVideoListener(componentListener); + } + Player.TextComponent newTextComponent = player.getTextComponent(); + if (newTextComponent != null) { + newTextComponent.addTextOutput(componentListener); + } + player.addListener(componentListener); + maybeShowController(false); + updateForCurrentTrackSelections(); + } else { + hideController(); + hideArtwork(); + } + } + + @Override + public void setVisibility(int visibility) { + super.setVisibility(visibility); + if (surfaceView instanceof SurfaceView) { + // Work around https://github.com/google/ExoPlayer/issues/3160. + surfaceView.setVisibility(visibility); + } + } + + /** + * Sets the resize mode. + * + * @param resizeMode The resize mode. + */ + public void setResizeMode(@ResizeMode int resizeMode) { + Assertions.checkState(contentFrame != null); + contentFrame.setResizeMode(resizeMode); + } + + /** Returns whether artwork is displayed if present in the media. */ + public boolean getUseArtwork() { + return useArtwork; + } + + /** + * Sets whether artwork is displayed if present in the media. + * + * @param useArtwork Whether artwork is displayed. + */ + public void setUseArtwork(boolean useArtwork) { + Assertions.checkState(!useArtwork || artworkView != null); + if (this.useArtwork != useArtwork) { + this.useArtwork = useArtwork; + updateForCurrentTrackSelections(); + } + } + + /** Returns the default artwork to display. */ + public Bitmap getDefaultArtwork() { + return defaultArtwork; + } + + /** + * Sets the default artwork to display if {@code useArtwork} is {@code true} and no artwork is + * present in the media. + * + * @param defaultArtwork the default artwork to display. + */ + public void setDefaultArtwork(Bitmap defaultArtwork) { + if (this.defaultArtwork != defaultArtwork) { + this.defaultArtwork = defaultArtwork; + updateForCurrentTrackSelections(); + } + } + + /** Returns whether the playback controls can be shown. */ + public boolean getUseController() { + return useController; + } + + /** + * 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 the playback controls can be shown. + */ + public void setUseController(boolean useController) { + Assertions.checkState(!useController || controller != null); + if (this.useController == useController) { + return; + } + this.useController = useController; + if (useController) { + controller.setPlayer(player); + } else if (controller != null) { + controller.hide(); + controller.setPlayer(null); + } + } + + /** + * Sets the background color of the {@code exo_shutter} view. + * + * @param color The background color. + */ + public void setShutterBackgroundColor(int color) { + if (shutterView != null) { + shutterView.setBackgroundColor(color); + } + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + if (player != null && player.isPlayingAd()) { + // Focus any overlay UI now, in case it's provided by a WebView whose contents may update + // dynamically. This is needed to make the "Skip ad" button focused on Android TV when using + // IMA [Internal: b/62371030]. + overlayFrameLayout.requestFocus(); + return super.dispatchKeyEvent(event); + } + boolean isDpadWhenControlHidden = + isDpadKey(event.getKeyCode()) && useController && !controller.isVisible(); + maybeShowController(true); + return isDpadWhenControlHidden || dispatchMediaKeyEvent(event) || super.dispatchKeyEvent(event); + } + + /** + * Called to process media key events. Any {@link KeyEvent} can be passed but only media key + * events will be handled. Does nothing if playback controls are disabled. + * + * @param event A key event. + * @return Whether the key event was handled. + */ + public boolean dispatchMediaKeyEvent(KeyEvent event) { + return useController && controller.dispatchMediaKeyEvent(event); + } + + /** + * Shows the playback controls. Does nothing if playback controls are disabled. + * + *

          The playback controls are automatically hidden during playback after {{@link + * #getControllerShowTimeoutMs()}}. They are shown indefinitely when playback has not started yet, + * is paused, has ended or failed. + */ + public void showController() { + showController(shouldShowControllerIndefinitely()); + } + + /** Hides the playback controls. Does nothing if playback controls are disabled. */ + public void hideController() { + if (controller != null) { + controller.hide(); + } + } + + /** + * Returns the playback controls timeout. The playback controls are automatically hidden after + * this duration of time has elapsed without user input and with playback or buffering in + * progress. + * + * @return The timeout in milliseconds. A non-positive value will cause the controller to remain + * visible indefinitely. + */ + public int getControllerShowTimeoutMs() { + return controllerShowTimeoutMs; + } + + /** + * Sets the playback controls timeout. The playback controls are automatically hidden after this + * duration of time has elapsed without user input and with playback or buffering in progress. + * + * @param controllerShowTimeoutMs The timeout in milliseconds. A non-positive value will cause the + * controller to remain visible indefinitely. + */ + public void setControllerShowTimeoutMs(int controllerShowTimeoutMs) { + Assertions.checkState(controller != null); + this.controllerShowTimeoutMs = controllerShowTimeoutMs; + if (controller.isVisible()) { + // Update the controller's timeout if necessary. + showController(); + } + } + + /** 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; + } + + /** + * Returns whether the playback controls are automatically shown when playback starts, pauses, + * ends, or fails. If set to false, the playback controls can be manually operated with {@link + * #showController()} and {@link #hideController()}. + */ + public boolean getControllerAutoShow() { + return controllerAutoShow; + } + + /** + * Sets whether the playback controls are automatically shown when playback starts, pauses, ends, + * or fails. If set to false, the playback controls can be manually operated with {@link + * #showController()} and {@link #hideController()}. + * + * @param controllerAutoShow Whether the playback controls are allowed to show automatically. + */ + public void setControllerAutoShow(boolean controllerAutoShow) { + this.controllerAutoShow = controllerAutoShow; + } + + /** + * Sets whether the playback controls are hidden when ads are playing. Controls are always shown + * during ads if they are enabled and the player is paused. + * + * @param controllerHideDuringAds Whether the playback controls are hidden when ads are playing. + */ + public void setControllerHideDuringAds(boolean controllerHideDuringAds) { + this.controllerHideDuringAds = controllerHideDuringAds; + } + + /** + * Set the {@link PlayerControlView.VisibilityListener}. + * + * @param listener The listener to be notified about visibility changes. + */ + public void setControllerVisibilityListener(PlayerControlView.VisibilityListener listener) { + Assertions.checkState(controller != null); + controller.setVisibilityListener(listener); + } + + /** + * Sets the {@link ControlDispatcher}. + * + * @param controlDispatcher The {@link ControlDispatcher}, or null to use {@link + * DefaultControlDispatcher}. + */ + public void setControlDispatcher(@Nullable ControlDispatcher controlDispatcher) { + Assertions.checkState(controller != null); + controller.setControlDispatcher(controlDispatcher); + } + + /** + * Sets the rewind increment in milliseconds. + * + * @param rewindMs The rewind increment in milliseconds. A non-positive value will cause the + * rewind button to be disabled. + */ + public void setRewindIncrementMs(int rewindMs) { + Assertions.checkState(controller != null); + controller.setRewindIncrementMs(rewindMs); + } + + /** + * Sets the fast forward increment in milliseconds. + * + * @param fastForwardMs The fast forward increment in milliseconds. A non-positive value will + * cause the fast forward button to be disabled. + */ + public void setFastForwardIncrementMs(int fastForwardMs) { + Assertions.checkState(controller != null); + controller.setFastForwardIncrementMs(fastForwardMs); + } + + /** + * Sets which repeat toggle modes are enabled. + * + * @param repeatToggleModes A set of {@link RepeatModeUtil.RepeatToggleModes}. + */ + public void setRepeatToggleModes(@RepeatModeUtil.RepeatToggleModes int repeatToggleModes) { + Assertions.checkState(controller != null); + controller.setRepeatToggleModes(repeatToggleModes); + } + + /** + * Sets whether the shuffle button is shown. + * + * @param showShuffleButton Whether the shuffle button is shown. + */ + public void setShowShuffleButton(boolean showShuffleButton) { + Assertions.checkState(controller != null); + controller.setShowShuffleButton(showShuffleButton); + } + + /** + * 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 a: + * + *

            + *
          • {@link SurfaceView} by default, or if the {@code surface_type} attribute is set to {@code + * surface_view}. + *
          • {@link TextureView} if {@code surface_type} is {@code texture_view}. + *
          • {@code null} if {@code surface_type} is {@code none}. + *
          + * + * @return The {@link SurfaceView}, {@link TextureView} or {@code null}. + */ + public View getVideoSurfaceView() { + return surfaceView; + } + + /** + * Gets the overlay {@link FrameLayout}, which can be populated with UI elements to show on top of + * the player. + * + * @return The overlay {@link FrameLayout}, or {@code null} if the layout has been customized and + * the overlay is not present. + */ + public FrameLayout getOverlayFrameLayout() { + return overlayFrameLayout; + } + + /** + * Gets the {@link SubtitleView}. + * + * @return The {@link SubtitleView}, or {@code null} if the layout has been customized and the + * subtitle view is not present. + */ + public SubtitleView getSubtitleView() { + return subtitleView; + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + if (!useController || player == null || ev.getActionMasked() != MotionEvent.ACTION_DOWN) { + return false; + } + if (!controller.isVisible()) { + maybeShowController(true); + } else if (controllerHideOnTouch) { + controller.hide(); + } + return true; + } + + @Override + public boolean onTrackballEvent(MotionEvent ev) { + if (!useController || player == null) { + return false; + } + maybeShowController(true); + return true; + } + + /** Shows the playback controls, but only if forced or shown indefinitely. */ + private void maybeShowController(boolean isForced) { + if (isPlayingAd() && controllerHideDuringAds) { + return; + } + if (useController) { + boolean wasShowingIndefinitely = controller.isVisible() && controller.getShowTimeoutMs() <= 0; + boolean shouldShowIndefinitely = shouldShowControllerIndefinitely(); + if (isForced || wasShowingIndefinitely || shouldShowIndefinitely) { + showController(shouldShowIndefinitely); + } + } + } + + private boolean shouldShowControllerIndefinitely() { + if (player == null) { + return true; + } + int playbackState = player.getPlaybackState(); + return controllerAutoShow + && (playbackState == Player.STATE_IDLE + || playbackState == Player.STATE_ENDED + || !player.getPlayWhenReady()); + } + + private void showController(boolean showIndefinitely) { + if (!useController) { + return; + } + controller.setShowTimeoutMs(showIndefinitely ? 0 : controllerShowTimeoutMs); + controller.show(); + } + + private boolean isPlayingAd() { + return player != null && player.isPlayingAd() && player.getPlayWhenReady(); + } + + private void updateForCurrentTrackSelections() { + if (player == null) { + return; + } + TrackSelectionArray selections = player.getCurrentTrackSelections(); + for (int i = 0; i < selections.length; i++) { + if (player.getRendererType(i) == C.TRACK_TYPE_VIDEO && selections.get(i) != null) { + // Video enabled so artwork must be hidden. If the shutter is closed, it will be opened in + // onRenderedFirstFrame(). + hideArtwork(); + return; + } + } + // Video disabled so the shutter must be closed. + if (shutterView != null) { + shutterView.setVisibility(VISIBLE); + } + // Display artwork if enabled and available, else hide it. + if (useArtwork) { + for (int i = 0; i < selections.length; i++) { + TrackSelection selection = selections.get(i); + if (selection != null) { + for (int j = 0; j < selection.length(); j++) { + Metadata metadata = selection.getFormat(j).metadata; + if (metadata != null && setArtworkFromMetadata(metadata)) { + return; + } + } + } + } + if (setArtworkFromBitmap(defaultArtwork)) { + return; + } + } + // Artwork disabled or unavailable. + hideArtwork(); + } + + private boolean setArtworkFromMetadata(Metadata metadata) { + for (int i = 0; i < metadata.length(); i++) { + Metadata.Entry metadataEntry = metadata.get(i); + if (metadataEntry instanceof ApicFrame) { + byte[] bitmapData = ((ApicFrame) metadataEntry).pictureData; + Bitmap bitmap = BitmapFactory.decodeByteArray(bitmapData, 0, bitmapData.length); + return setArtworkFromBitmap(bitmap); + } + } + return false; + } + + private boolean setArtworkFromBitmap(Bitmap bitmap) { + if (bitmap != null) { + int bitmapWidth = bitmap.getWidth(); + int bitmapHeight = bitmap.getHeight(); + if (bitmapWidth > 0 && bitmapHeight > 0) { + if (contentFrame != null) { + contentFrame.setAspectRatio((float) bitmapWidth / bitmapHeight); + } + artworkView.setImageBitmap(bitmap); + artworkView.setVisibility(VISIBLE); + return true; + } + } + return false; + } + + private void hideArtwork() { + if (artworkView != null) { + artworkView.setImageResource(android.R.color.transparent); // Clears any bitmap reference. + artworkView.setVisibility(INVISIBLE); + } + } + + @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); + } + + /** Applies a texture rotation to a {@link TextureView}. */ + private static void applyTextureViewRotation(TextureView textureView, int textureViewRotation) { + float textureViewWidth = textureView.getWidth(); + float textureViewHeight = textureView.getHeight(); + if (textureViewWidth == 0 || textureViewHeight == 0 || textureViewRotation == 0) { + textureView.setTransform(null); + } else { + Matrix transformMatrix = new Matrix(); + float pivotX = textureViewWidth / 2; + float pivotY = textureViewHeight / 2; + transformMatrix.postRotate(textureViewRotation, pivotX, pivotY); + + // After rotation, scale the rotated texture to fit the TextureView size. + RectF originalTextureRect = new RectF(0, 0, textureViewWidth, textureViewHeight); + RectF rotatedTextureRect = new RectF(); + transformMatrix.mapRect(rotatedTextureRect, originalTextureRect); + transformMatrix.postScale( + textureViewWidth / rotatedTextureRect.width(), + textureViewHeight / rotatedTextureRect.height(), + pivotX, + pivotY); + textureView.setTransform(transformMatrix); + } + } + + @SuppressLint("InlinedApi") + private boolean isDpadKey(int keyCode) { + return keyCode == KeyEvent.KEYCODE_DPAD_UP + || keyCode == KeyEvent.KEYCODE_DPAD_UP_RIGHT + || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT + || keyCode == KeyEvent.KEYCODE_DPAD_DOWN_RIGHT + || keyCode == KeyEvent.KEYCODE_DPAD_DOWN + || keyCode == KeyEvent.KEYCODE_DPAD_DOWN_LEFT + || keyCode == KeyEvent.KEYCODE_DPAD_LEFT + || keyCode == KeyEvent.KEYCODE_DPAD_UP_LEFT + || keyCode == KeyEvent.KEYCODE_DPAD_CENTER; + } + + private final class ComponentListener extends Player.DefaultEventListener + implements TextOutput, VideoListener, OnLayoutChangeListener { + + // TextOutput implementation + + @Override + public void onCues(List cues) { + if (subtitleView != null) { + subtitleView.onCues(cues); + } + } + + // VideoListener implementation + + @Override + public void onVideoSizeChanged( + int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { + if (contentFrame == null) { + return; + } + float videoAspectRatio = + (height == 0 || width == 0) ? 1 : (width * pixelWidthHeightRatio) / height; + + if (surfaceView instanceof TextureView) { + // Try to apply rotation transformation when our surface is a TextureView. + if (unappliedRotationDegrees == 90 || unappliedRotationDegrees == 270) { + // We will apply a rotation 90/270 degree to the output texture of the TextureView. + // In this case, the output video's width and height will be swapped. + videoAspectRatio = 1 / videoAspectRatio; + } + if (textureViewRotation != 0) { + surfaceView.removeOnLayoutChangeListener(this); + } + textureViewRotation = unappliedRotationDegrees; + if (textureViewRotation != 0) { + // The texture view's dimensions might be changed after layout step. + // So add an OnLayoutChangeListener to apply rotation after layout step. + surfaceView.addOnLayoutChangeListener(this); + } + applyTextureViewRotation((TextureView) surfaceView, textureViewRotation); + } + + contentFrame.setAspectRatio(videoAspectRatio); + } + + @Override + public void onRenderedFirstFrame() { + if (shutterView != null) { + shutterView.setVisibility(INVISIBLE); + } + } + + @Override + public void onTracksChanged(TrackGroupArray tracks, TrackSelectionArray selections) { + updateForCurrentTrackSelections(); + } + + // Player.EventListener implementation + + @Override + public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + if (isPlayingAd() && controllerHideDuringAds) { + hideController(); + } else { + maybeShowController(false); + } + } + + @Override + public void onPositionDiscontinuity(@DiscontinuityReason int reason) { + if (isPlayingAd() && controllerHideDuringAds) { + hideController(); + } + } + + // OnLayoutChangeListener implementation + + @Override + public void onLayoutChange( + View view, + int left, + int top, + int right, + int bottom, + int oldLeft, + int oldTop, + int oldRight, + int oldBottom) { + applyTextureViewRotation((TextureView) view, textureViewRotation); + } + } +} diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index 6e69a31fd9..b8098b6fa7 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -15,360 +15,28 @@ */ package com.google.android.exoplayer2.ui; -import android.annotation.SuppressLint; 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.graphics.Matrix; -import android.graphics.RectF; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.AttributeSet; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.MotionEvent; -import android.view.SurfaceView; -import android.view.TextureView; -import android.view.View; -import android.view.ViewGroup; -import android.widget.FrameLayout; -import android.widget.ImageView; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ControlDispatcher; -import com.google.android.exoplayer2.DefaultControlDispatcher; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.Player.DiscontinuityReason; import com.google.android.exoplayer2.SimpleExoPlayer; -import com.google.android.exoplayer2.metadata.Metadata; -import com.google.android.exoplayer2.metadata.id3.ApicFrame; -import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.text.Cue; -import com.google.android.exoplayer2.text.TextOutput; -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.util.Assertions; -import com.google.android.exoplayer2.util.RepeatModeUtil; -import com.google.android.exoplayer2.util.Util; -import java.util.List; -/** - * A high level view for {@link SimpleExoPlayer} media playbacks. It displays video, subtitles and - * album art during playback, and displays playback controls using a {@link PlaybackControlView}. - * - *

          A SimpleExoPlayerView can be customized by setting attributes (or calling corresponding - * methods), overriding the view's layout file or by specifying a custom view layout file, as - * outlined below. - * - *

          Attributes

          - * - * The following attributes can be set on a SimpleExoPlayerView when used in a layout XML file: - * - *

          - * - *

            - *
          • {@code use_artwork} - Whether artwork is used if available in audio streams. - *
              - *
            • Corresponding method: {@link #setUseArtwork(boolean)} - *
            • Default: {@code true} - *
            - *
          • {@code default_artwork} - Default artwork to use if no artwork available in audio - * streams. - *
              - *
            • Corresponding method: {@link #setDefaultArtwork(Bitmap)} - *
            • Default: {@code null} - *
            - *
          • {@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 auto_show} - Whether the playback controls are automatically shown when - * playback starts, pauses, ends, or fails. If set to false, the playback controls can be - * manually operated with {@link #showController()} and {@link #hideController()}. - *
              - *
            • Corresponding method: {@link #setControllerAutoShow(boolean)} - *
            • Default: {@code true} - *
            - *
          • {@code hide_during_ads} - Whether the playback controls are hidden during ads. - * Controls are always shown during ads if they are enabled and the player is paused. - *
              - *
            • Corresponding method: {@link #setControllerHideDuringAds(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}. - *
              - *
            • Corresponding method: {@link #setResizeMode(int)} - *
            • Default: {@code fit} - *
            - *
          • {@code surface_type} - The type of surface view used for video playbacks. Valid - * values are {@code surface_view}, {@code texture_view} and {@code none}. Using {@code none} - * is recommended for audio only applications, since creating the surface can be expensive. - * Using {@code surface_view} is recommended for video applications. - *
              - *
            • Corresponding method: None - *
            • Default: {@code surface_view} - *
            - *
          • {@code shutter_background_color} - The background color of the {@code exo_shutter} - * view. - *
              - *
            • Corresponding method: {@link #setShutterBackgroundColor(int)} - *
            • Default: {@code unset} - *
            - *
          • {@code player_layout_id} - Specifies the id of the layout to be inflated. See below - * for more details. - *
              - *
            • Corresponding method: None - *
            • Default: {@code R.id.exo_simple_player_view} - *
            - *
          • {@code controller_layout_id} - Specifies the id of the layout resource to be - * inflated by the child {@link PlaybackControlView}. See below for more details. - *
              - *
            • Corresponding method: None - *
            • Default: {@code R.id.exo_playback_control_view} - *
            - *
          • All attributes that can be set on a {@link PlaybackControlView} can also be set on a - * SimpleExoPlayerView, and will be propagated to the inflated {@link PlaybackControlView} - * unless the layout is overridden to specify a custom {@code exo_controller} (see below). - *
          - * - *

          Overriding the layout file

          - * - * To customize the layout of SimpleExoPlayerView throughout your app, or just for certain - * configurations, you can define {@code exo_simple_player_view.xml} layout files in your - * application {@code res/layout*} directories. These layouts will override the one provided by the - * ExoPlayer library, and will be inflated for use by SimpleExoPlayerView. The view identifies and - * binds its children by looking for the following ids: - * - *

          - * - *

            - *
          • {@code exo_content_frame} - A frame whose aspect ratio is resized based on the video - * or album art of the media being played, and the configured {@code resize_mode}. The video - * surface view is inflated into this frame as its first child. - *
              - *
            • Type: {@link AspectRatioFrameLayout} - *
            - *
          • {@code exo_shutter} - A view that's made visible when video should be hidden. This - * view is typically an opaque view that covers the video surface view, thereby obscuring it - * when visible. - *
              - *
            • Type: {@link View} - *
            - *
          • {@code exo_subtitles} - Displays subtitles. - *
              - *
            • Type: {@link SubtitleView} - *
            - *
          • {@code exo_artwork} - Displays album art. - *
              - *
            • Type: {@link ImageView} - *
            - *
          • {@code exo_controller_placeholder} - A placeholder that's replaced with the inflated - * {@link PlaybackControlView}. Ignored if an {@code exo_controller} view exists. - *
              - *
            • Type: {@link View} - *
            - *
          • {@code exo_controller} - An already inflated {@link PlaybackControlView}. Allows use - * of a custom extension of {@link PlaybackControlView}. Note that attributes such as {@code - * rewind_increment} will not be automatically propagated through to this instance. If a view - * exists with this id, any {@code exo_controller_placeholder} view will be ignored. - *
              - *
            • Type: {@link PlaybackControlView} - *
            - *
          • {@code exo_overlay} - A {@link FrameLayout} positioned on top of the player which - * the app can access via {@link #getOverlayFrameLayout()}, provided for convenience. - *
              - *
            • Type: {@link FrameLayout} - *
            - *
          - * - *

          All child views are optional and so can be omitted if not required, however where defined they - * must be of the expected type. - * - *

          Specifying a custom layout file

          - * - * Defining your own {@code exo_simple_player_view.xml} is useful to customize the layout of - * SimpleExoPlayerView throughout your application. It's also possible to customize the layout for a - * single instance in a layout file. This is achieved by setting the {@code player_layout_id} - * attribute on a SimpleExoPlayerView. This will cause the specified layout to be inflated instead - * of {@code exo_simple_player_view.xml} for only the instance on which the attribute is set. - */ +/** @deprecated Use {@link PlayerView}. */ +@Deprecated @TargetApi(16) -public final class SimpleExoPlayerView extends FrameLayout { - - private static final int SURFACE_TYPE_NONE = 0; - private static final int SURFACE_TYPE_SURFACE_VIEW = 1; - private static final int SURFACE_TYPE_TEXTURE_VIEW = 2; - - private final AspectRatioFrameLayout contentFrame; - private final View shutterView; - private final View surfaceView; - private final ImageView artworkView; - private final SubtitleView subtitleView; - private final PlaybackControlView controller; - private final ComponentListener componentListener; - private final FrameLayout overlayFrameLayout; - - private SimpleExoPlayer player; - private boolean useController; - private boolean useArtwork; - private Bitmap defaultArtwork; - private int controllerShowTimeoutMs; - private boolean controllerAutoShow; - private boolean controllerHideDuringAds; - private boolean controllerHideOnTouch; - private int textureViewRotation; +public final class SimpleExoPlayerView extends PlayerView { public SimpleExoPlayerView(Context context) { - this(context, null); + super(context); } public SimpleExoPlayerView(Context context, AttributeSet attrs) { - this(context, attrs, 0); + super(context, attrs); } 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); - if (Util.SDK_INT >= 23) { - configureEditModeLogoV23(getResources(), logo); - } else { - configureEditModeLogo(getResources(), logo); - } - addView(logo); - return; - } - - boolean shutterColorSet = false; - int shutterColor = 0; - int playerLayoutId = R.layout.exo_simple_player_view; - boolean useArtwork = true; - int defaultArtworkId = 0; - boolean useController = true; - int surfaceType = SURFACE_TYPE_SURFACE_VIEW; - int resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT; - int controllerShowTimeoutMs = PlaybackControlView.DEFAULT_SHOW_TIMEOUT_MS; - boolean controllerHideOnTouch = true; - boolean controllerAutoShow = true; - boolean controllerHideDuringAds = true; - if (attrs != null) { - TypedArray a = - context.getTheme().obtainStyledAttributes(attrs, R.styleable.SimpleExoPlayerView, 0, 0); - try { - shutterColorSet = a.hasValue(R.styleable.SimpleExoPlayerView_shutter_background_color); - shutterColor = - a.getColor(R.styleable.SimpleExoPlayerView_shutter_background_color, shutterColor); - playerLayoutId = - a.getResourceId(R.styleable.SimpleExoPlayerView_player_layout_id, playerLayoutId); - useArtwork = a.getBoolean(R.styleable.SimpleExoPlayerView_use_artwork, useArtwork); - defaultArtworkId = - a.getResourceId(R.styleable.SimpleExoPlayerView_default_artwork, defaultArtworkId); - useController = a.getBoolean(R.styleable.SimpleExoPlayerView_use_controller, useController); - surfaceType = a.getInt(R.styleable.SimpleExoPlayerView_surface_type, surfaceType); - 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); - controllerAutoShow = - a.getBoolean(R.styleable.SimpleExoPlayerView_auto_show, controllerAutoShow); - controllerHideDuringAds = - a.getBoolean(R.styleable.SimpleExoPlayerView_hide_during_ads, controllerHideDuringAds); - } finally { - a.recycle(); - } - } - - LayoutInflater.from(context).inflate(playerLayoutId, this); - componentListener = new ComponentListener(); - setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); - - // Content frame. - contentFrame = findViewById(R.id.exo_content_frame); - if (contentFrame != null) { - setResizeModeRaw(contentFrame, resizeMode); - } - - // Shutter view. - shutterView = findViewById(R.id.exo_shutter); - if (shutterView != null && shutterColorSet) { - shutterView.setBackgroundColor(shutterColor); - } - - // Create a surface view and insert it into the content frame, if there is one. - if (contentFrame != null && surfaceType != SURFACE_TYPE_NONE) { - ViewGroup.LayoutParams params = - new ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); - surfaceView = - surfaceType == SURFACE_TYPE_TEXTURE_VIEW - ? new TextureView(context) - : new SurfaceView(context); - surfaceView.setLayoutParams(params); - contentFrame.addView(surfaceView, 0); - } else { - surfaceView = null; - } - - // Overlay frame layout. - overlayFrameLayout = findViewById(R.id.exo_overlay); - - // Artwork view. - artworkView = findViewById(R.id.exo_artwork); - this.useArtwork = useArtwork && artworkView != null; - if (defaultArtworkId != 0) { - defaultArtwork = BitmapFactory.decodeResource(context.getResources(), defaultArtworkId); - } - - // Subtitle view. - subtitleView = findViewById(R.id.exo_subtitles); - if (subtitleView != null) { - subtitleView.setUserDefaultStyle(); - subtitleView.setUserDefaultTextSize(); - } - - // Playback control view. - PlaybackControlView customController = findViewById(R.id.exo_controller); - View controllerPlaceholder = findViewById(R.id.exo_controller_placeholder); - if (customController != null) { - this.controller = customController; - } else if (controllerPlaceholder != null) { - // Propagate attrs as playbackAttrs so that PlaybackControlView's custom attributes are - // transferred, but standard FrameLayout attributes (e.g. background) are not. - this.controller = new PlaybackControlView(context, null, 0, attrs); - controller.setLayoutParams(controllerPlaceholder.getLayoutParams()); - ViewGroup parent = ((ViewGroup) controllerPlaceholder.getParent()); - int controllerIndex = parent.indexOfChild(controllerPlaceholder); - parent.removeView(controllerPlaceholder); - parent.addView(controller, controllerIndex); - } else { - this.controller = null; - } - this.controllerShowTimeoutMs = controller != null ? controllerShowTimeoutMs : 0; - this.controllerHideOnTouch = controllerHideOnTouch; - this.controllerAutoShow = controllerAutoShow; - this.controllerHideDuringAds = controllerHideDuringAds; - this.useController = useController && controller != null; - hideController(); } /** @@ -382,674 +50,7 @@ public final class SimpleExoPlayerView extends FrameLayout { @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); - } + PlayerView.switchTargetView(player, oldPlayerView, newPlayerView); } - /** Returns the player currently set on this view, or null if no player is set. */ - public SimpleExoPlayer getPlayer() { - return player; - } - - /** - * Set the {@link SimpleExoPlayer} to use. - * - *

          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. - */ - public void setPlayer(SimpleExoPlayer player) { - if (this.player == player) { - return; - } - if (this.player != null) { - this.player.removeListener(componentListener); - this.player.removeTextOutput(componentListener); - this.player.removeVideoListener(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) { - controller.setPlayer(player); - } - if (shutterView != null) { - shutterView.setVisibility(VISIBLE); - } - if (subtitleView != null) { - subtitleView.setCues(null); - } - if (player != null) { - if (surfaceView instanceof TextureView) { - player.setVideoTextureView((TextureView) surfaceView); - } else if (surfaceView instanceof SurfaceView) { - player.setVideoSurfaceView((SurfaceView) surfaceView); - } - player.addVideoListener(componentListener); - player.addTextOutput(componentListener); - player.addListener(componentListener); - maybeShowController(false); - updateForCurrentTrackSelections(); - } else { - hideController(); - hideArtwork(); - } - } - - @Override - public void setVisibility(int visibility) { - super.setVisibility(visibility); - if (surfaceView instanceof SurfaceView) { - // Work around https://github.com/google/ExoPlayer/issues/3160. - surfaceView.setVisibility(visibility); - } - } - - /** - * Sets the resize mode. - * - * @param resizeMode The resize mode. - */ - public void setResizeMode(@ResizeMode int resizeMode) { - Assertions.checkState(contentFrame != null); - contentFrame.setResizeMode(resizeMode); - } - - /** Returns whether artwork is displayed if present in the media. */ - public boolean getUseArtwork() { - return useArtwork; - } - - /** - * Sets whether artwork is displayed if present in the media. - * - * @param useArtwork Whether artwork is displayed. - */ - public void setUseArtwork(boolean useArtwork) { - Assertions.checkState(!useArtwork || artworkView != null); - if (this.useArtwork != useArtwork) { - this.useArtwork = useArtwork; - updateForCurrentTrackSelections(); - } - } - - /** Returns the default artwork to display. */ - public Bitmap getDefaultArtwork() { - return defaultArtwork; - } - - /** - * Sets the default artwork to display if {@code useArtwork} is {@code true} and no artwork is - * present in the media. - * - * @param defaultArtwork the default artwork to display. - */ - public void setDefaultArtwork(Bitmap defaultArtwork) { - if (this.defaultArtwork != defaultArtwork) { - this.defaultArtwork = defaultArtwork; - updateForCurrentTrackSelections(); - } - } - - /** Returns whether the playback controls can be shown. */ - public boolean getUseController() { - return useController; - } - - /** - * 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 the playback controls can be shown. - */ - public void setUseController(boolean useController) { - Assertions.checkState(!useController || controller != null); - if (this.useController == useController) { - return; - } - this.useController = useController; - if (useController) { - controller.setPlayer(player); - } else if (controller != null) { - controller.hide(); - controller.setPlayer(null); - } - } - - /** - * Sets the background color of the {@code exo_shutter} view. - * - * @param color The background color. - */ - public void setShutterBackgroundColor(int color) { - if (shutterView != null) { - shutterView.setBackgroundColor(color); - } - } - - @Override - public boolean dispatchKeyEvent(KeyEvent event) { - if (player != null && player.isPlayingAd()) { - // Focus any overlay UI now, in case it's provided by a WebView whose contents may update - // dynamically. This is needed to make the "Skip ad" button focused on Android TV when using - // IMA [Internal: b/62371030]. - overlayFrameLayout.requestFocus(); - return super.dispatchKeyEvent(event); - } - boolean isDpadWhenControlHidden = - isDpadKey(event.getKeyCode()) && useController && !controller.isVisible(); - maybeShowController(true); - return isDpadWhenControlHidden || dispatchMediaKeyEvent(event) || super.dispatchKeyEvent(event); - } - - /** - * Called to process media key events. Any {@link KeyEvent} can be passed but only media key - * events will be handled. Does nothing if playback controls are disabled. - * - * @param event A key event. - * @return Whether the key event was handled. - */ - public boolean dispatchMediaKeyEvent(KeyEvent event) { - return useController && controller.dispatchMediaKeyEvent(event); - } - - /** - * Shows the playback controls. Does nothing if playback controls are disabled. - * - *

          The playback controls are automatically hidden during playback after {{@link - * #getControllerShowTimeoutMs()}}. They are shown indefinitely when playback has not started yet, - * is paused, has ended or failed. - */ - public void showController() { - showController(shouldShowControllerIndefinitely()); - } - - /** Hides the playback controls. Does nothing if playback controls are disabled. */ - public void hideController() { - if (controller != null) { - controller.hide(); - } - } - - /** - * Returns the playback controls timeout. The playback controls are automatically hidden after - * this duration of time has elapsed without user input and with playback or buffering in - * progress. - * - * @return The timeout in milliseconds. A non-positive value will cause the controller to remain - * visible indefinitely. - */ - public int getControllerShowTimeoutMs() { - return controllerShowTimeoutMs; - } - - /** - * Sets the playback controls timeout. The playback controls are automatically hidden after this - * duration of time has elapsed without user input and with playback or buffering in progress. - * - * @param controllerShowTimeoutMs The timeout in milliseconds. A non-positive value will cause the - * controller to remain visible indefinitely. - */ - public void setControllerShowTimeoutMs(int controllerShowTimeoutMs) { - Assertions.checkState(controller != null); - this.controllerShowTimeoutMs = controllerShowTimeoutMs; - // If controller is already visible, call showController to update the controller's timeout - // if necessary. - if (controller.isVisible()) { - showController(); - } - } - - /** 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; - } - - /** - * Returns whether the playback controls are automatically shown when playback starts, pauses, - * ends, or fails. If set to false, the playback controls can be manually operated with {@link - * #showController()} and {@link #hideController()}. - */ - public boolean getControllerAutoShow() { - return controllerAutoShow; - } - - /** - * Sets whether the playback controls are automatically shown when playback starts, pauses, ends, - * or fails. If set to false, the playback controls can be manually operated with {@link - * #showController()} and {@link #hideController()}. - * - * @param controllerAutoShow Whether the playback controls are allowed to show automatically. - */ - public void setControllerAutoShow(boolean controllerAutoShow) { - this.controllerAutoShow = controllerAutoShow; - } - - /** - * Sets whether the playback controls are hidden when ads are playing. Controls are always shown - * during ads if they are enabled and the player is paused. - * - * @param controllerHideDuringAds Whether the playback controls are hidden when ads are playing. - */ - public void setControllerHideDuringAds(boolean controllerHideDuringAds) { - this.controllerHideDuringAds = controllerHideDuringAds; - } - - /** - * Set the {@link PlaybackControlView.VisibilityListener}. - * - * @param listener The listener to be notified about visibility changes. - */ - public void setControllerVisibilityListener(PlaybackControlView.VisibilityListener listener) { - Assertions.checkState(controller != null); - controller.setVisibilityListener(listener); - } - - /** - * Sets the {@link ControlDispatcher}. - * - * @param controlDispatcher The {@link ControlDispatcher}, or null to use {@link - * DefaultControlDispatcher}. - */ - public void setControlDispatcher(@Nullable ControlDispatcher controlDispatcher) { - Assertions.checkState(controller != null); - controller.setControlDispatcher(controlDispatcher); - } - - /** - * Sets the rewind increment in milliseconds. - * - * @param rewindMs The rewind increment in milliseconds. A non-positive value will cause the - * rewind button to be disabled. - */ - public void setRewindIncrementMs(int rewindMs) { - Assertions.checkState(controller != null); - controller.setRewindIncrementMs(rewindMs); - } - - /** - * Sets the fast forward increment in milliseconds. - * - * @param fastForwardMs The fast forward increment in milliseconds. A non-positive value will - * cause the fast forward button to be disabled. - */ - public void setFastForwardIncrementMs(int fastForwardMs) { - Assertions.checkState(controller != null); - controller.setFastForwardIncrementMs(fastForwardMs); - } - - /** - * Sets which repeat toggle modes are enabled. - * - * @param repeatToggleModes A set of {@link RepeatModeUtil.RepeatToggleModes}. - */ - public void setRepeatToggleModes(@RepeatModeUtil.RepeatToggleModes int repeatToggleModes) { - Assertions.checkState(controller != null); - controller.setRepeatToggleModes(repeatToggleModes); - } - - /** - * Sets whether the shuffle button is shown. - * - * @param showShuffleButton Whether the shuffle button is shown. - */ - public void setShowShuffleButton(boolean showShuffleButton) { - Assertions.checkState(controller != null); - controller.setShowShuffleButton(showShuffleButton); - } - - /** - * 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 a: - * - *

            - *
          • {@link SurfaceView} by default, or if the {@code surface_type} attribute is set to {@code - * surface_view}. - *
          • {@link TextureView} if {@code surface_type} is {@code texture_view}. - *
          • {@code null} if {@code surface_type} is {@code none}. - *
          - * - * @return The {@link SurfaceView}, {@link TextureView} or {@code null}. - */ - public View getVideoSurfaceView() { - return surfaceView; - } - - /** - * Gets the overlay {@link FrameLayout}, which can be populated with UI elements to show on top of - * the player. - * - * @return The overlay {@link FrameLayout}, or {@code null} if the layout has been customized and - * the overlay is not present. - */ - public FrameLayout getOverlayFrameLayout() { - return overlayFrameLayout; - } - - /** - * Gets the {@link SubtitleView}. - * - * @return The {@link SubtitleView}, or {@code null} if the layout has been customized and the - * subtitle view is not present. - */ - public SubtitleView getSubtitleView() { - return subtitleView; - } - - @Override - public boolean onTouchEvent(MotionEvent ev) { - if (!useController || player == null || ev.getActionMasked() != MotionEvent.ACTION_DOWN) { - return false; - } - if (!controller.isVisible()) { - maybeShowController(true); - } else if (controllerHideOnTouch) { - controller.hide(); - } - return true; - } - - @Override - public boolean onTrackballEvent(MotionEvent ev) { - if (!useController || player == null) { - return false; - } - maybeShowController(true); - return true; - } - - /** Shows the playback controls, but only if forced or shown indefinitely. */ - private void maybeShowController(boolean isForced) { - if (isPlayingAd() && controllerHideDuringAds) { - return; - } - if (useController) { - boolean wasShowingIndefinitely = controller.isVisible() && controller.getShowTimeoutMs() <= 0; - boolean shouldShowIndefinitely = shouldShowControllerIndefinitely(); - if (isForced || wasShowingIndefinitely || shouldShowIndefinitely) { - showController(shouldShowIndefinitely); - } - } - } - - private boolean shouldShowControllerIndefinitely() { - if (player == null) { - return true; - } - int playbackState = player.getPlaybackState(); - return controllerAutoShow - && (playbackState == Player.STATE_IDLE - || playbackState == Player.STATE_ENDED - || !player.getPlayWhenReady()); - } - - private void showController(boolean showIndefinitely) { - if (!useController) { - return; - } - controller.setShowTimeoutMs(showIndefinitely ? 0 : controllerShowTimeoutMs); - controller.show(); - } - - private boolean isPlayingAd() { - return player != null && player.isPlayingAd() && player.getPlayWhenReady(); - } - - private void updateForCurrentTrackSelections() { - if (player == null) { - return; - } - TrackSelectionArray selections = player.getCurrentTrackSelections(); - for (int i = 0; i < selections.length; i++) { - if (player.getRendererType(i) == C.TRACK_TYPE_VIDEO && selections.get(i) != null) { - // Video enabled so artwork must be hidden. If the shutter is closed, it will be opened in - // onRenderedFirstFrame(). - hideArtwork(); - return; - } - } - // Video disabled so the shutter must be closed. - if (shutterView != null) { - shutterView.setVisibility(VISIBLE); - } - // Display artwork if enabled and available, else hide it. - if (useArtwork) { - for (int i = 0; i < selections.length; i++) { - TrackSelection selection = selections.get(i); - if (selection != null) { - for (int j = 0; j < selection.length(); j++) { - Metadata metadata = selection.getFormat(j).metadata; - if (metadata != null && setArtworkFromMetadata(metadata)) { - return; - } - } - } - } - if (setArtworkFromBitmap(defaultArtwork)) { - return; - } - } - // Artwork disabled or unavailable. - hideArtwork(); - } - - private boolean setArtworkFromMetadata(Metadata metadata) { - for (int i = 0; i < metadata.length(); i++) { - Metadata.Entry metadataEntry = metadata.get(i); - if (metadataEntry instanceof ApicFrame) { - byte[] bitmapData = ((ApicFrame) metadataEntry).pictureData; - Bitmap bitmap = BitmapFactory.decodeByteArray(bitmapData, 0, bitmapData.length); - return setArtworkFromBitmap(bitmap); - } - } - return false; - } - - private boolean setArtworkFromBitmap(Bitmap bitmap) { - if (bitmap != null) { - int bitmapWidth = bitmap.getWidth(); - int bitmapHeight = bitmap.getHeight(); - if (bitmapWidth > 0 && bitmapHeight > 0) { - if (contentFrame != null) { - contentFrame.setAspectRatio((float) bitmapWidth / bitmapHeight); - } - artworkView.setImageBitmap(bitmap); - artworkView.setVisibility(VISIBLE); - return true; - } - } - return false; - } - - private void hideArtwork() { - if (artworkView != null) { - artworkView.setImageResource(android.R.color.transparent); // Clears any bitmap reference. - artworkView.setVisibility(INVISIBLE); - } - } - - @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); - } - - /** Applies a texture rotation to a {@link TextureView}. */ - private static void applyTextureViewRotation(TextureView textureView, int textureViewRotation) { - float textureViewWidth = textureView.getWidth(); - float textureViewHeight = textureView.getHeight(); - if (textureViewWidth == 0 || textureViewHeight == 0 || textureViewRotation == 0) { - textureView.setTransform(null); - } else { - Matrix transformMatrix = new Matrix(); - float pivotX = textureViewWidth / 2; - float pivotY = textureViewHeight / 2; - transformMatrix.postRotate(textureViewRotation, pivotX, pivotY); - - // After rotation, scale the rotated texture to fit the TextureView size. - RectF originalTextureRect = new RectF(0, 0, textureViewWidth, textureViewHeight); - RectF rotatedTextureRect = new RectF(); - transformMatrix.mapRect(rotatedTextureRect, originalTextureRect); - transformMatrix.postScale( - textureViewWidth / rotatedTextureRect.width(), - textureViewHeight / rotatedTextureRect.height(), - pivotX, - pivotY); - textureView.setTransform(transformMatrix); - } - } - - @SuppressLint("InlinedApi") - private boolean isDpadKey(int keyCode) { - return keyCode == KeyEvent.KEYCODE_DPAD_UP - || keyCode == KeyEvent.KEYCODE_DPAD_UP_RIGHT - || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT - || keyCode == KeyEvent.KEYCODE_DPAD_DOWN_RIGHT - || keyCode == KeyEvent.KEYCODE_DPAD_DOWN - || keyCode == KeyEvent.KEYCODE_DPAD_DOWN_LEFT - || keyCode == KeyEvent.KEYCODE_DPAD_LEFT - || keyCode == KeyEvent.KEYCODE_DPAD_UP_LEFT - || keyCode == KeyEvent.KEYCODE_DPAD_CENTER; - } - - private final class ComponentListener extends Player.DefaultEventListener - implements TextOutput, SimpleExoPlayer.VideoListener, OnLayoutChangeListener { - - // TextOutput implementation - - @Override - public void onCues(List cues) { - if (subtitleView != null) { - subtitleView.onCues(cues); - } - } - - // SimpleExoPlayer.VideoInfoListener implementation - - @Override - public void onVideoSizeChanged( - int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { - if (contentFrame == null) { - return; - } - float videoAspectRatio = - (height == 0 || width == 0) ? 1 : (width * pixelWidthHeightRatio) / height; - - if (surfaceView instanceof TextureView) { - // Try to apply rotation transformation when our surface is a TextureView. - if (unappliedRotationDegrees == 90 || unappliedRotationDegrees == 270) { - // We will apply a rotation 90/270 degree to the output texture of the TextureView. - // In this case, the output video's width and height will be swapped. - videoAspectRatio = 1 / videoAspectRatio; - } - if (textureViewRotation != 0) { - surfaceView.removeOnLayoutChangeListener(this); - } - textureViewRotation = unappliedRotationDegrees; - if (textureViewRotation != 0) { - // The texture view's dimensions might be changed after layout step. - // So add an OnLayoutChangeListener to apply rotation after layout step. - surfaceView.addOnLayoutChangeListener(this); - } - applyTextureViewRotation((TextureView) surfaceView, textureViewRotation); - } - - contentFrame.setAspectRatio(videoAspectRatio); - } - - @Override - public void onRenderedFirstFrame() { - if (shutterView != null) { - shutterView.setVisibility(INVISIBLE); - } - } - - @Override - public void onTracksChanged(TrackGroupArray tracks, TrackSelectionArray selections) { - updateForCurrentTrackSelections(); - } - - // Player.EventListener implementation - - @Override - public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { - if (isPlayingAd() && controllerHideDuringAds) { - hideController(); - } else { - maybeShowController(false); - } - } - - @Override - public void onPositionDiscontinuity(@DiscontinuityReason int reason) { - if (isPlayingAd() && controllerHideDuringAds) { - hideController(); - } - } - - // OnLayoutChangeListener implementation - - @Override - public void onLayoutChange( - View view, - int left, - int top, - int right, - int bottom, - int oldLeft, - int oldTop, - int oldRight, - int oldBottom) { - applyTextureViewRotation((TextureView) view, textureViewRotation); - } - } } diff --git a/library/ui/src/main/res/layout/exo_player_control_view.xml b/library/ui/src/main/res/layout/exo_player_control_view.xml new file mode 100644 index 0000000000..fd221e5d84 --- /dev/null +++ b/library/ui/src/main/res/layout/exo_player_control_view.xml @@ -0,0 +1,18 @@ + + + + + diff --git a/library/ui/src/main/res/layout/exo_player_view.xml b/library/ui/src/main/res/layout/exo_player_view.xml new file mode 100644 index 0000000000..dc6dda1667 --- /dev/null +++ b/library/ui/src/main/res/layout/exo_player_view.xml @@ -0,0 +1,18 @@ + + + + + diff --git a/library/ui/src/main/res/values/attrs.xml b/library/ui/src/main/res/values/attrs.xml index b6ed4b17af..24fa8a2091 100644 --- a/library/ui/src/main/res/values/attrs.xml +++ b/library/ui/src/main/res/values/attrs.xml @@ -41,7 +41,7 @@
          - + @@ -52,7 +52,7 @@ - + @@ -65,7 +65,7 @@ - + diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java index 7164fa13ab..40d5b6c3f9 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java @@ -32,6 +32,16 @@ import com.google.android.exoplayer2.trackselection.TrackSelectionArray; */ public abstract class StubExoPlayer implements ExoPlayer { + @Override + public VideoComponent getVideoComponent() { + throw new UnsupportedOperationException(); + } + + @Override + public TextComponent getTextComponent() { + throw new UnsupportedOperationException(); + } + @Override public Looper getPlaybackLooper() { throw new UnsupportedOperationException(); From 029c95832cea5a4ac81b1afb75b764595e7338d2 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 18 Jan 2018 08:20:35 -0800 Subject: [PATCH 1059/2472] Add queue abstraction to ExoPlayerImplInternal. This gets rid of the manual tracking of this queue with reading, playing, and loading period holders. Still keeping these names for queue access methods. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=182378944 --- .../exoplayer2/ExoPlayerImplInternal.java | 420 +++++++++++------- 1 file changed, 269 insertions(+), 151 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 2647a44dee..b8667ce6d1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -118,6 +118,7 @@ import java.util.Collections; private final PlaybackInfoUpdate playbackInfoUpdate; private final ArrayList customMessageInfos; private final Clock clock; + private final MediaPeriodHolderQueue queue; @SuppressWarnings("unused") private SeekParameters seekParameters; @@ -136,10 +137,6 @@ import java.util.Collections; private long rendererPositionUs; private int nextCustomMessageInfoIndex; - private MediaPeriodHolder loadingPeriodHolder; - private MediaPeriodHolder readingPeriodHolder; - private MediaPeriodHolder playingPeriodHolder; - public ExoPlayerImplInternal( Renderer[] renderers, TrackSelector trackSelector, @@ -161,6 +158,7 @@ import java.util.Collections; this.eventHandler = eventHandler; this.player = player; this.clock = clock; + this.queue = new MediaPeriodHolderQueue(); backBufferDurationUs = loadControl.getBackBufferDurationUs(); retainBackBufferFromKeyframe = loadControl.retainBackBufferFromKeyframe(); @@ -444,8 +442,7 @@ import java.util.Collections; private void validateExistingPeriodHolders() throws ExoPlaybackException { // Find the last existing period holder that matches the new period order. - MediaPeriodHolder lastValidPeriodHolder = playingPeriodHolder != null - ? playingPeriodHolder : loadingPeriodHolder; + MediaPeriodHolder lastValidPeriodHolder = queue.getFrontPeriod(); if (lastValidPeriodHolder == null) { return; } @@ -465,30 +462,19 @@ import java.util.Collections; } // Release any period holders that don't match the new period order. - int loadingPeriodHolderIndex = loadingPeriodHolder.index; - int readingPeriodHolderIndex = - readingPeriodHolder != null ? readingPeriodHolder.index : C.INDEX_UNSET; - if (lastValidPeriodHolder.next != null) { - releasePeriodHoldersFrom(lastValidPeriodHolder.next); - lastValidPeriodHolder.next = null; - } + boolean readingPeriodRemoved = queue.removeAfter(lastValidPeriodHolder); // Update the period info for the last holder, as it may now be the last period in the timeline. lastValidPeriodHolder.info = mediaPeriodInfoSequence.getUpdatedMediaPeriodInfo(lastValidPeriodHolder.info); - // Handle cases where loadingPeriodHolder or readingPeriodHolder have been removed. - boolean seenLoadingPeriodHolder = loadingPeriodHolderIndex <= lastValidPeriodHolder.index; - if (!seenLoadingPeriodHolder) { - loadingPeriodHolder = lastValidPeriodHolder; - } - boolean seenReadingPeriodHolder = readingPeriodHolderIndex != C.INDEX_UNSET - && readingPeriodHolderIndex <= lastValidPeriodHolder.index; - if (!seenReadingPeriodHolder && playingPeriodHolder != null) { + if (readingPeriodRemoved && queue.hasPlayingPeriod()) { // Renderers may have read from a period that's been removed. Seek back to the current // position of the playing period to make sure none of the removed period is played. - MediaPeriodId periodId = playingPeriodHolder.info.id; - long newPositionUs = seekToPeriodPosition(periodId, playbackInfo.positionUs); + MediaPeriodId periodId = queue.getPlayingPeriod().info.id; + long newPositionUs = + seekToPeriodPosition( + periodId, playbackInfo.positionUs, /* forceDisableRenderers= */ true); if (newPositionUs != playbackInfo.positionUs) { playbackInfo = playbackInfo.fromNewPosition(periodId, newPositionUs, playbackInfo.contentPositionUs); @@ -513,11 +499,12 @@ import java.util.Collections; } private void updatePlaybackPositions() throws ExoPlaybackException { - if (playingPeriodHolder == null) { + if (!queue.hasPlayingPeriod()) { return; } // Update the playback position. + MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod(); long periodPositionUs = playingPeriodHolder.mediaPeriod.readDiscontinuity(); if (periodPositionUs != C.TIME_UNSET) { resetRendererPosition(periodPositionUs); @@ -545,12 +532,13 @@ import java.util.Collections; private void doSomeWork() throws ExoPlaybackException, IOException { long operationStartTimeMs = clock.uptimeMillis(); updatePeriods(); - if (playingPeriodHolder == null) { + if (!queue.hasPlayingPeriod()) { // We're still waiting for the first period to be prepared. maybeThrowPeriodPrepareError(); scheduleNextWork(operationStartTimeMs, PREPARING_SOURCE_INTERVAL_MS); return; } + MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod(); TraceUtil.beginSection("doSomeWork"); @@ -594,10 +582,13 @@ import java.util.Collections; stopRenderers(); } else if (playbackInfo.playbackState == Player.STATE_BUFFERING) { float playbackSpeed = mediaClock.getPlaybackParameters().speed; - boolean isNewlyReady = enabledRenderers.length > 0 - ? (allRenderersReadyOrEnded && loadingPeriodHolder.haveSufficientBuffer( - rendererPositionUs, playbackSpeed, rebuffering)) - : isTimelineReady(playingPeriodDurationUs); + boolean isNewlyReady = + enabledRenderers.length > 0 + ? (allRenderersReadyOrEnded + && queue + .getLoadingPeriod() + .haveSufficientBuffer(rendererPositionUs, playbackSpeed, rebuffering)) + : isTimelineReady(playingPeriodDurationUs); if (isNewlyReady) { setState(Player.STATE_READY); if (playWhenReady) { @@ -672,6 +663,7 @@ import java.util.Collections; try { long newPeriodPositionUs = periodPositionUs; if (periodId.equals(playbackInfo.periodId)) { + MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod(); if (playingPeriodHolder != null && newPeriodPositionUs != 0) { newPeriodPositionUs = playingPeriodHolder.mediaPeriod.getAdjustedSeekPositionUs( @@ -698,58 +690,50 @@ import java.util.Collections; private long seekToPeriodPosition(MediaPeriodId periodId, long periodPositionUs) throws ExoPlaybackException { + // Force disable renderers if they are reading from a period other than the one being played. + return seekToPeriodPosition( + periodId, periodPositionUs, queue.getPlayingPeriod() != queue.getReadingPeriod()); + } + + private long seekToPeriodPosition( + MediaPeriodId periodId, long periodPositionUs, boolean forceDisableRenderers) + throws ExoPlaybackException { stopRenderers(); rebuffering = false; setState(Player.STATE_BUFFERING); - MediaPeriodHolder newPlayingPeriodHolder = null; - if (playingPeriodHolder == null) { - // We're still waiting for the first period to be prepared. - if (loadingPeriodHolder != null) { - loadingPeriodHolder.release(); - } - } else { - // Clear the timeline, but keep the requested period if it is already prepared. - MediaPeriodHolder periodHolder = playingPeriodHolder; - while (periodHolder != null) { - if (newPlayingPeriodHolder == null - && shouldKeepPeriodHolder(periodId, periodPositionUs, periodHolder)) { - newPlayingPeriodHolder = periodHolder; - } else { - periodHolder.release(); - } - periodHolder = periodHolder.next; + // Clear the timeline, but keep the requested period if it is already prepared. + MediaPeriodHolder oldPlayingPeriodHolder = queue.getPlayingPeriod(); + MediaPeriodHolder newPlayingPeriodHolder = oldPlayingPeriodHolder; + while (newPlayingPeriodHolder != null) { + if (shouldKeepPeriodHolder(periodId, periodPositionUs, newPlayingPeriodHolder)) { + queue.removeAfter(newPlayingPeriodHolder); + break; } + newPlayingPeriodHolder = queue.advancePlayingPeriod(); } - // Disable all the renderers if the period being played is changing, or if the renderers are - // reading from a period other than the one being played. - if (playingPeriodHolder != newPlayingPeriodHolder - || playingPeriodHolder != readingPeriodHolder) { + // Disable all the renderers if the period being played is changing, or if forced. + if (oldPlayingPeriodHolder != newPlayingPeriodHolder || forceDisableRenderers) { for (Renderer renderer : enabledRenderers) { disableRenderer(renderer); } enabledRenderers = new Renderer[0]; - playingPeriodHolder = null; + oldPlayingPeriodHolder = null; } // Update the holders. if (newPlayingPeriodHolder != null) { - newPlayingPeriodHolder.next = null; - loadingPeriodHolder = newPlayingPeriodHolder; - readingPeriodHolder = newPlayingPeriodHolder; - setPlayingPeriodHolder(newPlayingPeriodHolder); - if (playingPeriodHolder.hasEnabledTracks) { - periodPositionUs = playingPeriodHolder.mediaPeriod.seekToUs(periodPositionUs); - playingPeriodHolder.mediaPeriod.discardBuffer(periodPositionUs - backBufferDurationUs, - retainBackBufferFromKeyframe); + updatePlayingPeriodRenderers(oldPlayingPeriodHolder); + if (newPlayingPeriodHolder.hasEnabledTracks) { + periodPositionUs = newPlayingPeriodHolder.mediaPeriod.seekToUs(periodPositionUs); + newPlayingPeriodHolder.mediaPeriod.discardBuffer( + periodPositionUs - backBufferDurationUs, retainBackBufferFromKeyframe); } resetRendererPosition(periodPositionUs); maybeContinueLoading(); } else { - loadingPeriodHolder = null; - readingPeriodHolder = null; - playingPeriodHolder = null; + queue.clear(); resetRendererPosition(periodPositionUs); } @@ -771,9 +755,10 @@ import java.util.Collections; } private void resetRendererPosition(long periodPositionUs) throws ExoPlaybackException { - rendererPositionUs = playingPeriodHolder == null - ? periodPositionUs + RENDERER_TIMESTAMP_OFFSET_US - : playingPeriodHolder.toRendererTime(periodPositionUs); + rendererPositionUs = + !queue.hasPlayingPeriod() + ? periodPositionUs + RENDERER_TIMESTAMP_OFFSET_US + : queue.getPlayingPeriod().toRendererTime(periodPositionUs); mediaClock.resetPosition(rendererPositionUs); for (Renderer renderer : enabledRenderers) { renderer.resetPosition(rendererPositionUs); @@ -825,11 +810,7 @@ import java.util.Collections; } } enabledRenderers = new Renderer[0]; - releasePeriodHoldersFrom(playingPeriodHolder != null ? playingPeriodHolder - : loadingPeriodHolder); - loadingPeriodHolder = null; - readingPeriodHolder = null; - playingPeriodHolder = null; + queue.clear(); setIsLoading(false); Timeline timeline = playbackInfo.timeline; int firstPeriodIndex = @@ -1030,13 +1011,14 @@ import java.util.Collections; } private void reselectTracksInternal() throws ExoPlaybackException { - if (playingPeriodHolder == null) { + if (!queue.hasPlayingPeriod()) { // We don't have tracks yet, so we don't care. return; } float playbackSpeed = mediaClock.getPlaybackParameters().speed; // Reselect tracks on each period in turn, until the selection changes. - MediaPeriodHolder periodHolder = playingPeriodHolder; + MediaPeriodHolder periodHolder = queue.getPlayingPeriod(); + MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod(); boolean selectionsChangedForReadPeriod = true; while (true) { if (periodHolder == null || !periodHolder.prepared) { @@ -1056,11 +1038,8 @@ import java.util.Collections; if (selectionsChangedForReadPeriod) { // Update streams and rebuffer for the new selection, recreating all streams if reading ahead. - boolean recreateStreams = readingPeriodHolder != playingPeriodHolder; - releasePeriodHoldersFrom(playingPeriodHolder.next); - playingPeriodHolder.next = null; - loadingPeriodHolder = playingPeriodHolder; - readingPeriodHolder = playingPeriodHolder; + MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod(); + boolean recreateStreams = queue.removeAfter(playingPeriodHolder); boolean[] streamResetFlags = new boolean[renderers.length]; long periodPositionUs = playingPeriodHolder.updatePeriodTrackSelection( @@ -1092,21 +1071,17 @@ import java.util.Collections; } } } - playbackInfo = playbackInfo.copyWithTrackSelectorResult(periodHolder.trackSelectorResult); + playbackInfo = + playbackInfo.copyWithTrackSelectorResult(playingPeriodHolder.trackSelectorResult); enableRenderers(rendererWasEnabledFlags, enabledRendererCount); } else { // Release and re-prepare/buffer periods after the one whose selection changed. - loadingPeriodHolder = periodHolder; - periodHolder = loadingPeriodHolder.next; - while (periodHolder != null) { - periodHolder.release(); - periodHolder = periodHolder.next; - } - loadingPeriodHolder.next = null; - if (loadingPeriodHolder.prepared) { - long loadingPeriodPositionUs = Math.max(loadingPeriodHolder.info.startPositionUs, - loadingPeriodHolder.toPeriodTime(rendererPositionUs)); - loadingPeriodHolder.updatePeriodTrackSelection(loadingPeriodPositionUs, false); + queue.removeAfter(periodHolder); + if (periodHolder.prepared) { + long loadingPeriodPositionUs = + Math.max( + periodHolder.info.startPositionUs, periodHolder.toPeriodTime(rendererPositionUs)); + periodHolder.updatePeriodTrackSelection(loadingPeriodPositionUs, false); } } if (playbackInfo.playbackState != Player.STATE_ENDED) { @@ -1117,8 +1092,7 @@ import java.util.Collections; } private void updateTrackSelectionPlaybackSpeed(float playbackSpeed) { - MediaPeriodHolder periodHolder = - playingPeriodHolder != null ? playingPeriodHolder : loadingPeriodHolder; + MediaPeriodHolder periodHolder = queue.getFrontPeriod(); while (periodHolder != null) { if (periodHolder.trackSelectorResult != null) { TrackSelection[] trackSelections = periodHolder.trackSelectorResult.selections.getAll(); @@ -1133,6 +1107,7 @@ import java.util.Collections; } private boolean isTimelineReady(long playingPeriodDurationUs) { + MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod(); return playingPeriodDurationUs == C.TIME_UNSET || playbackInfo.positionUs < playingPeriodDurationUs || (playingPeriodHolder.next != null @@ -1140,6 +1115,8 @@ import java.util.Collections; } private void maybeThrowPeriodPrepareError() throws IOException { + MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod(); + MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod(); if (loadingPeriodHolder != null && !loadingPeriodHolder.prepared && (readingPeriodHolder == null || readingPeriodHolder.next == loadingPeriodHolder)) { for (Renderer renderer : enabledRenderers) { @@ -1202,8 +1179,7 @@ import java.util.Collections; } int playingPeriodIndex = playbackInfo.periodId.periodIndex; - MediaPeriodHolder periodHolder = playingPeriodHolder != null ? playingPeriodHolder - : loadingPeriodHolder; + MediaPeriodHolder periodHolder = queue.getFrontPeriod(); if (periodHolder == null && playingPeriodIndex >= oldTimeline.getPeriodCount()) { return; } @@ -1283,22 +1259,15 @@ import java.util.Collections; periodHolder = updatePeriodInfo(periodHolder, periodIndex); } else { // The holder is inconsistent with the new timeline. - boolean seenReadingPeriodHolder = - readingPeriodHolder != null && readingPeriodHolder.index < periodHolder.index; - if (!seenReadingPeriodHolder) { + boolean readingPeriodRemoved = queue.removeAfter(previousPeriodHolder); + if (readingPeriodRemoved) { // Renderers may have read from a period that's been removed. Seek back to the current // position of the playing period to make sure none of the removed period is played. + MediaPeriodId id = queue.getPlayingPeriod().info.id; long newPositionUs = - seekToPeriodPosition(playingPeriodHolder.info.id, playbackInfo.positionUs); - playbackInfo = playbackInfo.fromNewPosition(playingPeriodHolder.info.id, newPositionUs, - playbackInfo.contentPositionUs); - } else { - // Update the loading period to be the last period that's still valid, and release all - // subsequent periods. - loadingPeriodHolder = previousPeriodHolder; - loadingPeriodHolder.next = null; - // Release the rest of the timeline. - releasePeriodHoldersFrom(periodHolder); + seekToPeriodPosition(id, playbackInfo.positionUs, /* forceDisableRenderers= */ true); + playbackInfo = + playbackInfo.fromNewPosition(id, newPositionUs, playbackInfo.contentPositionUs); } break; } @@ -1426,19 +1395,21 @@ import java.util.Collections; // Update the loading period if required. maybeUpdateLoadingPeriod(); - + MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod(); if (loadingPeriodHolder == null || loadingPeriodHolder.isFullyBuffered()) { setIsLoading(false); } else if (loadingPeriodHolder != null && !playbackInfo.isLoading) { maybeContinueLoading(); } - if (playingPeriodHolder == null) { + if (!queue.hasPlayingPeriod()) { // We're waiting for the first period to be prepared. return; } // Advance the playing period if necessary. + MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod(); + MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod(); boolean advancedPlayingPeriod = false; while (playWhenReady && playingPeriodHolder != readingPeriodHolder && rendererPositionUs >= playingPeriodHolder.next.rendererPositionOffsetUs) { @@ -1452,8 +1423,9 @@ import java.util.Collections; playingPeriodHolder.info.isLastInTimelinePeriod ? Player.DISCONTINUITY_REASON_PERIOD_TRANSITION : Player.DISCONTINUITY_REASON_AD_INSERTION; - playingPeriodHolder.release(); - setPlayingPeriodHolder(playingPeriodHolder.next); + MediaPeriodHolder oldPlayingPeriodHolder = playingPeriodHolder; + playingPeriodHolder = queue.advancePlayingPeriod(); + updatePlayingPeriodRenderers(oldPlayingPeriodHolder); playbackInfo = playbackInfo.fromNewPosition(playingPeriodHolder.info.id, playingPeriodHolder.info.startPositionUs, playingPeriodHolder.info.contentPositionUs); playbackInfoUpdate.setPositionDiscontinuity(discontinuityReason); @@ -1492,7 +1464,7 @@ import java.util.Collections; } TrackSelectorResult oldTrackSelectorResult = readingPeriodHolder.trackSelectorResult; - readingPeriodHolder = readingPeriodHolder.next; + readingPeriodHolder = queue.advanceReadingPeriod(); TrackSelectorResult newTrackSelectorResult = readingPeriodHolder.trackSelectorResult; boolean initialDiscontinuity = @@ -1536,6 +1508,7 @@ import java.util.Collections; private void maybeUpdateLoadingPeriod() throws IOException { MediaPeriodInfo info; + MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod(); if (loadingPeriodHolder == null) { info = mediaPeriodInfoSequence.getFirstMediaPeriodInfo(playbackInfo); } else { @@ -1544,12 +1517,9 @@ import java.util.Collections; || loadingPeriodHolder.info.durationUs == C.TIME_UNSET) { return; } - if (playingPeriodHolder != null) { - int bufferAheadPeriodCount = loadingPeriodHolder.index - playingPeriodHolder.index; - if (bufferAheadPeriodCount == MAXIMUM_BUFFER_AHEAD_PERIODS) { - // We are already buffering the maximum number of periods ahead. - return; - } + if (queue.getLength() == MAXIMUM_BUFFER_AHEAD_PERIODS) { + // We are already buffering the maximum number of periods ahead. + return; } info = mediaPeriodInfoSequence.getNextMediaPeriodInfo(loadingPeriodHolder.info, loadingPeriodHolder.getRendererOffset(), rendererPositionUs); @@ -1563,34 +1533,40 @@ import java.util.Collections; loadingPeriodHolder == null ? (info.startPositionUs + RENDERER_TIMESTAMP_OFFSET_US) : (loadingPeriodHolder.getRendererOffset() + loadingPeriodHolder.info.durationUs); - int holderIndex = loadingPeriodHolder == null ? 0 : loadingPeriodHolder.index + 1; Object uid = playbackInfo.timeline.getPeriod(info.id.periodIndex, period, true).uid; - MediaPeriodHolder newPeriodHolder = new MediaPeriodHolder(renderers, rendererCapabilities, - rendererPositionOffsetUs, trackSelector, loadControl, mediaSource, uid, holderIndex, info); - if (loadingPeriodHolder != null) { - loadingPeriodHolder.next = newPeriodHolder; - } - loadingPeriodHolder = newPeriodHolder; - loadingPeriodHolder.mediaPeriod.prepare(this, info.startPositionUs); + MediaPeriodHolder newPeriodHolder = + new MediaPeriodHolder( + renderers, + rendererCapabilities, + rendererPositionOffsetUs, + trackSelector, + loadControl, + mediaSource, + uid, + info); + queue.enqueueLoadingPeriod(newPeriodHolder); + newPeriodHolder.mediaPeriod.prepare(this, info.startPositionUs); setIsLoading(true); } private void handlePeriodPrepared(MediaPeriod period) throws ExoPlaybackException { + MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod(); if (loadingPeriodHolder == null || loadingPeriodHolder.mediaPeriod != period) { // Stale event. return; } loadingPeriodHolder.handlePrepared(mediaClock.getPlaybackParameters().speed); - if (playingPeriodHolder == null) { + if (!queue.hasPlayingPeriod()) { // This is the first prepared period, so start playing it. - readingPeriodHolder = loadingPeriodHolder; - resetRendererPosition(readingPeriodHolder.info.startPositionUs); - setPlayingPeriodHolder(readingPeriodHolder); + MediaPeriodHolder playingPeriodHolder = queue.advancePlayingPeriod(); + resetRendererPosition(playingPeriodHolder.info.startPositionUs); + updatePlayingPeriodRenderers(/* oldPlayingPeriodHolder= */ null); } maybeContinueLoading(); } private void handleContinueLoadingRequested(MediaPeriod period) { + MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod(); if (loadingPeriodHolder == null || loadingPeriodHolder.mediaPeriod != period) { // Stale event. return; @@ -1600,6 +1576,7 @@ import java.util.Collections; } private void maybeContinueLoading() { + MediaPeriodHolder loadingPeriodHolder = queue.getLoadingPeriod(); boolean continueLoading = loadingPeriodHolder.shouldContinueLoading( rendererPositionUs, mediaClock.getPlaybackParameters().speed); setIsLoading(continueLoading); @@ -1608,38 +1585,32 @@ import java.util.Collections; } } - private void releasePeriodHoldersFrom(MediaPeriodHolder periodHolder) { - while (periodHolder != null) { - periodHolder.release(); - periodHolder = periodHolder.next; - } - } - - private void setPlayingPeriodHolder(MediaPeriodHolder periodHolder) throws ExoPlaybackException { - if (playingPeriodHolder == periodHolder) { + private void updatePlayingPeriodRenderers(@Nullable MediaPeriodHolder oldPlayingPeriodHolder) + throws ExoPlaybackException { + MediaPeriodHolder newPlayingPeriodHolder = queue.getPlayingPeriod(); + if (newPlayingPeriodHolder == null || oldPlayingPeriodHolder == newPlayingPeriodHolder) { return; } - int enabledRendererCount = 0; boolean[] rendererWasEnabledFlags = new boolean[renderers.length]; for (int i = 0; i < renderers.length; i++) { Renderer renderer = renderers[i]; rendererWasEnabledFlags[i] = renderer.getState() != Renderer.STATE_DISABLED; - if (periodHolder.trackSelectorResult.renderersEnabled[i]) { + if (newPlayingPeriodHolder.trackSelectorResult.renderersEnabled[i]) { enabledRendererCount++; } - if (rendererWasEnabledFlags[i] && (!periodHolder.trackSelectorResult.renderersEnabled[i] - || (renderer.isCurrentStreamFinal() - && renderer.getStream() == playingPeriodHolder.sampleStreams[i]))) { + if (rendererWasEnabledFlags[i] + && (!newPlayingPeriodHolder.trackSelectorResult.renderersEnabled[i] + || (renderer.isCurrentStreamFinal() + && renderer.getStream() == oldPlayingPeriodHolder.sampleStreams[i]))) { // The renderer should be disabled before playing the next period, either because it's not // needed to play the next period, or because we need to re-enable it as its current stream // is final and it's not reading ahead. disableRenderer(renderer); } } - - playingPeriodHolder = periodHolder; - playbackInfo = playbackInfo.copyWithTrackSelectorResult(periodHolder.trackSelectorResult); + playbackInfo = + playbackInfo.copyWithTrackSelectorResult(newPlayingPeriodHolder.trackSelectorResult); enableRenderers(rendererWasEnabledFlags, enabledRendererCount); } @@ -1647,6 +1618,7 @@ import java.util.Collections; throws ExoPlaybackException { enabledRenderers = new Renderer[totalEnabledRendererCount]; int enabledRendererCount = 0; + MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod(); for (int i = 0; i < renderers.length; i++) { if (playingPeriodHolder.trackSelectorResult.renderersEnabled[i]) { enableRenderer(i, rendererWasEnabledFlags[i], enabledRendererCount++); @@ -1656,6 +1628,7 @@ import java.util.Collections; private void enableRenderer(int rendererIndex, boolean wasRendererEnabled, int enabledRendererIndex) throws ExoPlaybackException { + MediaPeriodHolder playingPeriodHolder = queue.getPlayingPeriod(); Renderer renderer = renderers[rendererIndex]; enabledRenderers[enabledRendererIndex] = renderer; if (renderer.getState() == Renderer.STATE_DISABLED) { @@ -1681,6 +1654,7 @@ import java.util.Collections; } private boolean rendererWaitingForNextStream(Renderer renderer) { + MediaPeriodHolder readingPeriodHolder = queue.getReadingPeriod(); return readingPeriodHolder.next != null && readingPeriodHolder.next.prepared && renderer.hasReadStreamToEnd(); } @@ -1696,6 +1670,146 @@ import java.util.Collections; return formats; } + /** + * Holds a queue of {@link MediaPeriodHolder}s from the currently playing period holder at the + * front to the loading period holder at the end of the queue. Also has a reference to the reading + * period holder. + */ + private static final class MediaPeriodHolderQueue { + + private MediaPeriodHolder playing; + private MediaPeriodHolder reading; + private MediaPeriodHolder loading; + private int length; + + /** + * Returns the loading period holder which is at the end of the queue, or null if the queue is + * empty. + */ + public MediaPeriodHolder getLoadingPeriod() { + return loading; + } + + /** + * Returns the playing period holder which is at the front of the queue, or null if the queue is + * empty or hasn't started playing. + */ + public MediaPeriodHolder getPlayingPeriod() { + return playing; + } + + /** + * Returns the reading period holder, or null if the queue is empty or the player hasn't started + * reading. + */ + public MediaPeriodHolder getReadingPeriod() { + return reading; + } + + /** + * Returns the period holder in the front of the queue which is the playing period holder when + * playing, or null if the queue is empty. + */ + public MediaPeriodHolder getFrontPeriod() { + return hasPlayingPeriod() ? playing : loading; + } + + /** Returns the current length of the queue. */ + public int getLength() { + return length; + } + + /** Returns whether the reading and playing period holders are set. */ + public boolean hasPlayingPeriod() { + return playing != null; + } + + /** + * Continues reading from the next period holder in the queue. + * + * @return The updated reading period holder. + */ + public MediaPeriodHolder advanceReadingPeriod() { + Assertions.checkState(reading != null && reading.next != null); + reading = reading.next; + return reading; + } + + /** Enqueues a new period holder at the end, which becomes the new loading period holder. */ + public void enqueueLoadingPeriod(MediaPeriodHolder mediaPeriodHolder) { + Assertions.checkState(mediaPeriodHolder != null); + if (loading != null) { + Assertions.checkState(hasPlayingPeriod()); + loading.next = mediaPeriodHolder; + } + loading = mediaPeriodHolder; + length++; + } + + /** + * Dequeues the playing period holder from the front of the queue and advances the playing + * period holder to be the next item in the queue. If the playing period holder is unset, set it + * to the item in the front of the queue. + * + * @return The updated playing period holder, or null if the queue is or becomes empty. + */ + public MediaPeriodHolder advancePlayingPeriod() { + if (playing != null) { + if (playing == reading) { + reading = playing.next; + } + playing.release(); + playing = playing.next; + length--; + if (length == 0) { + loading = null; + } + } else { + playing = loading; + reading = loading; + } + return playing; + } + + /** + * Removes all period holders after the given period holder. This process may also remove the + * currently reading period holder. If that is the case, the reading period holder is set to be + * the same as the playing period holder at the front of the queue. + * + * @param mediaPeriodHolder The media period holder that shall be the new end of the queue. + * @return Whether the reading period has been removed. + */ + public boolean removeAfter(MediaPeriodHolder mediaPeriodHolder) { + Assertions.checkState(mediaPeriodHolder != null); + boolean removedReading = false; + loading = mediaPeriodHolder; + while (mediaPeriodHolder.next != null) { + mediaPeriodHolder = mediaPeriodHolder.next; + if (mediaPeriodHolder == reading) { + reading = playing; + removedReading = true; + } + mediaPeriodHolder.release(); + length--; + } + loading.next = null; + return removedReading; + } + + /** Clears the queue. */ + public void clear() { + MediaPeriodHolder front = getFrontPeriod(); + if (front != null) { + front.release(); + removeAfter(front); + } + playing = null; + loading = null; + reading = null; + length = 0; + } + } + /** * Holds a {@link MediaPeriod} with information required to play it as part of a timeline. */ @@ -1703,7 +1817,6 @@ import java.util.Collections; public final MediaPeriod mediaPeriod; public final Object uid; - public final int index; public final SampleStream[] sampleStreams; public final boolean[] mayRetainStreamFlags; @@ -1722,9 +1835,15 @@ import java.util.Collections; private TrackSelectorResult periodTrackSelectorResult; - public MediaPeriodHolder(Renderer[] renderers, RendererCapabilities[] rendererCapabilities, - long rendererPositionOffsetUs, TrackSelector trackSelector, LoadControl loadControl, - MediaSource mediaSource, Object periodUid, int index, MediaPeriodInfo info) { + public MediaPeriodHolder( + Renderer[] renderers, + RendererCapabilities[] rendererCapabilities, + long rendererPositionOffsetUs, + TrackSelector trackSelector, + LoadControl loadControl, + MediaSource mediaSource, + Object periodUid, + MediaPeriodInfo info) { this.renderers = renderers; this.rendererCapabilities = rendererCapabilities; this.rendererPositionOffsetUs = rendererPositionOffsetUs - info.startPositionUs; @@ -1732,7 +1851,6 @@ import java.util.Collections; this.loadControl = loadControl; this.mediaSource = mediaSource; this.uid = Assertions.checkNotNull(periodUid); - this.index = index; this.info = info; sampleStreams = new SampleStream[renderers.length]; mayRetainStreamFlags = new boolean[renderers.length]; From 68387f98ee836d4b4491866162ac74693f441063 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 18 Jan 2018 09:11:46 -0800 Subject: [PATCH 1060/2472] Simplify demo app by moving EventLogger into core It seems good to have EventLogger available from the library. In particular because when app developers use it and then submit bug reports, it makes it much easier to work out what happened. It will also allow EventLogger to be used across our (now multiple) demo apps. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=182389407 --- RELEASENOTES.md | 1 + .../com/google/android/exoplayer2/demo/PlayerActivity.java | 1 + .../com/google/android/exoplayer2/util}/EventLogger.java | 6 +++--- 3 files changed, 5 insertions(+), 3 deletions(-) rename {demos/main/src/main/java/com/google/android/exoplayer2/demo => library/core/src/main/java/com/google/android/exoplayer2/util}/EventLogger.java (99%) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 1a54a44058..b787838f41 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -69,6 +69,7 @@ cache after deciding to bypass cache. * IMA extension: Add support for playing non-Extractor content MediaSources in the IMA demo app ([#3676](https://github.com/google/ExoPlayer/issues/3676)). +* `EventLogger` moved from the demo app into the core library. ### 2.6.1 ### diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index a2e671bd11..f2c728f516 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -73,6 +73,7 @@ import com.google.android.exoplayer2.ui.PlayerView; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.upstream.HttpDataSource; +import com.google.android.exoplayer2.util.EventLogger; import com.google.android.exoplayer2.util.Util; import java.net.CookieHandler; import java.net.CookieManager; diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java b/library/core/src/main/java/com/google/android/exoplayer2/util/EventLogger.java similarity index 99% rename from demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java rename to library/core/src/main/java/com/google/android/exoplayer2/util/EventLogger.java index 9d28aa47f0..3a178a7f4a 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/EventLogger.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/EventLogger.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.demo; +package com.google.android.exoplayer2.util; import android.os.SystemClock; import android.util.Log; @@ -53,8 +53,8 @@ import java.io.IOException; import java.text.NumberFormat; import java.util.Locale; -/** Logs player events using {@link Log}. */ -/* package */ final class EventLogger +/** Logs events from {@link Player} and other core components using {@link Log}. */ +public class EventLogger implements Player.EventListener, MetadataOutput, AudioRendererEventListener, From 06be0fd712e6c48741752f4f43738ba522e152d0 Mon Sep 17 00:00:00 2001 From: kqyang Date: Thu, 18 Jan 2018 11:11:25 -0800 Subject: [PATCH 1061/2472] Add SAMPLE-AES-CTR, which replaces SAMPLE-AES-CENC per latest spefication: https://storage.googleapis.com/wvdocs/Widevine_DRM_HLS.pdf. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=182407790 --- .../hls/playlist/HlsPlaylistParser.java | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/library/hls/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 index 9bdb01c2e4..100c4c78e6 100644 --- a/library/hls/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 @@ -76,7 +76,9 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser Date: Fri, 19 Jan 2018 01:45:09 -0800 Subject: [PATCH 1062/2472] Mitigate OOM at poorly interleaved Mp4 streams. When determining the next sample to load, the Mp4Extractor now takes into account how far one stream is reading ahead of the others. If one stream is reading ahead more than a threshold (default: 10 seconds), the extractor continues reading the other stream even though it needs to reload the source at a new position. GitHub:#3481 GitHub:#3214 GitHub:#3670 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=182504396 --- .../extractor/mp4/Mp4Extractor.java | 127 ++++++++++++++---- 1 file changed, 104 insertions(+), 23 deletions(-) diff --git a/library/core/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 index 2c56f9ac2f..112c2d1ba0 100644 --- a/library/core/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 @@ -88,6 +88,12 @@ public final class Mp4Extractor implements Extractor, SeekMap { */ private static final long RELOAD_MINIMUM_SEEK_DISTANCE = 256 * 1024; + /** + * For poorly interleaved streams, the maximum byte difference one track is allowed to be read + * ahead before the source will be reloaded at a new position to read another track. + */ + private static final long MAXIMUM_READ_AHEAD_BYTES_STREAM = 10 * 1024 * 1024; + private final @Flags int flags; // Temporary arrays. @@ -103,12 +109,14 @@ public final class Mp4Extractor implements Extractor, SeekMap { private int atomHeaderBytesRead; private ParsableByteArray atomData; + private int sampleTrackIndex; private int sampleBytesWritten; private int sampleCurrentNalBytesRemaining; // Extractor outputs. private ExtractorOutput extractorOutput; private Mp4Track[] tracks; + private long[][] accumulatedSampleSizes; private int firstVideoTrackIndex; private long durationUs; private boolean isQuickTime; @@ -132,6 +140,7 @@ public final class Mp4Extractor implements Extractor, SeekMap { containerAtoms = new Stack<>(); nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE); nalLength = new ParsableByteArray(4); + sampleTrackIndex = C.INDEX_UNSET; } @Override @@ -148,6 +157,7 @@ public final class Mp4Extractor implements Extractor, SeekMap { public void seek(long position, long timeUs) { containerAtoms.clear(); atomHeaderBytesRead = 0; + sampleTrackIndex = C.INDEX_UNSET; sampleBytesWritten = 0; sampleCurrentNalBytesRemaining = 0; if (position == 0) { @@ -426,6 +436,7 @@ public final class Mp4Extractor implements Extractor, SeekMap { this.firstVideoTrackIndex = firstVideoTrackIndex; this.durationUs = durationUs; this.tracks = tracks.toArray(new Mp4Track[tracks.size()]); + accumulatedSampleSizes = calculateAccumulatedSampleSizes(this.tracks); extractorOutput.endTracks(); extractorOutput.seekMap(this); @@ -449,26 +460,29 @@ public final class Mp4Extractor implements Extractor, SeekMap { */ private int readSample(ExtractorInput input, PositionHolder positionHolder) throws IOException, InterruptedException { - int trackIndex = getTrackIndexOfEarliestCurrentSample(); - if (trackIndex == C.INDEX_UNSET) { - return RESULT_END_OF_INPUT; + long inputPosition = input.getPosition(); + if (sampleTrackIndex == C.INDEX_UNSET) { + sampleTrackIndex = getTrackIndexOfNextReadSample(inputPosition); + if (sampleTrackIndex == C.INDEX_UNSET) { + return RESULT_END_OF_INPUT; + } } - Mp4Track track = tracks[trackIndex]; + Mp4Track track = tracks[sampleTrackIndex]; TrackOutput trackOutput = track.trackOutput; int sampleIndex = track.sampleIndex; long position = track.sampleTable.offsets[sampleIndex]; int sampleSize = track.sampleTable.sizes[sampleIndex]; - if (track.track.sampleTransformation == Track.TRANSFORMATION_CEA608_CDAT) { - // The sample information is contained in a cdat atom. The header must be discarded for - // committing. - position += Atom.HEADER_SIZE; - sampleSize -= Atom.HEADER_SIZE; - } - long skipAmount = position - input.getPosition() + sampleBytesWritten; + long skipAmount = position - inputPosition + sampleBytesWritten; if (skipAmount < 0 || skipAmount >= RELOAD_MINIMUM_SEEK_DISTANCE) { positionHolder.position = position; return RESULT_SEEK; } + if (track.track.sampleTransformation == Track.TRANSFORMATION_CEA608_CDAT) { + // The sample information is contained in a cdat atom. The header must be discarded for + // committing. + skipAmount += Atom.HEADER_SIZE; + sampleSize -= Atom.HEADER_SIZE; + } input.skipFully((int) skipAmount); if (track.track.nalUnitLengthFieldLength != 0) { // Zero the top three bytes of the array that we'll use to decode nal unit lengths, in case @@ -510,33 +524,61 @@ public final class Mp4Extractor implements Extractor, SeekMap { trackOutput.sampleMetadata(track.sampleTable.timestampsUs[sampleIndex], track.sampleTable.flags[sampleIndex], sampleSize, 0, null); track.sampleIndex++; + sampleTrackIndex = C.INDEX_UNSET; sampleBytesWritten = 0; sampleCurrentNalBytesRemaining = 0; return RESULT_CONTINUE; } /** - * Returns the index of the track that contains the earliest current sample, or - * {@link C#INDEX_UNSET} if no samples remain. + * Returns the index of the track that contains the next sample to be read, or {@link + * C#INDEX_UNSET} if no samples remain. + * + *

          The preferred choice is the sample with the smallest offset not requiring a source reload, + * or if not available the sample with the smallest overall offset to avoid subsequent source + * reloads. + * + *

          To deal with poor sample interleaving, we also check whether the required memory to catch up + * with the next logical sample (based on sample time) exceeds {@link + * #MAXIMUM_READ_AHEAD_BYTES_STREAM}. If this is the case, we continue with this sample even + * though it may require a source reload. */ - private int getTrackIndexOfEarliestCurrentSample() { - int earliestSampleTrackIndex = C.INDEX_UNSET; - long earliestSampleOffset = Long.MAX_VALUE; + private int getTrackIndexOfNextReadSample(long inputPosition) { + long preferredSkipAmount = Long.MAX_VALUE; + boolean preferredRequiresReload = true; + int preferredTrackIndex = C.INDEX_UNSET; + long preferredAccumulatedBytes = Long.MAX_VALUE; + long minAccumulatedBytes = Long.MAX_VALUE; + boolean minAccumulatedBytesRequiresReload = true; + int minAccumulatedBytesTrackIndex = C.INDEX_UNSET; for (int trackIndex = 0; trackIndex < tracks.length; trackIndex++) { Mp4Track track = tracks[trackIndex]; int sampleIndex = track.sampleIndex; if (sampleIndex == track.sampleTable.sampleCount) { continue; } - - long trackSampleOffset = track.sampleTable.offsets[sampleIndex]; - if (trackSampleOffset < earliestSampleOffset) { - earliestSampleOffset = trackSampleOffset; - earliestSampleTrackIndex = trackIndex; + long sampleOffset = track.sampleTable.offsets[sampleIndex]; + long sampleAccumulatedBytes = accumulatedSampleSizes[trackIndex][sampleIndex]; + long skipAmount = sampleOffset - inputPosition; + boolean requiresReload = skipAmount < 0 || skipAmount >= RELOAD_MINIMUM_SEEK_DISTANCE; + if ((!requiresReload && preferredRequiresReload) + || (requiresReload == preferredRequiresReload && skipAmount < preferredSkipAmount)) { + preferredRequiresReload = requiresReload; + preferredSkipAmount = skipAmount; + preferredTrackIndex = trackIndex; + preferredAccumulatedBytes = sampleAccumulatedBytes; + } + if (sampleAccumulatedBytes < minAccumulatedBytes) { + minAccumulatedBytes = sampleAccumulatedBytes; + minAccumulatedBytesRequiresReload = requiresReload; + minAccumulatedBytesTrackIndex = trackIndex; } } - - return earliestSampleTrackIndex; + return minAccumulatedBytes == Long.MAX_VALUE + || !minAccumulatedBytesRequiresReload + || preferredAccumulatedBytes < minAccumulatedBytes + MAXIMUM_READ_AHEAD_BYTES_STREAM + ? preferredTrackIndex + : minAccumulatedBytesTrackIndex; } /** @@ -554,6 +596,45 @@ public final class Mp4Extractor implements Extractor, SeekMap { } } + /** + * For each sample of each track, calculates accumulated size of all samples which need to be read + * before this sample can be used. + */ + private static long[][] calculateAccumulatedSampleSizes(Mp4Track[] tracks) { + long[][] accumulatedSampleSizes = new long[tracks.length][]; + int[] nextSampleIndex = new int[tracks.length]; + long[] nextSampleTimesUs = new long[tracks.length]; + boolean[] tracksFinished = new boolean[tracks.length]; + for (int i = 0; i < tracks.length; i++) { + accumulatedSampleSizes[i] = new long[tracks[i].sampleTable.sampleCount]; + nextSampleTimesUs[i] = tracks[i].sampleTable.timestampsUs[0]; + } + long accumulatedSampleSize = 0; + int finishedTracks = 0; + while (finishedTracks < tracks.length) { + long minTimeUs = Long.MAX_VALUE; + int minTimeTrackIndex = -1; + for (int i = 0; i < tracks.length; i++) { + if (!tracksFinished[i] && nextSampleTimesUs[i] <= minTimeUs) { + minTimeTrackIndex = i; + minTimeUs = nextSampleTimesUs[i]; + } + } + int trackSampleIndex = nextSampleIndex[minTimeTrackIndex]; + accumulatedSampleSizes[minTimeTrackIndex][trackSampleIndex] = accumulatedSampleSize; + accumulatedSampleSize += tracks[minTimeTrackIndex].sampleTable.sizes[trackSampleIndex]; + nextSampleIndex[minTimeTrackIndex] = ++trackSampleIndex; + if (trackSampleIndex < accumulatedSampleSizes[minTimeTrackIndex].length) { + nextSampleTimesUs[minTimeTrackIndex] = + tracks[minTimeTrackIndex].sampleTable.timestampsUs[trackSampleIndex]; + } else { + tracksFinished[minTimeTrackIndex] = true; + finishedTracks++; + } + } + return accumulatedSampleSizes; + } + /** * Adjusts a seek point offset to take into account the track with the given {@code sampleTable}, * for a given {@code seekTimeUs}. From 4828f275c7177a486fa1518b6242ac962741cb87 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 19 Jan 2018 02:10:57 -0800 Subject: [PATCH 1063/2472] Make play button behave differently in IDLE and ENDED states - In IDLE, the button will now call a preparer. This allows removal of the separate retry button from the demo app. - In ENDED, the button will seek back to the default position and play. - Behavior is made consistent with LeanbackPlayerAdapter. Issue: #3689 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=182506855 --- RELEASENOTES.md | 5 +++- .../exoplayer2/demo/PlayerActivity.java | 24 +++++++-------- .../src/main/res/layout/player_activity.xml | 10 +------ demos/main/src/main/res/values/strings.xml | 2 -- .../ext/leanback/LeanbackPlayerAdapter.java | 17 ++++++++++- .../android/exoplayer2/PlaybackPreparer.java | 23 +++++++++++++++ .../exoplayer2/ui/PlayerControlView.java | 29 +++++++++++++++++-- .../android/exoplayer2/ui/PlayerView.java | 11 +++++++ 8 files changed, 94 insertions(+), 27 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/PlaybackPreparer.java diff --git a/RELEASENOTES.md b/RELEASENOTES.md index b787838f41..d60dc147d8 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -19,12 +19,15 @@ periods. * Replaced `ExoPlayer.sendMessages` with `ExoPlayer.createMessage` to allow more customization of the message. Now supports setting a message delivery - playback position and/or a delivery handler. + playback position and/or a delivery handler ([#2189](https://github.com/google/ExoPlayer/issues/2189)). * UI components: * Generalized player and control views to allow them to bind with any `Player`, and renamed them to `PlayerView` and `PlayerControlView` respectively. + * Made `PlayerView`'s play button behave correctly when the player is ended + ([#3689](https://github.com/google/ExoPlayer/issues/3689)), and call a + `PlaybackPreparer` when the player is idle. * Buffering: * Allow a back-buffer of media to be retained behind the current playback position, for fast backward seeking. The back-buffer can be configured by diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index f2c728f516..f9185ec2d2 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -39,6 +39,7 @@ import com.google.android.exoplayer2.C.ContentType; import com.google.android.exoplayer2.DefaultRenderersFactory; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayerFactory; +import com.google.android.exoplayer2.PlaybackPreparer; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; @@ -82,7 +83,7 @@ import java.util.UUID; /** An activity that plays media using {@link SimpleExoPlayer}. */ public class PlayerActivity extends Activity - implements OnClickListener, PlayerControlView.VisibilityListener { + implements OnClickListener, PlaybackPreparer, PlayerControlView.VisibilityListener { public static final String DRM_SCHEME_EXTRA = "drm_scheme"; public static final String DRM_LICENSE_URL = "drm_license_url"; @@ -114,7 +115,6 @@ public class PlayerActivity extends Activity private PlayerView playerView; private LinearLayout debugRootView; private TextView debugTextView; - private Button retryButton; private DataSource.Factory mediaDataSourceFactory; private SimpleExoPlayer player; @@ -152,8 +152,6 @@ public class PlayerActivity extends Activity rootView.setOnClickListener(this); debugRootView = findViewById(R.id.controls_root); debugTextView = findViewById(R.id.debug_text_view); - retryButton = findViewById(R.id.retry_button); - retryButton.setOnClickListener(this); playerView = findViewById(R.id.player_view); playerView.setControllerVisibilityListener(this); @@ -229,9 +227,7 @@ public class PlayerActivity extends Activity @Override public void onClick(View view) { - if (view == retryButton) { - initializePlayer(); - } else if (view.getParent() == debugRootView) { + if (view.getParent() == debugRootView) { MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo(); if (mappedTrackInfo != null) { trackSelectionHelper.showSelectionDialog( @@ -240,6 +236,13 @@ public class PlayerActivity extends Activity } } + // PlaybackControlView.PlaybackPreparer implementation + + @Override + public void preparePlayback() { + initializePlayer(); + } + // PlaybackControlView.VisibilityListener implementation @Override @@ -301,9 +304,10 @@ public class PlayerActivity extends Activity player.addMetadataOutput(eventLogger); player.addAudioDebugListener(eventLogger); player.addVideoDebugListener(eventLogger); + player.setPlayWhenReady(shouldAutoPlay); playerView.setPlayer(player); - player.setPlayWhenReady(shouldAutoPlay); + playerView.setPlaybackPreparer(this); debugViewHelper = new DebugTextViewHelper(player, debugTextView); debugViewHelper.start(); } @@ -502,10 +506,6 @@ public class PlayerActivity extends Activity private void updateButtonVisibilities() { debugRootView.removeAllViews(); - - retryButton.setVisibility(inErrorState ? View.VISIBLE : View.GONE); - debugRootView.addView(retryButton); - if (player == null) { return; } diff --git a/demos/main/src/main/res/layout/player_activity.xml b/demos/main/src/main/res/layout/player_activity.xml index b2894140fe..6b84033273 100644 --- a/demos/main/src/main/res/layout/player_activity.xml +++ b/demos/main/src/main/res/layout/player_activity.xml @@ -42,15 +42,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" - android:visibility="gone"> - -